diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index 6939c84cf0..7f354789a8 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -507,6 +507,16 @@ export function createModel (builder: Builder): void { pinned: true }) + builder.mixin(core.class.Account, core.class.Class, view.mixin.Aggregation, { + createAggregationManager: contact.aggregation.CreatePersonAggregationManager, + setStoreFunc: contact.function.SetPersonStore, + filterFunc: contact.function.PersonFilterFunction + }) + + builder.mixin(core.class.Account, core.class.Class, view.mixin.Groupping, { + grouppingManager: contact.aggregation.GrouppingPersonManager + }) + builder.mixin(contact.mixin.Employee, core.class.Class, view.mixin.ObjectEditor, { editor: contact.component.EditEmployee, pinned: true diff --git a/models/contact/src/plugin.ts b/models/contact/src/plugin.ts index 4fce9257e1..ff404adb50 100644 --- a/models/contact/src/plugin.ts +++ b/models/contact/src/plugin.ts @@ -16,7 +16,7 @@ import { contactId } from '@hcengineering/contact' import contact from '@hcengineering/contact-resources/src/plugin' -import type { Client, Doc, Ref } from '@hcengineering/core' +import type { Client, Doc, DocManager, Ref } from '@hcengineering/core' import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation' import { type NotificationGroup } from '@hcengineering/notification' import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform' @@ -139,6 +139,8 @@ export default mergeIds(contactId, contact, { GetContactLastName: '' as Resource, ContactTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise>, ChannelTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise>, - ChannelIdentifierProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise> + ChannelIdentifierProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise>, + SetPersonStore: '' as Resource<(manager: DocManager) => void>, + PersonFilterFunction: '' as Resource<(doc: Doc, target: Doc) => boolean> } }) diff --git a/models/core/src/security.ts b/models/core/src/security.ts index cd995e6c3f..c4934e58c0 100644 --- a/models/core/src/security.ts +++ b/models/core/src/security.ts @@ -166,7 +166,7 @@ export class TSpacesTypeData extends TSpace implements RolesAssignment { } @Model(core.class.Account, core.class.Doc, DOMAIN_MODEL) -@UX(core.string.Account) +@UX(core.string.Account, undefined, undefined, 'name') export class TAccount extends TDoc implements Account { email!: string role!: AccountRole diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 75b87f5a3c..a848027e85 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -94,7 +94,13 @@ function defineSortAndGrouping (builder: Builder): void { }) builder.mixin(tracker.class.Component, core.class.Class, view.mixin.Aggregation, { - createAggregationManager: tracker.aggregation.CreateComponentAggregationManager + createAggregationManager: tracker.aggregation.CreateComponentAggregationManager, + setStoreFunc: tracker.function.SetComponentStore, + filterFunc: tracker.function.ComponentFilterFunction + }) + + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.Groupping, { + grouppingManager: tracker.aggregation.GrouppingComponentManager }) builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AllValuesFunc, { diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index 2625c898f1..663b70c190 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -15,7 +15,7 @@ // import { type DocUpdateMessageViewlet } from '@hcengineering/activity' import { type ChatMessageViewlet } from '@hcengineering/chunter' -import { type StatusCategory, type Doc, type Ref } from '@hcengineering/core' +import { type StatusCategory, type Doc, type Ref, type DocManager } from '@hcengineering/core' import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation' import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform' @@ -119,5 +119,10 @@ export default mergeIds(trackerId, tracker, { Started: '' as Ref, Completed: '' as Ref, Canceled: '' as Ref + }, + + function: { + SetComponentStore: '' as Resource<(manager: DocManager) => void>, + ComponentFilterFunction: '' as Resource<(doc: Doc, target: Doc) => boolean> } }) diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 2419882382..e12b9886df 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -20,6 +20,7 @@ import { DOMAIN_MODEL, type Data, type Doc, + type DocManager, type DocumentQuery, type Domain, type Ref, @@ -274,6 +275,8 @@ export class TGroupping extends TClass implements Groupping { @Mixin(view.mixin.Aggregation, core.class.Class) export class TAggregation extends TClass implements Aggregation { createAggregationManager!: CreateAggregationManagerFunc + setStoreFunc!: Resource<(manager: DocManager) => void> + filterFunc!: Resource<(doc: Doc, target: Doc) => boolean> } @Mixin(view.mixin.ObjectIcon, core.class.Class) diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index f2bcb9917e..299f6fd985 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -315,29 +315,36 @@ export class AggregateValue { */ export type CategoryType = number | string | undefined | Ref | AggregateValue +export interface IDocManager { + get: (ref: Ref) => T | undefined + getDocs: () => T[] + getIdMap: () => IdMap + filter: (predicate: (value: T) => boolean) => T[] +} + /** * @public */ -export class DocManager { - protected readonly byId: IdMap +export class DocManager implements IDocManager { + protected readonly byId: IdMap - constructor (protected readonly docs: Doc[]) { + constructor (protected readonly docs: T[]) { this.byId = toIdMap(docs) } - get (ref: Ref): Doc | undefined { + get (ref: Ref): T | undefined { return this.byId.get(ref) } - getDocs (): Doc[] { + getDocs (): T[] { return this.docs } - getIdMap (): IdMap { + getIdMap (): IdMap { return this.byId } - filter (predicate: (value: Doc) => boolean): Doc[] { + filter (predicate: (value: T) => boolean): T[] { return this.docs.filter(predicate) } } diff --git a/packages/kanban/src/components/KanbanRow.svelte b/packages/kanban/src/components/KanbanRow.svelte index 3dfabe60a4..40ca360441 100644 --- a/packages/kanban/src/components/KanbanRow.svelte +++ b/packages/kanban/src/components/KanbanRow.svelte @@ -74,8 +74,15 @@ let limitedObjects: IdMap = new Map() const docQuery = createQuery() - - $: groupQuery = { ...query, [groupByKey]: typeof state === 'object' ? { $in: state.values } : state } + $: groupQuery = { + ...query, + [groupByKey]: + typeof state === 'object' + ? state.name !== undefined + ? { $in: state.values.flatMap((x) => x._id) } + : undefined + : state + } $: void limiter.add(async () => { docQuery.query( diff --git a/plugins/contact-resources/src/components/PersonAccountRefPresenter.svelte b/plugins/contact-resources/src/components/PersonAccountRefPresenter.svelte index a42e9ecd1f..5e5cb0327d 100644 --- a/plugins/contact-resources/src/components/PersonAccountRefPresenter.svelte +++ b/plugins/contact-resources/src/components/PersonAccountRefPresenter.svelte @@ -15,12 +15,13 @@ --> {#if account} diff --git a/plugins/contact-resources/src/index.ts b/plugins/contact-resources/src/index.ts index 7e172c7e71..cf1c39e67d 100644 --- a/plugins/contact-resources/src/index.ts +++ b/plugins/contact-resources/src/index.ts @@ -15,14 +15,16 @@ // import { + type Channel, + type AvatarInfo, + type Contact, getGravatarUrl, getName, - type AvatarInfo, - type Channel, - type Contact, - type Person + type Person, + type PersonAccount } from '@hcengineering/contact' import { + DocManager, type Class, type Client, type Data, @@ -120,8 +122,9 @@ import NameChangedActivityMessage from './components/activity/NameChangedActivit import IconAddMember from './components/icons/AddMember.svelte' import ExpandRightDouble from './components/icons/ExpandRightDouble.svelte' import IconMembers from './components/icons/Members.svelte' +import { AggregationManager } from '@hcengineering/view-resources' -import { get } from 'svelte/store' +import { get, writable } from 'svelte/store' import contact from './plugin' import { channelIdentifierProvider, @@ -140,6 +143,7 @@ import { getCurrentEmployeeName, getCurrentEmployeePosition, getPersonTooltip, + grouppingPersonManager, resolveLocation } from './utils' @@ -293,6 +297,16 @@ async function openChannelURL (doc: Channel): Promise { } } +function filterPerson (doc: PersonAccount, target: PersonAccount): boolean { + return doc.person === target.person && doc._id !== target._id +} + +export const personStore = writable>(new DocManager([])) + +function setStore (manager: DocManager): void { + personStore.set(manager) +} + export interface PersonLabelTooltip { personLabel?: IntlString placeholderLabel?: IntlString @@ -431,9 +445,16 @@ export default async (): Promise => ({ ContactTitleProvider: contactTitleProvider, PersonTooltipProvider: getPersonTooltip, ChannelTitleProvider: channelTitleProvider, - ChannelIdentifierProvider: channelIdentifierProvider + ChannelIdentifierProvider: channelIdentifierProvider, + SetPersonStore: setStore, + PersonFilterFunction: filterPerson }, resolver: { Location: resolveLocation + }, + aggregation: { + // eslint-disable-next-line @typescript-eslint/unbound-method + CreatePersonAggregationManager: AggregationManager.create, + GrouppingPersonManager: grouppingPersonManager } }) diff --git a/plugins/contact-resources/src/plugin.ts b/plugins/contact-resources/src/plugin.ts index 569ca6d572..38f8104b47 100644 --- a/plugins/contact-resources/src/plugin.ts +++ b/plugins/contact-resources/src/plugin.ts @@ -18,7 +18,12 @@ import contact, { contactId } from '@hcengineering/contact' import { type Client, type Doc } from '@hcengineering/core' import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform' import { type LabelAndProps, type Location } from '@hcengineering/ui' -import { type FilterFunction, type SortFunc } from '@hcengineering/view' +import { + type CreateAggregationManagerFunc, + type GrouppingManagerResource, + type FilterFunction, + type SortFunc +} from '@hcengineering/view' export default mergeIds(contactId, contact, { string: { @@ -86,5 +91,9 @@ export default mergeIds(contactId, contact, { FilterChannelHasMessagesResult: '' as FilterFunction, FilterChannelHasNewMessagesResult: '' as FilterFunction, PersonTooltipProvider: '' as Resource<(client: Client, doc?: Doc | null) => Promise> + }, + aggregation: { + CreatePersonAggregationManager: '' as CreateAggregationManagerFunc, + GrouppingPersonManager: '' as GrouppingManagerResource } }) diff --git a/plugins/contact-resources/src/utils.ts b/plugins/contact-resources/src/utils.ts index 7dad189a49..647d727047 100644 --- a/plugins/contact-resources/src/utils.ts +++ b/plugins/contact-resources/src/utils.ts @@ -41,7 +41,13 @@ import core, { type Timestamp, type TxOperations, type UserStatus, - type WithLookup + type WithLookup, + AggregateValue, + type Space, + type Hierarchy, + type DocumentQuery, + AggregateValueData, + matchQuery } from '@hcengineering/core' import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification' import { getEmbeddedLabel, getResource, translate } from '@hcengineering/platform' @@ -55,11 +61,12 @@ import { type ResolvedLocation, type TabItem } from '@hcengineering/ui' -import view, { type Filter } from '@hcengineering/view' +import view, { type GrouppingManager, type Filter } from '@hcengineering/view' import { FilterQuery, accessDeniedStore } from '@hcengineering/view-resources' import { derived, get, writable } from 'svelte/store' import contact from './plugin' +import { personStore } from '.' export function formatDate (dueDateMs: Timestamp): string { return new Date(dueDateMs).toLocaleString('default', { @@ -431,3 +438,109 @@ export async function channelTitleProvider (client: Client, ref: Ref, d return channel.value } + +/** + * @public + */ +export const grouppingPersonManager: GrouppingManager = { + groupByCategories: groupByPersonAccountCategories, + groupValues: groupPersonAccountValues, + groupValuesWithEmpty: groupPersonAccountValuesWithEmpty, + hasValue: hasPersonAccountValue +} + +/** + * @public + */ +export function groupByPersonAccountCategories (categories: any[]): AggregateValue[] { + const mgr = get(personStore) + + const existingCategories: AggregateValue[] = [new AggregateValue(undefined, [])] + const personMap = new Map() + + const usedSpaces = new Set>() + const personAccountList: Array> = [] + for (const v of categories) { + const personAccount = mgr.getIdMap().get(v) + if (personAccount !== undefined) { + personAccountList.push(personAccount) + usedSpaces.add(personAccount.space) + } + } + + for (const personAccount of personAccountList) { + if (personAccount !== undefined) { + let fst = personMap.get(personAccount.person) + if (fst === undefined) { + const people = mgr + .getDocs() + .filter( + (it) => it.person === personAccount.person && (categories.includes(it._id) || usedSpaces.has(it.space)) + ) + .sort((a, b) => a.email.localeCompare(b.email)) + .map((it) => new AggregateValueData(it.person, it._id, it.space)) + fst = new AggregateValue(personAccount.person, people) + personMap.set(personAccount.person, fst) + existingCategories.push(fst) + } + } + } + return existingCategories +} + +/** + * @public + */ +export function groupPersonAccountValues (val: Doc[], targets: Set): Doc[] { + const values = val + const result: Doc[] = [] + const unique = [...new Set(val.map((c) => (c as PersonAccount).person))] + unique.forEach((label, i) => { + let exists = false + values.forEach((c) => { + if ((c as PersonAccount).person === label) { + if (!exists) { + result[i] = c + exists = targets.has(c?._id) + } + } + }) + }) + return result +} + +/** + * @public + */ +export function hasPersonAccountValue (value: Doc | undefined | null, values: any[]): boolean { + const mgr = get(personStore) + const personSet = new Set(mgr.filter((it) => it.person === (value as PersonAccount)?.person).map((it) => it._id)) + return values.some((it) => personSet.has(it)) +} + +/** + * @public + */ +export function groupPersonAccountValuesWithEmpty ( + hierarchy: Hierarchy, + _class: Ref>, + key: string, + query: DocumentQuery | undefined +): Array> { + const mgr = get(personStore) + let personAccountList = mgr.getDocs() + if (query !== undefined) { + const { [key]: st, space } = query + const resQuery: DocumentQuery = {} + if (space !== undefined) { + resQuery.space = space + } + if (st !== undefined) { + resQuery._id = st + } + personAccountList = matchQuery(personAccountList, resQuery, _class, hierarchy) as unknown as Array< + WithLookup + > + } + return personAccountList.map((it) => it._id) +} diff --git a/plugins/tracker-resources/src/component.ts b/plugins/tracker-resources/src/component.ts index 078008a32c..5bea1a4645 100644 --- a/plugins/tracker-resources/src/component.ts +++ b/plugins/tracker-resources/src/component.ts @@ -16,104 +16,21 @@ import { AggregateValue, AggregateValueData, - type AnyAttribute, type Class, - type Client, type Doc, + DocManager, type DocumentQuery, type Hierarchy, type Ref, - SortingOrder, type Space, - type Tx, type WithLookup, matchQuery } from '@hcengineering/core' -import { LiveQuery } from '@hcengineering/query' -import tracker, { type Component, ComponentManager } from '@hcengineering/tracker' -import { type AggregationManager, type GrouppingManager } from '@hcengineering/view' +import { type Component } from '@hcengineering/tracker' +import { type GrouppingManager } from '@hcengineering/view' import { get, writable } from 'svelte/store' -export const componentStore = writable(new ComponentManager([])) - -/** - * @public - */ -export class ComponentAggregationManager implements AggregationManager { - docs: Doc[] | undefined - mgr: ComponentManager | Promise | undefined - query: (() => void) | undefined - lq: LiveQuery - lqCallback: () => void - - private constructor (client: Client, lqCallback: () => void) { - this.lq = new LiveQuery(client) - this.lqCallback = lqCallback ?? (() => {}) - } - - static create (client: Client, lqCallback: () => void): ComponentAggregationManager { - return new ComponentAggregationManager(client, lqCallback) - } - - private async getManager (): Promise { - if (this.mgr !== undefined) { - if (this.mgr instanceof Promise) { - this.mgr = await this.mgr - } - return this.mgr - } - this.mgr = new Promise((resolve) => { - this.query = this.lq.query( - tracker.class.Component, - {}, - (res) => { - const first = this.docs === undefined - this.docs = res - this.mgr = new ComponentManager(res) - componentStore.set(this.mgr) - if (!first) { - this.lqCallback() - } - resolve(this.mgr) - }, - { - sort: { - label: SortingOrder.Ascending - } - } - ) - }) - - return await this.mgr - } - - close (): void { - this.query?.() - } - - async notifyTx (...tx: Tx[]): Promise { - await this.lq.tx(...tx) - } - - getAttrClass (): Ref> { - return tracker.class.Component - } - - async categorize (target: Array>, attr: AnyAttribute): Promise>> { - const mgr = await this.getManager() - for (const sid of [...target]) { - const c = mgr.getIdMap().get(sid as Ref) as WithLookup - if (c !== undefined) { - let components = mgr.getDocs() - components = components.filter( - (it) => it.label.toLowerCase().trim() === c.label.toLowerCase().trim() && it._id !== c._id - ) - target.push(...components.map((it) => it._id)) - } - } - return target.filter((it, idx, arr) => arr.indexOf(it) === idx) - } -} +export const componentStore = writable>(new DocManager([])) /** * @public @@ -136,6 +53,7 @@ export function groupByComponentCategories (categories: any[]): AggregateValue[] const usedSpaces = new Set>() const componentsList: Array> = [] + // console.log('mgr docs', mgr.getDocs()) for (const v of categories) { const component = mgr.getIdMap().get(v) if (component !== undefined) { diff --git a/plugins/tracker-resources/src/index.ts b/plugins/tracker-resources/src/index.ts index ae7153a7b7..5ca9262f5f 100644 --- a/plugins/tracker-resources/src/index.ts +++ b/plugins/tracker-resources/src/index.ts @@ -30,12 +30,13 @@ import core, { type Ref, type RelatedDocument, type TxOperations, + type DocManager, AccountRole } from '@hcengineering/core' import chunter, { type ChatMessage } from '@hcengineering/chunter' import { type Status, translate, type Resources } from '@hcengineering/platform' import { getClient, MessageBox, type ObjectSearchResult } from '@hcengineering/presentation' -import { type Issue, type Milestone, type Project } from '@hcengineering/tracker' +import { type Component, type Issue, type Milestone, type Project } from '@hcengineering/tracker' import { getCurrentLocation, navigate, showPopup, themeStore } from '@hcengineering/ui' import ComponentEditor from './components/components/ComponentEditor.svelte' import ComponentFilterValuePresenter from './components/components/ComponentFilterValuePresenter.svelte' @@ -122,7 +123,7 @@ import ComponentSelector from './components/components/ComponentSelector.svelte' import IssueTemplatePresenter from './components/templates/IssueTemplatePresenter.svelte' import IssueTemplates from './components/templates/IssueTemplates.svelte' -import { deleteObject, deleteObjects } from '@hcengineering/view-resources' +import { deleteObject, deleteObjects, AggregationManager } from '@hcengineering/view-resources' import MoveAndDeleteMilestonePopup from './components/milestones/MoveAndDeleteMilestonePopup.svelte' import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte' import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte' @@ -143,7 +144,7 @@ import { subIssueQuery } from './utils' -import { ComponentAggregationManager, grouppingComponentManager } from './component' +import { componentStore, grouppingComponentManager } from './component' import PriorityIcon from './components/activity/PriorityIcon.svelte' import StatusIcon from './components/activity/StatusIcon.svelte' import DeleteComponentPresenter from './components/components/DeleteComponentPresenter.svelte' @@ -591,6 +592,14 @@ export async function importTasks (tasks: File, space: Ref): Promise): void { + componentStore.set(manager) +} + export default async (): Promise => ({ activity: { PriorityIcon, @@ -710,7 +719,9 @@ export default async (): Promise => ({ GetVisibleFilters: getVisibleFilters, IssueChatTitleProvider: getIssueChatTitle, IsProjectJoined: async (project: Project) => project.members.includes(getCurrentAccount()._id), - GetIssueStatusCategories: getIssueStatusCategories + GetIssueStatusCategories: getIssueStatusCategories, + SetComponentStore: setStore, + ComponentFilterFunction: filterComponents }, actionImpl: { Move: move, @@ -726,7 +737,7 @@ export default async (): Promise => ({ }, aggregation: { // eslint-disable-next-line @typescript-eslint/unbound-method - CreateComponentAggregationManager: ComponentAggregationManager.create, + CreateComponentAggregationManager: AggregationManager.create, GrouppingComponentManager: grouppingComponentManager } }) diff --git a/plugins/tracker/src/index.ts b/plugins/tracker/src/index.ts index 17ddc4fdf9..6156e12ec4 100644 --- a/plugins/tracker/src/index.ts +++ b/plugins/tracker/src/index.ts @@ -21,8 +21,6 @@ import { CollectionSize, Data, Doc, - DocManager, - IdMap, Markup, Mixin, Ref, @@ -30,8 +28,7 @@ import { Space, Status, Timestamp, - Type, - WithLookup + Type } from '@hcengineering/core' import { Asset, IntlString, Plugin, Resource, plugin } from '@hcengineering/platform' import { Preference } from '@hcengineering/preference' @@ -351,29 +348,6 @@ export interface Component extends Doc { attachments?: number } -/** - * @public - * - * Allow to query for status keys/values. - */ -export class ComponentManager extends DocManager { - get (ref: Ref>): WithLookup | undefined { - return this.getIdMap().get(ref) as WithLookup - } - - getDocs (): Array> { - return this.docs as Component[] - } - - getIdMap (): IdMap> { - return this.byId as IdMap> - } - - filter (predicate: (value: Component) => boolean): Component[] { - return this.getDocs().filter(predicate) - } -} - /** * @public */ diff --git a/plugins/view-resources/src/components/list/List.svelte b/plugins/view-resources/src/components/list/List.svelte index 8c78361b88..2966137ea4 100644 --- a/plugins/view-resources/src/components/list/List.svelte +++ b/plugins/view-resources/src/components/list/List.svelte @@ -97,6 +97,7 @@ queryNoLookup, (res) => { fastDocs = res + // console.log('query, res', queryNoLookup, res) fastQueryIds = new Set(res.map((it) => it._id)) }, { ...categoryQueryOptions, limit: 1000 } diff --git a/plugins/view-resources/src/components/list/ListCategories.svelte b/plugins/view-resources/src/components/list/ListCategories.svelte index e2236811ff..117cd36b69 100644 --- a/plugins/view-resources/src/components/list/ListCategories.svelte +++ b/plugins/view-resources/src/components/list/ListCategories.svelte @@ -349,130 +349,149 @@ const listCategory: SvelteComponentTyped[] = [] const listListCategory: ListCategory[] = [] + function getGroupByKey ( + docKeys: Partial>>, + category: CategoryType, + resultQuery: DocumentQuery> + ): Partial> { + return { + ...docKeys, + [groupByKey]: + typeof category === 'object' + ? category.name !== undefined + ? { $in: category.values.flatMap((x) => x._id) } + : resultQuery[groupByKey]?.$in?.length !== 0 + ? undefined + : [] + : category + } + } {#each categories as category, i (typeof category === 'object' ? category.name : category)} {@const items = groupByKey === noCategory ? docs : getGroupByValues(groupByDocs, category)} - {@const categoryDocKeys = { ...docKeys, [groupByKey]: category }} - { - dispatch('dragstart', { - target: e.detail.target, - index: e.detail.index + getInitIndex(categories, i) - }) - }} - on:collapsed - {flatHeaders} - {disableHeader} - {props} - {listDiv} - bind:dragItem - > - { + dispatch('dragstart', { + target: e.detail.target, + index: e.detail.index + getInitIndex(categories, i) + }) + }} + on:collapsed + {flatHeaders} + {disableHeader} + {props} + {listDiv} + bind:dragItem > - { - select(0, evt.detail) - }} - on:select-next={(evt) => { - if (level !== 0) { - dispatch('select-next', evt.detail) - } else { - select(2, evt.detail) - } - }} - on:select-prev={(evt) => { - if (level !== 0) { - dispatch('select-prev', evt.detail) - } else { - select(-2, evt.detail) - } - }} - /> - - + + { + select(0, evt.detail) + }} + on:select-next={(evt) => { + if (level !== 0) { + dispatch('select-next', evt.detail) + } else { + select(2, evt.detail) + } + }} + on:select-prev={(evt) => { + if (level !== 0) { + dispatch('select-prev', evt.detail) + } else { + select(-2, evt.detail) + } + }} + /> + + + {/if} {/each} diff --git a/plugins/view-resources/src/middleware.ts b/plugins/view-resources/src/middleware.ts index f8ac2e3f9e..68afd5843e 100644 --- a/plugins/view-resources/src/middleware.ts +++ b/plugins/view-resources/src/middleware.ts @@ -20,7 +20,7 @@ import core, { } from '@hcengineering/core' import { getResource, translate } from '@hcengineering/platform' import { BasePresentationMiddleware, type PresentationMiddleware } from '@hcengineering/presentation' -import view, { type AggregationManager } from '@hcengineering/view' +import view, { type IAggregationManager } from '@hcengineering/view' /** * @public @@ -39,7 +39,7 @@ export interface DocSubScriber { * @public */ export class AggregationMiddleware extends BasePresentationMiddleware implements PresentationMiddleware { - mgrs: Map>, AggregationManager> = new Map>, AggregationManager>() + mgrs: Map>, IAggregationManager> = new Map>, IAggregationManager>() docs: Doc[] | undefined subscribers: Map = new Map() @@ -121,17 +121,30 @@ export class AggregationMiddleware extends BasePresentationMiddleware implements return { unsubscribe: ret.unsubscribe } } - private async getAggregationManager (_class: Ref>): Promise { + private async getAggregationManager (_class: Ref>): Promise | undefined> { let mgr = this.mgrs.get(_class) if (mgr === undefined) { const h = this.client.getHierarchy() const mixin = h.classHierarchyMixin(_class, view.mixin.Aggregation) - if (mixin?.createAggregationManager !== undefined) { + if ( + mixin?.createAggregationManager !== undefined && + mixin?.setStoreFunc !== undefined && + mixin?.filterFunc !== undefined && + mixin?._class !== undefined + ) { const f = await getResource(mixin.createAggregationManager) - mgr = f(this.client, () => { - this.refreshSubscribers() - }) + const storeFunc = await getResource(mixin.setStoreFunc) + const filterFunc = await getResource(mixin.filterFunc) + mgr = f( + this.client, + () => { + this.refreshSubscribers() + }, + storeFunc, + filterFunc, + _class + ) this.mgrs.set(_class, mgr) } } diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index 56b0252763..bf06c679d5 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -49,7 +49,12 @@ import core, { type TxOperations, type TxUpdateDoc, type TypeAny, - type TypedSpace + type TypedSpace, + type WithLookup, + type AnyAttribute, + DocManager, + SortingOrder, + type Tx } from '@hcengineering/core' import { type Restrictions } from '@hcengineering/guest' import type { Asset, IntlString } from '@hcengineering/platform' @@ -63,6 +68,7 @@ import { isAdminUser, type KeyedAttribute } from '@hcengineering/presentation' +import { LiveQuery } from '@hcengineering/query' import { type CollaborationUser } from '@hcengineering/text-editor' import { ErrorPresenter, @@ -79,6 +85,7 @@ import { type Location } from '@hcengineering/ui' import view, { + type IAggregationManager, AttributeCategoryOrder, type AttributeCategory, type AttributeModel, @@ -105,6 +112,102 @@ export interface LoadingProps { length: number } +/** + * @public + */ +export class AggregationManager implements IAggregationManager { + docs: T[] | undefined + mgr: DocManager | Promise> | undefined + query: (() => void) | undefined + lq: LiveQuery + lqCallback: () => void + private readonly setStore: (manager: DocManager) => void + private readonly filter: (doc: T, target: T) => boolean + private readonly _class: Ref> + + private constructor ( + client: Client, + lqCallback: () => void, + setStore: (manager: DocManager) => void, + categorizingFunc: (doc: T, target: T) => boolean, + _class: Ref> + ) { + this.lq = new LiveQuery(client) + this.lqCallback = lqCallback ?? (() => {}) + this.setStore = setStore + this.filter = categorizingFunc + this._class = _class + void this.getManager() + } + + static create( + client: Client, + lqCallback: () => void, + setStore: (manager: DocManager) => void, + categorizingFunc: (doc: T, target: T) => boolean, + _class: Ref> + ): AggregationManager { + return new AggregationManager(client, lqCallback, setStore, categorizingFunc, _class) + } + + private async getManager (): Promise> { + if (this.mgr !== undefined) { + if (this.mgr instanceof Promise) { + this.mgr = await this.mgr + } + return this.mgr + } + this.mgr = new Promise>((resolve) => { + this.query = this.lq.query( + this._class, + {}, + (res) => { + const first = this.docs === undefined + this.docs = res + this.mgr = new DocManager(res as T[]) + this.setStore(this.mgr) + if (!first) { + this.lqCallback() + } + resolve(this.mgr) + }, + { + sort: { + label: SortingOrder.Ascending + } + } + ) + }) + + return await this.mgr + } + + close (): void { + this.query?.() + } + + async notifyTx (...tx: Tx[]): Promise { + await this.lq.tx(...tx) + } + + getAttrClass (): Ref> { + return this._class + } + + async categorize (target: Array>, attr: AnyAttribute): Promise>> { + const mgr = await this.getManager() + for (const sid of [...target]) { + const c = mgr.getIdMap().get(sid) as WithLookup + if (c !== undefined) { + let docs = mgr.getDocs() + docs = docs.filter((it: T) => this.filter(it, c)) + target.push(...docs.map((it) => it._id)) + } + } + return target.filter((it, idx, arr) => arr.indexOf(it) === idx) + } +} + /** * @public */ diff --git a/plugins/view/src/types.ts b/plugins/view/src/types.ts index dbbfbdff25..1ba1262afd 100644 --- a/plugins/view/src/types.ts +++ b/plugins/view/src/types.ts @@ -22,6 +22,7 @@ import { Class, Client, Doc, + DocManager, DocumentQuery, FindOptions, Hierarchy, @@ -373,29 +374,39 @@ export interface Groupping extends Class { /** * @public */ -export interface AggregationManager { +export interface IAggregationManager { close: () => void notifyTx: (...tx: Tx[]) => Promise - categorize: (target: Array>, attr: AnyAttribute) => Promise>> - getAttrClass: () => Ref> - updateSorting?: (finalOptions: FindOptions, attr: AnyAttribute) => Promise + categorize: (target: Array>, attr: AnyAttribute) => Promise>> + getAttrClass: () => Ref> + updateSorting?: (finalOptions: FindOptions, attr: AnyAttribute) => Promise } /** * @public */ -export type AggregationManagerResource = Resource +export type AggregationManagerResource = Resource> /** * @public */ -export type CreateAggregationManagerFunc = Resource<(client: Client, lqCallback: () => void) => AggregationManager> +export type CreateAggregationManagerFunc = Resource< +( + client: Client, + lqCallback: () => void, + setStore: (manager: DocManager) => void, + categorizingFunc: (doc: any, target: any) => boolean, + _class: Ref> +) => IAggregationManager +> /** * @public */ export interface Aggregation extends Class { createAggregationManager: CreateAggregationManagerFunc + setStoreFunc: Resource<(manager: DocManager) => void> + filterFunc: Resource<(doc: Doc, target: Doc) => boolean> } /**