From 585d82320e0a84f2296e75f8bda23e6ed1bc4598 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Tue, 4 Apr 2023 13:11:49 +0700 Subject: [PATCH] TSK-943:General Status support (#2842) Signed-off-by: Andrey Sobolev --- dev/generator/src/kanban.ts | 12 +- models/board/src/index.ts | 3 +- models/board/src/migration.ts | 98 +---- models/core/src/index.ts | 6 +- models/core/src/status.ts | 57 +++ models/lead/src/index.ts | 42 ++- models/lead/src/migration.ts | 16 +- models/recruit/src/index.ts | 37 +- models/recruit/src/migration.ts | 12 +- models/server-task/src/index.ts | 15 +- models/task/src/index.ts | 140 ++----- models/task/src/migration.ts | 90 +++-- models/task/src/plugin.ts | 1 - models/tracker/src/index.ts | 84 ++--- models/tracker/src/migration.ts | 56 +-- models/view/src/index.ts | 19 +- models/view/src/plugin.ts | 4 +- packages/core/src/component.ts | 26 +- packages/core/src/index.ts | 1 + packages/core/src/lang/en.json | 4 +- packages/core/src/lang/ru.json | 4 +- packages/core/src/query.ts | 40 +- packages/core/src/status.ts | 91 +++++ packages/core/src/storage.ts | 30 +- packages/core/src/tx.ts | 26 +- packages/kanban/src/components/Kanban.svelte | 206 ++++++----- .../kanban/src/components/KanbanRow.svelte | 32 +- packages/kanban/src/types.ts | 2 +- packages/model/src/dsl.ts | 5 +- packages/presentation/src/pipeline.ts | 234 ++++++++++++ packages/presentation/src/status.ts | 299 +++++++++++++++ packages/presentation/src/utils.ts | 65 +++- packages/query/src/index.ts | 97 +++-- .../src/components/KanbanView.svelte | 70 +++- .../src/components/ListHeader.svelte | 28 +- plugins/client-resources/src/connection.ts | 9 +- .../src/components/ScheduleView.svelte | 4 +- .../src/components/CreateApplication.svelte | 17 +- .../src/components/MoveApplication.svelte | 3 +- .../src/components/statuses/Templates.svelte | 10 +- .../components/typeEditors/RefEditor.svelte | 5 +- plugins/task-assets/lang/en.json | 1 - plugins/task-assets/lang/ru.json | 1 - .../src/components/CreateProject.svelte | 75 ---- .../src/components/Dashboard.svelte | 2 +- .../src/components/EditIssue.svelte | 78 ---- .../src/components/KanbanCard.svelte | 78 ---- .../src/components/StatusTableView.svelte | 2 +- .../src/components/TaskItem.svelte | 48 --- .../src/components/TaskPresenter.svelte | 4 +- .../components/kanban/KanbanDragDone.svelte | 4 +- .../src/components/kanban/KanbanEditor.svelte | 6 +- .../kanban/KanbanTemplateEditor.svelte | 6 +- .../src/components/kanban/KanbanView.svelte | 285 +++++++++++--- .../state/DoneStatePresenter.svelte | 2 +- .../state/DoneStateRefPresenter.svelte | 14 +- .../components/state/DoneStatesPopup.svelte | 2 +- .../components/state/StatePresenter.svelte | 11 +- .../components/state/StateRefPresenter.svelte | 19 +- .../src/components/state/StatesBar.svelte | 28 +- .../src/components/state/StatesEditor.svelte | 18 +- .../src/components/state/StatesPopup.svelte | 9 +- plugins/task-resources/src/index.ts | 6 - plugins/task-resources/src/plugin.ts | 1 - plugins/task/src/index.ts | 92 +++-- plugins/tracker-assets/lang/en.json | 4 +- plugins/tracker-assets/lang/ru.json | 2 + .../SetParentIssueActionPopup.svelte | 4 +- .../src/components/icons/StatusIcon.svelte | 4 +- .../issues/IssueStatusActivity.svelte | 2 +- .../components/issues/IssueStatusIcon.svelte | 16 +- .../src/components/issues/KanbanView.svelte | 350 ++++++++---------- .../src/components/issues/StatusEditor.svelte | 4 +- .../components/issues/StatusPresenter.svelte | 6 +- .../issues/StatusRefPresenter.svelte | 9 +- .../issues/edit/SubIssueSelector.svelte | 4 +- .../issues/edit/SubIssuesSelector.svelte | 3 +- .../related/RelatedIssueSelector.svelte | 3 +- .../components/projects/ChangeIdentity.svelte | 41 ++ .../components/projects/CreateProject.svelte | 63 ++-- .../components/sprints/IssueStatistics.svelte | 2 +- .../src/components/workflow/Statuses.svelte | 42 +-- plugins/tracker-resources/src/index.ts | 38 +- plugins/tracker-resources/src/plugin.ts | 20 +- plugins/tracker-resources/src/utils.ts | 218 ++--------- plugins/tracker/src/index.ts | 50 ++- .../components/filter/FilterSection.svelte | 2 +- .../src/components/filter/ObjectFilter.svelte | 59 ++- .../src/components/list/List.svelte | 7 +- .../src/components/list/ListCategories.svelte | 33 +- .../src/components/list/ListCategory.svelte | 97 +++-- .../src/components/list/ListHeader.svelte | 4 +- .../components/status/StatusPresenter.svelte | 37 ++ .../status/StatusRefPresenter.svelte | 30 ++ plugins/view-resources/src/index.ts | 48 ++- plugins/view-resources/src/plugin.ts | 5 +- plugins/view-resources/src/utils.ts | 202 +++++++++- plugins/view-resources/src/viewOptions.ts | 21 +- plugins/view/src/index.ts | 40 +- .../src/components/SpaceView.svelte | 2 +- .../src/components/SpecialView.svelte | 98 +++-- plugins/workbench-resources/src/connect.ts | 6 +- server-plugins/task-resources/src/index.ts | 33 +- server/core/src/indexer/indexer.ts | 23 +- server/core/src/storage.ts | 23 +- server/mongo/src/storage.ts | 126 +++++-- tests/sanity/tests/playwright.config.ts | 5 +- tests/sanity/tests/tracker.layout.spec.ts | 27 +- tests/sanity/tests/tracker.spec.ts | 26 +- tests/sanity/tests/tracker.utils.ts | 4 +- 110 files changed, 2816 insertions(+), 1819 deletions(-) create mode 100644 models/core/src/status.ts create mode 100644 packages/core/src/status.ts create mode 100644 packages/presentation/src/pipeline.ts create mode 100644 packages/presentation/src/status.ts delete mode 100644 plugins/task-resources/src/components/CreateProject.svelte delete mode 100644 plugins/task-resources/src/components/EditIssue.svelte delete mode 100644 plugins/task-resources/src/components/KanbanCard.svelte delete mode 100644 plugins/task-resources/src/components/TaskItem.svelte create mode 100644 plugins/tracker-resources/src/components/projects/ChangeIdentity.svelte create mode 100644 plugins/view-resources/src/components/status/StatusPresenter.svelte create mode 100644 plugins/view-resources/src/components/status/StatusRefPresenter.svelte diff --git a/dev/generator/src/kanban.ts b/dev/generator/src/kanban.ts index 09d8f1bd8c..5e6f13f0e5 100644 --- a/dev/generator/src/kanban.ts +++ b/dev/generator/src/kanban.ts @@ -28,7 +28,8 @@ export async function createUpdateSpaceKanban ( await ctx.with('find-or-update', {}, (ctx) => findOrUpdate(ctx, client, spaceId, task.class.State, sid, { - title: st.name, + ofAttribute: task.attribute.State, + name: st.name, color: st.color, rank }) @@ -37,8 +38,8 @@ export async function createUpdateSpaceKanban ( } const doneStates = [ - { class: task.class.WonState, title: 'Won' }, - { class: task.class.LostState, title: 'Lost' } + { class: task.class.WonState, name: 'Won' }, + { class: task.class.LostState, name: 'Lost' } ] const doneStateRanks = genRanks(doneStates.length) for (const st of doneStates) { @@ -49,10 +50,11 @@ export async function createUpdateSpaceKanban ( break } - const sid = ('generated-' + spaceId + '.done-state.' + st.title.toLowerCase().replace(' ', '_')) as Ref + const sid = `generated-${spaceId}.done-state.${st.name.toLowerCase().replace(' ', '_')}` as Ref await ctx.with('gen-done-state', {}, (ctx) => findOrUpdate(ctx, client, spaceId, st.class, sid, { - title: st.title, + ofAttribute: task.attribute.DoneState, + name: st.name, rank }) ) diff --git a/models/board/src/index.ts b/models/board/src/index.ts index c27486fbac..80b666caae 100644 --- a/models/board/src/index.ts +++ b/models/board/src/index.ts @@ -264,7 +264,8 @@ export function createModel (builder: Builder): void { task.class.WonState, core.space.Model, { - title: board.string.Completed, + ofAttribute: task.attribute.DoneState, + name: board.string.Completed, rank: '0' }, board.state.Completed diff --git a/models/board/src/migration.ts b/models/board/src/migration.ts index bef0d38024..f7f7cfa919 100644 --- a/models/board/src/migration.ts +++ b/models/board/src/migration.ts @@ -13,26 +13,11 @@ // limitations under the License. // -import { Board, Card } from '@hcengineering/board' -import { - AttachedDoc, - Class, - Doc, - DOMAIN_TX, - generateId, - Ref, - TxCollectionCUD, - TxCreateDoc, - TxCUD, - TxOperations, - TxProcessor, - TxUpdateDoc -} from '@hcengineering/core' +import { Ref, TxOperations } from '@hcengineering/core' import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model' import core from '@hcengineering/model-core' -import { DOMAIN_TAGS } from '@hcengineering/model-tags' -import { createKanbanTemplate, createSequence, DOMAIN_TASK } from '@hcengineering/model-task' -import tags, { TagElement, TagReference } from '@hcengineering/tags' +import { createKanbanTemplate, createSequence } from '@hcengineering/model-task' +import tags from '@hcengineering/tags' import task, { createKanban, KanbanTemplate } from '@hcengineering/task' import board from './plugin' @@ -77,12 +62,12 @@ async function createSpace (tx: TxOperations): Promise { async function createDefaultKanbanTemplate (tx: TxOperations): Promise> { const defaultKanban = { states: [ - { color: 9, title: 'To do' }, - { color: 9, title: 'Done' } + { color: 9, name: 'To do' }, + { color: 9, name: 'Done' } ], doneStates: [ - { isWon: true, title: 'Won' }, - { isWon: false, title: 'Lost' } + { isWon: true, name: 'Won' }, + { isWon: false, name: 'Lost' } ] } @@ -123,74 +108,7 @@ async function createDefaults (tx: TxOperations): Promise { ) } -interface CardLabel extends AttachedDoc { - title: string - color: number - isHidden?: boolean -} -async function migrateLabels (client: MigrationClient): Promise { - const objectClass = 'board:class:CardLabel' as Ref> - const txes = await client.find>(DOMAIN_TX, { objectClass }, { sort: { modifiedOn: 1 } }) - const collectionTxes = await client.find>( - DOMAIN_TX, - { 'tx.objectClass': objectClass }, - { sort: { modifiedOn: 1 } } - ) - await Promise.all([...txes, ...collectionTxes].map(({ _id }) => client.delete(DOMAIN_TX, _id))) - const removed = txes.filter(({ _class }) => _class === core.class.TxRemoveDoc).map(({ objectId }) => objectId) - const createTxes = txes.filter( - ({ _class, objectId }) => _class === core.class.TxCreateDoc && !removed.includes(objectId) - ) as unknown as TxCreateDoc[] - const cardLabels = createTxes.map((createTx) => { - const cardLabel = TxProcessor.createDoc2Doc(createTx) - const updateTxes = collectionTxes - .map(({ tx }) => tx) - .filter( - ({ _class, objectId }) => _class === core.class.TxUpdateDoc && objectId === createTx.objectId - ) as unknown as TxUpdateDoc[] - return updateTxes.reduce((label, updateTx) => TxProcessor.updateDoc2Doc(label, updateTx), cardLabel) - }) - await Promise.all( - cardLabels.map((cardLabel) => - client.create(DOMAIN_TAGS, { - _class: tags.class.TagElement, - space: tags.space.Tags, - targetClass: board.class.Card, - category: board.category.Other, - _id: cardLabel._id as unknown as Ref, - modifiedBy: cardLabel.modifiedBy, - modifiedOn: cardLabel.modifiedOn, - title: cardLabel.title, - color: cardLabel.color, - description: '' - }) - ) - ) - const cards = (await client.find(DOMAIN_TASK, { _class: board.class.Card })).filter((card) => - Array.isArray(card.labels) - ) - for (const card of cards) { - const labelRefs = card.labels as unknown as Array> - await client.update(DOMAIN_TASK, { _id: card._id }, { labels: labelRefs.length }) - for (const labelRef of labelRefs) { - const cardLabel = cardLabels.find(({ _id }) => _id === labelRef) - if (cardLabel === undefined) continue - await client.create(DOMAIN_TAGS, { - _class: tags.class.TagReference, - attachedToClass: board.class.Card, - _id: generateId(), - attachedTo: card._id, - space: card.space, - tag: cardLabel._id as unknown as Ref, - title: cardLabel.title, - color: cardLabel.color, - modifiedBy: cardLabel.modifiedBy, - modifiedOn: cardLabel.modifiedOn, - collection: 'labels' - }) - } - } -} +async function migrateLabels (client: MigrationClient): Promise {} export const boardOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await Promise.all([migrateLabels(client)]) diff --git a/models/core/src/index.ts b/models/core/src/index.ts index 1e2f9b8a73..64ff9b648b 100644 --- a/models/core/src/index.ts +++ b/models/core/src/index.ts @@ -60,6 +60,7 @@ import { TVersion } from './core' import { TAccount, TSpace } from './security' +import { TStatus, TStatusCategory } from './status' import { TUserStatus } from './transient' import { TTx, TTxApplyIf, TTxCollectionCUD, TTxCreateDoc, TTxCUD, TTxMixin, TTxRemoveDoc, TTxUpdateDoc } from './tx' @@ -67,6 +68,7 @@ export * from './core' export { coreOperation } from './migration' export * from './security' export * from './tx' +export * from './status' export { core as default } export function createModel (builder: Builder): void { @@ -114,7 +116,9 @@ export function createModel (builder: Builder): void { TFullTextSearchContext, TConfiguration, TConfigurationElement, - TIndexConfiguration + TIndexConfiguration, + TStatus, + TStatusCategory ) builder.createDoc( diff --git a/models/core/src/status.ts b/models/core/src/status.ts new file mode 100644 index 0000000000..7ec8002365 --- /dev/null +++ b/models/core/src/status.ts @@ -0,0 +1,57 @@ +// +// 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 { Attribute, DOMAIN_STATUS, DOMAIN_MODEL, Ref, Status, StatusCategory } from '@hcengineering/core' +import { Model, Prop, TypeRef, TypeString, UX } from '@hcengineering/model' +import { Asset, IntlString } from '@hcengineering/platform' +import core from './component' +import { TDoc } from './core' + +// S T A T U S + +@Model(core.class.Status, core.class.Doc, DOMAIN_STATUS) +@UX(core.string.Status) +export class TStatus extends TDoc implements Status { + // We attach to attribute, so we could distinguish between + ofAttribute!: Ref> + + @Prop(TypeRef(core.class.StatusCategory), core.string.StatusCategory) + category!: Ref + + @Prop(TypeString(), core.string.Name) + name!: string + + // @Prop(TypeNumber(), core.string.Color) + color!: number + + @Prop(TypeString(), core.string.Description) + description!: string + + // @Prop(TypeString(), core.string.Rank) + rank!: string +} + +@Model(core.class.StatusCategory, core.class.Doc, DOMAIN_MODEL) +@UX(core.string.StatusCategory) +export class TStatusCategory extends TDoc implements StatusCategory { + // We attach to attribute, so we could distinguish between + ofAttribute!: Ref> + + icon!: Asset + label!: IntlString + color!: number + defaultStatusName!: string + order!: number +} diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index faa32e3870..8539eb3915 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -15,7 +15,7 @@ // To help typescript locate view plugin properly import type { Employee } from '@hcengineering/contact' -import { Doc, FindOptions, IndexKind, Ref } from '@hcengineering/core' +import { FindOptions, IndexKind, Ref, SortingOrder } from '@hcengineering/core' import { Customer, Funnel, Lead, leadId } from '@hcengineering/lead' import { Builder, @@ -33,10 +33,11 @@ import attachment from '@hcengineering/model-attachment' import chunter from '@hcengineering/model-chunter' import contact, { TContact } from '@hcengineering/model-contact' import core from '@hcengineering/model-core' -import task, { actionTemplates, TSpaceWithStates, TTask } from '@hcengineering/model-task' -import view, { actionTemplates as viewTemplates, createAction } from '@hcengineering/model-view' +import task, { TSpaceWithStates, TTask, actionTemplates } from '@hcengineering/model-task' +import view, { createAction, actionTemplates as viewTemplates } from '@hcengineering/model-view' import workbench from '@hcengineering/model-workbench' import setting from '@hcengineering/setting' +import { ViewOptionsModel } from '@hcengineering/view' import lead from './plugin' @Model(lead.class.Funnel, task.class.SpaceWithStates) @@ -256,6 +257,31 @@ export function createModel (builder: Builder): void { }, lead.viewlet.ListLead ) + const leadViewOptions: ViewOptionsModel = { + groupBy: ['state', 'assignee'], + orderBy: [ + ['state', SortingOrder.Ascending], + ['modifiedOn', SortingOrder.Descending], + ['dueDate', SortingOrder.Descending], + ['rank', SortingOrder.Ascending] + ], + other: [ + { + key: 'shouldShowAll', + type: 'toggle', + defaultValue: false, + actionTarget: 'category', + action: view.function.ShowEmptyGroups, + label: view.string.ShowEmptyGroups + } + ] + } + + const lookupLeadOptions: FindOptions = { + lookup: { + attachedTo: lead.mixin.Customer + } + } builder.createDoc( view.class.Viewlet, @@ -264,11 +290,11 @@ export function createModel (builder: Builder): void { attachTo: lead.class.Lead, descriptor: task.viewlet.Kanban, // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - options: { - lookup: { - attachedTo: lead.mixin.Customer - } - } as FindOptions, // TODO: fix + viewOptions: { + ...leadViewOptions, + groupDepth: 1 + }, + options: lookupLeadOptions, config: [] }, lead.viewlet.KanbanLead diff --git a/models/lead/src/migration.ts b/models/lead/src/migration.ts index 8c4ddebdc3..6152421fba 100644 --- a/models/lead/src/migration.ts +++ b/models/lead/src/migration.ts @@ -62,16 +62,16 @@ async function createSpace (tx: TxOperations): Promise { async function createDefaultKanbanTemplate (tx: TxOperations): Promise> { const defaultKanban = { states: [ - { color: 9, title: 'Incoming' }, - { color: 10, title: 'Negotation' }, - { color: 1, title: 'Offer preparing' }, - { color: 0, title: 'Make a decision' }, - { color: 11, title: 'Contract conclusion' }, - { color: 9, title: 'Done' } + { color: 9, name: 'Incoming' }, + { color: 10, name: 'Negotation' }, + { color: 1, name: 'Offer preparing' }, + { color: 0, name: 'Make a decision' }, + { color: 11, name: 'Contract conclusion' }, + { color: 9, name: 'Done' } ], doneStates: [ - { isWon: true, title: 'Won' }, - { isWon: false, title: 'Lost' } + { isWon: true, name: 'Won' }, + { isWon: false, name: 'Lost' } ] } diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 74d3909b6f..edaeab5cdb 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -14,7 +14,7 @@ // import type { Employee, Organization } from '@hcengineering/contact' -import { Doc, FindOptions, IndexKind, Lookup, Ref, Timestamp } from '@hcengineering/core' +import { IndexKind, Lookup, Ref, SortingOrder, Timestamp } from '@hcengineering/core' import { Builder, Collection, @@ -41,9 +41,9 @@ import presentation from '@hcengineering/model-presentation' import tags from '@hcengineering/model-tags' import task, { actionTemplates, DOMAIN_TASK, TSpaceWithStates, TTask } from '@hcengineering/model-task' import tracker from '@hcengineering/model-tracker' -import notification from '@hcengineering/notification' import view, { actionTemplates as viewTemplates, createAction } from '@hcengineering/model-view' import workbench, { Application, createNavigateAction } from '@hcengineering/model-workbench' +import notification from '@hcengineering/notification' import { getEmbeddedLabel, IntlString } from '@hcengineering/platform' import { Applicant, @@ -55,7 +55,7 @@ import { VacancyList } from '@hcengineering/recruit' import setting from '@hcengineering/setting' -import { KeyBinding } from '@hcengineering/view' +import { KeyBinding, ViewOptionsModel } from '@hcengineering/view' import recruit from './plugin' import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review' import { TOpinion, TReview } from './review-model' @@ -281,6 +281,7 @@ export function createModel (builder: Builder): void { label: recruit.string.Applications, createLabel: recruit.string.ApplicationCreateLabel, createComponent: recruit.component.CreateApplication, + descriptors: [view.viewlet.Table, task.viewlet.Kanban, recruit.viewlet.ApplicantDashboard], baseQuery: { doneState: null } @@ -408,7 +409,8 @@ export function createModel (builder: Builder): void { { attachTo: recruit.class.Applicant, descriptor: view.viewlet.Table, - config: ['', 'attachedTo', 'state', 'doneState', 'modifiedOn'] + config: ['', 'attachedTo', 'state', 'doneState', 'modifiedOn'], + variant: 'short' }, recruit.viewlet.VacancyApplicationsShort ) @@ -588,6 +590,27 @@ export function createModel (builder: Builder): void { } } + const applicantViewOptions: ViewOptionsModel = { + groupBy: ['state', 'doneState', 'assignee'], + orderBy: [ + ['state', SortingOrder.Ascending], + ['doneState', SortingOrder.Ascending], + ['modifiedOn', SortingOrder.Descending], + ['dueDate', SortingOrder.Descending], + ['rank', SortingOrder.Ascending] + ], + other: [ + { + key: 'shouldShowAll', + type: 'toggle', + defaultValue: false, + actionTarget: 'category', + action: view.function.ShowEmptyGroups, + label: view.string.ShowEmptyGroups + } + ] + } + builder.createDoc( view.class.Viewlet, core.space.Model, @@ -595,9 +618,13 @@ export function createModel (builder: Builder): void { attachTo: recruit.class.Applicant, descriptor: task.viewlet.Kanban, // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + viewOptions: { + ...applicantViewOptions, + groupDepth: 1 + }, options: { lookup: applicantKanbanLookup - } as FindOptions, // TODO: fix + }, config: [] }, recruit.viewlet.ApplicantKanban diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index 99e4507801..cde1b41346 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -232,14 +232,14 @@ async function createDefaults (tx: TxOperations): Promise { async function createDefaultKanbanTemplate (tx: TxOperations): Promise> { const defaultKanban = { states: [ - { color: 9, title: 'HR Interview' }, - { color: 10, title: 'Technical Interview' }, - { color: 1, title: 'Test task' }, - { color: 0, title: 'Offer' } + { color: 9, name: 'HR Interview' }, + { color: 10, name: 'Technical Interview' }, + { color: 1, name: 'Test task' }, + { color: 0, name: 'Offer' } ], doneStates: [ - { isWon: true, title: 'Won' }, - { isWon: false, title: 'Lost' } + { isWon: true, name: 'Won' }, + { isWon: false, name: 'Lost' } ] } diff --git a/models/server-task/src/index.ts b/models/server-task/src/index.ts index 83f9d3a798..b1cd96c411 100644 --- a/models/server-task/src/index.ts +++ b/models/server-task/src/index.ts @@ -15,17 +15,4 @@ import { Builder } from '@hcengineering/model' -import core from '@hcengineering/core' -import serverTask from '@hcengineering/server-task' -import task from '@hcengineering/task' -import serverNotification from '@hcengineering/server-notification' - -export function createModel (builder: Builder): void { - builder.mixin(task.class.Issue, core.class.Class, serverNotification.mixin.HTMLPresenter, { - presenter: serverTask.function.IssueHTMLPresenter - }) - - builder.mixin(task.class.Issue, core.class.Class, serverNotification.mixin.TextPresenter, { - presenter: serverTask.function.IssueTextPresenter - }) -} +export function createModel (builder: Builder): void {} diff --git a/models/task/src/index.ts b/models/task/src/index.ts index f27527a7cf..f36aebb843 100644 --- a/models/task/src/index.ts +++ b/models/task/src/index.ts @@ -15,7 +15,7 @@ import type { Employee } from '@hcengineering/contact' import contact from '@hcengineering/contact' -import { Arr, Class, Doc, Domain, FindOptions, IndexKind, Ref, Space, Timestamp } from '@hcengineering/core' +import { Arr, Attribute, Class, Doc, Domain, IndexKind, Ref, Space, Status, Timestamp } from '@hcengineering/core' import { Builder, Collection, @@ -32,25 +32,20 @@ import { TypeString, UX } from '@hcengineering/model' -import attachment from '@hcengineering/model-attachment' -import chunter from '@hcengineering/model-chunter' -import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core' +import core, { TAttachedDoc, TClass, TDoc, TSpace, TStatus } from '@hcengineering/model-core' import view, { actionTemplates as viewTemplates, createAction, template } from '@hcengineering/model-view' import notification from '@hcengineering/notification' import { IntlString } from '@hcengineering/platform' import tags from '@hcengineering/tags' import { - DOMAIN_STATE, DoneState, DoneStateTemplate, - Issue, Kanban, KanbanCard, KanbanTemplate, KanbanTemplateSpace, LostState, LostStateTemplate, - Project, Sequence, State, StateTemplate, @@ -68,25 +63,15 @@ export { default } from './plugin' export const DOMAIN_TASK = 'task' as Domain export const DOMAIN_KANBAN = 'kanban' as Domain -@Model(task.class.State, core.class.Doc, DOMAIN_STATE, [task.interface.DocWithRank]) +@Model(task.class.State, core.class.Status) @UX(task.string.TaskState, task.icon.TaskState, undefined, 'rank') -export class TState extends TDoc implements State { - @Prop(TypeString(), task.string.TaskStateTitle) - title!: string - - color!: number - - declare rank: string +export class TState extends TStatus implements State { + isArchived!: boolean } -@Model(task.class.DoneState, core.class.Doc, DOMAIN_STATE, [task.interface.DocWithRank]) -@UX(task.string.TaskStateDone, task.icon.TaskState, undefined, 'title') -export class TDoneState extends TDoc implements DoneState { - @Prop(TypeString(), task.string.TaskStateTitle) - title!: string - - declare rank: string -} +@Model(task.class.DoneState, core.class.Status) +@UX(task.string.TaskStateDone, task.icon.TaskState, undefined, 'name') +export class TDoneState extends TStatus implements DoneState {} @Model(task.class.WonState, task.class.DoneState) export class TWonState extends TDoneState implements WonState {} @@ -99,13 +84,13 @@ export class TLostState extends TDoneState implements LostState {} * * No domain is specified, since pure Tasks could not exists */ -@Model(task.class.Task, core.class.AttachedDoc, DOMAIN_TASK, [task.interface.DocWithRank]) +@Model(task.class.Task, core.class.AttachedDoc, DOMAIN_TASK) @UX(task.string.Task, task.icon.Task, task.string.Task) export class TTask extends TAttachedDoc implements Task { - @Prop(TypeRef(task.class.State), task.string.TaskState) + @Prop(TypeRef(task.class.State), task.string.TaskState, { _id: task.attribute.State }) state!: Ref - @Prop(TypeRef(task.class.DoneState), task.string.TaskStateDone) + @Prop(TypeRef(task.class.DoneState), task.string.TaskStateDone, { _id: task.attribute.DoneState }) doneState!: Ref | null @Prop(TypeString(), task.string.TaskNumber) @@ -124,14 +109,11 @@ export class TTask extends TAttachedDoc implements Task { declare rank: string - // @Prop(Collection(task.class.TodoItem), task.string.Todos) - // todoItems!: number - @Prop(Collection(tags.class.TagReference, task.string.TaskLabels), task.string.TaskLabels) labels!: number } -@Model(task.class.TodoItem, core.class.AttachedDoc, DOMAIN_TASK, [task.interface.DocWithRank]) +@Model(task.class.TodoItem, core.class.AttachedDoc, DOMAIN_TASK) @UX(task.string.Todo) export class TTodoItem extends TAttachedDoc implements TodoItem { @Prop(TypeMarkup(), task.string.TodoName, task.icon.Task) @@ -153,38 +135,6 @@ export class TTodoItem extends TAttachedDoc implements TodoItem { declare rank: string } -@Model(task.class.SpaceWithStates, core.class.Space) -export class TSpaceWithStates extends TSpace {} - -@Model(task.class.Project, task.class.SpaceWithStates) -@UX(task.string.ProjectName, task.icon.Task) -export class TProject extends TSpaceWithStates implements Project {} - -@Model(task.class.Issue, task.class.Task, DOMAIN_TASK) -@UX(task.string.Task, task.icon.Task, task.string.Task, 'number') -export class TIssue extends TTask implements Issue { - // We need to declare, to provide property with label - @Prop(TypeRef(core.class.Doc), task.string.TaskParent) - declare attachedTo: Ref - - @Prop(TypeString(), task.string.IssueName) - @Index(IndexKind.FullText) - name!: string - - @Prop(TypeMarkup(), task.string.Description) - @Index(IndexKind.FullText) - description!: string - - @Prop(Collection(chunter.class.Comment), task.string.TaskComments) - comments!: number - - @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) - attachments!: number - - @Prop(TypeRef(contact.class.Employee), task.string.TaskAssignee) - declare assignee: Ref | null -} - @Mixin(task.mixin.KanbanCard, core.class.Class) export class TKanbanCard extends TClass implements KanbanCard { card!: AnyComponent @@ -197,6 +147,9 @@ export class TKanban extends TDoc implements Kanban { attachedTo!: Ref } +@Model(task.class.SpaceWithStates, core.class.Space) +export class TSpaceWithStates extends TSpace {} + @Model(task.class.KanbanTemplateSpace, core.class.Space) export class TKanbanTemplateSpace extends TSpace implements KanbanTemplateSpace { name!: IntlString @@ -205,10 +158,13 @@ export class TKanbanTemplateSpace extends TSpace implements KanbanTemplateSpace editor!: AnyComponent } -@Model(task.class.StateTemplate, core.class.AttachedDoc, DOMAIN_KANBAN, [task.interface.DocWithRank]) +@Model(task.class.StateTemplate, core.class.AttachedDoc, DOMAIN_KANBAN) export class TStateTemplate extends TAttachedDoc implements StateTemplate { + // We attach to attribute, so we could distinguish between + ofAttribute!: Ref> + @Prop(TypeString(), task.string.StateTemplateTitle) - title!: string + name!: string @Prop(TypeString(), task.string.StateTemplateColor) color!: number @@ -216,10 +172,13 @@ export class TStateTemplate extends TAttachedDoc implements StateTemplate { declare rank: string } -@Model(task.class.DoneStateTemplate, core.class.AttachedDoc, DOMAIN_KANBAN, [task.interface.DocWithRank]) +@Model(task.class.DoneStateTemplate, core.class.AttachedDoc, DOMAIN_KANBAN) export class TDoneStateTemplate extends TAttachedDoc implements DoneStateTemplate { + // We attach to attribute, so we could distinguish between + ofAttribute!: Ref> + @Prop(TypeString(), task.string.StateTemplateTitle) - title!: string + name!: string declare rank: string } @@ -334,10 +293,8 @@ export function createModel (builder: Builder): void { TKanbanTemplate, TSequence, TTask, - TSpaceWithStates, - TProject, - TIssue, - TTodoItem + TTodoItem, + TSpaceWithStates ) builder.createDoc( @@ -351,33 +308,14 @@ export function createModel (builder: Builder): void { task.viewlet.StatusTable ) - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: task.class.Issue, - descriptor: task.viewlet.StatusTable, - config: ['', 'name', 'assignee', 'state', 'doneState', 'attachments', 'comments', 'modifiedOn'] - }, - task.viewlet.TableIssue - ) - builder.mixin(task.class.Task, core.class.Class, view.mixin.ObjectPresenter, { presenter: view.component.ObjectPresenter }) - builder.mixin(task.class.Issue, core.class.Class, view.mixin.ObjectPresenter, { - presenter: task.component.TaskPresenter - }) - builder.mixin(task.class.KanbanTemplate, core.class.Class, view.mixin.ObjectPresenter, { presenter: task.component.KanbanTemplatePresenter }) - builder.mixin(task.class.Issue, core.class.Class, view.mixin.ObjectEditor, { - editor: task.component.EditIssue - }) - builder.mixin(task.class.Task, core.class.Class, view.mixin.ObjectEditorHeader, { editor: task.component.TaskHeader }) @@ -387,30 +325,6 @@ export function createModel (builder: Builder): void { fields: ['assignee'] }) - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: task.class.Issue, - descriptor: task.viewlet.Kanban, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - options: { - lookup: { - assignee: contact.class.Employee, - _id: { - todoItems: task.class.TodoItem - } - } - } as FindOptions, - config: [] - }, - task.viewlet.KanbanIssue - ) - - builder.mixin(task.class.Issue, core.class.Class, task.mixin.KanbanCard, { - card: task.component.KanbanCard - }) - builder.createDoc( view.class.ActionCategory, core.space.Model, diff --git a/models/task/src/migration.ts b/models/task/src/migration.ts index 274747ebde..26c0be62e2 100644 --- a/models/task/src/migration.ts +++ b/models/task/src/migration.ts @@ -13,13 +13,18 @@ // limitations under the License. // -import { Class, Doc, Ref, Space, TxOperations } from '@hcengineering/core' +import { Class, Doc, Domain, Ref, Space, TxOperations, DOMAIN_STATUS } from '@hcengineering/core' import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model' import core from '@hcengineering/model-core' -import { KanbanTemplate, StateTemplate, DoneStateTemplate, genRanks, createKanban } from '@hcengineering/task' -import { DOMAIN_TASK } from '.' -import task from './plugin' import tags from '@hcengineering/model-tags' +import { createKanban, DoneStateTemplate, genRanks, KanbanTemplate, StateTemplate } from '@hcengineering/task' +import { DOMAIN_TASK, DOMAIN_KANBAN } from '.' +import task from './plugin' + +/** + * @public + */ +export const DOMAIN_STATE = 'state' as Domain /** * @public @@ -30,8 +35,8 @@ export interface KanbanTemplateData { title: KanbanTemplate['title'] description?: string shortDescription?: string - states: Pick[] - doneStates: (Pick & { isWon: boolean })[] + states: Pick[] + doneStates: (Pick & { isWon: boolean })[] } /** @@ -79,8 +84,9 @@ export async function createKanbanTemplate ( task.class.KanbanTemplate, 'doneStatesC', { + ofAttribute: task.attribute.DoneState, rank: doneStateRanks[i], - title: st.title + name: st.name } ) ) @@ -90,8 +96,9 @@ export async function createKanbanTemplate ( await Promise.all( data.states.map((st, i) => client.addCollection(task.class.StateTemplate, data.space, data.kanbanId, task.class.KanbanTemplate, 'statesC', { + ofAttribute: task.attribute.State, rank: stateRanks[i], - title: st.title, + name: st.name, color: st.color }) ) @@ -100,26 +107,6 @@ export async function createKanbanTemplate ( return tmpl } -async function createDefaultProject (tx: TxOperations): Promise { - const createTx = await tx.findOne(core.class.TxCreateDoc, { - objectId: task.space.TasksPublic - }) - if (createTx === undefined) { - await tx.createDoc( - task.class.Project, - core.space.Space, - { - name: 'public', - description: 'Public tasks', - private: false, - archived: false, - members: [] - }, - task.space.TasksPublic - ) - } -} - async function createDefaultSequence (tx: TxOperations): Promise { const current = await tx.findOne(core.class.Space, { _id: task.space.Sequence @@ -143,15 +130,15 @@ async function createDefaultSequence (tx: TxOperations): Promise { async function createDefaultKanbanTemplate (tx: TxOperations): Promise> { const defaultKanban = { states: [ - { color: 9, title: 'Open' }, - { color: 10, title: 'In Progress' }, - { color: 1, title: 'Under review' }, - { color: 0, title: 'Done' }, - { color: 11, title: 'Invalid' } + { color: 9, name: 'Open' }, + { color: 10, name: 'In Progress' }, + { color: 1, name: 'Under review' }, + { color: 0, name: 'Done' }, + { color: 11, name: 'Invalid' } ], doneStates: [ - { isWon: true, title: 'Won' }, - { isWon: false, title: 'Lost' } + { isWon: true, name: 'Won' }, + { isWon: false, name: 'Lost' } ] } @@ -199,8 +186,6 @@ async function createSpace (tx: TxOperations): Promise { async function createDefaults (tx: TxOperations): Promise { await createSpace(tx) await createDefaultSequence(tx) - await createDefaultProject(tx) - await createSequence(tx, task.class.Issue) await createDefaultKanban(tx) } @@ -219,6 +204,37 @@ async function migrateTodoItems (client: MigrationClient): Promise { export const taskOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await Promise.all([migrateTodoItems(client)]) + + const stateClasses = client.hierarchy.getDescendants(task.class.State) + const doneStateClasses = client.hierarchy.getDescendants(task.class.DoneState) + + const stateTemplateClasses = client.hierarchy.getDescendants(task.class.StateTemplate) + const doneStateTemplatesClasses = client.hierarchy.getDescendants(task.class.DoneStateTemplate) + + await client.move(DOMAIN_STATE, { _class: { $in: [...stateClasses, ...doneStateClasses] } }, DOMAIN_STATUS) + + await client.update( + DOMAIN_STATUS, + { _class: { $in: stateClasses }, ofAttribute: { $exists: false } }, + { ofAttribute: task.attribute.State } + ) + await client.update( + DOMAIN_STATUS, + { _class: { $in: doneStateClasses }, ofAttribute: { $exists: false } }, + { ofAttribute: task.attribute.DoneState } + ) + + await client.update( + DOMAIN_STATUS, + { _class: { $in: [...stateClasses, ...doneStateClasses] }, title: { $exists: true } }, + { $rename: { title: 'name' } } + ) + + await client.update( + DOMAIN_KANBAN, + { _class: { $in: [...stateTemplateClasses, ...doneStateTemplatesClasses] }, title: { $exists: true } }, + { $rename: { title: 'name' } } + ) }, async upgrade (client: MigrationUpgradeClient): Promise { const tx = new TxOperations(client, core.account.System) diff --git a/models/task/src/plugin.ts b/models/task/src/plugin.ts index 6460749ab8..81e5cbb924 100644 --- a/models/task/src/plugin.ts +++ b/models/task/src/plugin.ts @@ -43,7 +43,6 @@ export default mergeIds(taskId, task, { }, component: { ProjectView: '' as AnyComponent, - CreateProject: '' as AnyComponent, EditIssue: '' as AnyComponent, TaskPresenter: '' as AnyComponent, KanbanTemplatePresenter: '' as AnyComponent, diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index f5f569a25f..ad6a92392e 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -47,30 +47,29 @@ import { } from '@hcengineering/model' import attachment from '@hcengineering/model-attachment' import chunter from '@hcengineering/model-chunter' -import core, { DOMAIN_SPACE, TAttachedDoc, TDoc, TSpace, TType } from '@hcengineering/model-core' +import core, { DOMAIN_SPACE, TAttachedDoc, TDoc, TSpace, TStatus, TType } from '@hcengineering/model-core' import view, { actionTemplates, classPresenter, createAction } from '@hcengineering/model-view' import workbench, { createNavigateAction } from '@hcengineering/model-workbench' import notification from '@hcengineering/notification' -import { Asset, IntlString } from '@hcengineering/platform' +import { IntlString } from '@hcengineering/platform' import setting from '@hcengineering/setting' import tags, { TagElement } from '@hcengineering/tags' import task from '@hcengineering/task' import { + Component, + ComponentStatus, Issue, IssueChildInfo, IssueParentInfo, IssuePriority, IssueStatus, - IssueStatusCategory, IssueTemplate, IssueTemplateChild, - Component, - ComponentStatus, + Project, Scrum, ScrumRecord, Sprint, SprintStatus, - Project, TimeReportDayType, TimeSpendReport, trackerId @@ -89,34 +88,8 @@ export const DOMAIN_TRACKER = 'tracker' as Domain /** * @public */ -@Model(tracker.class.IssueStatus, core.class.AttachedDoc, DOMAIN_TRACKER) -export class TIssueStatus extends TAttachedDoc implements IssueStatus { - @Index(IndexKind.Indexed) - name!: string - - description?: string - color?: number - - @Prop(TypeRef(tracker.class.IssueStatusCategory), tracker.string.StatusCategory) - @Index(IndexKind.Indexed) - category!: Ref - - @Prop(TypeString(), tracker.string.Rank) - @Hidden() - rank!: string -} - -/** - * @public - */ -@Model(tracker.class.IssueStatusCategory, core.class.Doc, DOMAIN_MODEL) -export class TIssueStatusCategory extends TDoc implements IssueStatusCategory { - label!: IntlString - icon!: Asset - color!: number - defaultStatusName!: string - order!: number -} +@Model(tracker.class.IssueStatus, core.class.Status) +export class TIssueStatus extends TStatus implements IssueStatus {} /** * @public @@ -207,7 +180,7 @@ export class TIssue extends TAttachedDoc implements Issue { @Index(IndexKind.FullText) description!: Markup - @Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status) + @Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status, { _id: tracker.attribute.IssueStatus }) @Index(IndexKind.Indexed) status!: Ref @@ -498,7 +471,6 @@ export function createModel (builder: Builder): void { TIssue, TIssueTemplate, TIssueStatus, - TIssueStatusCategory, TTypeIssuePriority, TTypeComponentStatus, TSprint, @@ -773,9 +745,10 @@ export function createModel (builder: Builder): void { ) builder.createDoc( - tracker.class.IssueStatusCategory, + core.class.StatusCategory, core.space.Model, { + ofAttribute: tracker.attribute.IssueStatus, label: tracker.string.CategoryBacklog, icon: tracker.icon.CategoryBacklog, color: 12, @@ -786,9 +759,10 @@ export function createModel (builder: Builder): void { ) builder.createDoc( - tracker.class.IssueStatusCategory, + core.class.StatusCategory, core.space.Model, { + ofAttribute: tracker.attribute.IssueStatus, label: tracker.string.CategoryUnstarted, icon: tracker.icon.CategoryUnstarted, color: 13, @@ -799,9 +773,10 @@ export function createModel (builder: Builder): void { ) builder.createDoc( - tracker.class.IssueStatusCategory, + core.class.StatusCategory, core.space.Model, { + ofAttribute: tracker.attribute.IssueStatus, label: tracker.string.CategoryStarted, icon: tracker.icon.CategoryStarted, color: 14, @@ -812,9 +787,10 @@ export function createModel (builder: Builder): void { ) builder.createDoc( - tracker.class.IssueStatusCategory, + core.class.StatusCategory, core.space.Model, { + ofAttribute: tracker.attribute.IssueStatus, label: tracker.string.CategoryCompleted, icon: tracker.icon.CategoryCompleted, color: 15, @@ -825,9 +801,10 @@ export function createModel (builder: Builder): void { ) builder.createDoc( - tracker.class.IssueStatusCategory, + core.class.StatusCategory, core.space.Model, { + ofAttribute: tracker.attribute.IssueStatus, label: tracker.string.CategoryCanceled, icon: tracker.icon.CategoryCanceled, color: 16, @@ -870,6 +847,14 @@ export function createModel (builder: Builder): void { presenters: [tracker.component.IssueStatistics] }) + builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.ObjectPresenter, { + presenter: tracker.component.StatusPresenter + }) + + builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.AttributePresenter, { + presenter: tracker.component.StatusRefPresenter + }) + builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.SortFuncs, { func: tracker.function.IssueStatusSort }) @@ -894,14 +879,6 @@ export function createModel (builder: Builder): void { component: view.component.ValueFilter }) - builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.ObjectPresenter, { - presenter: tracker.component.StatusPresenter - }) - - builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.AttributePresenter, { - presenter: tracker.component.StatusRefPresenter - }) - builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributePresenter, { presenter: tracker.component.PriorityRefPresenter }) @@ -947,10 +924,6 @@ export function createModel (builder: Builder): void { fields: ['assignee'] }) - builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.AllValuesFunc, { - func: tracker.function.GetAllStatuses - }) - builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AllValuesFunc, { func: tracker.function.GetAllPriority }) @@ -1342,7 +1315,7 @@ export function createModel (builder: Builder): void { const statusOptions: FindOptions = { lookup: { - category: tracker.class.IssueStatusCategory + category: core.class.StatusCategory }, sort: { rank: SortingOrder.Ascending } } @@ -1377,6 +1350,9 @@ export function createModel (builder: Builder): void { attribute: 'status', _class: tracker.class.IssueStatus, placeholder: tracker.string.SetStatus, + query: { + ofAttribute: tracker.attribute.IssueStatus + }, fillQuery: { space: 'space' }, diff --git a/models/tracker/src/migration.ts b/models/tracker/src/migration.ts index 0f4b5fad33..d5c7076815 100644 --- a/models/tracker/src/migration.ts +++ b/models/tracker/src/migration.ts @@ -21,11 +21,13 @@ import core, { generateId, Ref, SortingOrder, + StatusCategory, TxCollectionCUD, TxCreateDoc, TxOperations, TxResult, - TxUpdateDoc + TxUpdateDoc, + DOMAIN_STATUS } from '@hcengineering/core' import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model' import { DOMAIN_SPACE } from '@hcengineering/model-core' @@ -35,7 +37,6 @@ import { genRanks, Issue, IssueStatus, - IssueStatusCategory, IssueTemplate, IssueTemplateChild, Project, @@ -55,9 +56,9 @@ enum DeprecatedIssueStatus { interface CreateProjectIssueStatusesArgs { tx: TxOperations projectId: Ref - categories: IssueStatusCategory[] + categories: StatusCategory[] defaultStatusId?: Ref - defaultCategoryId?: Ref + defaultCategoryId?: Ref } const categoryByDeprecatedIssueStatus = { @@ -81,15 +82,14 @@ async function createProjectIssueStatuses ({ const { _id: category, defaultStatusName } = statusCategory const rank = issueStatusRanks[i] - await tx.addCollection( - tracker.class.IssueStatus, - attachedTo, - attachedTo, - tracker.class.Project, - 'issueStatuses', - { name: defaultStatusName, category, rank }, - category === defaultCategoryId ? defaultStatusId : undefined - ) + if (defaultStatusName !== undefined) { + await tx.createDoc( + tracker.class.IssueStatus, + attachedTo, + { ofAttribute: tracker.attribute.IssueStatus, name: defaultStatusName, category, rank }, + category === defaultCategoryId ? defaultStatusId : undefined + ) + } } } @@ -105,11 +105,7 @@ async function createDefaultProject (tx: TxOperations): Promise { // Create new if not deleted by customers. if (current === undefined && currentDeleted === undefined) { const defaultStatusId: Ref = generateId() - const categories = await tx.findAll( - tracker.class.IssueStatusCategory, - {}, - { sort: { order: SortingOrder.Ascending } } - ) + const categories = await tx.findAll(core.class.StatusCategory, {}, { sort: { order: SortingOrder.Ascending } }) await tx.createDoc( tracker.class.Project, @@ -137,7 +133,7 @@ async function fixProjectIssueStatusesOrder (tx: TxOperations, project: Project) const statuses = await tx.findAll( tracker.class.IssueStatus, { attachedTo: project._id }, - { lookup: { category: tracker.class.IssueStatusCategory } } + { lookup: { category: core.class.StatusCategory } } ) statuses.sort((a, b) => (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0)) const issueStatusRanks = genRanks(statuses.length) @@ -170,11 +166,7 @@ async function upgradeProjectIssueStatuses (tx: TxOperations): Promise { const projects = await tx.findAll(tracker.class.Project, { issueStatuses: undefined }) if (projects.length > 0) { - const categories = await tx.findAll( - tracker.class.IssueStatusCategory, - {}, - { sort: { order: SortingOrder.Ascending } } - ) + const categories = await tx.findAll(core.class.StatusCategory, {}, { sort: { order: SortingOrder.Ascending } }) for (const project of projects) { const defaultStatusId: Ref = generateId() @@ -754,6 +746,22 @@ export const trackerOperation: MigrateOperation = { await fillRank(client) await renameProject(client) await setCreate(client) + + // Move all status objects into status domain + await client.move( + DOMAIN_TRACKER, + { + _class: tracker.class.IssueStatus + }, + DOMAIN_STATUS + ) + await client.update( + DOMAIN_STATUS, + { _class: tracker.class.IssueStatus, ofAttribute: { $exists: false } }, + { + ofAttribute: tracker.attribute.IssueStatus + } + ) }, async upgrade (client: MigrationUpgradeClient): Promise { const tx = new TxOperations(client, core.account.System) diff --git a/models/view/src/index.ts b/models/view/src/index.ts index a4e90d3ae5..3657028b34 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -64,7 +64,8 @@ import type { ViewOptions, AllValuesFunc, LinkProvider, - ObjectPanel + ObjectPanel, + AllValuesFuncGetter } from '@hcengineering/view' import view from './plugin' @@ -220,9 +221,7 @@ export class TSortFuncs extends TClass implements ClassSortFuncs { @Mixin(view.mixin.AllValuesFunc, core.class.Class) export class TAllValuesFunc extends TClass implements AllValuesFunc { - func!: Resource< - (space: Ref | undefined, onUpdate: () => void, queryId: Ref) => Promise - > + func!: Resource } @Model(view.class.ViewletPreference, preference.class.Preference) @@ -774,6 +773,18 @@ export function createModel (builder: Builder): void { }, view.action.Open ) + + builder.mixin(core.class.Status, core.class.Class, view.mixin.SortFuncs, { + func: view.function.StatusSort + }) + + builder.mixin(core.class.Status, core.class.Class, view.mixin.ObjectPresenter, { + presenter: view.component.StatusPresenter + }) + + builder.mixin(core.class.Status, core.class.Class, view.mixin.AttributePresenter, { + presenter: view.component.StatusRefPresenter + }) } export default view diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index b9553ff0be..b2d0f3da9d 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -67,7 +67,9 @@ export default mergeIds(viewId, view, { ListView: '' as AnyComponent, IndexedDocumentPreview: '' as AnyComponent, SpaceRefPresenter: '' as AnyComponent, - EnumPresenter: '' as AnyComponent + EnumPresenter: '' as AnyComponent, + StatusPresenter: '' as AnyComponent, + StatusRefPresenter: '' as AnyComponent }, string: { Table: '' as IntlString, diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index c9d7862631..d9fd444239 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -23,13 +23,17 @@ import type { BlobData, Class, Collection, + Configuration, + ConfigurationElement, Doc, DocIndexState, - FullTextSearchContext, Enum, EnumOf, FullTextData, + FullTextSearchContext, Hyperlink, + IndexingConfiguration, + IndexStageState, Interface, Obj, PluginConfiguration, @@ -39,12 +43,9 @@ import type { Space, Timestamp, Type, - UserStatus, - Configuration, - ConfigurationElement, - IndexStageState, - IndexingConfiguration + UserStatus } from './classes' +import { Status, StatusCategory } from './status' import type { Tx, TxApplyIf, @@ -54,7 +55,8 @@ import type { TxMixin, TxModelUpgrade, TxRemoveDoc, - TxUpdateDoc + TxUpdateDoc, + TxWorkspaceEvent } from './tx' /** @@ -78,6 +80,7 @@ export default plugin(coreId, { Attribute: '' as Ref>, Tx: '' as Ref>, TxModelUpgrade: '' as Ref>, + TxWorkspaceEvent: '' as Ref>, TxApplyIf: '' as Ref>, TxCUD: '' as Ref>>, TxCreateDoc: '' as Ref>>, @@ -111,7 +114,10 @@ export default plugin(coreId, { DocIndexState: '' as Ref>, IndexStageState: '' as Ref>, - Configuration: '' as Ref> + Configuration: '' as Ref>, + + Status: '' as Ref>, + StatusCategory: '' as Ref> }, mixin: { FullTextSearchContext: '' as Ref>, @@ -161,6 +167,8 @@ export default plugin(coreId, { Private: '' as IntlString, Object: '' as IntlString, System: '' as IntlString, - CreatedBy: '' as IntlString + CreatedBy: '' as IntlString, + Status: '' as IntlString, + StatusCategory: '' as IntlString } }) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4bdbf1a828..a4bd3b1100 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -31,6 +31,7 @@ export * from './storage' export * from './tx' export * from './utils' export * from './backup' +export * from './status' addStringsLoader(coreId, async (lang: string) => { return await import(`./lang/${lang}.json`) diff --git a/packages/core/src/lang/en.json b/packages/core/src/lang/en.json index 9917a32f4f..81e8ebdf52 100644 --- a/packages/core/src/lang/en.json +++ b/packages/core/src/lang/en.json @@ -28,6 +28,8 @@ "Hyperlink": "URL", "Object": "Object", "System": "System", - "CreatedBy": "Reporter" + "CreatedBy": "Reporter", + "Status": "Status", + "StatusCategory": "Status category" } } diff --git a/packages/core/src/lang/ru.json b/packages/core/src/lang/ru.json index 5c7f1d1bca..bf581f1246 100644 --- a/packages/core/src/lang/ru.json +++ b/packages/core/src/lang/ru.json @@ -28,6 +28,8 @@ "Hyperlink": "URL", "Object": "Объект", "System": "Система", - "CreatedBy": "Автор" + "CreatedBy": "Автор", + "Status": "Статус", + "StatusCategory": "Категория статуса" } } diff --git a/packages/core/src/query.ts b/packages/core/src/query.ts index 8b468c831f..a4811cab99 100644 --- a/packages/core/src/query.ts +++ b/packages/core/src/query.ts @@ -4,7 +4,7 @@ import core from './component' import { Hierarchy } from './hierarchy' import { getObjectValue } from './objvalue' import { createPredicates, isPredicate } from './predicate' -import { SortingOrder, SortingQuery, Storage } from './storage' +import { SortQuerySelector, SortingOrder, SortingQuery, SortingRules, Storage } from './storage' /** * @public @@ -70,7 +70,30 @@ export async function resultSort ( result.sort(sortFunc) } -function getSortingResult (aValue: any, bValue: any, order: SortingOrder): number { +function mapSortingValue (order: SortingOrder | SortingRules, val: any): any { + if (typeof order !== 'object') { + return val + } + for (const r of order.cases) { + if (typeof r.query === 'object') { + const q: SortQuerySelector = r.query + if (q.$in?.includes(val) ?? false) { + return r.index + } + if (q.$nin !== undefined && !q.$nin.includes(val)) { + return r.index + } + if (q.$ne !== undefined && q.$ne !== val) { + return r.index + } + } + if (r.query === val) { + return r.index + } + } +} + +function getSortingResult (aValue: any, bValue: any, order: SortingOrder | SortingRules): number { let res = 0 if (typeof aValue === 'undefined') { return typeof bValue === 'undefined' ? 0 : -1 @@ -78,12 +101,19 @@ function getSortingResult (aValue: any, bValue: any, order: SortingOrder): numbe if (typeof bValue === 'undefined') { return 1 } + + const orderOrder = typeof order === 'object' ? order.order : order + if (Array.isArray(aValue) && Array.isArray(bValue)) { - res = (aValue.sort((a, b) => (a - b) * order)[0] ?? 0) - (bValue.sort((a, b) => (a - b) * order)[0] ?? 0) + res = + (aValue.map((it) => mapSortingValue(order, it)).sort((a, b) => (a - b) * orderOrder)[0] ?? 0) - + (bValue.map((it) => mapSortingValue(order, it)).sort((a, b) => (a - b) * orderOrder)[0] ?? 0) } else { - res = typeof aValue === 'string' ? aValue.localeCompare(bValue) : aValue - bValue + const aaValue = mapSortingValue(order, aValue) + const bbValue = mapSortingValue(order, bValue) + res = typeof aaValue === 'string' ? aaValue.localeCompare(bbValue) : aaValue - bbValue } - return res * order + return res * orderOrder } async function getEnums ( diff --git a/packages/core/src/status.ts b/packages/core/src/status.ts new file mode 100644 index 0000000000..f7b271839d --- /dev/null +++ b/packages/core/src/status.ts @@ -0,0 +1,91 @@ +// +// 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 { Asset, IntlString } from '@hcengineering/platform' +import { Attribute, Doc, Domain, Ref } from './classes' +import { WithLookup } from './storage' +import { IdMap, toIdMap } from './utils' + +/** + * @public + */ +export interface StatusCategory extends Doc { + ofAttribute: Ref> + icon: Asset + label: IntlString + color: number + defaultStatusName?: string + order: number // category order +} +/** + * @public + */ +export const DOMAIN_STATUS = 'status' as Domain + +/** + * @public + * + * Status is attached to attribute, and if user attribute will be removed, all status values will be remove as well. + */ +export interface Status extends Doc { + // We attach to attribute, so we could distinguish between + ofAttribute: Ref> + + // Optional category. + category?: Ref + + // Status with case insensitivity name match will be assumed same. + name: string + + // Optional color + color?: number + // Optional description + description?: string + // Lexorank rank for ordering. + rank: string +} + +/** + * @public + */ +export class StatusValue { + constructor (readonly name: string, readonly color: number | undefined, readonly values: WithLookup[]) {} +} + +/** + * @public + * + * Allow to query for status keys/values. + */ +export class StatusManager { + byId: IdMap> + + constructor (readonly statuses: WithLookup[]) { + this.byId = toIdMap(statuses) + } + + get (ref: Ref): WithLookup | undefined { + return this.byId.get(ref) + } + + filter (predicate: (value: WithLookup) => boolean): WithLookup[] { + return this.statuses.filter(predicate) + } +} + +/** + * @public + */ +export type CategoryType = number | string | undefined | Ref | StatusValue diff --git a/packages/core/src/storage.ts b/packages/core/src/storage.ts index d44eaf6236..e20351a6a1 100644 --- a/packages/core/src/storage.ts +++ b/packages/core/src/storage.ts @@ -123,15 +123,41 @@ export type FindOptions = { projection?: Projection } +/** + * @public + */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type SortQuerySelector = { + $in?: T[] + $nin?: T[] + $ne?: T +} +/** + * @public + */ +export type SortRuleQueryType = (T extends Array ? U | U[] : T) | SortQuerySelector + +/** + * @public + */ +export interface SortingRules { + order: SortingOrder + default?: string | number + cases: { + query: SortRuleQueryType + index: string | number + }[] +} + /** * @public */ export type SortingQuery = { - [P in keyof T]?: SortingOrder + [P in keyof T]?: SortingOrder | SortingRules } & { // support nested queries e.g. 'user.friends.name' // this will mark all unrecognized properties as any (including nested queries) - [key: string]: SortingOrder + [key: string]: SortingOrder | SortingRules } /** diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts index 86d8e25fab..e7fa57f180 100644 --- a/packages/core/src/tx.ts +++ b/packages/core/src/tx.ts @@ -13,6 +13,7 @@ // limitations under the License. // +import justClone from 'just-clone' import type { KeysByType } from 'simplytyped' import type { Account, @@ -34,7 +35,6 @@ import { _getOperator } from './operator' import { _toDoc } from './proxy' import type { DocumentQuery, TxResult } from './storage' import { generateId } from './utils' -import justClone from 'just-clone' /** * @public @@ -43,10 +43,34 @@ export interface Tx extends Doc { objectSpace: Ref // space where transaction will operate } +/** + * @public + */ +export enum WorkspaceEvent { + UpgradeScheduled, + Upgrade, + IndexingUpdate +} + /** * Event to be send by server during model upgrade procedure. * @public */ +export interface TxWorkspaceEvent extends Tx { + event: WorkspaceEvent + params: any +} + +/** + * @public + */ +export interface IndexingUpdateEvent { + _class: Ref>[] +} + +/** + * @public + */ export interface TxModelUpgrade extends Tx {} /** diff --git a/packages/kanban/src/components/Kanban.svelte b/packages/kanban/src/components/Kanban.svelte index 3741b8a67e..d6f222706d 100644 --- a/packages/kanban/src/components/Kanban.svelte +++ b/packages/kanban/src/components/Kanban.svelte @@ -13,69 +13,41 @@ // limitations under the License. --> -{#each stateObjects as object, i} +{#each limitedObjects as object, i (object._id)} {@const dragged = isDragging && object._id === dragCard?._id}
{/each} +{#if stateObjects.length > limitedObjects.length} +
+
+ + {limitedObjects.length}/{stateObjects.length} + +
+
+{/if} diff --git a/plugins/task-resources/src/components/KanbanCard.svelte b/plugins/task-resources/src/components/KanbanCard.svelte deleted file mode 100644 index 214c33858e..0000000000 --- a/plugins/task-resources/src/components/KanbanCard.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - - -
-
-
- - {#if todoItems.length > 0} - - ({doneTasks?.length}/{todoItems.length}) - - {/if} -
-
-
- -
- -
-
-
{object.name}
- -
-
- {#if (object.attachments ?? 0) > 0} -
- -
- {/if} - {#if (object.comments ?? 0) > 0} -
- -
- {/if} -
- -
-
diff --git a/plugins/task-resources/src/components/StatusTableView.svelte b/plugins/task-resources/src/components/StatusTableView.svelte index 7bad5df1db..d6b0f23e95 100644 --- a/plugins/task-resources/src/components/StatusTableView.svelte +++ b/plugins/task-resources/src/components/StatusTableView.svelte @@ -64,7 +64,7 @@ itemsDS = doneStates.map((s) => { return { id: s._id, - label: s.title, + label: s.name, icon: s._class === task.class.WonState ? Won : Lost, color: s._class === task.class.WonState ? 'var(--won-color)' : 'var(--lost-color)' } diff --git a/plugins/task-resources/src/components/TaskItem.svelte b/plugins/task-resources/src/components/TaskItem.svelte deleted file mode 100644 index d20b118aa8..0000000000 --- a/plugins/task-resources/src/components/TaskItem.svelte +++ /dev/null @@ -1,48 +0,0 @@ - - - -
- -
- {#if shortLabel}{shortLabel}-{/if}{value.number} -
- {#if name} -
{name}
- {/if} -
- - diff --git a/plugins/task-resources/src/components/TaskPresenter.svelte b/plugins/task-resources/src/components/TaskPresenter.svelte index 234619503c..02620df8e0 100644 --- a/plugins/task-resources/src/components/TaskPresenter.svelte +++ b/plugins/task-resources/src/components/TaskPresenter.svelte @@ -15,12 +15,12 @@ --> {#await cardPresenter then presenter} @@ -106,18 +221,41 @@ /> + groupByKey === noCategory ? tasks : getGroupByValues(groupByDocs, category)} + {setGroupByValues} + {getUpdateProps} + {groupByDocs} + on:content={(evt) => { + listProvider.update(evt.detail) + }} + on:obj-focus={(evt) => { + listProvider.updateFocus(evt.detail) + }} selection={listProvider.current($focusStore)} + checked={$selectionStore ?? []} + on:check={(evt) => { + listProvider.updateSelection(evt.detail.docs, evt.detail.value) + }} + on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)} > + + +
+
+ {#if groupByKey === noCategory} + + + {:else if headerComponent} + + {/if} +
+
+
@@ -127,9 +265,40 @@ {kanban} on:done={(e) => { // eslint-disable-next-line no-undef - onDone({ doneState: e.detail._id }) + onDone(getDoneUpdate(e)) }} />
{/await} + + diff --git a/plugins/task-resources/src/components/state/DoneStatePresenter.svelte b/plugins/task-resources/src/components/state/DoneStatePresenter.svelte index 5a57f259bd..3304098e6d 100644 --- a/plugins/task-resources/src/components/state/DoneStatePresenter.svelte +++ b/plugins/task-resources/src/components/state/DoneStatePresenter.svelte @@ -32,7 +32,7 @@ {#if showTitle} - {value.title} + {value.name} {/if} {/if} diff --git a/plugins/task-resources/src/components/state/DoneStateRefPresenter.svelte b/plugins/task-resources/src/components/state/DoneStateRefPresenter.svelte index 45ae413e9e..32b261da32 100644 --- a/plugins/task-resources/src/components/state/DoneStateRefPresenter.svelte +++ b/plugins/task-resources/src/components/state/DoneStateRefPresenter.svelte @@ -14,20 +14,16 @@ // limitations under the License. --> -{#if state} +{#if value} + {@const state = $statusStore.get(typeof value === 'string' ? value : value.values[0]._id)} {/if} diff --git a/plugins/task-resources/src/components/state/DoneStatesPopup.svelte b/plugins/task-resources/src/components/state/DoneStatesPopup.svelte index cea6a3e334..ad28fe1dc8 100644 --- a/plugins/task-resources/src/components/state/DoneStatesPopup.svelte +++ b/plugins/task-resources/src/components/state/DoneStatesPopup.svelte @@ -59,7 +59,7 @@
- {state.title} + {state.name} {/each} diff --git a/plugins/view-resources/src/components/list/List.svelte b/plugins/view-resources/src/components/list/List.svelte index cd617fd654..dbb3f1f9ad 100644 --- a/plugins/view-resources/src/components/list/List.svelte +++ b/plugins/view-resources/src/components/list/List.svelte @@ -131,14 +131,17 @@ return props.length } - let dragItem: Doc | undefined = undefined + let dragItem: { + doc?: Doc + revert?: () => void + } = {} let listDiv: HTMLDivElement
(space ? { space } : {})} {elementByIndex} {indexById} {docs} diff --git a/plugins/view-resources/src/components/list/ListCategories.svelte b/plugins/view-resources/src/components/list/ListCategories.svelte index 9576b70ed3..f0a3576a2d 100644 --- a/plugins/view-resources/src/components/list/ListCategories.svelte +++ b/plugins/view-resources/src/components/list/ListCategories.svelte @@ -13,13 +13,13 @@ // limitations under the License. --> -{#each categories as category, i (category)} - {@const items = groupedDocs[category] ?? []} +{#each categories as category, i (typeof category === 'object' ? category.name : category)} + {@const items = groupByKey === noCategory || category === undefined ? docs : getGroupByValues(groupByDocs, category)} diff --git a/plugins/view-resources/src/components/status/StatusPresenter.svelte b/plugins/view-resources/src/components/status/StatusPresenter.svelte new file mode 100644 index 0000000000..0ffc97f44a --- /dev/null +++ b/plugins/view-resources/src/components/status/StatusPresenter.svelte @@ -0,0 +1,37 @@ + + + +{#if value} +
+ {#if icon && typeof icon === 'string'} + + {:else if icon !== undefined && typeof icon !== 'string'} + + {/if} + + {value.name} + +
+{/if} diff --git a/plugins/view-resources/src/components/status/StatusRefPresenter.svelte b/plugins/view-resources/src/components/status/StatusRefPresenter.svelte new file mode 100644 index 0000000000..56d28ff6db --- /dev/null +++ b/plugins/view-resources/src/components/status/StatusRefPresenter.svelte @@ -0,0 +1,30 @@ + + + +{#if value} + +{/if} diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 01e846894c..a8cd9ed3d9 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -27,9 +27,12 @@ import ColorsPopup from './components/ColorsPopup.svelte' import DateEditor from './components/DateEditor.svelte' import DatePresenter from './components/DatePresenter.svelte' import DocAttributeBar from './components/DocAttributeBar.svelte' +import DocNavLink from './components/DocNavLink.svelte' import EditBoxPopup from './components/EditBoxPopup.svelte' import EditDoc from './components/EditDoc.svelte' +import EnumArrayEditor from './components/EnumArrayEditor.svelte' import EnumEditor from './components/EnumEditor.svelte' +import EnumPresenter from './components/EnumPresenter.svelte' import FilterBar from './components/filter/FilterBar.svelte' import FilterTypePopup from './components/filter/FilterTypePopup.svelte' import ObjectFilter from './components/filter/ObjectFilter.svelte' @@ -49,11 +52,14 @@ import MarkupEditor from './components/MarkupEditor.svelte' import MarkupEditorPopup from './components/MarkupEditorPopup.svelte' import MarkupPresenter from './components/MarkupPresenter.svelte' import Menu from './components/Menu.svelte' +import TreeItem from './components/navigator/TreeItem.svelte' +import TreeNode from './components/navigator/TreeNode.svelte' import NumberEditor from './components/NumberEditor.svelte' import NumberPresenter from './components/NumberPresenter.svelte' import ObjectPresenter from './components/ObjectPresenter.svelte' import RolePresenter from './components/RolePresenter.svelte' import SpacePresenter from './components/SpacePresenter.svelte' +import SpaceRefPresenter from './components/SpaceRefPresenter.svelte' import StringEditor from './components/StringEditor.svelte' import StringPresenter from './components/StringPresenter.svelte' import Table from './components/Table.svelte' @@ -62,12 +68,8 @@ import TimestampPresenter from './components/TimestampPresenter.svelte' import UpDownNavigator from './components/UpDownNavigator.svelte' import ValueSelector from './components/ValueSelector.svelte' import ViewletSettingButton from './components/ViewletSettingButton.svelte' -import SpaceRefPresenter from './components/SpaceRefPresenter.svelte' -import EnumArrayEditor from './components/EnumArrayEditor.svelte' -import EnumPresenter from './components/EnumPresenter.svelte' -import TreeNode from './components/navigator/TreeNode.svelte' -import TreeItem from './components/navigator/TreeItem.svelte' -import DocNavLink from './components/DocNavLink.svelte' +import StatusPresenter from './components/status/StatusPresenter.svelte' +import StatusRefPresenter from './components/status/StatusRefPresenter.svelte' import { afterResult, @@ -82,11 +84,7 @@ import { import { IndexedDocumentPreview } from '@hcengineering/presentation' import { showEmptyGroups } from './viewOptions' - -function PositionElementAlignment (e?: Event): PopupAlignment | undefined { - return getEventPopupPositionElement(e) -} - +import { statusSort } from './utils' export { getActions, invokeAction } from './actions' export { default as ActionContext } from './components/ActionContext.svelte' export { default as ActionHandler } from './components/ActionHandler.svelte' @@ -95,30 +93,33 @@ export { default as FixedColumn } from './components/FixedColumn.svelte' export { default as SourcePresenter } from './components/inference/SourcePresenter.svelte' export { default as LinkPresenter } from './components/LinkPresenter.svelte' export { default as List } from './components/list/List.svelte' +export { default as MarkupPresenter } from './components/MarkupPresenter.svelte' +export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte' export { default as ContextMenu } from './components/Menu.svelte' export { default as ObjectBox } from './components/ObjectBox.svelte' -export { default as ObjectSearchBox } from './components/ObjectSearchBox.svelte' export { default as ObjectPresenter } from './components/ObjectPresenter.svelte' +export { default as ObjectSearchBox } from './components/ObjectSearchBox.svelte' +export { default as StatusPresenter } from './components/status/StatusPresenter.svelte' +export { default as StatusRefPresenter } from './components/status/StatusRefPresenter.svelte' export { default as TableBrowser } from './components/TableBrowser.svelte' export { default as ValueSelector } from './components/ValueSelector.svelte' -export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte' -export { default as MarkupPresenter } from './components/MarkupPresenter.svelte' export * from './context' export * from './filter' export * from './selection' +export * from './utils' export { buildModel, getActiveViewletId, + getAdditionalHeader, + getCategories, getCollectionCounter, getFiltredKeys, getObjectPresenter, getObjectPreview, + groupBy, isCollectionAttr, LoadingProps, - setActiveViewletId, - getAdditionalHeader, - groupBy, - getCategories + setActiveViewletId } from './utils' export * from './viewOptions' export { @@ -152,6 +153,10 @@ export { EditBoxPopup } +function PositionElementAlignment (e?: Event): PopupAlignment | undefined { + return getEventPopupPositionElement(e) +} + export default async (): Promise => ({ actionImpl, component: { @@ -194,7 +199,9 @@ export default async (): Promise => ({ IndexedDocumentPreview, SpaceRefPresenter, EnumArrayEditor, - EnumPresenter + EnumPresenter, + StatusPresenter, + StatusRefPresenter }, popup: { PositionElementAlignment @@ -208,6 +215,7 @@ export default async (): Promise => ({ FilterAfterResult: afterResult, FilterNestedMatchResult: nestedMatchResult, FilterNestedDontMatchResult: nestedDontMatchResult, - ShowEmptyGroups: showEmptyGroups + ShowEmptyGroups: showEmptyGroups, + StatusSort: statusSort } }) diff --git a/plugins/view-resources/src/plugin.ts b/plugins/view-resources/src/plugin.ts index 63b1b8d1f4..08b08ce4fd 100644 --- a/plugins/view-resources/src/plugin.ts +++ b/plugins/view-resources/src/plugin.ts @@ -16,7 +16,7 @@ import { IntlString, mergeIds } from '@hcengineering/platform' import { AnyComponent } from '@hcengineering/ui' -import view, { viewId } from '@hcengineering/view' +import view, { SortFunc, viewId } from '@hcengineering/view' export default mergeIds(viewId, view, { component: { @@ -64,5 +64,8 @@ export default mergeIds(viewId, view, { Shown: '' as IntlString, ShowEmptyGroups: '' as IntlString, Total: '' as IntlString + }, + function: { + StatusSort: '' as SortFunc } }) diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index 02545fa23a..924bbe9d02 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -16,11 +16,13 @@ import core, { AttachedDoc, + CategoryType, Class, Client, Collection, Doc, DocumentUpdate, + getObjectValue, Hierarchy, Lookup, Obj, @@ -29,11 +31,14 @@ import core, { ReverseLookup, ReverseLookups, Space, + Status, + StatusManager, + StatusValue, TxOperations } from '@hcengineering/core' import type { IntlString } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform' -import { AttributeCategory, getAttributePresenterClass, KeyedAttribute } from '@hcengineering/presentation' +import { AttributeCategory, createQuery, getAttributePresenterClass, KeyedAttribute } from '@hcengineering/presentation' import { AnyComponent, ErrorPresenter, @@ -499,46 +504,180 @@ export type FixedWidthStore = Record export const fixedWidthStore = writable({}) -export function groupBy (docs: T[], key: string): { [key: string]: T[] } { +export function groupBy ( + docs: T[], + key: string, + categories?: CategoryType[] +): { [key: string | number]: T[] } { return docs.reduce((storage: { [key: string]: T[] }, item: T) => { - const group = (item as any)[key] ?? undefined + let group = getObjectValue(key, item) ?? undefined + + if (categories !== undefined) { + for (const c of categories) { + if (typeof c === 'object') { + const st = c.values.find((it) => it._id === group) + if (st !== undefined) { + group = st.name + break + } + } + } + } storage[group] = storage[group] ?? [] - storage[group].push(item) return storage }, {}) } +/** + * @public + */ +export function getGroupByValues ( + groupByDocs: Record, + category: CategoryType +): T[] { + if (typeof category === 'object') { + return groupByDocs[category.name] ?? [] + } else if (category !== undefined) { + return groupByDocs[category] ?? [] + } + return [] +} + +/** + * @public + */ +export function setGroupByValues ( + groupByDocs: Record, + category: CategoryType, + docs: Doc[] +): void { + if (typeof category === 'object') { + groupByDocs[category.name] = docs + } else if (category !== undefined) { + groupByDocs[category] = docs + } +} + +/** + * Group category references into categories. + * @public + */ +export async function groupByCategory ( + client: TxOperations, + _class: Ref>, + key: string, + categories: any[], + mgr: StatusManager, + viewletDescriptorId?: Ref +): Promise { + const h = client.getHierarchy() + const attr = h.getAttribute(_class, key) + if (attr === undefined) return categories + if (key === noCategory) return [undefined] + + const attrClass = getAttributePresenterClass(h, attr).attrClass + + const isStatusField = h.isDerived(attrClass, core.class.Status) + + let existingCategories: any[] = [] + + if (isStatusField) { + existingCategories = await groupByStatusCategories(h, attrClass, categories, mgr, viewletDescriptorId) + } else { + const valueSet = new Set() + for (const v of categories) { + if (!valueSet.has(v)) { + valueSet.add(v) + existingCategories.push(v) + } + } + } + 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() + + for (const v of categories) { + const status = mgr.byId.get(v) + 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) || it.space === status.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 { +): Promise { if (key === noCategory) return [undefined] - const existingCategories = Array.from(new Set(docs.map((x: any) => x[key] ?? undefined))) - return await sortCategories(client, _class, existingCategories, key, viewletDescriptorId) + return await groupByCategory( + client, + _class, + key, + docs.map((it) => getObjectValue(key, it) ?? undefined), + mgr, + viewletDescriptorId + ) } +/** + * @public + */ export async function sortCategories ( - client: TxOperations, - _class: Ref>, + hierarchy: Hierarchy, + attrClass: Ref>, existingCategories: any[], - key: string, viewletDescriptorId?: Ref ): Promise { - if (key === noCategory) return [undefined] - const hierarchy = client.getHierarchy() - const attr = hierarchy.getAttribute(_class, key) - if (attr === undefined) return existingCategories - const attrClass = getAttributePresenterClass(hierarchy, attr).attrClass const clazz = hierarchy.getClass(attrClass) const sortFunc = hierarchy.as(clazz, view.mixin.SortFuncs) - if (sortFunc?.func === undefined) return existingCategories + if (sortFunc?.func === undefined) { + const isStatusField = hierarchy.isDerived(attrClass, core.class.Status) + if (isStatusField) { + existingCategories.sort((a, b) => { + return a.values[0].rank.localeCompare(b.values[0].rank) + }) + } else { + existingCategories.sort((a, b) => { + return JSON.stringify(a).localeCompare(JSON.stringify(b)) + }) + } + + return existingCategories + } const f = await getResource(sortFunc.func) return await f(existingCategories, viewletDescriptorId) @@ -666,3 +805,34 @@ export async function getObjectLinkFragment ( loc.fragment = getPanelURI(component, object._id, Hierarchy.mixinOrClass(object), 'content') return loc } + +export async function statusSort ( + value: Array>, + viewletDescriptorId?: Ref +): Promise>> { + return await new Promise((resolve) => { + // TODO: How we track category updates. + const query = createQuery(true) + query.query( + core.class.Status, + { _id: { $in: value } }, + (res) => { + res.sort((a, b) => { + const res = (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0) + if (res === 0) { + return a.rank.localeCompare(b.rank) + } + return res + }) + + resolve(res.map((p) => p._id)) + query.unsubscribe() + }, + { + sort: { + rank: 1 + } + } + ) + }) +} diff --git a/plugins/view-resources/src/viewOptions.ts b/plugins/view-resources/src/viewOptions.ts index f208156147..5c0ea96958 100644 --- a/plugins/view-resources/src/viewOptions.ts +++ b/plugins/view-resources/src/viewOptions.ts @@ -1,4 +1,4 @@ -import { Class, Doc, Ref, SortingOrder, Space } from '@hcengineering/core' +import core, { Class, Doc, Ref, SortingOrder, Space, StatusManager } from '@hcengineering/core' import { getResource } from '@hcengineering/platform' import { createQuery, getAttributePresenterClass, getClient, LiveQuery } from '@hcengineering/presentation' import { getCurrentLocation, locationToUrl } from '@hcengineering/ui' @@ -6,12 +6,14 @@ import { DropdownViewOption, ToggleViewOption, Viewlet, + ViewletDescriptor, ViewOptionModel, ViewOptions, ViewOptionsModel } from '@hcengineering/view' import { get, writable } from 'svelte/store' import view from './plugin' +import { groupByCategory } from './utils' export const noCategory = '#no_category' @@ -105,7 +107,9 @@ export async function showEmptyGroups ( space: Ref | undefined, key: string, onUpdate: () => void, - queryId: Ref + queryId: Ref, + mgr: StatusManager, + viewletDescriptorId?: Ref ): Promise { const client = getClient() const hierarchy = client.getHierarchy() @@ -114,16 +118,19 @@ export async function showEmptyGroups ( if (attr === undefined) return 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. + const statuses = mgr.statuses.filter((it) => it.ofAttribute === attr._id).map((it) => it._id) + return await groupByCategory(client, _class, key, statuses, mgr) + } + const mixin = hierarchy.as(attributeClass, view.mixin.AllValuesFunc) if (mixin.func !== undefined) { const f = await getResource(mixin.func) const res = await f(space, onUpdate, queryId) if (res !== undefined) { - const sortFunc = hierarchy.as(attributeClass, view.mixin.SortFuncs) - if (sortFunc?.func === undefined) return res - const f = await getResource(sortFunc.func) - - return await f(res) + return await groupByCategory(client, _class, key, res, mgr, viewletDescriptorId) } } } diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 0cccc193c9..b1311a0d4e 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -16,6 +16,7 @@ import type { AnyAttribute, + CategoryType, Class, Client, Doc, @@ -25,9 +26,12 @@ import type { Mixin, Obj, ObjQueryType, + PrimitiveType, Ref, SortingOrder, Space, + StatusManager, + StatusValue, Type, UXObject } from '@hcengineering/core' @@ -36,10 +40,10 @@ import type { Preference } from '@hcengineering/preference' import type { AnyComponent, AnySvelteComponent, - PopupAlignment, - PopupPosAlignment, + Location, Location as PlatformLocation, - Location + PopupAlignment, + PopupPosAlignment } from '@hcengineering/ui' /** @@ -236,7 +240,9 @@ export interface ListHeaderExtra extends Class { /** * @public */ -export type SortFunc = Resource<(values: any[], viewletDescriptorId?: Ref) => Promise> +export type SortFunc = Resource< +(values: (PrimitiveType | StatusValue)[], viewletDescriptorId?: Ref) => Promise +> /** * @public @@ -245,11 +251,20 @@ export interface ClassSortFuncs extends Class { func: SortFunc } +/** + * @public + */ +export type AllValuesFuncGetter = ( + space: Ref | undefined, + onUpdate: () => void, + queryId: Ref +) => Promise + /** * @public */ export interface AllValuesFunc extends Class { - func: Resource<(space: Ref | undefined, onUpdate: () => void, queryId: Ref) => Promise> + func: Resource } /** @@ -486,19 +501,22 @@ export interface ViewOption { actionTarget?: 'query' | 'category' action?: Resource<(value: any, ...params: any) => any> } - /** * @public */ -export type ViewCategoryAction = Resource< -( +export type ViewCategoryActionFunc = ( _class: Ref>, space: Ref | undefined, key: string, onUpdate: () => void, - queryId: Ref -) => Promise -> + queryId: Ref, + mgr: StatusManager, + viewletDescriptorId?: Ref +) => Promise +/** + * @public + */ +export type ViewCategoryAction = Resource /** * @public diff --git a/plugins/workbench-resources/src/components/SpaceView.svelte b/plugins/workbench-resources/src/components/SpaceView.svelte index e2c0778540..fb74bd0b42 100644 --- a/plugins/workbench-resources/src/components/SpaceView.svelte +++ b/plugins/workbench-resources/src/components/SpaceView.svelte @@ -54,7 +54,7 @@ if (attachTo) { viewlets = await client.findAll( view.class.Viewlet, - { attachTo }, + { attachTo, variant: { $exists: false } }, { lookup: { descriptor: view.class.ViewletDescriptor diff --git a/plugins/workbench-resources/src/components/SpecialView.svelte b/plugins/workbench-resources/src/components/SpecialView.svelte index 8313c6bf49..3b46b06962 100644 --- a/plugins/workbench-resources/src/components/SpecialView.svelte +++ b/plugins/workbench-resources/src/components/SpecialView.svelte @@ -26,16 +26,18 @@ Label, Loading, SearchEdit, - showPopup + showPopup, + TabList } from '@hcengineering/ui' import view, { Viewlet, ViewletDescriptor, ViewletPreference } from '@hcengineering/view' import { FilterBar, FilterButton, + getActiveViewletId, getViewOptions, setActiveViewletId, - viewOptionStore, - ViewletSettingButton + ViewletSettingButton, + viewOptionStore } from '@hcengineering/view-resources' export let _class: Ref> @@ -46,7 +48,7 @@ export let createComponent: AnyComponent | undefined export let createComponentProps: Record = {} export let isCreationDisabled = false - export let descriptor: Ref | undefined = undefined + export let descriptors: Ref[] | undefined = [view.viewlet.Table] export let baseQuery: DocumentQuery = {} let search = '' @@ -63,36 +65,45 @@ const client = getClient() let loading = true - $: updateDescriptor(_class, descriptor) - function updateDescriptor (_class: Ref>, descriptor: Ref = view.viewlet.Table) { + let viewlets: WithLookup[] = [] + + $: update(_class, descriptors) + + async function update (_class: Ref>, descriptors?: Ref[]): Promise { loading = true - client - .findOne( - view.class.Viewlet, - { - attachTo: _class, - descriptor - }, - { lookup: { descriptor: view.class.ViewletDescriptor } } - ) - .then((res) => { - viewlet = res - if (res !== undefined) { - setActiveViewletId(res._id) - preferenceQuery.query( - view.class.ViewletPreference, - { - attachedTo: res._id - }, - (res) => { - preference = res[0] - loading = false - }, - { limit: 1 } - ) + viewlets = await client.findAll( + view.class.Viewlet, + { + attachTo: _class, + variant: { $exists: false }, + descriptor: { $in: descriptors ?? [view.viewlet.Table] } + }, + { + lookup: { + descriptor: view.class.ViewletDescriptor } - }) + } + ) + const _id = getActiveViewletId() + preference = undefined + viewlet = viewlets.find((viewlet) => viewlet._id === _id) || viewlets[0] + loading = false + } + + $: if (viewlet !== undefined) { + setActiveViewletId(viewlet._id) + preferenceQuery.query( + view.class.ViewletPreference, + { + attachedTo: viewlet._id + }, + (res) => { + preference = res[0] + loading = false + }, + { limit: 1 } + ) } function showCreateDialog () { @@ -103,6 +114,14 @@ $: twoRows = $deviceInfo.twoRows $: viewOptions = getViewOptions(viewlet, $viewOptionStore) + + $: viewslist = viewlets.map((views) => { + return { + id: views._id, + icon: views.$lookup?.descriptor?.icon, + tooltip: views.$lookup?.descriptor?.label + } + })
@@ -116,6 +135,23 @@
+ {#if viewlets.length > 1} + { + if (result.detail !== undefined) { + if (viewlet?._id === result.detail.id) return + viewlet = viewlets.find((vl) => vl._id === result.detail.id) + if (viewlet) setActiveViewletId(viewlet._id) + } + }} + /> + {/if} + {#if createLabel && createComponent}