From 20da8625d2671a77b3cb3c9a09346a5795999753 Mon Sep 17 00:00:00 2001 From: Ruslan Bayandinov <45530296+wazsone@users.noreply.github.com> Date: Tue, 6 Jun 2023 17:06:32 +0700 Subject: [PATCH] [UBER-197] Aggregate components properly (#3265) Signed-off-by: Ruslan Bayandinov --- common/config/rush/pnpm-lock.yaml | 9 +- models/presentation/src/index.ts | 12 +- models/tracker/src/index.ts | 15 +- models/view/package.json | 3 +- models/view/src/index.ts | 58 +++- models/view/src/plugin.ts | 7 +- packages/core/src/status.ts | 37 +- packages/core/src/utils.ts | 58 +++- packages/presentation/src/configuration.ts | 2 - packages/presentation/src/index.ts | 1 + packages/presentation/src/pipeline.ts | 8 + packages/presentation/src/plugin.ts | 4 +- packages/presentation/src/status.ts | 316 ------------------ packages/presentation/src/utils.ts | 7 +- .../src/components/kanban/KanbanView.svelte | 5 +- .../state/DoneStateRefPresenter.svelte | 7 +- .../components/state/StatePresenter.svelte | 2 - .../components/state/StateRefPresenter.svelte | 10 +- .../src/components/state/StatesBar.svelte | 3 +- plugins/tracker-resources/package.json | 3 +- plugins/tracker-resources/src/component.ts | 238 +++++++++++++ .../src/components/activity/StatusIcon.svelte | 4 +- .../components/ComponentPresenter.svelte | 47 +-- .../components/ComponentRefPresenter.svelte | 30 ++ .../issues/IssueStatusActivity.svelte | 4 +- .../components/issues/IssueStatusIcon.svelte | 3 +- .../src/components/issues/KanbanView.svelte | 5 +- .../src/components/issues/Move.svelte | 3 +- .../src/components/issues/StatusEditor.svelte | 15 +- .../issues/StatusFilterValuePresenter.svelte | 5 +- .../issues/StatusRefPresenter.svelte | 13 +- .../issues/edit/SubIssueSelector.svelte | 6 +- .../issues/edit/SubIssuesSelector.svelte | 6 +- .../components/issues/move/StatusMove.svelte | 3 +- .../issues/move/StatusMovePresenter.svelte | 2 +- .../related/RelatedIssuePresenter.svelte | 4 +- .../related/RelatedIssueSelector.svelte | 5 +- .../milestones/IssueStatistics.svelte | 6 +- .../components/workflow/RemoveStatus.svelte | 14 +- .../src/components/workflow/Statuses.svelte | 6 +- plugins/tracker-resources/src/index.ts | 7 + plugins/tracker-resources/src/plugin.ts | 15 +- plugins/tracker-resources/src/utils.ts | 6 +- plugins/tracker/src/index.ts | 30 +- plugins/view-resources/package.json | 1 + plugins/view-resources/src/actions.ts | 2 +- .../src/components/DocNavLink.svelte | 2 +- .../src/components/filter/ObjectFilter.svelte | 47 +-- .../src/components/list/ListCategories.svelte | 6 +- .../src/components/list/ListCategory.svelte | 4 +- .../src/components/list/ListHeader.svelte | 4 +- .../status/StatusRefPresenter.svelte | 6 +- plugins/view-resources/src/index.ts | 11 +- plugins/view-resources/src/middleware.ts | 234 +++++++++++++ plugins/view-resources/src/plugin.ts | 12 +- plugins/view-resources/src/status.ts | 283 ++++++++++++++++ plugins/view-resources/src/utils.ts | 79 +---- plugins/view-resources/src/viewOptions.ts | 51 +-- plugins/view/src/index.ts | 75 ++++- 59 files changed, 1263 insertions(+), 598 deletions(-) delete mode 100644 packages/presentation/src/status.ts create mode 100644 plugins/tracker-resources/src/component.ts create mode 100644 plugins/tracker-resources/src/components/components/ComponentRefPresenter.svelte create mode 100644 plugins/view-resources/src/middleware.ts create mode 100644 plugins/view-resources/src/status.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index d15549f26d..13739ac98d 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -19743,7 +19743,7 @@ packages: dev: false file:projects/model-view.tgz_typescript@4.8.4: - resolution: {integrity: sha512-MXdgL834gD1r+h6dZaXCpGCSiHrO2TxGXDHfF951mTThYcbz2X5P950/XcADZgvC4riAXQoJRVCjn1iR1paanA==, tarball: file:projects/model-view.tgz} + resolution: {integrity: sha512-nswV/jP/DzqP9yt1TnthbVdziRUYEW3rs/mqG3VTSIwO0Z0Sy0hGbFDr21n8YjxBf756QK3qAItBlJwRq4N3Tg==, tarball: file:projects/model-view.tgz} id: file:projects/model-view.tgz name: '@rush-temp/model-view' version: 0.0.0 @@ -22070,7 +22070,7 @@ packages: dev: false file:projects/tracker-resources.tgz_a1d864769aaf53d09b76fe134ab55e60: - resolution: {integrity: sha512-jMfAS0PwGvid0QCtAiVe3bDOLx4PnRdtmz23urZXRSf+s9mtZNpKIGPNDic8wragI9GjAbjJ85bHt8fZaxsSlw==, tarball: file:projects/tracker-resources.tgz} + resolution: {integrity: sha512-aahQ2kYGfdS+98z6xeLjeFffJYcfU106RpeslK2u9g9zaAPmdZa6L72ZRFiEDkW1y6n2d3TpDasCCshMw2lEHg==, tarball: file:projects/tracker-resources.tgz} id: file:projects/tracker-resources.tgz name: '@rush-temp/tracker-resources' version: 0.0.0 @@ -22210,7 +22210,7 @@ packages: dev: false file:projects/view-resources.tgz_a1d864769aaf53d09b76fe134ab55e60: - resolution: {integrity: sha512-jeJXZ80aZypm/6JrOsW+XJ6z8mzAUf7CJpVfPP8qTYIV2FsTqYBY4aHK0roPkaUVEAd/Ox9oAwKDksoR8EgB/w==, tarball: file:projects/view-resources.tgz} + resolution: {integrity: sha512-Taw9Dsr8rIkwsDJBqW4FddqW78UyIqHptCBk+J2c4nJlYnEUYKzbJaszb0N/fOkTN43I72SoxOQlyygIH85sKw==, tarball: file:projects/view-resources.tgz} id: file:projects/view-resources.tgz name: '@rush-temp/view-resources' version: 0.0.0 @@ -22245,7 +22245,7 @@ packages: dev: false file:projects/view.tgz: - resolution: {integrity: sha512-BKoOzFnUY/wlXgnnf7GsXZi9v3rZao+9EUVJznob9Dd0PK919rBLq/3XtXduTsX9I5XL63X6aThlzp6BpUyCNQ==, tarball: file:projects/view.tgz} + resolution: {integrity: sha512-Q5nEEe6n/qr7hJMMUeD2WSC2/MDdStM4h1maedLCV4OB6t2x0HTmNxm7b8mjROeuTV9lsxUezlMuq2xnBbF7ug==, tarball: file:projects/view.tgz} name: '@rush-temp/view' version: 0.0.0 dependencies: @@ -22259,6 +22259,7 @@ packages: eslint-plugin-n: 15.5.1_eslint@8.27.0 eslint-plugin-promise: 6.1.1_eslint@8.27.0 prettier: 2.8.8 + svelte: 3.55.1 typescript: 4.8.4 transitivePeerDependencies: - supports-color diff --git a/models/presentation/src/index.ts b/models/presentation/src/index.ts index fab507f784..f5a0491fca 100644 --- a/models/presentation/src/index.ts +++ b/models/presentation/src/index.ts @@ -16,10 +16,11 @@ import { DOMAIN_MODEL } from '@hcengineering/core' import { Builder, Model } from '@hcengineering/model' import core, { TDoc } from '@hcengineering/model-core' -import type { Asset, IntlString, Resource } from '@hcengineering/platform' +import { Asset, IntlString, Resource } from '@hcengineering/platform' // Import types to prevent .svelte components to being exposed to type typescript. import { ObjectSearchCategory, ObjectSearchFactory } from '@hcengineering/presentation/src/types' import presentation from './plugin' +import { PresentationMiddlewareCreator, PresentationMiddlewareFactory } from '@hcengineering/presentation' export { presentationId } from '@hcengineering/presentation/src/plugin' export { default } from './plugin' @@ -34,6 +35,11 @@ export class TObjectSearchCategory extends TDoc implements ObjectSearchCategory query!: Resource } -export function createModel (builder: Builder): void { - builder.createModel(TObjectSearchCategory) +@Model(presentation.class.PresentationMiddlewareFactory, core.class.Doc, DOMAIN_MODEL) +export class TPresentationMiddlewareFactory extends TDoc implements PresentationMiddlewareFactory { + createPresentationMiddleware!: Resource +} + +export function createModel (builder: Builder): void { + builder.createModel(TObjectSearchCategory, TPresentationMiddlewareFactory) } diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 03fd5b0df2..aa7f7f38f7 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -14,8 +14,7 @@ // import activity from '@hcengineering/activity' -import type { Employee, EmployeeAccount } from '@hcengineering/contact' -import contact from '@hcengineering/contact' +import contact, { Employee, EmployeeAccount } from '@hcengineering/contact' import { DOMAIN_MODEL, DateRangeMode, @@ -1011,6 +1010,18 @@ export function createModel (builder: Builder): void { inlineEditor: tracker.component.ComponentSelector }) + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.AttributePresenter, { + presenter: tracker.component.ComponentRefPresenter + }) + + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.Aggregation, { + createAggregationManager: tracker.aggregation.CreateComponentAggregationManager + }) + + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.Groupping, { + grouppingManager: tracker.aggregation.GrouppingComponentManager + }) + builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ObjectPresenter, { presenter: tracker.component.MilestonePresenter }) diff --git a/models/view/package.json b/models/view/package.json index 4ada58eb47..4f9716c93e 100644 --- a/models/view/package.json +++ b/models/view/package.json @@ -34,6 +34,7 @@ "@hcengineering/model-core": "^0.6.0", "@hcengineering/preference": "^0.6.6", "@hcengineering/model-preference": "^0.6.0", - "@hcengineering/model-presentation": "^0.6.0" + "@hcengineering/model-presentation": "^0.6.0", + "@hcengineering/presentation": "^0.6.2" } } diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 4d44c339a7..aeb783a242 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -13,14 +13,24 @@ // limitations under the License. // -import type { Account, Class, Client, Data, Doc, DocumentQuery, Domain, Ref, Space } from '@hcengineering/core' -import { DOMAIN_MODEL } from '@hcengineering/core' +import Core, { + DOMAIN_MODEL, + Account, + Class, + Client, + Data, + Doc, + DocumentQuery, + Domain, + Ref, + Space +} from '@hcengineering/core' import { Builder, Mixin, Model } from '@hcengineering/model' import core, { TClass, TDoc } from '@hcengineering/model-core' import preference, { TPreference } from '@hcengineering/model-preference' -import type { Asset, IntlString, Resource, Status } from '@hcengineering/platform' -import type { AnyComponent, Location } from '@hcengineering/ui' -import type { +import { Asset, IntlString, Resource, Status } from '@hcengineering/platform' +import { AnyComponent, Location } from '@hcengineering/ui' +import { Action, ActionCategory, ActivityAttributePresenter, @@ -68,8 +78,13 @@ import type { ViewOptionsModel, Viewlet, ViewletDescriptor, - ViewletPreference + ViewletPreference, + Aggregation, + CreateAggregationManagerFunc, + GrouppingManagerResource, + Groupping } from '@hcengineering/view' +import presentation from '@hcengineering/model-presentation' import view from './plugin' export { viewId } from '@hcengineering/view' @@ -260,6 +275,16 @@ export class TAllValuesFunc extends TClass implements AllValuesFunc { func!: GetAllValuesFunc } +@Mixin(view.mixin.Groupping, core.class.Class) +export class TGroupping extends TClass implements Groupping { + grouppingManager!: GrouppingManagerResource +} + +@Mixin(view.mixin.Aggregation, core.class.Class) +export class TAggregation extends TClass implements Aggregation { + createAggregationManager!: CreateAggregationManagerFunc +} + @Model(view.class.ViewletPreference, preference.class.Preference) export class TViewletPreference extends TPreference implements ViewletPreference { attachedTo!: Ref @@ -408,7 +433,9 @@ export function createModel (builder: Builder): void { TArrayEditor, TInlineAttributEditor, TFilteredView, - TAllValuesFunc + TAllValuesFunc, + TAggregation, + TGroupping ) classPresenter( @@ -517,6 +544,15 @@ export function createModel (builder: Builder): void { view.viewlet.List ) + builder.createDoc( + presentation.class.PresentationMiddlewareFactory, + core.space.Model, + { + createPresentationMiddleware: view.function.CreateDocMiddleware + }, + view.pipeline.PresentationMiddleware + ) + createAction( builder, { @@ -990,6 +1026,14 @@ export function createModel (builder: Builder): void { builder.mixin(core.class.Status, core.class.Class, view.mixin.AttributePresenter, { presenter: view.component.StatusRefPresenter }) + + builder.mixin(Core.class.Status, core.class.Class, view.mixin.Aggregation, { + createAggregationManager: view.aggregation.CreateStatusAggregationManager + }) + + builder.mixin(Core.class.Status, core.class.Class, view.mixin.Groupping, { + grouppingManager: view.aggregation.GrouppingStatusManager + }) } export default view diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index 6eafa7956b..231aef750b 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -13,9 +13,11 @@ // limitations under the License. // +import { Ref } from '@hcengineering/core' import { IntlString, mergeIds } from '@hcengineering/platform' -import type { AnyComponent } from '@hcengineering/ui' +import { AnyComponent } from '@hcengineering/ui' import { FilterFunction, ViewAction, ViewCategoryAction, viewId } from '@hcengineering/view' +import { PresentationMiddlewareFactory } from '@hcengineering/presentation' import view from '@hcengineering/view-resources/src/plugin' export default mergeIds(viewId, view, { @@ -118,5 +120,8 @@ export default mergeIds(viewId, view, { FilterDateNotSpecified: '' as FilterFunction, FilterDateCustom: '' as FilterFunction, ShowEmptyGroups: '' as ViewCategoryAction + }, + pipeline: { + PresentationMiddleware: '' as Ref } }) diff --git a/packages/core/src/status.ts b/packages/core/src/status.ts index 7d263ccfbc..275b6fadfb 100644 --- a/packages/core/src/status.ts +++ b/packages/core/src/status.ts @@ -15,8 +15,8 @@ import { Asset, IntlString } from '@hcengineering/platform' import { Attribute, Doc, Domain, Ref } from './classes' +import { AggregateValue, AggregateValueData, DocManager, IdMap } from './utils' import { WithLookup } from './storage' -import { IdMap, toIdMap } from './utils' /** * @public @@ -60,8 +60,14 @@ export interface Status extends Doc { /** * @public */ -export class StatusValue { - constructor (readonly name: string, readonly color: number | undefined, readonly values: WithLookup[]) {} +export class StatusValue extends AggregateValue { + constructor ( + readonly name: string | undefined, + readonly color: number | undefined, + readonly values: AggregateValueData[] + ) { + super(name, values) + } } /** @@ -69,23 +75,20 @@ export class StatusValue { * * Allow to query for status keys/values. */ -export class StatusManager { - byId: IdMap> - - constructor (readonly statuses: WithLookup[]) { - this.byId = toIdMap(statuses) +export class StatusManager extends DocManager { + get (ref: Ref>): WithLookup | undefined { + return this.getIdMap().get(ref) as WithLookup } - get (ref: Ref): WithLookup | undefined { - return this.byId.get(ref) + getDocs (): Array> { + return this.docs as Status[] } - filter (predicate: (value: WithLookup) => boolean): WithLookup[] { - return this.statuses.filter(predicate) + getIdMap (): IdMap> { + return this.byId as IdMap> + } + + filter (predicate: (value: Status) => boolean): Status[] { + return this.getDocs().filter(predicate) } } - -/** - * @public - */ -export type CategoryType = number | string | undefined | Ref | StatusValue diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 6835925182..94f5294dcc 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import { Account, AnyAttribute, Class, Doc, DocData, DocIndexState, IndexKind, Obj, Ref } from './classes' +import { Account, AnyAttribute, Class, Doc, DocData, DocIndexState, IndexKind, Obj, Ref, Space } from './classes' import core from './component' import { Hierarchy } from './hierarchy' import { FindResult } from './storage' @@ -224,3 +224,59 @@ export function fillDefaults ( } return object } + +/** + * @public + */ +export class AggregateValueData { + constructor ( + readonly name: string, + readonly _id: Ref, + readonly space: Ref, + readonly rank?: string, + readonly category?: Ref + ) {} + + getRank (): string { + return this.rank ?? '' + } +} + +/** + * @public + */ +export class AggregateValue { + constructor (readonly name: string | undefined, readonly values: AggregateValueData[]) {} +} + +/** + * @public + */ +export type CategoryType = number | string | undefined | Ref | AggregateValue + +/** + * @public + */ +export class DocManager { + protected readonly byId: IdMap + + constructor (protected readonly docs: Doc[]) { + this.byId = toIdMap(docs) + } + + get (ref: Ref): Doc | undefined { + return this.byId.get(ref) + } + + getDocs (): Doc[] { + return this.docs + } + + getIdMap (): IdMap { + return this.byId + } + + filter (predicate: (value: Doc) => boolean): Doc[] { + return this.docs.filter(predicate) + } +} diff --git a/packages/presentation/src/configuration.ts b/packages/presentation/src/configuration.ts index a7a75b05ea..00906a874a 100644 --- a/packages/presentation/src/configuration.ts +++ b/packages/presentation/src/configuration.ts @@ -17,8 +17,6 @@ import core, { PluginConfiguration, SortingOrder } from '@hcengineering/core' import { Plugin, Resource, getResourcePlugin } from '@hcengineering/platform' import { writable } from 'svelte/store' import { createQuery } from '.' -import { statusStore } from './status' -export { statusStore } /** * @public diff --git a/packages/presentation/src/index.ts b/packages/presentation/src/index.ts index 53920a25c0..1f07fef21f 100644 --- a/packages/presentation/src/index.ts +++ b/packages/presentation/src/index.ts @@ -49,6 +49,7 @@ export * from './drafts' export { presentationId } export * from './configuration' export * from './context' +export * from './pipeline' addStringsLoader(presentationId, async (lang: string) => { return await import(`../lang/${lang}.json`) diff --git a/packages/presentation/src/pipeline.ts b/packages/presentation/src/pipeline.ts index 73ba1e7ac7..6b2e9e1018 100644 --- a/packages/presentation/src/pipeline.ts +++ b/packages/presentation/src/pipeline.ts @@ -12,6 +12,7 @@ import { TxResult, WithLookup } from '@hcengineering/core' +import { Resource } from '@hcengineering/platform' /** * @public @@ -232,3 +233,10 @@ export abstract class BasePresentationMiddleware { return { unsubscribe: () => {} } } } + +/** + * @public + */ +export interface PresentationMiddlewareFactory extends Doc { + createPresentationMiddleware: Resource +} diff --git a/packages/presentation/src/plugin.ts b/packages/presentation/src/plugin.ts index b957fadcb9..521b637cb4 100644 --- a/packages/presentation/src/plugin.ts +++ b/packages/presentation/src/plugin.ts @@ -18,6 +18,7 @@ import { Class, Ref } from '@hcengineering/core' import type { Asset, IntlString, Metadata, Plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import { ObjectSearchCategory } from './types' +import { PresentationMiddlewareFactory } from './pipeline' /** * @public @@ -26,7 +27,8 @@ export const presentationId = 'presentation' as Plugin export default plugin(presentationId, { class: { - ObjectSearchCategory: '' as Ref> + ObjectSearchCategory: '' as Ref>, + PresentationMiddlewareFactory: '' as Ref> }, string: { Create: '' as IntlString, diff --git a/packages/presentation/src/status.ts b/packages/presentation/src/status.ts deleted file mode 100644 index 98c5d3b791..0000000000 --- a/packages/presentation/src/status.ts +++ /dev/null @@ -1,316 +0,0 @@ -// -// Copyright © 2023 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import core, { - AnyAttribute, - Attribute, - Class, - Client, - Doc, - DocumentQuery, - FindOptions, - FindResult, - generateId, - Hierarchy, - Ref, - RefTo, - SortingOrder, - SortingRules, - Status, - StatusManager, - Tx, - TxResult -} from '@hcengineering/core' -import { LiveQuery } from '@hcengineering/query' -import { writable } from 'svelte/store' -import { BasePresentationMiddleware, PresentationMiddleware } from './pipeline' - -// Issue status live query -export const statusStore = writable(new StatusManager([])) - -interface StatusSubscriber { - attributes: Array> - - _class: Ref> - query: DocumentQuery - options?: FindOptions - - refresh: () => void -} - -/** - * @public - */ -export class StatusMiddleware extends BasePresentationMiddleware implements PresentationMiddleware { - mgr: StatusManager | Promise | undefined - status: Status[] | undefined - statusQuery: (() => void) | undefined - lq: LiveQuery - - subscribers: Map = new Map() - private constructor (client: Client, next?: PresentationMiddleware) { - super(client, next) - this.lq = new LiveQuery(client) - } - - async notifyTx (tx: Tx): Promise { - await this.lq.tx(tx) - await this.provideNotifyTx(tx) - } - - async close (): Promise { - this.statusQuery?.() - return await this.provideClose() - } - - 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.statusQuery = this.lq.query( - core.class.Status, - {}, - (res) => { - const first = this.status === undefined - this.status = res - this.mgr = new StatusManager(res) - statusStore.set(this.mgr) - if (!first) { - this.refreshSubscribers() - } - resolve(this.mgr) - }, - { - lookup: { - category: core.class.StatusCategory - }, - sort: { - rank: SortingOrder.Ascending - } - } - ) - }) - - return await this.mgr - } - - private refreshSubscribers (): void { - for (const s of this.subscribers.values()) { - // TODO: Do something more smart and track if used status field is changed. - s.refresh() - } - } - - async subscribe( - _class: Ref>, - query: DocumentQuery, - options: FindOptions | undefined, - refresh: () => void - ): Promise<{ - unsubscribe: () => void - query?: DocumentQuery - options?: FindOptions - }> { - const ret = await this.provideSubscribe(_class, query, options, refresh) - const h = this.client.getHierarchy() - - const id = generateId() - const s: StatusSubscriber = { - _class, - query, - refresh, - options, - attributes: [] - } - const statusFields: Array> = [] - const allAttrs = h.getAllAttributes(_class) - - const updatedQuery: DocumentQuery = { ...(ret.query ?? query) } - const finalOptions = { ...(ret.options ?? options ?? {}) } - - await this.updateQueryOptions(allAttrs, h, statusFields, updatedQuery, finalOptions) - - if (statusFields.length > 0) { - this.subscribers.set(id, s) - return { - unsubscribe: () => { - ret.unsubscribe() - this.subscribers.delete(id) - }, - query: updatedQuery, - options: finalOptions - } - } - return { unsubscribe: (await ret).unsubscribe } - } - - static create (client: Client, next?: PresentationMiddleware): StatusMiddleware { - return new StatusMiddleware(client, next) - } - - async findAll( - _class: Ref>, - query: DocumentQuery, - options?: FindOptions | undefined - ): Promise> { - const statusFields: Array> = [] - const h = this.client.getHierarchy() - const allAttrs = h.getAllAttributes(_class) - const finalOptions = options ?? {} - - await this.updateQueryOptions(allAttrs, h, statusFields, query, finalOptions) - - const result = await this.provideFindAll(_class, query, finalOptions) - // We need to add $ - if (statusFields.length > 0) { - // We need to update $lookup for status fields and provide $status group fields. - for (const attr of statusFields) { - for (const r of result) { - const resultDoc = Hierarchy.toDoc(r) - if (resultDoc.$lookup === undefined) { - resultDoc.$lookup = {} - } - - // TODO: Check for mixin? - const stateValue = (r as any)[attr.name] - const status = (await this.getManager()).byId.get(stateValue) - if (status !== undefined) { - ;(resultDoc.$lookup as any)[attr.name] = status - } - } - } - } - return result - } - - private categorizeStatus (mgr: StatusManager, attr: AnyAttribute, target: Array>): Array> { - for (const sid of [...target]) { - const s = mgr.byId.get(sid) - if (s !== undefined) { - const statuses = mgr.statuses.filter( - (it) => - it.ofAttribute === attr._id && - it.name.toLowerCase().trim() === s.name.toLowerCase().trim() && - it._id !== s._id - ) - target.push(...statuses.map((it) => it._id)) - } - } - return target.filter((it, idx, arr) => arr.indexOf(it) === idx) - } - - private async updateQueryOptions( - allAttrs: Map, - h: Hierarchy, - statusFields: Array>, - query: DocumentQuery, - finalOptions: FindOptions - ): Promise { - for (const attr of allAttrs.values()) { - try { - if (attr.type._class === core.class.RefTo && h.isDerived((attr.type as RefTo).to, core.class.Status)) { - const mgr = await this.getManager() - let target: Array> = [] - let targetNin: Array> = [] - statusFields.push(attr) - const v = (query as any)[attr.name] - - if (v != null) { - // Only add filter if we have filer inside. - if (typeof v === 'string') { - target.push(v as Ref) - } else { - if (v.$in !== undefined) { - target.push(...v.$in) - } else if (v.$nin !== undefined) { - targetNin.push(...v.$nin) - } else if (v.$ne !== undefined) { - targetNin.push(v.$ne) - } - } - - // Find all similar name statues for same attribute name. - target = this.categorizeStatus(mgr, attr, target) - targetNin = this.categorizeStatus(mgr, attr, targetNin) - if (target.length > 0 || targetNin.length > 0) { - ;(query as any)[attr.name] = {} - if (target.length > 0) { - ;(query as any)[attr.name].$in = target - } - if (targetNin.length > 0) { - ;(query as any)[attr.name].$nin = targetNin - } - } - } - if (finalOptions.lookup !== undefined) { - // Remove lookups by status field - if ((finalOptions.lookup as any)[attr.name] !== undefined) { - const { [attr.name]: _, ...newLookup } = finalOptions.lookup as any - finalOptions.lookup = newLookup - } - } - - // Update sorting if defined. - this.updateCustomSorting(finalOptions, attr, mgr) - } - } catch (err: any) { - console.error(err) - } - } - } - - private updateCustomSorting( - finalOptions: FindOptions, - attr: AnyAttribute, - mgr: StatusManager - ): void { - const attrSort = finalOptions.sort?.[attr.name] - if (attrSort !== undefined && typeof attrSort !== 'object') { - // Fill custom sorting. - const statuses = mgr.statuses.filter((it) => it.ofAttribute === attr._id) - statuses.sort((a, b) => { - let ret = 0 - if (a.category !== undefined && b.category !== undefined) { - ret = (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0) - } - if (ret === 0) { - if (a.name.toLowerCase().trim() === b.name.toLowerCase().trim()) { - return 0 - } - ret = a.rank.localeCompare(b.rank) - } - return ret - }) - if (finalOptions.sort === undefined) { - finalOptions.sort = {} - } - - const rules: SortingRules = { - order: attrSort, - cases: statuses.map((it, idx) => ({ query: it._id, index: idx })), - default: statuses.length + 1 - } - ;(finalOptions.sort as any)[attr.name] = rules - } - } - - async tx (tx: Tx): Promise { - return await this.provideTx(tx) - } -} diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index 1bcfcddc62..c0d53468f8 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -45,8 +45,6 @@ import { onDestroy } from 'svelte' import { KeyedAttribute } from '..' import { PresentationPipeline, PresentationPipelineImpl } from './pipeline' import plugin from './plugin' -import { StatusMiddleware, statusStore } from './status' -export { statusStore } let liveQuery: LQ let client: TxOperations @@ -114,7 +112,10 @@ export async function setClient (_client: Client): Promise { if (pipeline !== undefined) { await pipeline.close() } - pipeline = PresentationPipelineImpl.create(_client, [StatusMiddleware.create]) + const factories = await _client.findAll(plugin.class.PresentationMiddlewareFactory, {}) + const promises = factories.map(async (it) => await getResource(it.createPresentationMiddleware)) + const creators = await Promise.all(promises) + pipeline = PresentationPipelineImpl.create(_client, creators) const needRefresh = liveQuery !== undefined liveQuery = new LQ(pipeline) diff --git a/plugins/task-resources/src/components/kanban/KanbanView.svelte b/plugins/task-resources/src/components/kanban/KanbanView.svelte index 715637d4a6..da49aca4b2 100644 --- a/plugins/task-resources/src/components/kanban/KanbanView.svelte +++ b/plugins/task-resources/src/components/kanban/KanbanView.svelte @@ -26,7 +26,7 @@ } from '@hcengineering/core' import { Item, Kanban as KanbanUI } from '@hcengineering/kanban' import { getResource } from '@hcengineering/platform' - import { createQuery, getClient, statusStore, ActionContext } from '@hcengineering/presentation' + import { createQuery, getClient, ActionContext } from '@hcengineering/presentation' import { Kanban, SpaceWithStates, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task' import { ColorDefinition, @@ -173,7 +173,7 @@ viewOptions: ViewOptions, viewOptionsModel: ViewOptionModel[] | undefined ) { - categories = await getCategories(client, _class, docs, groupByKey, $statusStore, viewlet.descriptor) + categories = await getCategories(client, _class, docs, groupByKey, viewlet.descriptor) for (const viewOption of viewOptionsModel ?? []) { if (viewOption.actionTarget !== 'category') continue const categoryFunc = viewOption as CategoryOption @@ -191,7 +191,6 @@ groupByKey, update, queryId, - $statusStore, viewlet.descriptor ) if (res !== undefined) { diff --git a/plugins/task-resources/src/components/state/DoneStateRefPresenter.svelte b/plugins/task-resources/src/components/state/DoneStateRefPresenter.svelte index 41db180dae..5ed3ee03fa 100644 --- a/plugins/task-resources/src/components/state/DoneStateRefPresenter.svelte +++ b/plugins/task-resources/src/components/state/DoneStateRefPresenter.svelte @@ -14,8 +14,8 @@ // limitations under the License. --> {#if value} - {@const state = $statusStore.get(typeof value === 'string' ? value : value.values[0]._id)} {#if onChange !== undefined && state !== undefined} {:else} diff --git a/plugins/task-resources/src/components/state/StatePresenter.svelte b/plugins/task-resources/src/components/state/StatePresenter.svelte index 696c027f38..46bf5b5441 100644 --- a/plugins/task-resources/src/components/state/StatePresenter.svelte +++ b/plugins/task-resources/src/components/state/StatePresenter.svelte @@ -27,8 +27,6 @@ export let value: State | undefined export let shouldShowAvatar = true export let inline: boolean = false - export let colorInherit: boolean = false - export let accent: boolean = false export let disabled: boolean = false export let oneLine: boolean = false diff --git a/plugins/task-resources/src/components/state/StateRefPresenter.svelte b/plugins/task-resources/src/components/state/StateRefPresenter.svelte index 80556b13ad..9b3d3f51a5 100644 --- a/plugins/task-resources/src/components/state/StateRefPresenter.svelte +++ b/plugins/task-resources/src/components/state/StateRefPresenter.svelte @@ -14,25 +14,23 @@ // limitations under the License. --> {#if value} - {@const state = $statusStore.get(typeof value === 'string' ? value : value.values?.[0]?._id)} {#if onChange !== undefined && state !== undefined} {:else} - + {/if} {/if} diff --git a/plugins/task-resources/src/components/state/StatesBar.svelte b/plugins/task-resources/src/components/state/StatesBar.svelte index e05641b669..c4b4f38405 100644 --- a/plugins/task-resources/src/components/state/StatesBar.svelte +++ b/plugins/task-resources/src/components/state/StatesBar.svelte @@ -15,9 +15,10 @@ -->
diff --git a/plugins/tracker-resources/src/components/components/ComponentPresenter.svelte b/plugins/tracker-resources/src/components/components/ComponentPresenter.svelte index 475e36e5e6..ab69f48c39 100644 --- a/plugins/tracker-resources/src/components/components/ComponentPresenter.svelte +++ b/plugins/tracker-resources/src/components/components/ComponentPresenter.svelte @@ -19,8 +19,9 @@ import tracker from '../../plugin' import view from '@hcengineering/view' import { DocNavLink } from '@hcengineering/view-resources' + import { translate } from '@hcengineering/platform' - export let value: WithLookup + export let value: WithLookup | undefined export let shouldShowAvatar = true export let onClick: (() => void) | undefined = undefined export let disabled = false @@ -28,24 +29,32 @@ export let accent: boolean = false export let noUnderline = false export let kind: 'list' | undefined = undefined + + let label: string + + $: if (value !== undefined) { + label = value.label + } else { + translate(tracker.string.NoComponent, {}) + .then((r) => { + label = r + }) + .catch((err) => { + console.error(err) + }) + } + $: disabled = disabled || value === undefined -{#if value} - - - {#if !inline && shouldShowAvatar} -
- -
- {/if} - - {value.label} - + + + {#if !inline && shouldShowAvatar} +
+ +
+ {/if} + + {label} -
-{/if} +
+
diff --git a/plugins/tracker-resources/src/components/components/ComponentRefPresenter.svelte b/plugins/tracker-resources/src/components/components/ComponentRefPresenter.svelte new file mode 100644 index 0000000000..8bc16402c6 --- /dev/null +++ b/plugins/tracker-resources/src/components/components/ComponentRefPresenter.svelte @@ -0,0 +1,30 @@ + + + + diff --git a/plugins/tracker-resources/src/components/issues/IssueStatusActivity.svelte b/plugins/tracker-resources/src/components/issues/IssueStatusActivity.svelte index 795e9a054e..9cb69f84c2 100644 --- a/plugins/tracker-resources/src/components/issues/IssueStatusActivity.svelte +++ b/plugins/tracker-resources/src/components/issues/IssueStatusActivity.svelte @@ -3,8 +3,8 @@ import { createQuery } from '@hcengineering/presentation' import { Issue, IssueStatus } from '@hcengineering/tracker' import { Label, ticker, Row } from '@hcengineering/ui' + import { statusStore } from '@hcengineering/view-resources' import tracker from '../../plugin' - import { statusStore } from '@hcengineering/presentation' import Duration from './Duration.svelte' import StatusPresenter from './StatusPresenter.svelte' @@ -84,7 +84,7 @@ displaySt = result } - $: updateStatus(txes, $statusStore.byId, $ticker) + $: updateStatus(txes, $statusStore.getIdMap(), $ticker) diff --git a/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte b/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte index f032c08f2b..8d8f5494c2 100644 --- a/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte +++ b/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte @@ -14,9 +14,10 @@ -->
diff --git a/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte b/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte index 02697a1c05..9ec8304f66 100644 --- a/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte +++ b/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte @@ -14,7 +14,7 @@ --> {#if value} - + {/if} diff --git a/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.svelte b/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.svelte index f22f4c7586..5742e5f94f 100644 --- a/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.svelte +++ b/plugins/tracker-resources/src/components/issues/edit/SubIssueSelector.svelte @@ -14,7 +14,7 @@ --> {#if parentIssue} diff --git a/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte b/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte index 192ebd206f..34a0617ad1 100644 --- a/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte +++ b/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte @@ -14,7 +14,7 @@ --> diff --git a/plugins/tracker-resources/src/components/issues/related/RelatedIssueSelector.svelte b/plugins/tracker-resources/src/components/issues/related/RelatedIssueSelector.svelte index 0c4aa572be..1db3480078 100644 --- a/plugins/tracker-resources/src/components/issues/related/RelatedIssueSelector.svelte +++ b/plugins/tracker-resources/src/components/issues/related/RelatedIssueSelector.svelte @@ -26,9 +26,9 @@ showPanel, showPopup } from '@hcengineering/ui' + import { statusStore } from '@hcengineering/view-resources' import tracker from '../../../plugin' import { subIssueListProvider } from '../../../utils' - import { statusStore } from '@hcengineering/presentation' import RelatedIssuePresenter from './RelatedIssuePresenter.svelte' export let object: WithLookup | undefined @@ -69,7 +69,8 @@ } $: if (subIssues) { - const doneStatuses = $statusStore.statuses + const doneStatuses = $statusStore + .getDocs() .filter((s) => s.category === tracker.issueStatusCategory.Completed) .map((p) => p._id) countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length diff --git a/plugins/tracker-resources/src/components/milestones/IssueStatistics.svelte b/plugins/tracker-resources/src/components/milestones/IssueStatistics.svelte index a35f4c3d01..fc727e7063 100644 --- a/plugins/tracker-resources/src/components/milestones/IssueStatistics.svelte +++ b/plugins/tracker-resources/src/components/milestones/IssueStatistics.svelte @@ -17,7 +17,7 @@ import { Issue } from '@hcengineering/tracker' import { floorFractionDigits, Label } from '@hcengineering/ui' import tracker from '../../plugin' - import { statusStore } from '@hcengineering/presentation' + import { statusStore } from '@hcengineering/view-resources' import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte' import TimePresenter from '../issues/timereport/TimePresenter.svelte' import { FixedColumn } from '@hcengineering/view-resources' @@ -30,13 +30,13 @@ $: noParents = docs?.filter((it) => !ids.has(it.attachedTo as Ref)) $: rootNoBacklogIssues = noParents?.filter( - (it) => $statusStore.byId.get(it.status)?.category !== tracker.issueStatusCategory.Backlog + (it) => $statusStore.getIdMap().get(it.status)?.category !== tracker.issueStatusCategory.Backlog ) $: totalEstimation = floorFractionDigits( (rootNoBacklogIssues ?? [{ estimation: 0, childInfo: [] } as unknown as Issue]) .map((it) => { - const cat = $statusStore.byId.get(it.status)?.category + const cat = $statusStore.getIdMap().get(it.status)?.category let retEst = it.estimation if (it.childInfo?.length > 0) { diff --git a/plugins/tracker-resources/src/components/workflow/RemoveStatus.svelte b/plugins/tracker-resources/src/components/workflow/RemoveStatus.svelte index 827d69269f..1794661dfa 100644 --- a/plugins/tracker-resources/src/components/workflow/RemoveStatus.svelte +++ b/plugins/tracker-resources/src/components/workflow/RemoveStatus.svelte @@ -2,11 +2,11 @@ import { Ref } from '@hcengineering/core' import { Issue, IssueStatus, Project } from '@hcengineering/tracker' import { Button, Label, SelectPopup, eventToHTMLElement, showPopup } from '@hcengineering/ui' - import presentation, { getClient, statusStore } from '@hcengineering/presentation' + import presentation, { getClient } from '@hcengineering/presentation' import tracker from '../../plugin' import { createEventDispatcher } from 'svelte' import IssueStatusIcon from '../issues/IssueStatusIcon.svelte' - import { StatusPresenter } from '@hcengineering/view-resources' + import { StatusPresenter, statusStore } from '@hcengineering/view-resources' export let projectId: Ref export let issues: Issue[] @@ -17,10 +17,10 @@ const client = getClient() let newStatus: IssueStatus = - $statusStore.statuses.find( - (s) => s._id !== status._id && s.category === status.category && s.space === projectId - ) ?? - $statusStore.statuses.find((s) => s._id !== status._id && s.space === projectId) ?? + $statusStore + .getDocs() + .find((s) => s._id !== status._id && s.category === status.category && s.space === projectId) ?? + $statusStore.getDocs().find((s) => s._id !== status._id && s.space === projectId) ?? status async function remove () { @@ -54,7 +54,7 @@ SelectPopup, { value: statusesInfo, placeholder: tracker.string.SetStatus, searchable: true }, eventToHTMLElement(event), - (val) => (newStatus = $statusStore.byId.get(val) ?? newStatus) + (val) => (newStatus = $statusStore.getIdMap().get(val) ?? newStatus) ) } diff --git a/plugins/tracker-resources/src/components/workflow/Statuses.svelte b/plugins/tracker-resources/src/components/workflow/Statuses.svelte index 0aec62e2a3..98640d333b 100644 --- a/plugins/tracker-resources/src/components/workflow/Statuses.svelte +++ b/plugins/tracker-resources/src/components/workflow/Statuses.svelte @@ -28,13 +28,13 @@ Scroller, showPopup } from '@hcengineering/ui' - import { statusStore } from '@hcengineering/presentation' import { createEventDispatcher } from 'svelte' import { flip } from 'svelte/animate' import tracker from '../../plugin' import StatusEditor from './StatusEditor.svelte' import StatusPresenter from './StatusPresenter.svelte' import RemoveStatus from './RemoveStatus.svelte' + import { statusStore } from '@hcengineering/view-resources' export let projectId: Ref export let projectClass: Ref> @@ -88,7 +88,7 @@ async function editStatus () { if (statusCategories && editingStatus?.name && editingStatus?.category && '_id' in editingStatus) { const statusId = '_id' in editingStatus ? editingStatus._id : undefined - const status = statusId && $statusStore.byId.get(statusId) + const status = statusId && $statusStore.getIdMap().get(statusId) if (!status) { return @@ -238,7 +238,7 @@ $: projectQuery.query(projectClass, { _id: projectId }, (result) => ([project] = result), { limit: 1 }) $: updateStatusCategories() - $: projectStatuses = $statusStore.statuses.filter((status) => status.space === projectId) + $: projectStatuses = $statusStore.getDocs().filter((status) => status.space === projectId) dispatch('close')}> diff --git a/plugins/tracker-resources/src/index.ts b/plugins/tracker-resources/src/index.ts index ca237f0821..51fcd4499d 100644 --- a/plugins/tracker-resources/src/index.ts +++ b/plugins/tracker-resources/src/index.ts @@ -29,6 +29,7 @@ import { Issue, Project, Scrum, ScrumRecord, Milestone } from '@hcengineering/tr import { showPopup } from '@hcengineering/ui' import ComponentEditor from './components/components/ComponentEditor.svelte' import ComponentPresenter from './components/components/ComponentPresenter.svelte' +import ComponentRefPresenter from './components/components/ComponentRefPresenter.svelte' import Components from './components/components/Components.svelte' import ComponentTitlePresenter from './components/components/ComponentTitlePresenter.svelte' import EditComponent from './components/components/EditComponent.svelte' @@ -132,6 +133,7 @@ import ProjectSpacePresenter from './components/projects/ProjectSpacePresenter.s import IssueStatistics from './components/milestones/IssueStatistics.svelte' import MilestoneRefPresenter from './components/milestones/MilestoneRefPresenter.svelte' import MilestoneFilter from './components/milestones/MilestoneFilter.svelte' +import { ComponentAggregationManager, grouppingComponentManager } from './component' export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte' @@ -385,6 +387,7 @@ export default async (): Promise => ({ Components, IssuePresenter, ComponentPresenter, + ComponentRefPresenter, ComponentTitlePresenter, TitlePresenter, ModificationDatePresenter, @@ -473,5 +476,9 @@ export default async (): Promise => ({ }, resolver: { Location: resolveLocation + }, + aggregation: { + CreateComponentAggregationManager: ComponentAggregationManager.create, + GrouppingComponentManager: grouppingComponentManager } }) diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index a5b5604940..95d30139d0 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -17,7 +17,15 @@ import type { Asset, IntlString, Metadata, Resource } from '@hcengineering/platf import { mergeIds } from '@hcengineering/platform' import { IssueDraft } from '@hcengineering/tracker' import { AnyComponent, Location } from '@hcengineering/ui' -import { GetAllValuesFunc, SortFunc, Viewlet, ViewletDescriptor, ViewQueryAction } from '@hcengineering/view' +import { + CreateAggregationManagerFunc, + GetAllValuesFunc, + GrouppingManagerResource, + SortFunc, + Viewlet, + ViewletDescriptor, + ViewQueryAction +} from '@hcengineering/view' import tracker, { trackerId } from '../../tracker/lib' export default mergeIds(trackerId, tracker, { @@ -314,6 +322,7 @@ export default mergeIds(trackerId, tracker, { IssuePresenter: '' as AnyComponent, ComponentTitlePresenter: '' as AnyComponent, ComponentPresenter: '' as AnyComponent, + ComponentRefPresenter: '' as AnyComponent, TitlePresenter: '' as AnyComponent, ModificationDatePresenter: '' as AnyComponent, PriorityPresenter: '' as AnyComponent, @@ -382,5 +391,9 @@ export default mergeIds(trackerId, tracker, { GetAllPriority: '' as GetAllValuesFunc, GetAllComponents: '' as GetAllValuesFunc, GetAllMilestones: '' as GetAllValuesFunc + }, + aggregation: { + CreateComponentAggregationManager: '' as CreateAggregationManagerFunc, + GrouppingComponentManager: '' as GrouppingManagerResource } }) diff --git a/plugins/tracker-resources/src/utils.ts b/plugins/tracker-resources/src/utils.ts index b8d7a83c92..5423f5fe1b 100644 --- a/plugins/tracker-resources/src/utils.ts +++ b/plugins/tracker-resources/src/utils.ts @@ -61,7 +61,7 @@ import { import { ViewletDescriptor } from '@hcengineering/view' import { CategoryQuery, groupBy, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources' import tracker from './plugin' -import { defaultMilestoneStatuses, defaultPriorities } from './types' +import { defaultPriorities, defaultMilestoneStatuses } from './types' export * from './types' @@ -296,7 +296,7 @@ export async function issueStatusSort ( listIssueKanbanStatusOrder.indexOf(a.values[0].category as Ref) - listIssueKanbanStatusOrder.indexOf(b.values[0].category as Ref) if (res === 0) { - return a.values[0].rank.localeCompare(b.values[0].rank) + return a.values[0].getRank().localeCompare(b.values[0].getRank()) } return res }) @@ -306,7 +306,7 @@ export async function issueStatusSort ( listIssueStatusOrder.indexOf(a.values[0].category as Ref) - listIssueStatusOrder.indexOf(b.values[0].category as Ref) if (res === 0) { - return a.values[0].rank.localeCompare(b.values[0].rank) + return a.values[0].getRank().localeCompare(b.values[0].getRank()) } return res }) diff --git a/plugins/tracker/src/index.ts b/plugins/tracker/src/index.ts index 7f1c22dd11..09525e6015 100644 --- a/plugins/tracker/src/index.ts +++ b/plugins/tracker/src/index.ts @@ -14,11 +14,13 @@ // import { Employee, EmployeeAccount } from '@hcengineering/contact' -import type { +import { AttachedDoc, Attribute, Class, Doc, + DocManager, + IdMap, Markup, Ref, RelatedDocument, @@ -26,7 +28,8 @@ import type { Status, StatusCategory, Timestamp, - Type + Type, + WithLookup } from '@hcengineering/core' import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' @@ -310,6 +313,29 @@ 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/package.json b/plugins/view-resources/package.json index 20b304d18e..52ec7a81a9 100644 --- a/plugins/view-resources/package.json +++ b/plugins/view-resources/package.json @@ -46,6 +46,7 @@ "@hcengineering/presentation": "^0.6.2", "@hcengineering/setting": "^0.6.7", "@hcengineering/text-editor": "^0.6.0", + "@hcengineering/query": "^0.6.5", "fast-equals": "^2.0.3" } } diff --git a/plugins/view-resources/src/actions.ts b/plugins/view-resources/src/actions.ts index 6bbadd9424..81190d4a29 100644 --- a/plugins/view-resources/src/actions.ts +++ b/plugins/view-resources/src/actions.ts @@ -25,7 +25,7 @@ import core, { Ref } from '@hcengineering/core' import { getResource } from '@hcengineering/platform' -import type { Action, ViewAction, ViewActionInput, ViewContextType } from '@hcengineering/view' +import { Action, ViewAction, ViewActionInput, ViewContextType } from '@hcengineering/view' import view from './plugin' import { FocusSelection } from './selection' diff --git a/plugins/view-resources/src/components/DocNavLink.svelte b/plugins/view-resources/src/components/DocNavLink.svelte index bbdcc316cb..6e490c6cf4 100644 --- a/plugins/view-resources/src/components/DocNavLink.svelte +++ b/plugins/view-resources/src/components/DocNavLink.svelte @@ -19,7 +19,7 @@ import view from '../plugin' import { getObjectLinkFragment } from '../utils' - export let object: Doc + export let object: Doc | undefined export let disabled = false export let onClick: ((event: MouseEvent) => void) | undefined = undefined export let noUnderline = false diff --git a/plugins/view-resources/src/components/filter/ObjectFilter.svelte b/plugins/view-resources/src/components/filter/ObjectFilter.svelte index 8f45bf4427..b15d07c138 100644 --- a/plugins/view-resources/src/components/filter/ObjectFilter.svelte +++ b/plugins/view-resources/src/components/filter/ObjectFilter.svelte @@ -13,9 +13,9 @@ // limitations under the License. --> {#if value} - + {/if} diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 8adbd1033d..e8ff0a8f65 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -104,6 +104,8 @@ import { import { IndexedDocumentPreview } from '@hcengineering/presentation' import { statusSort } from './utils' import { showEmptyGroups } from './viewOptions' +import { AggregationMiddleware } from './middleware' +import { grouppingStatusManager, StatusAggregationManager } from './status' export { getActions, invokeAction } from './actions' export { default as ActionHandler } from './components/ActionHandler.svelte' export { default as AddSavedView } from './components/filter/AddSavedView.svelte' @@ -129,6 +131,8 @@ export { default as ParentsNavigator } from './components/ParentsNavigator.svelt export * from './filter' export * from './selection' export * from './utils' +export * from './status' +export * from './middleware' export { buildModel, getActiveViewletId, @@ -257,6 +261,11 @@ export default async (): Promise => ({ FilterDateMonth: dateMonth, FilterDateNextMonth: dateNextMonth, FilterDateNotSpecified: dateNotSpecified, - FilterDateCustom: dateCustom + FilterDateCustom: dateCustom, + CreateDocMiddleware: AggregationMiddleware.create + }, + aggregation: { + CreateStatusAggregationManager: StatusAggregationManager.create, + GrouppingStatusManager: grouppingStatusManager } }) diff --git a/plugins/view-resources/src/middleware.ts b/plugins/view-resources/src/middleware.ts new file mode 100644 index 0000000000..09b94c40a5 --- /dev/null +++ b/plugins/view-resources/src/middleware.ts @@ -0,0 +1,234 @@ +import core, { + Doc, + Ref, + AnyAttribute, + Class, + DocumentQuery, + FindOptions, + Client, + Tx, + TxResult, + FindResult, + Attribute, + Hierarchy, + RefTo, + generateId +} from '@hcengineering/core' +import { BasePresentationMiddleware, PresentationMiddleware } from '@hcengineering/presentation' +import view, { AggregationManager } from '@hcengineering/view' +import { getResource } from '@hcengineering/platform' + +/** + * @public + */ +export interface DocSubScriber { + attributes: Array> + + _class: Ref> + query: DocumentQuery + options?: FindOptions + + refresh: () => void +} + +/** + * @public + */ +export class AggregationMiddleware extends BasePresentationMiddleware implements PresentationMiddleware { + mgrs: Map>, AggregationManager> = new Map>, AggregationManager>() + docs: Doc[] | undefined + + subscribers: Map = new Map() + private constructor (client: Client, next?: PresentationMiddleware) { + super(client, next) + } + + static create (client: Client, next?: PresentationMiddleware): AggregationMiddleware { + return new AggregationMiddleware(client, next) + } + + async notifyTx (tx: Tx): Promise { + const promises: Array> = [] + for (const [, value] of this.mgrs) { + promises.push(value.notifyTx(tx)) + } + await Promise.all(promises) + await this.provideNotifyTx(tx) + } + + async close (): Promise { + this.mgrs.forEach((mgr) => mgr.close()) + return await this.provideClose() + } + + async tx (tx: Tx): Promise { + return await this.provideTx(tx) + } + + private refreshSubscribers (): void { + for (const s of this.subscribers.values()) { + // TODO: Do something more smart and track if used component field is changed. + s.refresh() + } + } + + async subscribe( + _class: Ref>, + query: DocumentQuery, + options: FindOptions | undefined, + refresh: () => void + ): Promise<{ + unsubscribe: () => void + query?: DocumentQuery + options?: FindOptions + }> { + const ret = await this.provideSubscribe(_class, query, options, refresh) + const h = this.client.getHierarchy() + + const id = generateId() + const s: DocSubScriber = { + _class, + query, + refresh, + options, + attributes: [] + } + const statusFields: Array> = [] + const allAttrs = h.getAllAttributes(_class) + + const updatedQuery: DocumentQuery = { ...(ret.query ?? query) } + const finalOptions = { ...(ret.options ?? options ?? {}) } + + await this.updateQueryOptions(allAttrs, h, statusFields, updatedQuery, finalOptions) + + if (statusFields.length > 0) { + this.subscribers.set(id, s) + return { + unsubscribe: () => { + ret.unsubscribe() + this.subscribers.delete(id) + }, + query: updatedQuery, + options: finalOptions + } + } + return { unsubscribe: (await ret).unsubscribe } + } + + private async getAggregationManager (_class: Ref>): Promise { + 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) { + const f = await getResource(mixin.createAggregationManager) + mgr = f(this.client, this.refreshSubscribers) + this.mgrs.set(_class, mgr) + } + } + + return mgr + } + + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions | undefined + ): Promise> { + const docFields: Array> = [] + const h = this.client.getHierarchy() + const allAttrs = h.getAllAttributes(_class) + const finalOptions = options ?? {} + + await this.updateQueryOptions(allAttrs, h, docFields, query, finalOptions) + + const result = await this.provideFindAll(_class, query, finalOptions) + // We need to add $ + if (docFields.length > 0) { + // We need to update $lookup for doc fields and provide $doc group fields. + for (const attr of docFields) { + for (const r of result) { + const resultDoc = Hierarchy.toDoc(r) + if (resultDoc.$lookup === undefined) { + resultDoc.$lookup = {} + } + + const mgr = await this.getAggregationManager((attr.type as RefTo).to) + if (mgr !== undefined) { + await mgr.updateLookup(resultDoc, attr) + } + } + } + } + return result + } + + private async updateQueryOptions( + allAttrs: Map, + h: Hierarchy, + docFields: Array>, + query: DocumentQuery, + finalOptions: FindOptions + ): Promise { + for (const attr of allAttrs.values()) { + try { + if (attr.type._class !== core.class.RefTo) { + continue + } + const mgr = await this.getAggregationManager((attr.type as RefTo).to) + if (mgr === undefined) { + continue + } + if (h.isDerived((attr.type as RefTo).to, mgr.getAttrClass())) { + let target: Array> = [] + let targetNin: Array> = [] + docFields.push(attr) + const v = (query as any)[attr.name] + + if (v != null) { + // Only add filter if we have filer inside. + if (typeof v === 'string') { + target.push(v as Ref) + } else { + if (v.$in !== undefined) { + target.push(...v.$in) + } else if (v.$nin !== undefined) { + targetNin.push(...v.$nin) + } else if (v.$ne !== undefined) { + targetNin.push(v.$ne) + } + } + + // Find all similar name statues for same attribute name. + target = await mgr.categorize(target, attr) + targetNin = await mgr.categorize(targetNin, attr) + if (target.length > 0 || targetNin.length > 0) { + ;(query as any)[attr.name] = {} + if (target.length > 0) { + ;(query as any)[attr.name].$in = target + } + if (targetNin.length > 0) { + ;(query as any)[attr.name].$nin = targetNin + } + } + } + if (finalOptions.lookup !== undefined) { + // Remove lookups by status field + if ((finalOptions.lookup as any)[attr.name] !== undefined) { + const { [attr.name]: _, ...newLookup } = finalOptions.lookup as any + finalOptions.lookup = newLookup + } + } + + // Update sorting if defined. + if (mgr.updateSorting !== undefined) { + await mgr.updateSorting(finalOptions, attr) + } + } + } catch (err: any) { + console.error(err) + } + } + } +} diff --git a/plugins/view-resources/src/plugin.ts b/plugins/view-resources/src/plugin.ts index 63170e74d6..4a3a9d8185 100644 --- a/plugins/view-resources/src/plugin.ts +++ b/plugins/view-resources/src/plugin.ts @@ -14,9 +14,10 @@ // limitations under the License. // -import { IntlString, mergeIds } from '@hcengineering/platform' +import { IntlString, Resource, mergeIds } from '@hcengineering/platform' +import { PresentationMiddlewareCreator } from '@hcengineering/presentation' import { AnyComponent } from '@hcengineering/ui' -import view, { SortFunc, viewId } from '@hcengineering/view' +import view, { CreateAggregationManagerFunc, GrouppingManagerResource, SortFunc, viewId } from '@hcengineering/view' export default mergeIds(viewId, view, { component: { @@ -94,6 +95,11 @@ export default mergeIds(viewId, view, { Show: '' as IntlString }, function: { - StatusSort: '' as SortFunc + StatusSort: '' as SortFunc, + CreateDocMiddleware: '' as Resource + }, + aggregation: { + CreateStatusAggregationManager: '' as CreateAggregationManagerFunc, + GrouppingStatusManager: '' as GrouppingManagerResource } }) diff --git a/plugins/view-resources/src/status.ts b/plugins/view-resources/src/status.ts new file mode 100644 index 0000000000..5c7b6a82c8 --- /dev/null +++ b/plugins/view-resources/src/status.ts @@ -0,0 +1,283 @@ +// +// Copyright © 2023 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { + AggregateValue, + AggregateValueData, + AnyAttribute, + Attribute, + Class, + Client, + Doc, + DocumentQuery, + FindOptions, + Hierarchy, + Ref, + SortingOrder, + SortingRules, + Space, + Status, + StatusManager, + StatusValue, + Tx, + WithLookup, + matchQuery +} from '@hcengineering/core' +import { LiveQuery } from '@hcengineering/query' +import { AggregationManager, GrouppingManager } from '@hcengineering/view' +import { get, writable } from 'svelte/store' + +// Issue status live query +export const statusStore = writable(new StatusManager([])) + +/** + * @public + */ +export class StatusAggregationManager implements AggregationManager { + docs: Doc[] | undefined + mgr: StatusManager | 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): StatusAggregationManager { + return new StatusAggregationManager(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( + core.class.Status, + {}, + (res) => { + const first = this.docs === undefined + this.docs = res + this.mgr = new StatusManager(res) + statusStore.set(this.mgr) + if (!first) { + this.lqCallback() + } + resolve(this.mgr) + }, + { + lookup: { + category: core.class.StatusCategory + }, + sort: { + rank: SortingOrder.Ascending + } + } + ) + }) + + return await this.mgr + } + + close (): void { + this.query?.() + } + + async notifyTx (tx: Tx): Promise { + await this.lq.tx(tx) + } + + getAttrClass (): Ref> { + return core.class.Status + } + + async categorize (target: Array>, attr: AnyAttribute): Promise>> { + const mgr = await this.getManager() + for (const sid of [...target]) { + const s = mgr.getIdMap().get(sid as Ref) as WithLookup + if (s !== undefined) { + let statuses = mgr.getDocs() + statuses = statuses.filter( + (it) => + it.ofAttribute === attr._id && + it.name.toLowerCase().trim() === s.name.toLowerCase().trim() && + it._id !== s._id + ) + target.push(...statuses.map((it) => it._id)) + } + } + return target.filter((it, idx, arr) => arr.indexOf(it) === idx) + } + + async updateLookup (resultDoc: WithLookup, attr: Attribute): Promise { + const value = (resultDoc as any)[attr.name] + const doc = (await this.getManager()).getIdMap().get(value) + if (doc !== undefined) { + ;(resultDoc.$lookup as any)[attr.name] = doc + } + } + + async updateSorting(finalOptions: FindOptions, attr: AnyAttribute): Promise { + const attrSort = finalOptions.sort?.[attr.name] + if (attrSort !== undefined && typeof attrSort !== 'object') { + // Fill custom sorting. + let statuses = (await this.getManager()).getDocs() + statuses = statuses.filter((it) => it.ofAttribute === attr._id) + statuses.sort((a, b) => { + let ret = 0 + if (a.category !== undefined && b.category !== undefined) { + ret = (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0) + } + if (ret === 0) { + if (a.name.toLowerCase().trim() === b.name.toLowerCase().trim()) { + return 0 + } + ret = a.rank.localeCompare(b.rank) + } + return ret + }) + if (finalOptions.sort === undefined) { + finalOptions.sort = {} + } + + const rules: SortingRules = { + order: attrSort, + cases: statuses.map((it, idx) => ({ query: it._id, index: idx })), + default: statuses.length + 1 + } + ;(finalOptions.sort as any)[attr.name] = rules + } + } +} + +/** + * @public + */ +export const grouppingStatusManager: GrouppingManager = { + groupByCategories: groupByStatusCategories, + groupValues: groupStatusValues, + groupValuesWithEmpty: groupStatusValuesWithEmpty, + hasValue: hasStatusValue +} + +/** + * @public + */ +export function groupByStatusCategories (categories: any[]): AggregateValue[] { + const mgr = get(statusStore) + + const existingCategories: AggregateValue[] = [] + const statusMap = new Map() + + const usedSpaces = new Set>() + const statusesList: Array> = [] + for (const v of categories) { + const status = mgr.getIdMap().get(v) + if (status !== undefined) { + statusesList.push(status) + usedSpaces.add(status.space) + } + } + + for (const status of statusesList) { + if (status !== undefined) { + let fst = statusMap.get(status.name.toLowerCase().trim()) + if (fst === undefined) { + const statuses = mgr + .getDocs() + .filter( + (it) => + it.ofAttribute === status.ofAttribute && + it.name.toLowerCase().trim() === status.name.toLowerCase().trim() && + (categories.includes(it._id) || usedSpaces.has(it.space)) + ) + .sort((a, b) => a.rank.localeCompare(b.rank)) + .map((it) => new AggregateValueData(it.name, it._id, it.space, it.rank, it.category)) + fst = new StatusValue(status.name, status.color, statuses) + statusMap.set(status.name.toLowerCase().trim(), fst) + existingCategories.push(fst) + } + } + } + return existingCategories +} + +/** + * @public + */ +export function groupStatusValues (val: Doc[], targets: Set): Doc[] { + const values = val + const result: Doc[] = [] + const unique = [...new Set(val.map((v) => (v as Status).name.trim().toLocaleLowerCase()))] + unique.forEach((label, i) => { + let exists = false + values.forEach((value) => { + if ((value as Status).name.trim().toLocaleLowerCase() === label) { + if (!exists) { + result[i] = value + exists = targets.has(value?._id) + } + } + }) + }) + return result +} + +/** + * @public + */ +export function hasStatusValue (value: Doc | undefined | null, values: any[]): boolean { + const mgr = get(statusStore) + const statusSet = new Set( + mgr + .filter((it) => it.name.trim().toLocaleLowerCase() === (value as Status)?.name?.trim()?.toLocaleLowerCase()) + .map((it) => it._id) + ) + return values.some((it) => statusSet.has(it)) +} + +/** + * @public + */ +export function groupStatusValuesWithEmpty ( + hierarchy: Hierarchy, + _class: Ref>, + key: string, + query: DocumentQuery | undefined +): Array> { + const mgr = get(statusStore) + const attr = hierarchy.getAttribute(_class, key) + // We do not need extensions for all status categories. + let statusList = mgr.filter((it) => { + return it.ofAttribute === attr._id + }) + if (query !== undefined) { + const { [key]: st, space } = query + const resQuery: DocumentQuery = {} + if (space !== undefined) { + resQuery.space = space + } + if (st !== undefined) { + resQuery._id = st + } + statusList = matchQuery(statusList, resQuery, _class, hierarchy) as unknown as Array> + } + return statusList.map((it) => it._id) +} diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index 6235627f1c..64fff00e81 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -17,6 +17,7 @@ import core, { AccountRole, AttachedDoc, + AggregateValue, CategoryType, Class, Client, @@ -34,10 +35,7 @@ import core, { ReverseLookups, Space, Status, - StatusManager, - StatusValue, - TxOperations, - WithLookup + TxOperations } from '@hcengineering/core' import type { IntlString } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform' @@ -59,6 +57,7 @@ import { } from '@hcengineering/ui' import type { BuildModelOptions, Viewlet, ViewletDescriptor } from '@hcengineering/view' import view, { AttributeModel, BuildModelKey } from '@hcengineering/view' + import { get, writable } from 'svelte/store' import plugin from './plugin' import { noCategory } from './viewOptions' @@ -597,7 +596,7 @@ export function groupBy (docs: T[], key: string, categories?: Cat */ export function getGroupByValues (groupByDocs: Record, category: CategoryType): T[] { if (typeof category === 'object') { - return groupByDocs[category.name] ?? [] + return groupByDocs[category.name as any] ?? [] } else { return groupByDocs[category as any] ?? [] } @@ -612,7 +611,7 @@ export function setGroupByValues ( docs: Doc[] ): void { if (typeof category === 'object') { - groupByDocs[category.name] = docs + groupByDocs[category.name as any] = docs } else if (category !== undefined) { groupByDocs[category] = docs } @@ -626,8 +625,7 @@ export async function groupByCategory ( client: TxOperations, _class: Ref>, key: string, - categories: any[], - mgr: StatusManager, + categories: CategoryType[], viewletDescriptorId?: Ref ): Promise { const h = client.getHierarchy() @@ -636,13 +634,12 @@ export async function groupByCategory ( if (key === noCategory) return [undefined] const attrClass = getAttributePresenterClass(h, attr).attrClass - - const isStatusField = h.isDerived(attrClass, core.class.Status) - + const mixin = h.classHierarchyMixin(attrClass, view.mixin.Groupping) let existingCategories: any[] = [] - if (isStatusField) { - existingCategories = await groupByStatusCategories(h, attrClass, categories, mgr, viewletDescriptorId) + if (mixin?.grouppingManager !== undefined) { + const grouppingManager = await getResource(mixin.grouppingManager) + existingCategories = grouppingManager.groupByCategories(categories) } else { const valueSet = new Set() for (const v of categories) { @@ -655,56 +652,11 @@ export async function groupByCategory ( return await sortCategories(h, attrClass, existingCategories, viewletDescriptorId) } -/** - * @public - */ -export async function groupByStatusCategories ( - hierarchy: Hierarchy, - attrClass: Ref>, - categories: any[], - mgr: StatusManager, - viewletDescriptorId?: Ref -): Promise { - const existingCategories: StatusValue[] = [] - const statusMap = new Map() - - const usedSpaces = new Set>() - const statusesList: Array> = [] - for (const v of categories) { - const status = mgr.byId.get(v) - if (status !== undefined) { - statusesList.push(status) - usedSpaces.add(status.space) - } - } - - for (const status of statusesList) { - if (status !== undefined) { - let fst = statusMap.get(status.name.toLowerCase().trim()) - if (fst === undefined) { - const statuses = mgr.statuses - .filter( - (it) => - it.ofAttribute === status.ofAttribute && - it.name.toLowerCase().trim() === status.name.toLowerCase().trim() && - (categories.includes(it._id) || usedSpaces.has(it.space)) - ) - .sort((a, b) => a.rank.localeCompare(b.rank)) - fst = new StatusValue(status.name, status.color, statuses) - statusMap.set(status.name.toLowerCase().trim(), fst) - existingCategories.push(fst) - } - } - } - return await sortCategories(hierarchy, attrClass, existingCategories, viewletDescriptorId) -} - export async function getCategories ( client: TxOperations, _class: Ref>, docs: Doc[], key: string, - mgr: StatusManager, viewletDescriptorId?: Ref ): Promise { if (key === noCategory) return [undefined] @@ -714,7 +666,6 @@ export async function getCategories ( _class, key, docs.map((it) => getObjectValue(key, it) ?? undefined), - mgr, viewletDescriptorId ) } @@ -724,7 +675,7 @@ export async function getCategories ( */ export function getCategorySpaces (categories: CategoryType[]): Array> { return Array.from( - (categories.filter((it) => typeof it === 'object') as StatusValue[]).reduce>>((arr, val) => { + (categories.filter((it) => typeof it === 'object') as AggregateValue[]).reduce>>((arr, val) => { val.values.forEach((it) => arr.add(it.space)) return arr }, new Set()) @@ -733,12 +684,12 @@ export function getCategorySpaces (categories: CategoryType[]): Array export function concatCategories (arr1: CategoryType[], arr2: CategoryType[]): CategoryType[] { const uniqueValues: Set = new Set() - const uniqueObjects: Map = new Map() + const uniqueObjects: Map = new Map() for (const item of arr1) { if (typeof item === 'object') { const id = item.name - uniqueObjects.set(id, item) + uniqueObjects.set(id as any, item) } else { uniqueValues.add(item) } @@ -747,8 +698,8 @@ export function concatCategories (arr1: CategoryType[], arr2: CategoryType[]): C for (const item of arr2) { if (typeof item === 'object') { const id = item.name - if (!uniqueObjects.has(id)) { - uniqueObjects.set(id, item) + if (!uniqueObjects.has(id as any)) { + uniqueObjects.set(id as any, item) } } else { uniqueValues.add(item) diff --git a/plugins/view-resources/src/viewOptions.ts b/plugins/view-resources/src/viewOptions.ts index 9c59363b2b..bcb30ec541 100644 --- a/plugins/view-resources/src/viewOptions.ts +++ b/plugins/view-resources/src/viewOptions.ts @@ -1,19 +1,10 @@ -import core, { - Class, - Doc, - DocumentQuery, - Ref, - SortingOrder, - Status, - StatusManager, - WithLookup, - matchQuery -} from '@hcengineering/core' +import { Class, Doc, DocumentQuery, Ref, SortingOrder } from '@hcengineering/core' import { getResource } from '@hcengineering/platform' import { LiveQuery, createQuery, getAttributePresenterClass, getClient } from '@hcengineering/presentation' import { locationToUrl, getCurrentResolvedLocation } from '@hcengineering/ui' import { DropdownViewOption, + Groupping, ToggleViewOption, ViewOptionModel, ViewOptions, @@ -118,7 +109,6 @@ export async function showEmptyGroups ( key: string, onUpdate: () => void, queryId: Ref, - mgr: StatusManager, viewletDescriptorId?: Ref ): Promise { const client = getClient() @@ -129,32 +119,27 @@ export async function showEmptyGroups ( const { attrClass } = getAttributePresenterClass(hierarchy, attr) const attributeClass = hierarchy.getClass(attrClass) - if (hierarchy.isDerived(attrClass, core.class.Status)) { - // We do not need extensions for all status categories. - let statusList = mgr.filter((it) => { - return it.ofAttribute === attr._id - }) - if (query !== undefined) { - const { [key]: st, space } = query - const resQuery: DocumentQuery = {} - if (space !== undefined) { - resQuery.space = space - } - if (st !== undefined) { - resQuery._id = st - } - statusList = matchQuery(statusList, resQuery, _class, hierarchy) as unknown as Array> + let groupMixin: Groupping | undefined + if (hierarchy.hasMixin(attributeClass, view.mixin.Groupping)) { + groupMixin = hierarchy.as(attributeClass, view.mixin.Groupping) + } else { + const _attributeClass = hierarchy.classHierarchyMixin(attrClass, view.mixin.Groupping) + if (_attributeClass !== undefined) { + groupMixin = hierarchy.as(_attributeClass, view.mixin.Groupping) } - const statuses = statusList.map((it) => it._id) - return await groupByCategory(client, _class, key, statuses, mgr, viewletDescriptorId) + } + if (groupMixin?.grouppingManager !== undefined) { + const grouppingManager = await getResource(groupMixin.grouppingManager) + const docs = grouppingManager.groupValuesWithEmpty(hierarchy, _class, key, query) + return await groupByCategory(client, _class, key, docs, viewletDescriptorId) } - const mixin = hierarchy.as(attributeClass, view.mixin.AllValuesFunc) - if (mixin.func !== undefined) { - const f = await getResource(mixin.func) + const allValuesMixin = hierarchy.as(attributeClass, view.mixin.AllValuesFunc) + if (allValuesMixin.func !== undefined) { + const f = await getResource(allValuesMixin.func) const res = await f(query, onUpdate, queryId) if (res !== undefined) { - return await groupByCategory(client, _class, key, res, mgr, viewletDescriptorId) + return await groupByCategory(client, _class, key, res, viewletDescriptorId) } } } diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 67bb297629..1e0d7e9c54 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -14,15 +14,18 @@ // limitations under the License. // -import type { +import { Account, + AggregateValue, AnyAttribute, + Attribute, CategoryType, Class, Client, Doc, DocumentQuery, FindOptions, + Hierarchy, Lookup, Mixin, Obj, @@ -31,14 +34,15 @@ import type { Ref, SortingOrder, Space, - StatusManager, StatusValue, + Tx, Type, - UXObject + UXObject, + WithLookup } from '@hcengineering/core' import { Asset, IntlString, Plugin, Resource, Status, plugin } from '@hcengineering/platform' -import type { Preference } from '@hcengineering/preference' -import type { +import { Preference } from '@hcengineering/preference' +import { AnyComponent, AnySvelteComponent, Location, @@ -308,6 +312,62 @@ export interface AllValuesFunc extends Class { func: GetAllValuesFunc } +/** + * @public + */ +export interface GrouppingManager { + groupByCategories: (categories: any[]) => AggregateValue[] + groupValues: (val: Doc[], targets: Set) => Doc[] + groupValuesWithEmpty: ( + hierarchy: Hierarchy, + _class: Ref>, + key: string, + query: DocumentQuery | undefined + ) => Array> + hasValue: (value: Doc | undefined | null, values: any[]) => boolean +} + +/** + * @public + */ +export type GrouppingManagerResource = Resource + +/** + * @public + */ +export interface Groupping extends Class { + grouppingManager: GrouppingManagerResource +} + +/** + * @public + */ +export interface AggregationManager { + close: () => void + notifyTx: (tx: Tx) => Promise + updateLookup: (resultDoc: WithLookup, attr: Attribute) => Promise + categorize: (target: Array>, attr: AnyAttribute) => Promise>> + getAttrClass: () => Ref> + updateSorting?: (finalOptions: FindOptions, attr: AnyAttribute) => Promise +} + +/** + * @public + */ +export type AggregationManagerResource = Resource + +/** + * @public + */ +export type CreateAggregationManagerFunc = Resource<(client: Client, lqCallback: () => void) => AggregationManager> + +/** + * @public + */ +export interface Aggregation extends Class { + createAggregationManager: CreateAggregationManagerFunc +} + /** * @public */ @@ -578,7 +638,6 @@ export type ViewCategoryActionFunc = ( key: string, onUpdate: () => void, queryId: Ref, - mgr: StatusManager, viewletDescriptorId?: Ref ) => Promise /** @@ -690,7 +749,9 @@ const view = plugin(viewId, { ObjectPanel: '' as Ref>, LinkProvider: '' as Ref>, SpacePresenter: '' as Ref>, - AttributeFilterPresenter: '' as Ref> + AttributeFilterPresenter: '' as Ref>, + Aggregation: '' as Ref>, + Groupping: '' as Ref> }, class: { ViewletPreference: '' as Ref>,