From bc3598ce15c7d8b7706b527dc9d4c07a83b47746 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Wed, 11 Oct 2023 13:20:39 +0700 Subject: [PATCH] UBER-953: Fix related issues (#3821) Signed-off-by: Andrey Sobolev --- models/lead/src/index.ts | 25 + models/lead/src/migration.ts | 19 +- models/lead/src/plugin.ts | 3 +- models/recruit/src/index.ts | 2 - models/recruit/src/migration.ts | 32 +- models/tracker/src/actions.ts | 756 ++++++ models/tracker/src/index.ts | 2082 ++--------------- models/tracker/src/migration.ts | 2 +- models/tracker/src/plugin.ts | 7 +- models/tracker/src/presenters.ts | 144 ++ models/tracker/src/types.ts | 388 +++ models/tracker/src/viewlets.ts | 542 +++++ packages/core/src/operations.ts | 13 +- .../src/components/SpaceCreateCard.svelte | 3 +- .../src/components/SpaceSelect.svelte | 3 +- .../src/components/SpaceSelector.svelte | 2 + plugins/lead-assets/lang/en.json | 3 +- plugins/lead-assets/lang/ru.json | 3 +- .../src/components/CreateFunnel.svelte | 47 +- .../src/components/CreateLead.svelte | 2 +- plugins/task-resources/src/plugin.ts | 1 - plugins/tracker-assets/lang/en.json | 4 +- plugins/tracker-assets/lang/ru.json | 4 +- plugins/tracker-resources/package.json | 3 +- .../src/components/CreateIssue.svelte | 100 +- .../src/components/EditRelatedTargets.svelte | 137 ++ .../components/EditRelatedTargetsPopup.svelte | 23 + plugins/tracker-resources/src/index.ts | 6 +- plugins/tracker-resources/src/plugin.ts | 5 +- plugins/tracker/src/index.ts | 32 +- 30 files changed, 2503 insertions(+), 1890 deletions(-) create mode 100644 models/tracker/src/actions.ts create mode 100644 models/tracker/src/presenters.ts create mode 100644 models/tracker/src/types.ts create mode 100644 models/tracker/src/viewlets.ts create mode 100644 plugins/tracker-resources/src/components/EditRelatedTargets.svelte create mode 100644 plugins/tracker-resources/src/components/EditRelatedTargetsPopup.svelte diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index 8bfba3750e..030291c2d2 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -189,6 +189,9 @@ export function createModel (builder: Builder): void { createAction(builder, { ...viewTemplates.open, target: lead.class.Funnel, + query: { + archived: true + }, context: { mode: ['browser', 'context'], group: 'create' @@ -645,4 +648,26 @@ export function createModel (builder: Builder): void { }, lead.action.CreateGlobalLead ) + + createAction(builder, { + action: view.actionImpl.ShowPopup, + actionProps: { + component: lead.component.CreateFunnel, + _id: 'customer', + element: 'top', + fillProps: { + _object: 'funnel' + } + }, + label: lead.string.EditFunnel, + icon: lead.icon.Funnel, + input: 'focus', + category: lead.category.Lead, + target: lead.class.Funnel, + override: [view.action.Open], + context: { + mode: ['context', 'browser'], + group: 'associate' + } + }) } diff --git a/models/lead/src/migration.ts b/models/lead/src/migration.ts index bc47ee5770..d599a028d6 100644 --- a/models/lead/src/migration.ts +++ b/models/lead/src/migration.ts @@ -14,9 +14,11 @@ // import { Ref, TxOperations } from '@hcengineering/core' -import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model' +import { leadId } from '@hcengineering/lead' +import { MigrateOperation, MigrationClient, MigrationUpgradeClient, tryUpgrade } from '@hcengineering/model' import core from '@hcengineering/model-core' import { createKanbanTemplate, createSequence } from '@hcengineering/model-task' +import tracker from '@hcengineering/model-tracker' import task, { KanbanTemplate, createStates } from '@hcengineering/task' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' import lead from './plugin' @@ -120,5 +122,20 @@ export const leadOperation: MigrateOperation = { async upgrade (client: MigrationUpgradeClient): Promise { const ops = new TxOperations(client, core.account.System) await createDefaults(ops) + + await tryUpgrade(client, leadId, [ + { + state: 'related-targets', + func: async (client): Promise => { + const ops = new TxOperations(client, core.account.ConfigUser) + await ops.createDoc(tracker.class.RelatedIssueTarget, core.space.Configuration, { + rule: { + kind: 'classRule', + ofClass: lead.class.Lead + } + }) + } + } + ]) } } diff --git a/models/lead/src/plugin.ts b/models/lead/src/plugin.ts index b409b0b879..a350c4c216 100644 --- a/models/lead/src/plugin.ts +++ b/models/lead/src/plugin.ts @@ -32,7 +32,8 @@ export default mergeIds(leadId, lead, { Title: '' as IntlString, ManageFunnelStatuses: '' as IntlString, GotoLeadApplication: '' as IntlString, - ConfigDescription: '' as IntlString + ConfigDescription: '' as IntlString, + EditFunnel: '' as IntlString }, component: { CreateLead: '' as AnyComponent, diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 0e7d0f8fdb..83413190f2 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -88,8 +88,6 @@ export class TVacancy extends TSpaceWithStates implements Vacancy { @Prop(Collection(chunter.class.Comment), chunter.string.Comments) comments?: number - relations!: number - @Prop(TypeString(), recruit.string.Vacancy) @Index(IndexKind.FullText) @Hidden() diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index 8c920a442f..724a71bb45 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -15,12 +15,20 @@ import { getCategories } from '@anticrm/skillset' import core, { Doc, Ref, Space, TxOperations } from '@hcengineering/core' -import { MigrateOperation, MigrationClient, MigrationUpgradeClient, createOrUpdate } from '@hcengineering/model' +import { + MigrateOperation, + MigrationClient, + MigrationUpgradeClient, + createOrUpdate, + tryUpgrade +} from '@hcengineering/model' import tags, { TagCategory } from '@hcengineering/model-tags' import { createKanbanTemplate, createSequence } from '@hcengineering/model-task' import task, { KanbanTemplate } from '@hcengineering/task' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' import recruit from './plugin' +import { recruitId } from '@hcengineering/recruit' +import tracker from '@hcengineering/model-tracker' export const recruitOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise {}, @@ -28,6 +36,28 @@ export const recruitOperation: MigrateOperation = { const tx = new TxOperations(client, core.account.System) await createDefaults(tx) await fixTemplateSpace(tx) + + await tryUpgrade(client, recruitId, [ + { + state: 'related-targets', + func: async (client): Promise => { + const ops = new TxOperations(client, core.account.ConfigUser) + await ops.createDoc(tracker.class.RelatedIssueTarget, core.space.Configuration, { + rule: { + kind: 'classRule', + ofClass: recruit.class.Vacancy + } + }) + + await ops.createDoc(tracker.class.RelatedIssueTarget, core.space.Configuration, { + rule: { + kind: 'classRule', + ofClass: recruit.class.Applicant + } + }) + } + } + ]) } } diff --git a/models/tracker/src/actions.ts b/models/tracker/src/actions.ts new file mode 100644 index 0000000000..73d62c9f38 --- /dev/null +++ b/models/tracker/src/actions.ts @@ -0,0 +1,756 @@ +// +// 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 contact from '@hcengineering/contact' +import { Builder } from '@hcengineering/model' +import core from '@hcengineering/model-core' +import task from '@hcengineering/model-task' +import view, { actionTemplates, createAction } from '@hcengineering/model-view' +import workbench, { createNavigateAction } from '@hcengineering/model-workbench' +import { IntlString } from '@hcengineering/platform' +import { trackerId } from '@hcengineering/tracker' +import { KeyBinding, ViewAction } from '@hcengineering/view' +import tracker from './plugin' + +import tags from '@hcengineering/tags' +import { defaultPriorities, issuePriorities } from '@hcengineering/tracker-resources/src/types' + +function createGotoSpecialAction ( + builder: Builder, + id: string, + key: KeyBinding, + label: IntlString, + query?: Record +): void { + createNavigateAction(builder, key, label, tracker.app.Tracker, { + application: trackerId, + mode: 'space', + spaceSpecial: id, + spaceClass: tracker.class.Project, + query + }) +} +export function createActions (builder: Builder, issuesId: string, componentsId: string, myIssuesId: string): void { + createGotoSpecialAction(builder, issuesId, 'keyG->keyE', tracker.string.GotoIssues) + createGotoSpecialAction(builder, issuesId, 'keyG->keyA', tracker.string.GotoActive, { mode: 'active' }) + createGotoSpecialAction(builder, issuesId, 'keyG->keyB', tracker.string.GotoBacklog, { mode: 'backlog' }) + createGotoSpecialAction(builder, componentsId, 'keyG->keyC', tracker.string.GotoComponents) + createNavigateAction(builder, 'keyG->keyM', tracker.string.GotoMyIssues, tracker.app.Tracker, { + application: trackerId, + mode: 'special', + special: myIssuesId + }) + + createAction(builder, { + action: workbench.actionImpl.Navigate, + actionProps: { + mode: 'app', + application: trackerId + }, + label: tracker.string.GotoTrackerApplication, + icon: view.icon.ArrowRight, + input: 'none', + category: view.category.Navigation, + target: core.class.Doc, + context: { + mode: ['workbench', 'browser', 'editor', 'panel', 'popup'] + } + }) + + createAction( + builder, + { + action: tracker.actionImpl.EditWorkflowStatuses, + label: tracker.string.EditWorkflowStatuses, + icon: view.icon.Statuses, + input: 'focus', + category: tracker.category.Tracker, + target: tracker.class.Project, + override: [task.action.EditStatuses], + query: {}, + context: { + mode: ['context', 'browser'], + group: 'edit' + } + }, + tracker.action.EditWorkflowStatuses + ) + + createAction( + builder, + { + action: tracker.actionImpl.EditProject, + label: tracker.string.EditProject, + icon: contact.icon.Edit, + input: 'focus', + category: tracker.category.Tracker, + target: tracker.class.Project, + query: {}, + context: { + mode: ['context', 'browser'], + group: 'edit' + } + }, + tracker.action.EditProject + ) + + createAction( + builder, + { + action: tracker.actionImpl.DeleteProject, + label: workbench.string.Archive, + icon: view.icon.Archive, + input: 'focus', + category: tracker.category.Tracker, + target: tracker.class.Project, + query: { + archived: false + }, + context: { + mode: ['context', 'browser'], + group: 'edit' + }, + override: [view.action.Archive, view.action.Delete] + }, + tracker.action.DeleteProject + ) + createAction( + builder, + { + action: tracker.actionImpl.DeleteProject, + label: workbench.string.Delete, + icon: view.icon.Delete, + input: 'focus', + category: tracker.category.Tracker, + target: tracker.class.Project, + query: { + archived: true + }, + context: { + mode: ['context', 'browser'], + group: 'edit' + }, + override: [view.action.Archive, view.action.Delete] + }, + tracker.action.DeleteProjectClean + ) + createAction(builder, { + label: tracker.string.Unarchive, + icon: view.icon.Archive, + action: view.actionImpl.UpdateDocument as ViewAction, + actionProps: { + key: 'archived', + ask: true, + value: false, + label: tracker.string.Unarchive, + message: tracker.string.UnarchiveConfirm + }, + input: 'any', + category: tracker.category.Tracker, + query: { + archived: true + }, + context: { + mode: ['context', 'browser'], + group: 'tools' + }, + target: tracker.class.Project + }) + + createAction( + builder, + { + action: tracker.actionImpl.DeleteIssue, + label: workbench.string.Delete, + icon: view.icon.Delete, + input: 'any', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + group: 'remove' + }, + override: [view.action.Delete] + }, + tracker.action.DeleteIssue + ) + + builder.createDoc( + view.class.ActionCategory, + core.space.Model, + { label: tracker.string.TrackerApplication, visible: true }, + tracker.category.Tracker + ) + + createAction( + builder, + { + action: view.actionImpl.ShowPopup, + actionProps: { + component: tracker.component.CreateIssue, + element: 'top' + }, + label: tracker.string.NewIssue, + icon: tracker.icon.NewIssue, + keyBinding: ['keyC'], + input: 'none', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['browser'], + application: tracker.app.Tracker, + group: 'create' + } + }, + tracker.action.NewIssue + ) + + createAction( + builder, + { + action: view.actionImpl.ShowPopup, + actionProps: { + component: tracker.component.CreateIssue, + element: 'top', + fillProps: { + _object: 'parentIssue', + space: 'space' + } + }, + label: tracker.string.NewSubIssue, + icon: tracker.icon.Subissue, + keyBinding: [], + input: 'focus', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'associate' + } + }, + tracker.action.NewSubIssue + ) + + createAction( + builder, + { + action: view.actionImpl.ShowPopup, + actionProps: { + component: tracker.component.SetParentIssueActionPopup, + element: 'top', + fillProps: { + _objects: 'value' + } + }, + label: tracker.string.SetParent, + icon: tracker.icon.Parent, + keyBinding: [], + input: 'none', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context'], + application: tracker.app.Tracker, + group: 'associate' + } + }, + tracker.action.SetParent + ) + + createAction( + builder, + { + action: view.actionImpl.ShowPopup, + actionProps: { + component: tracker.component.CreateIssue, + element: 'top', + fillProps: { + _object: 'relatedTo', + space: 'space' + } + }, + label: tracker.string.NewRelatedIssue, + icon: tracker.icon.NewIssue, + keyBinding: [], + input: 'focus', + category: tracker.category.Tracker, + target: core.class.Doc, + context: { + mode: ['context', 'browser', 'editor'], + group: 'associate' + } + }, + tracker.action.NewRelatedIssue + ) + + createAction(builder, { + action: view.actionImpl.ShowPopup, + actionPopup: tracker.component.SetParentIssueActionPopup, + actionProps: { + component: tracker.component.SetParentIssueActionPopup, + element: 'top', + fillProps: { + _object: 'value' + } + }, + label: tracker.string.SetParent, + icon: tracker.icon.Parent, + keyBinding: [], + input: 'none', + category: tracker.category.Tracker, + target: tracker.class.Issue, + override: [tracker.action.SetParent], + context: { + mode: ['browser'], + application: tracker.app.Tracker, + group: 'associate' + } + }) + + createAction(builder, { + ...actionTemplates.open, + actionProps: { + component: tracker.component.EditIssue + }, + target: tracker.class.Issue, + context: { + mode: ['browser', 'context'], + group: 'create' + }, + override: [view.action.Open] + }) + + createAction(builder, { + ...actionTemplates.open, + actionProps: { + component: tracker.component.EditIssueTemplate + }, + target: tracker.class.IssueTemplate, + context: { + mode: ['browser', 'context'], + group: 'create' + }, + override: [view.action.Open] + }) + + createAction(builder, { + action: view.actionImpl.ShowPopup, + actionProps: { + component: tracker.component.TimeSpendReportPopup, + fillProps: { + _object: 'issue' + } + }, + label: tracker.string.TimeSpendReportAdd, + icon: tracker.icon.TimeReport, + input: 'focus', + keyBinding: ['keyT'], + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'edit' + } + }) + + createAction( + builder, + { + action: task.actionImpl.SelectStatus, + actionPopup: task.component.StatusSelector, + actionProps: { + _class: tracker.class.IssueStatus, + ofAttribute: tracker.attribute.IssueStatus, + placeholder: tracker.string.Status + }, + label: tracker.string.Status, + icon: tracker.icon.CategoryBacklog, + keyBinding: ['keyS->keyS'], + input: 'any', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'edit' + } + }, + tracker.action.SetStatus + ) + + createAction( + builder, + { + action: view.actionImpl.ValueSelector, + actionPopup: view.component.ValueSelector, + actionProps: { + attribute: 'priority', + values: defaultPriorities.map((p) => ({ id: p, ...issuePriorities[p] })), + placeholder: tracker.string.SetPriority + }, + label: tracker.string.Priority, + icon: tracker.icon.PriorityHigh, + keyBinding: ['keyP->keyR'], + input: 'any', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'edit' + } + }, + tracker.action.SetPriority + ) + createAction( + builder, + { + action: view.actionImpl.ValueSelector, + actionPopup: view.component.ValueSelector, + actionProps: { + attribute: 'assignee', + _class: contact.mixin.Employee, + query: {}, + placeholder: tracker.string.AssignTo + }, + label: tracker.string.Assignee, + icon: contact.icon.Person, + keyBinding: ['keyA'], + input: 'any', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'edit' + } + }, + tracker.action.SetAssignee + ) + + createAction( + builder, + { + action: view.actionImpl.ValueSelector, + actionPopup: view.component.ValueSelector, + actionProps: { + attribute: 'component', + _class: tracker.class.Component, + query: {}, + fillQuery: { space: 'space' }, + docMatches: ['space'], + searchField: 'label', + placeholder: tracker.string.Component + }, + label: tracker.string.Component, + icon: tracker.icon.Component, + keyBinding: ['keyM->keyT'], + input: 'any', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'edit' + } + }, + tracker.action.SetComponent + ) + + createAction( + builder, + { + action: view.actionImpl.AttributeSelector, + actionPopup: tracker.component.MilestoneEditor, + actionProps: { + attribute: 'milestone', + isAction: true + }, + label: tracker.string.Milestone, + icon: tracker.icon.Milestone, + keyBinding: ['keyS->keyP'], + input: 'any', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'edit' + } + }, + tracker.action.SetMilestone + ) + + createAction( + builder, + { + action: view.actionImpl.ShowPopup, + actionProps: { + component: tags.component.TagsEditorPopup, + element: 'top', + fillProps: { + _object: 'object' + } + }, + label: tracker.string.Labels, + icon: tags.icon.Tags, + keyBinding: ['keyL'], + input: 'focus', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'edit' + } + }, + tracker.action.SetLabels + ) + + createAction( + builder, + { + action: view.actionImpl.ShowPopup, + actionProps: { + component: tags.component.ObjectsTagsEditorPopup, + element: 'top', + fillProps: { + _objects: 'value' + } + }, + label: tracker.string.Labels, + icon: tags.icon.Tags, + keyBinding: ['keyL'], + input: 'selection', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'edit' + } + }, + tracker.action.SetLabels + ) + + createAction( + builder, + { + action: view.actionImpl.ShowPopup, + actionProps: { + component: tracker.component.SetDueDateActionPopup, + props: { mondayStart: true, withTime: false }, + element: 'top', + fillProps: { + _objects: 'value' + } + }, + label: tracker.string.SetDueDate, + icon: tracker.icon.DueDate, + keyBinding: ['keyD'], + input: 'any', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'edit' + } + }, + tracker.action.SetDueDate + ) + createAction( + builder, + { + action: view.actionImpl.CopyTextToClipboard, + actionProps: { + textProvider: tracker.function.GetIssueId + }, + label: tracker.string.CopyIssueId, + icon: view.icon.CopyId, + keyBinding: [], + input: 'focus', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'copy' + } + }, + tracker.action.CopyIssueId + ) + createAction( + builder, + { + action: view.actionImpl.CopyTextToClipboard, + actionProps: { + textProvider: tracker.function.GetIssueTitle + }, + label: tracker.string.CopyIssueTitle, + icon: tracker.icon.CopyBranch, + keyBinding: [], + input: 'focus', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'copy' + } + }, + tracker.action.CopyIssueTitle + ) + createAction( + builder, + { + action: view.actionImpl.CopyTextToClipboard, + actionProps: { + textProvider: tracker.function.GetIssueLink + }, + label: tracker.string.CopyIssueUrl, + icon: view.icon.CopyLink, + keyBinding: [], + input: 'focus', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'copy' + } + }, + tracker.action.CopyIssueLink + ) + createAction( + builder, + { + action: tracker.actionImpl.Move, + label: tracker.string.MoveToProject, + icon: view.icon.Move, + keyBinding: [], + input: 'any', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'associate' + }, + override: [task.action.Move] + }, + tracker.action.MoveToProject + ) + createAction( + builder, + { + action: view.actionImpl.ValueSelector, + actionPopup: tracker.component.RelationsPopup, + actionProps: { + attribute: '' + }, + label: tracker.string.Relations, + icon: tracker.icon.Relations, + keyBinding: [], + input: 'focus', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'associate' + } + }, + tracker.action.Relations + ) + createAction( + builder, + { + action: view.actionImpl.ShowPopup, + actionProps: { + component: tracker.component.CreateIssue, + element: 'top', + fillProps: { + _object: 'originalIssue', + space: 'space' + } + }, + label: tracker.string.Duplicate, + icon: tracker.icon.Duplicate, + keyBinding: [], + input: 'focus', + category: tracker.category.Tracker, + target: tracker.class.Issue, + context: { + mode: ['context', 'browser'], + application: tracker.app.Tracker, + group: 'associate' + } + }, + tracker.action.Duplicate + ) + + createAction( + builder, + { + action: tracker.actionImpl.DeleteMilestone, + label: view.string.Delete, + icon: view.icon.Delete, + keyBinding: ['Meta + Backspace'], + category: tracker.category.Tracker, + input: 'any', + target: tracker.class.Milestone, + context: { mode: ['context', 'browser'], group: 'remove' } + }, + tracker.action.DeleteMilestone + ) + builder.mixin(tracker.class.Project, core.class.Class, view.mixin.IgnoreActions, { + actions: [view.action.Open] + }) + builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.IgnoreActions, { + actions: [view.action.Delete] + }) + + createAction( + builder, + { + action: view.actionImpl.ShowPopup, + actionProps: { + component: tracker.component.EditRelatedTargetsPopup, + element: 'top', + fillProps: { + _objects: 'value' + } + }, + label: tracker.string.MapRelatedIssues, + icon: tracker.icon.Relations, + keyBinding: [], + input: 'none', + category: tracker.category.Tracker, + target: core.class.Space, + query: { + _class: { $nin: [tracker.class.Project] } + }, + context: { + mode: ['context'], + application: tracker.app.Tracker, + group: 'associate' + } + }, + tracker.action.EditRelatedTargets + ) +} diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 78ff93bb8e..08bcb6fcf2 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -14,800 +14,121 @@ // import activity from '@hcengineering/activity' -import contact, { Employee, Person } from '@hcengineering/contact' -import { - DOMAIN_MODEL, - DateRangeMode, - Domain, - IndexKind, - Markup, - Ref, - RelatedDocument, - SortingOrder, - Timestamp, - Type -} from '@hcengineering/core' -import { - ArrOf, - Builder, - Collection, - Hidden, - Index, - Mixin, - Model, - Prop, - ReadOnly, - TypeDate, - TypeMarkup, - TypeNumber, - TypeRef, - TypeString, - UX -} from '@hcengineering/model' -import attachment from '@hcengineering/model-attachment' -import chunter from '@hcengineering/model-chunter' -import core, { TAttachedDoc, TClass, TDoc, TStatus, TType } from '@hcengineering/model-core' -import task, { TSpaceWithStates, TTask } from '@hcengineering/model-task' -import view, { actionTemplates, classPresenter, createAction, showColorsViewOption } from '@hcengineering/model-view' -import workbench, { createNavigateAction } from '@hcengineering/model-workbench' +import { Builder } from '@hcengineering/model' +import core from '@hcengineering/model-core' +import task from '@hcengineering/model-task' +import view from '@hcengineering/model-view' +import workbench from '@hcengineering/model-workbench' import notification from '@hcengineering/notification' -import { IntlString, Resource } from '@hcengineering/platform' import setting from '@hcengineering/setting' -import tags, { TagElement } from '@hcengineering/tags' -import { DoneState } from '@hcengineering/task' -import { - Component, - Issue, - IssueChildInfo, - IssueParentInfo, - IssuePriority, - IssueStatus, - IssueTemplate, - IssueTemplateChild, - IssueUpdateFunction, - Milestone, - MilestoneStatus, - Project, - ProjectIssueTargetOptions, - TimeReportDayType, - TimeSpendReport, - trackerId -} from '@hcengineering/tracker' -import { BuildModelKey, KeyBinding, ViewAction, ViewOptionsModel } from '@hcengineering/view' +import { trackerId } from '@hcengineering/tracker' import tracker from './plugin' import { generateClassNotificationTypes } from '@hcengineering/model-notification' import presentation from '@hcengineering/model-presentation' -import { defaultPriorities, issuePriorities } from '@hcengineering/tracker-resources/src/types' -import { AnyComponent } from '@hcengineering/ui' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' +import { createActions as defineActions } from './actions' +import { definePresenters } from './presenters' +import { + TComponent, + TIssue, + TIssueStatus, + TIssueTemplate, + TMilestone, + TProject, + TProjectIssueTargetOptions, + TRelatedIssueTarget, + TTimeSpendReport, + TTypeIssuePriority, + TTypeMilestoneStatus, + TTypeReportedTime +} from './types' +import { defineViewlets } from './viewlets' + +export * from './types' +export { issuesOptions } from './viewlets' export { trackerId } from '@hcengineering/tracker' export { trackerOperation } from './migration' export { default } from './plugin' -export const DOMAIN_TRACKER = 'tracker' as Domain - -/** - * @public - */ -@Model(tracker.class.IssueStatus, core.class.Status) -@UX(tracker.string.IssueStatuses, undefined, undefined, 'rank', 'name') -export class TIssueStatus extends TStatus implements IssueStatus {} - -/** - * @public - */ -export function TypeIssuePriority (): Type { - return { _class: tracker.class.TypeIssuePriority, label: tracker.string.TypeIssuePriority } -} - -/** - * @public - */ -@Model(tracker.class.TypeIssuePriority, core.class.Type, DOMAIN_MODEL) -export class TTypeIssuePriority extends TType {} - -/** - * @public - */ -export function TypeMilestoneStatus (): Type { - return { _class: tracker.class.TypeMilestoneStatus, label: 'TypeMilestoneStatus' as IntlString } -} - -/** - * @public - */ -@Model(tracker.class.TypeMilestoneStatus, core.class.Type, DOMAIN_MODEL) -export class TTypeMilestoneStatus extends TType {} - -/** - * @public - */ -@Model(tracker.class.Project, task.class.SpaceWithStates) -@UX(tracker.string.Project, tracker.icon.Issues, 'Project', 'name') -export class TProject extends TSpaceWithStates implements Project { - @Prop(TypeString(), tracker.string.ProjectIdentifier) - @Index(IndexKind.FullText) - identifier!: IntlString - - @Prop(TypeNumber(), tracker.string.Number) - @Hidden() - sequence!: number - - @Prop(TypeRef(tracker.class.IssueStatus), tracker.string.DefaultIssueStatus) - defaultIssueStatus!: Ref - - @Prop(TypeRef(contact.mixin.Employee), tracker.string.DefaultAssignee) - defaultAssignee!: Ref - - declare defaultTimeReportDay: TimeReportDayType -} - -/** - * @public - */ -export function TypeReportedTime (): Type { - return { _class: tracker.class.TypeReportedTime, label: core.string.Number } -} - -/** - * @public - */ -@Model(tracker.class.Issue, task.class.Task) -@UX(tracker.string.Issue, tracker.icon.Issue, 'TSK', 'title') -export class TIssue extends TTask implements Issue { - @Prop(TypeRef(tracker.class.Issue), tracker.string.Parent) - declare attachedTo: Ref - - @Prop(TypeString(), tracker.string.Title) - @Index(IndexKind.FullText) - title!: string - - @Prop(TypeMarkup(), tracker.string.Description) - @Index(IndexKind.FullText) - description!: Markup - - @Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status, { - _id: tracker.attribute.IssueStatus, - iconComponent: tracker.activity.StatusIcon +function defineSortAndGrouping (builder: Builder): void { + builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.SortFuncs, { + func: tracker.function.IssueStatusSort }) - @Index(IndexKind.Indexed) - declare status: Ref - @Prop(TypeIssuePriority(), tracker.string.Priority, { - iconComponent: tracker.activity.PriorityIcon + builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.SortFuncs, { + func: tracker.function.IssuePrioritySort }) - @Index(IndexKind.Indexed) - priority!: IssuePriority - @Prop(TypeNumber(), tracker.string.Number) - @Index(IndexKind.FullText) - @ReadOnly() - declare number: number + builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.SortFuncs, { + func: tracker.function.MilestoneSort + }) - @Prop(TypeRef(contact.class.Person), tracker.string.Assignee) - @Index(IndexKind.Indexed) - declare assignee: Ref | null + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.Aggregation, { + createAggregationManager: tracker.aggregation.CreateComponentAggregationManager + }) - @Prop(TypeRef(tracker.class.Component), tracker.string.Component, { icon: tracker.icon.Component }) - @Index(IndexKind.Indexed) - component!: Ref | null + builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AllValuesFunc, { + func: tracker.function.GetAllPriority + }) - @Prop(Collection(tracker.class.Issue), tracker.string.SubIssues) - subIssues!: number + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.AllValuesFunc, { + func: tracker.function.GetAllComponents + }) - @Prop(ArrOf(TypeRef(core.class.TypeRelatedDocument)), tracker.string.BlockedBy) - blockedBy!: RelatedDocument[] - - @Prop(ArrOf(TypeRef(core.class.TypeRelatedDocument)), tracker.string.RelatedTo) - @Index(IndexKind.Indexed) - relations!: RelatedDocument[] - - parents!: IssueParentInfo[] - - @Prop(Collection(tags.class.TagReference), tracker.string.Labels) - declare labels: number - - @Prop(TypeRef(task.class.DoneState), task.string.TaskStateDone, { _id: task.attribute.DoneState }) - @Hidden() - declare doneState: Ref | null - - @Prop(TypeRef(tracker.class.Project), tracker.string.Project, { icon: tracker.icon.Issues }) - @Index(IndexKind.Indexed) - @ReadOnly() - declare space: Ref - - @Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.DueDate) - declare dueDate: Timestamp | null - - @Prop(TypeString(), tracker.string.Rank) - @Hidden() - declare rank: string - - @Prop(TypeRef(tracker.class.Milestone), tracker.string.Milestone, { icon: tracker.icon.Milestone }) - @Index(IndexKind.Indexed) - milestone!: Ref | null - - @Prop(TypeNumber(), tracker.string.Estimation) - estimation!: number - - @Prop(TypeReportedTime(), tracker.string.ReportedTime) - @ReadOnly() - reportedTime!: number - - // A fully virtual property with calculated content. - // TODO: Add proper support for this kind of fields - @Prop(TypeNumber(), tracker.string.RemainingTime) - @ReadOnly() - @Hidden() - remainingTime!: number - - @Prop(Collection(tracker.class.TimeSpendReport), tracker.string.TimeSpendReports) - reports!: number - - declare childInfo: IssueChildInfo[] + builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.AllValuesFunc, { + func: tracker.function.GetAllMilestones + }) } -/** - * @public - */ -@Model(tracker.class.IssueTemplate, core.class.Doc, DOMAIN_TRACKER) -@UX(tracker.string.IssueTemplate, tracker.icon.Issue, 'PROCESS') -export class TIssueTemplate extends TDoc implements IssueTemplate { - @Prop(TypeString(), tracker.string.Title) - @Index(IndexKind.FullText) - title!: string - - @Prop(TypeMarkup(), tracker.string.Description) - @Index(IndexKind.FullText) - description!: Markup - - @Prop(TypeIssuePriority(), tracker.string.Priority) - priority!: IssuePriority - - @Prop(TypeRef(contact.class.Person), tracker.string.Assignee) - assignee!: Ref | null - - @Prop(TypeRef(tracker.class.Component), tracker.string.Component) - component!: Ref | null - - @Prop(ArrOf(TypeRef(tags.class.TagElement)), tracker.string.Labels) - labels?: Ref[] - - declare space: Ref - - @Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.DueDate) - dueDate!: Timestamp | null - - @Prop(TypeRef(tracker.class.Milestone), tracker.string.Milestone) - milestone!: Ref | null - - @Prop(TypeNumber(), tracker.string.Estimation) - estimation!: number - - @Prop(ArrOf(TypeRef(tracker.class.IssueTemplate)), tracker.string.IssueTemplate) - children!: IssueTemplateChild[] - - @Prop(Collection(chunter.class.Comment), tracker.string.Comments) - comments!: number - - @Prop(Collection(attachment.class.Attachment), tracker.string.Attachments) - attachments!: number - - @Prop(ArrOf(TypeRef(core.class.TypeRelatedDocument)), tracker.string.RelatedTo) - relations!: RelatedDocument[] -} - -/** - * @public - */ -@Model(tracker.class.TimeSpendReport, core.class.AttachedDoc, DOMAIN_TRACKER) -@UX(tracker.string.TimeSpendReport, tracker.icon.TimeReport) -export class TTimeSpendReport extends TAttachedDoc implements TimeSpendReport { - @Prop(TypeRef(tracker.class.Issue), tracker.string.Issue) - declare attachedTo: Ref - - @Prop(TypeRef(contact.mixin.Employee), contact.string.Employee) - employee!: Ref - - @Prop(TypeDate(), tracker.string.TimeSpendReportDate) - date!: Timestamp | null - - @Prop(TypeNumber(), tracker.string.TimeSpendReportValue) - value!: number - - @Prop(TypeString(), tracker.string.TimeSpendReportDescription) - description!: string -} - -/** - * @public - */ -@Model(tracker.class.Component, core.class.Doc, DOMAIN_TRACKER) -@UX(tracker.string.Component, tracker.icon.Component, 'COMPONENT', 'label') -export class TComponent extends TDoc implements Component { - @Prop(TypeString(), tracker.string.Title) - @Index(IndexKind.FullText) - label!: string - - @Prop(TypeMarkup(), tracker.string.Description) - description?: Markup - - @Prop(TypeRef(contact.mixin.Employee), tracker.string.ComponentLead) - lead!: Ref | null - - @Prop(Collection(chunter.class.Comment), chunter.string.Comments) - comments!: number - - @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) - attachments?: number - - declare space: Ref -} - -@Mixin(tracker.mixin.ProjectIssueTargetOptions, core.class.Class) -export class TProjectIssueTargetOptions extends TClass implements ProjectIssueTargetOptions { - headerComponent!: AnyComponent - bodyComponent!: AnyComponent - footerComponent!: AnyComponent - - update!: Resource -} -/** - * @public - */ -@Model(tracker.class.Milestone, core.class.Doc, DOMAIN_TRACKER) -@UX(tracker.string.Milestone, tracker.icon.Milestone, '', 'label') -export class TMilestone extends TDoc implements Milestone { - @Prop(TypeString(), tracker.string.Title) - // @Index(IndexKind.FullText) - label!: string - - @Prop(TypeMarkup(), tracker.string.Description) - description?: Markup - - @Prop(TypeMilestoneStatus(), tracker.string.Status) - @Index(IndexKind.Indexed) - status!: MilestoneStatus - - @Prop(Collection(chunter.class.Comment), chunter.string.Comments) - comments!: number - - @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) - attachments?: number - - @Prop(TypeDate(), tracker.string.TargetDate) - targetDate!: Timestamp - - declare space: Ref -} - -@UX(core.string.Number) -@Model(tracker.class.TypeReportedTime, core.class.Type) -export class TTypeReportedTime extends TType {} - -export const issuesOptions = (kanban: boolean): ViewOptionsModel => ({ - groupBy: ['status', 'assignee', 'priority', 'component', 'milestone', 'createdBy', 'modifiedBy'], - orderBy: [ - ['status', SortingOrder.Ascending], - ['priority', SortingOrder.Descending], - ['modifiedOn', SortingOrder.Descending], - ['createdOn', SortingOrder.Descending], - ['dueDate', SortingOrder.Ascending], - ['rank', SortingOrder.Ascending] - ], - other: [ +function defineNotifications (builder: Builder): void { + builder.createDoc( + notification.class.NotificationGroup, + core.space.Model, { - key: 'shouldShowSubIssues', - type: 'toggle', - defaultValue: true, - actionTarget: 'query', - action: tracker.function.SubIssueQuery, - label: tracker.string.SubIssues + label: tracker.string.Issues, + icon: tracker.icon.Issues, + objectClass: tracker.class.Issue }, - { - key: 'shouldShowAll', - type: 'toggle', - defaultValue: false, - actionTarget: 'category', - action: view.function.ShowEmptyGroups, - label: view.string.ShowEmptyGroups - }, - ...(!kanban ? [showColorsViewOption] : []) - ] -}) -export function createModel (builder: Builder): void { - builder.createModel( - TProject, - TComponent, - TIssue, - TIssueTemplate, - TIssueStatus, - TTypeIssuePriority, - TMilestone, - TTypeMilestoneStatus, - TTimeSpendReport, - TTypeReportedTime, - TProjectIssueTargetOptions + tracker.ids.TrackerNotificationGroup ) - function issueConfig ( - key: string = '', - compact: boolean = false, - milestone: boolean = true, - component: boolean = true - ): (BuildModelKey | string)[] { - return [ - { - key: '', - label: tracker.string.Priority, - presenter: tracker.component.PriorityEditor, - props: { type: 'priority', kind: 'list', size: 'small' }, - displayProps: { key: 'priority' } + builder.createDoc( + notification.class.NotificationType, + core.space.Model, + { + hidden: false, + generated: false, + label: task.string.AssignedToMe, + group: tracker.ids.TrackerNotificationGroup, + field: 'assignee', + txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc], + objectClass: tracker.class.Issue, + onlyOwn: true, + templates: { + textTemplate: '{doc} was assigned to you by {sender}', + htmlTemplate: '

{doc} was assigned to you by {sender}

', + subjectTemplate: '{doc} was assigned to you' }, - { - key: '', - label: tracker.string.Identifier, - presenter: tracker.component.IssuePresenter, - displayProps: { key: key + 'issue', fixed: 'left' } - }, - { - key: '', - label: tracker.string.Status, - presenter: tracker.component.StatusEditor, - props: { kind: 'list', size: 'small', justify: 'center' }, - displayProps: { key: key + 'status' } - }, - { - key: '', - label: tracker.string.Title, - presenter: tracker.component.TitlePresenter, - props: compact ? { shouldUseMargin: true, showParent: false } : {}, - displayProps: { key: key + 'title' } - }, - { - key: '', - label: tracker.string.SubIssues, - presenter: tracker.component.SubIssuesSelector, - props: {} - }, - { key: 'comments', displayProps: { key: key + 'comments', suffix: true } }, - { key: 'attachments', displayProps: { key: key + 'attachments', suffix: true } }, - { key: '', displayProps: { grow: true } }, - { - key: 'labels', - presenter: tags.component.LabelsPresenter, - displayProps: { compression: true }, - props: { kind: 'list', full: false } - }, - ...(milestone - ? [ - { - key: '', - label: tracker.string.Milestone, - presenter: tracker.component.MilestoneEditor, - props: { - kind: 'list', - size: 'small', - shouldShowPlaceholder: false - }, - displayProps: { - key: key + 'milestone', - excludeByKey: 'milestone', - compression: true - } - } - ] - : []), - ...(component - ? [ - { - key: '', - label: tracker.string.Component, - presenter: tracker.component.ComponentEditor, - props: { - kind: 'list', - size: 'small', - shouldShowPlaceholder: false - }, - displayProps: { - key: key + 'component', - excludeByKey: 'component', - compression: true - } - } - ] - : []), - { - key: '', - label: tracker.string.DueDate, - presenter: tracker.component.DueDatePresenter, - displayProps: { key: key + 'dueDate', compression: true }, - props: { kind: 'list' } - }, - { - key: '', - label: tracker.string.Estimation, - presenter: tracker.component.EstimationEditor, - props: { kind: 'list', size: 'small' }, - displayProps: { key: key + 'estimation', fixed: 'left', dividerBefore: true, optional: true } - }, - { - key: 'modifiedOn', - presenter: tracker.component.ModificationDatePresenter, - displayProps: { key: key + 'modified', fixed: 'left', dividerBefore: true } - }, - { - key: 'assignee', - presenter: tracker.component.AssigneeEditor, - displayProps: { key: 'assignee', fixed: 'right' }, - props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' } + providers: { + [notification.providers.PlatformNotification]: true, + [notification.providers.EmailNotification]: true } - ] - } - - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: tracker.class.Issue, - descriptor: view.viewlet.List, - viewOptions: issuesOptions(false), - configOptions: { - strict: true, - hiddenKeys: [ - 'title', - 'blockedBy', - 'relations', - 'description', - 'number', - 'reportedTime', - 'reports', - 'priority', - 'component', - 'milestone', - 'estimation', - 'status', - 'dueDate', - 'attachedTo', - 'createdBy', - 'modifiedBy' - ] - }, - config: issueConfig() }, - tracker.viewlet.IssueList + tracker.ids.AssigneeNotification ) - const subIssuesOptions: ViewOptionsModel = { - groupBy: ['status', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'], - orderBy: [ - ['rank', SortingOrder.Ascending], - ['status', SortingOrder.Ascending], - ['priority', SortingOrder.Ascending], - ['modifiedOn', SortingOrder.Descending], - ['createdOn', SortingOrder.Descending], - ['dueDate', SortingOrder.Ascending] - ], - groupDepth: 1, - other: [showColorsViewOption] - } - - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: tracker.class.Issue, - descriptor: view.viewlet.List, - viewOptions: subIssuesOptions, - variant: 'subissue', - configOptions: { - strict: true, - hiddenKeys: [ - 'priority', - 'number', - 'status', - 'title', - 'dueDate', - 'milestone', - 'estimation', - 'createdBy', - 'modifiedBy' - ] - }, - config: issueConfig('sub', true, true) - }, - tracker.viewlet.SubIssues - ) - - const milestoneIssueOptions: ViewOptionsModel = { - groupBy: ['status', 'assignee', 'priority', 'component', 'createdBy', 'modifiedBy'], - orderBy: [ - ['rank', SortingOrder.Ascending], - ['status', SortingOrder.Ascending], - ['priority', SortingOrder.Ascending], - ['modifiedOn', SortingOrder.Descending], - ['createdOn', SortingOrder.Descending], - ['dueDate', SortingOrder.Ascending] - ], - groupDepth: 1, - other: [showColorsViewOption] - } - - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: tracker.class.Issue, - descriptor: view.viewlet.List, - viewOptions: milestoneIssueOptions, - variant: 'milestone', - configOptions: { - strict: true, - hiddenKeys: [ - 'priority', - 'number', - 'status', - 'title', - 'dueDate', - 'milestone', - 'estimation', - 'createdBy', - 'modifiedBy' - ] - }, - config: issueConfig('sub', true, false, true) - }, - tracker.viewlet.MilestoneIssuesList - ) - - const componentIssueOptions: ViewOptionsModel = { - groupBy: ['status', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'], - orderBy: [ - ['rank', SortingOrder.Ascending], - ['status', SortingOrder.Ascending], - ['priority', SortingOrder.Ascending], - ['modifiedOn', SortingOrder.Descending], - ['createdOn', SortingOrder.Descending], - ['dueDate', SortingOrder.Ascending] - ], - groupDepth: 1, - other: [showColorsViewOption] - } - - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: tracker.class.Issue, - descriptor: view.viewlet.List, - viewOptions: componentIssueOptions, - variant: 'component', - configOptions: { - strict: true, - hiddenKeys: [ - 'priority', - 'number', - 'status', - 'title', - 'dueDate', - 'component', - 'estimation', - 'createdBy', - 'modifiedBy' - ] - }, - config: issueConfig('sub', true, true, false) - }, - tracker.viewlet.ComponentIssuesList - ) - - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: tracker.class.IssueTemplate, - descriptor: view.viewlet.List, - viewOptions: { - groupBy: ['assignee', 'priority', 'component', 'milestone', 'createdBy', 'modifiedBy'], - orderBy: [ - ['priority', SortingOrder.Ascending], - ['modifiedOn', SortingOrder.Descending], - ['dueDate', SortingOrder.Ascending], - ['rank', SortingOrder.Ascending] - ], - other: [showColorsViewOption] - }, - configOptions: { - strict: true, - hiddenKeys: ['milestone', 'estimation', 'component', 'title', 'description', 'createdBy', 'modifiedBy'] - }, - config: [ - // { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } }, - { - key: '', - presenter: tracker.component.IssueTemplatePresenter, - props: { type: 'issue', shouldUseMargin: true } - }, - // { key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } }, - { - key: '', - presenter: tracker.component.ComponentEditor, - label: tracker.string.Component, - props: { - kind: 'list', - size: 'small', - shouldShowPlaceholder: false - }, - displayProps: { key: 'component', compression: true } - }, - { - key: '', - label: tracker.string.Milestone, - presenter: tracker.component.MilestoneEditor, - props: { - kind: 'list', - size: 'small', - shouldShowPlaceholder: false - }, - displayProps: { key: 'milestone', compression: true } - }, - { - key: '', - label: tracker.string.Estimation, - presenter: tracker.component.TemplateEstimationEditor, - props: { - kind: 'list', - size: 'small' - }, - displayProps: { key: 'estimation', compression: true } - }, - { key: '', displayProps: { grow: true } }, - { - key: 'modifiedOn', - presenter: tracker.component.ModificationDatePresenter, - displayProps: { fixed: 'right', dividerBefore: true } - }, - { - key: 'assignee', - presenter: tracker.component.AssigneeEditor, - props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' } - } - ] - }, - tracker.viewlet.IssueTemplateList - ) - - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: tracker.class.Issue, - descriptor: tracker.viewlet.Kanban, - viewOptions: { - ...issuesOptions(true), - groupDepth: 1 - }, - configOptions: { - strict: true - }, - config: ['subIssues', 'priority', 'component', 'dueDate', 'labels', 'estimation', 'attachments', 'comments'] - }, - tracker.viewlet.IssueKanban - ) - - builder.createDoc( - view.class.ViewletDescriptor, - core.space.Model, - { - label: tracker.string.Board, - icon: task.icon.Kanban, - component: tracker.component.KanbanView - }, - tracker.viewlet.Kanban + generateClassNotificationTypes( + builder, + tracker.class.Issue, + tracker.ids.TrackerNotificationGroup, + [], + ['comments', 'status', 'priority', 'assignee', 'subIssues', 'blockedBy', 'milestone', 'dueDate'] ) +} +function defineStatusCategories (builder: Builder): void { builder.createDoc( core.class.StatusCategory, core.space.Model, @@ -877,187 +198,117 @@ export function createModel (builder: Builder): void { }, tracker.issueStatusCategory.Canceled ) +} - const issuesId = 'issues' - const componentsId = 'components' - const milestonesId = 'milestones' - const templatesId = 'templates' - const myIssuesId = 'my-issues' - const allIssuesId = 'all-issues' - // const scrumsId = 'scrums' - - builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectPresenter, { - presenter: tracker.component.IssuePresenter - }) - - builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.NotificationObjectPresenter, { - presenter: tracker.component.NotificationIssuePresenter - }) - - builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ObjectPresenter, { - presenter: tracker.component.IssueTemplatePresenter - }) - - builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.PreviewPresenter, { - presenter: tracker.component.IssuePreview - }) - - builder.mixin(tracker.class.TimeSpendReport, core.class.Class, view.mixin.ObjectPresenter, { - presenter: tracker.component.TimeSpendReport - }) - - builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectTitle, { - titleProvider: tracker.function.IssueTitleProvider - }) - - builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ListHeaderExtra, { - 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 - }) - - builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.SortFuncs, { - func: tracker.function.IssuePrioritySort - }) - - builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.SortFuncs, { - func: tracker.function.MilestoneSort - }) - - builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.ObjectPresenter, { - presenter: tracker.component.PriorityPresenter +/** + * Define filters + */ +function defineFilters (builder: Builder): void { + // + // Issue + // + builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, { + filters: [ + 'status', + 'priority', + 'space', + 'createdBy', + 'assignee', + { + _class: tracker.class.Issue, + key: 'component', + component: view.component.ObjectFilter, + showNested: false + }, + { + _class: tracker.class.Issue, + key: 'milestone', + component: view.component.ObjectFilter, + showNested: false + } + ], + ignoreKeys: ['number', 'estimation', 'attachedTo'], + getVisibleFilters: tracker.function.GetVisibleFilters }) + // + // Issue Status + // builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.AttributeFilterPresenter, { presenter: tracker.component.StatusFilterValuePresenter }) - builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributeFilterPresenter, { - presenter: tracker.component.PriorityFilterValuePresenter + // + // Issue Template + // + builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ClassFilters, { + filters: [] }) - builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.ClassCollaborators, { - fields: ['createdBy', 'assignee'] + // + // Milestone + // + builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ClassFilters, { + filters: ['status'], + strict: true + }) + + builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.AttributeFilter, { + component: tracker.component.MilestoneFilter + }) + + // + // Project + // + builder.mixin(tracker.class.Project, core.class.Class, view.mixin.AttributeFilter, { + component: view.component.ValueFilter + }) + builder.mixin(tracker.class.Project, core.class.Class, view.mixin.AttributeFilterPresenter, { + presenter: tracker.component.ProjectFilterValuePresenter + }) + + // + // Component + // + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.AttributeFilterPresenter, { + presenter: tracker.component.ComponentFilterValuePresenter + }) + + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ClassFilters, { + filters: [] + }) + + // + // Type Milestone Status + // + + builder.mixin(tracker.class.TypeMilestoneStatus, core.class.Class, view.mixin.AttributeFilter, { + component: view.component.ValueFilter + }) + + // + // Type Issue Priority + // + builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributeFilterPresenter, { + presenter: tracker.component.PriorityFilterValuePresenter }) builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributeFilter, { component: view.component.ValueFilter }) +} - builder.mixin(tracker.class.Project, core.class.Class, view.mixin.AttributeFilter, { - component: view.component.ValueFilter - }) - - builder.mixin(tracker.class.Project, core.class.Class, view.mixin.AttributeFilterPresenter, { - presenter: tracker.component.ProjectFilterValuePresenter - }) - - builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributePresenter, { - presenter: tracker.component.PriorityRefPresenter - }) - - builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ObjectEditor, { - editor: tracker.component.EditComponent - }) - - builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ObjectPresenter, { - presenter: tracker.component.ComponentPresenter - }) - - builder.mixin(tracker.class.Component, core.class.Class, view.mixin.AttributeFilterPresenter, { - presenter: tracker.component.ComponentFilterValuePresenter - }) - - builder.mixin(tracker.class.Project, core.class.Class, view.mixin.ObjectPresenter, { - presenter: tracker.component.ProjectPresenter - }) - - builder.mixin(tracker.class.Project, core.class.Class, view.mixin.SpacePresenter, { - presenter: tracker.component.ProjectSpacePresenter - }) - - classPresenter( - builder, - tracker.class.Component, - tracker.component.ComponentSelector, - tracker.component.ComponentSelector - ) - - builder.mixin(tracker.class.Component, core.class.Class, view.mixin.AttributeEditor, { - 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.Milestone, core.class.Class, view.mixin.ObjectPresenter, { - presenter: tracker.component.MilestonePresenter - }) - - builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.AttributePresenter, { - presenter: tracker.component.MilestoneRefPresenter - }) - - builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ObjectEditor, { - editor: tracker.component.EditMilestone - }) - - builder.mixin(tracker.class.Issue, core.class.Class, setting.mixin.Editable, { - value: true - }) - - builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AllValuesFunc, { - func: tracker.function.GetAllPriority - }) - - builder.mixin(tracker.class.Component, core.class.Class, view.mixin.AllValuesFunc, { - func: tracker.function.GetAllComponents - }) - - builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.AllValuesFunc, { - func: tracker.function.GetAllMilestones - }) - - builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.LinkProvider, { - encode: tracker.function.GetIssueLinkFragment - }) - - builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectPanel, { - component: tracker.component.EditIssue - }) - - builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ObjectPanel, { - component: tracker.component.EditIssueTemplate - }) - - builder.createDoc( - activity.class.TxViewlet, - core.space.Model, - { - objectClass: tracker.class.Issue, - icon: tracker.icon.Issue, - txClass: core.class.TxCreateDoc, - labelComponent: tracker.activity.TxIssueCreated, - display: 'inline' - }, - tracker.ids.TxIssueCreated - ) - +function defineApplication ( + builder: Builder, + opt: { + myIssuesId: string + allIssuesId: string + issuesId: string + componentsId: string + milestonesId: string + templatesId: string + } +): void { builder.createDoc( workbench.class.Application, core.space.Model, @@ -1070,7 +321,7 @@ export function createModel (builder: Builder): void { navigatorModel: { specials: [ { - id: myIssuesId, + id: opt.myIssuesId, position: 'top', label: tracker.string.MyIssues, icon: tracker.icon.MyIssues, @@ -1084,7 +335,7 @@ export function createModel (builder: Builder): void { } }, { - id: allIssuesId, + id: opt.allIssuesId, position: 'top', label: tracker.string.AllIssues, icon: tracker.icon.Issues, @@ -1125,7 +376,7 @@ export function createModel (builder: Builder): void { icon: tracker.icon.Home, specials: [ { - id: issuesId, + id: opt.issuesId, label: tracker.string.Issues, icon: tracker.icon.Issues, component: tracker.component.Issues, @@ -1139,19 +390,19 @@ export function createModel (builder: Builder): void { } }, { - id: componentsId, + id: opt.componentsId, label: tracker.string.Components, icon: tracker.icon.Components, component: tracker.component.ProjectComponents }, { - id: milestonesId, + id: opt.milestonesId, label: tracker.string.Milestones, icon: tracker.icon.Milestone, component: tracker.component.Milestones }, { - id: templatesId, + id: opt.templatesId, label: tracker.string.IssueTemplates, icon: tracker.icon.IssueTemplates, component: tracker.component.IssueTemplates @@ -1164,382 +415,86 @@ export function createModel (builder: Builder): void { }, tracker.app.Tracker ) +} - function createGotoSpecialAction ( - builder: Builder, - id: string, - key: KeyBinding, - label: IntlString, - query?: Record - ): void { - createNavigateAction(builder, key, label, tracker.app.Tracker, { - application: trackerId, - mode: 'space', - spaceSpecial: id, - spaceClass: tracker.class.Project, - query - }) - } +export function createModel (builder: Builder): void { + builder.createModel( + TProject, + TComponent, + TIssue, + TIssueTemplate, + TIssueStatus, + TTypeIssuePriority, + TMilestone, + TTypeMilestoneStatus, + TTimeSpendReport, + TTypeReportedTime, + TProjectIssueTargetOptions, + TRelatedIssueTarget + ) - createGotoSpecialAction(builder, issuesId, 'keyG->keyE', tracker.string.GotoIssues) - createGotoSpecialAction(builder, issuesId, 'keyG->keyA', tracker.string.GotoActive, { mode: 'active' }) - createGotoSpecialAction(builder, issuesId, 'keyG->keyB', tracker.string.GotoBacklog, { mode: 'backlog' }) - createGotoSpecialAction(builder, componentsId, 'keyG->keyC', tracker.string.GotoComponents) - createNavigateAction(builder, 'keyG->keyM', tracker.string.GotoMyIssues, tracker.app.Tracker, { - application: trackerId, - mode: 'special', - special: myIssuesId + defineViewlets(builder) + + defineStatusCategories(builder) + + const issuesId = 'issues' + const componentsId = 'components' + const milestonesId = 'milestones' + const templatesId = 'templates' + const myIssuesId = 'my-issues' + const allIssuesId = 'all-issues' + // const scrumsId = 'scrums' + + definePresenters(builder) + + builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectTitle, { + titleProvider: tracker.function.IssueTitleProvider }) - createAction(builder, { - action: workbench.actionImpl.Navigate, - actionProps: { - mode: 'app', - application: trackerId - }, - label: tracker.string.GotoTrackerApplication, - icon: view.icon.ArrowRight, - input: 'none', - category: view.category.Navigation, - target: core.class.Doc, - context: { - mode: ['workbench', 'browser', 'editor', 'panel', 'popup'] - } + builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ListHeaderExtra, { + presenters: [tracker.component.IssueStatistics] }) - createAction( - builder, - { - action: tracker.actionImpl.EditWorkflowStatuses, - label: tracker.string.EditWorkflowStatuses, - icon: view.icon.Statuses, - input: 'focus', - category: tracker.category.Tracker, - target: tracker.class.Project, - override: [task.action.EditStatuses], - query: {}, - context: { - mode: ['context', 'browser'], - group: 'edit' - } - }, - tracker.action.EditWorkflowStatuses - ) + defineSortAndGrouping(builder) - createAction( - builder, - { - action: tracker.actionImpl.EditProject, - label: tracker.string.EditProject, - icon: contact.icon.Edit, - input: 'focus', - category: tracker.category.Tracker, - target: tracker.class.Project, - query: {}, - context: { - mode: ['context', 'browser'], - group: 'edit' - } - }, - tracker.action.EditProject - ) - - createAction( - builder, - { - action: tracker.actionImpl.DeleteProject, - label: workbench.string.Archive, - icon: view.icon.Archive, - input: 'focus', - category: tracker.category.Tracker, - target: tracker.class.Project, - query: { - archived: false - }, - context: { - mode: ['context', 'browser'], - group: 'edit' - }, - override: [view.action.Archive, view.action.Delete] - }, - tracker.action.DeleteProject - ) - createAction( - builder, - { - action: tracker.actionImpl.DeleteProject, - label: workbench.string.Delete, - icon: view.icon.Delete, - input: 'focus', - category: tracker.category.Tracker, - target: tracker.class.Project, - query: { - archived: true - }, - context: { - mode: ['context', 'browser'], - group: 'edit' - }, - override: [view.action.Archive, view.action.Delete] - }, - tracker.action.DeleteProjectClean - ) - createAction(builder, { - label: tracker.string.Unarchive, - icon: view.icon.Archive, - action: view.actionImpl.UpdateDocument as ViewAction, - actionProps: { - key: 'archived', - ask: true, - value: false, - label: tracker.string.Unarchive, - message: tracker.string.UnarchiveConfirm - }, - input: 'any', - category: tracker.category.Tracker, - query: { - archived: true - }, - context: { - mode: ['context', 'browser'], - group: 'tools' - }, - target: tracker.class.Project + builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.ClassCollaborators, { + fields: ['createdBy', 'assignee'] }) - createAction( - builder, - { - action: tracker.actionImpl.DeleteIssue, - label: workbench.string.Delete, - icon: view.icon.Delete, - input: 'any', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - group: 'remove' - }, - override: [view.action.Delete] - }, - tracker.action.DeleteIssue - ) + builder.mixin(tracker.class.Issue, core.class.Class, setting.mixin.Editable, { + value: true + }) + + builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.LinkProvider, { + encode: tracker.function.GetIssueLinkFragment + }) + + builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectPanel, { + component: tracker.component.EditIssue + }) + + builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ObjectPanel, { + component: tracker.component.EditIssueTemplate + }) builder.createDoc( - view.class.ActionCategory, + activity.class.TxViewlet, core.space.Model, - { label: tracker.string.TrackerApplication, visible: true }, - tracker.category.Tracker - ) - - createAction( - builder, { - action: view.actionImpl.ShowPopup, - actionProps: { - component: tracker.component.CreateIssue, - element: 'top' - }, - label: tracker.string.NewIssue, - icon: tracker.icon.NewIssue, - keyBinding: ['keyC'], - input: 'none', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['browser'], - application: tracker.app.Tracker, - group: 'create' - } + objectClass: tracker.class.Issue, + icon: tracker.icon.Issue, + txClass: core.class.TxCreateDoc, + labelComponent: tracker.activity.TxIssueCreated, + display: 'inline' }, - tracker.action.NewIssue + tracker.ids.TxIssueCreated ) - createAction( - builder, - { - action: view.actionImpl.ShowPopup, - actionProps: { - component: tracker.component.CreateIssue, - element: 'top', - fillProps: { - _object: 'parentIssue', - space: 'space' - } - }, - label: tracker.string.NewSubIssue, - icon: tracker.icon.Subissue, - keyBinding: [], - input: 'focus', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'associate' - } - }, - tracker.action.NewSubIssue - ) + defineApplication(builder, { myIssuesId, allIssuesId, issuesId, componentsId, milestonesId, templatesId }) - createAction( - builder, - { - action: view.actionImpl.ShowPopup, - actionProps: { - component: tracker.component.SetParentIssueActionPopup, - element: 'top', - fillProps: { - _objects: 'value' - } - }, - label: tracker.string.SetParent, - icon: tracker.icon.Parent, - keyBinding: [], - input: 'none', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context'], - application: tracker.app.Tracker, - group: 'associate' - } - }, - tracker.action.SetParent - ) + defineActions(builder, issuesId, componentsId, myIssuesId) - createAction( - builder, - { - action: view.actionImpl.ShowPopup, - actionProps: { - component: tracker.component.CreateIssue, - element: 'top', - fillProps: { - _object: 'relatedTo', - space: 'space' - } - }, - label: tracker.string.NewRelatedIssue, - icon: tracker.icon.NewIssue, - keyBinding: [], - input: 'focus', - category: tracker.category.Tracker, - target: core.class.Doc, - context: { - mode: ['context', 'browser', 'editor'], - group: 'associate' - } - }, - tracker.action.NewRelatedIssue - ) - - createAction(builder, { - action: view.actionImpl.ShowPopup, - actionPopup: tracker.component.SetParentIssueActionPopup, - actionProps: { - component: tracker.component.SetParentIssueActionPopup, - element: 'top', - fillProps: { - _object: 'value' - } - }, - label: tracker.string.SetParent, - icon: tracker.icon.Parent, - keyBinding: [], - input: 'none', - category: tracker.category.Tracker, - target: tracker.class.Issue, - override: [tracker.action.SetParent], - context: { - mode: ['browser'], - application: tracker.app.Tracker, - group: 'associate' - } - }) - - createAction(builder, { - ...actionTemplates.open, - actionProps: { - component: tracker.component.EditIssue - }, - target: tracker.class.Issue, - context: { - mode: ['browser', 'context'], - group: 'create' - }, - override: [view.action.Open] - }) - - createAction(builder, { - ...actionTemplates.open, - actionProps: { - component: tracker.component.EditIssueTemplate - }, - target: tracker.class.IssueTemplate, - context: { - mode: ['browser', 'context'], - group: 'create' - }, - override: [view.action.Open] - }) - - builder.mixin(tracker.class.Project, core.class.Class, view.mixin.IgnoreActions, { - actions: [view.action.Open] - }) - - builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, { - filters: [ - 'status', - 'priority', - 'space', - 'createdBy', - 'assignee', - { - _class: tracker.class.Issue, - key: 'component', - component: view.component.ObjectFilter, - showNested: false - }, - { - _class: tracker.class.Issue, - key: 'milestone', - component: view.component.ObjectFilter, - showNested: false - } - ], - ignoreKeys: ['number', 'estimation', 'attachedTo'], - getVisibleFilters: tracker.function.GetVisibleFilters - }) - - builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ClassFilters, { - filters: [] - }) - - builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ClassFilters, { - filters: ['status'], - strict: true - }) - - builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.AttributeFilter, { - component: tracker.component.MilestoneFilter - }) - - builder.mixin(tracker.class.TypeMilestoneStatus, core.class.Class, view.mixin.AttributePresenter, { - presenter: tracker.component.MilestoneStatusPresenter - }) - - builder.mixin(tracker.class.TypeMilestoneStatus, core.class.Class, view.mixin.AttributeEditor, { - inlineEditor: tracker.component.MilestoneStatusEditor - }) - - builder.mixin(tracker.class.TypeMilestoneStatus, core.class.Class, view.mixin.AttributeFilter, { - component: view.component.ValueFilter - }) - - builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ClassFilters, { - filters: [] - }) + defineFilters(builder) builder.createDoc( presentation.class.ObjectSearchCategory, @@ -1552,540 +507,15 @@ export function createModel (builder: Builder): void { tracker.completion.IssueCategory ) - createAction(builder, { - action: view.actionImpl.ShowPopup, - actionProps: { - component: tracker.component.TimeSpendReportPopup, - fillProps: { - _object: 'issue' - } - }, - label: tracker.string.TimeSpendReportAdd, - icon: tracker.icon.TimeReport, - input: 'focus', - keyBinding: ['keyT'], - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'edit' - } + defineNotifications(builder) + + builder.createDoc(setting.class.WorkspaceSettingCategory, core.space.Model, { + name: 'relations', + label: tracker.string.RelatedIssues, + icon: tracker.icon.Relations, + component: tracker.component.EditRelatedTargets, + group: 'settings-editor', + secured: false, + order: 4000 }) - - createAction( - builder, - { - action: task.actionImpl.SelectStatus, - actionPopup: task.component.StatusSelector, - actionProps: { - _class: tracker.class.IssueStatus, - ofAttribute: tracker.attribute.IssueStatus, - placeholder: tracker.string.Status - }, - label: tracker.string.Status, - icon: tracker.icon.CategoryBacklog, - keyBinding: ['keyS->keyS'], - input: 'any', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'edit' - } - }, - tracker.action.SetStatus - ) - - createAction( - builder, - { - action: view.actionImpl.ValueSelector, - actionPopup: view.component.ValueSelector, - actionProps: { - attribute: 'priority', - values: defaultPriorities.map((p) => ({ id: p, ...issuePriorities[p] })), - placeholder: tracker.string.SetPriority - }, - label: tracker.string.Priority, - icon: tracker.icon.PriorityHigh, - keyBinding: ['keyP->keyR'], - input: 'any', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'edit' - } - }, - tracker.action.SetPriority - ) - createAction( - builder, - { - action: view.actionImpl.ValueSelector, - actionPopup: view.component.ValueSelector, - actionProps: { - attribute: 'assignee', - _class: contact.mixin.Employee, - query: {}, - placeholder: tracker.string.AssignTo - }, - label: tracker.string.Assignee, - icon: contact.icon.Person, - keyBinding: ['keyA'], - input: 'any', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'edit' - } - }, - tracker.action.SetAssignee - ) - - createAction( - builder, - { - action: view.actionImpl.ValueSelector, - actionPopup: view.component.ValueSelector, - actionProps: { - attribute: 'component', - _class: tracker.class.Component, - query: {}, - fillQuery: { space: 'space' }, - docMatches: ['space'], - searchField: 'label', - placeholder: tracker.string.Component - }, - label: tracker.string.Component, - icon: tracker.icon.Component, - keyBinding: ['keyM->keyT'], - input: 'any', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'edit' - } - }, - tracker.action.SetComponent - ) - - createAction( - builder, - { - action: view.actionImpl.AttributeSelector, - actionPopup: tracker.component.MilestoneEditor, - actionProps: { - attribute: 'milestone', - isAction: true - }, - label: tracker.string.Milestone, - icon: tracker.icon.Milestone, - keyBinding: ['keyS->keyP'], - input: 'any', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'edit' - } - }, - tracker.action.SetMilestone - ) - - createAction( - builder, - { - action: view.actionImpl.ShowPopup, - actionProps: { - component: tags.component.TagsEditorPopup, - element: 'top', - fillProps: { - _object: 'object' - } - }, - label: tracker.string.Labels, - icon: tags.icon.Tags, - keyBinding: ['keyL'], - input: 'focus', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'edit' - } - }, - tracker.action.SetLabels - ) - - createAction( - builder, - { - action: view.actionImpl.ShowPopup, - actionProps: { - component: tags.component.ObjectsTagsEditorPopup, - element: 'top', - fillProps: { - _objects: 'value' - } - }, - label: tracker.string.Labels, - icon: tags.icon.Tags, - keyBinding: ['keyL'], - input: 'selection', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'edit' - } - }, - tracker.action.SetLabels - ) - - createAction( - builder, - { - action: view.actionImpl.ShowPopup, - actionProps: { - component: tracker.component.SetDueDateActionPopup, - props: { mondayStart: true, withTime: false }, - element: 'top', - fillProps: { - _objects: 'value' - } - }, - label: tracker.string.SetDueDate, - icon: tracker.icon.DueDate, - keyBinding: ['keyD'], - input: 'any', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'edit' - } - }, - tracker.action.SetDueDate - ) - createAction( - builder, - { - action: view.actionImpl.CopyTextToClipboard, - actionProps: { - textProvider: tracker.function.GetIssueId - }, - label: tracker.string.CopyIssueId, - icon: view.icon.CopyId, - keyBinding: [], - input: 'focus', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'copy' - } - }, - tracker.action.CopyIssueId - ) - createAction( - builder, - { - action: view.actionImpl.CopyTextToClipboard, - actionProps: { - textProvider: tracker.function.GetIssueTitle - }, - label: tracker.string.CopyIssueTitle, - icon: tracker.icon.CopyBranch, - keyBinding: [], - input: 'focus', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'copy' - } - }, - tracker.action.CopyIssueTitle - ) - createAction( - builder, - { - action: view.actionImpl.CopyTextToClipboard, - actionProps: { - textProvider: tracker.function.GetIssueLink - }, - label: tracker.string.CopyIssueUrl, - icon: view.icon.CopyLink, - keyBinding: [], - input: 'focus', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'copy' - } - }, - tracker.action.CopyIssueLink - ) - createAction( - builder, - { - action: tracker.actionImpl.Move, - label: tracker.string.MoveToProject, - icon: view.icon.Move, - keyBinding: [], - input: 'any', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'associate' - }, - override: [task.action.Move] - }, - tracker.action.MoveToProject - ) - createAction( - builder, - { - action: view.actionImpl.ValueSelector, - actionPopup: tracker.component.RelationsPopup, - actionProps: { - attribute: '' - }, - label: tracker.string.Relations, - icon: tracker.icon.Relations, - keyBinding: [], - input: 'focus', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'associate' - } - }, - tracker.action.Relations - ) - createAction( - builder, - { - action: view.actionImpl.ShowPopup, - actionProps: { - component: tracker.component.CreateIssue, - element: 'top', - fillProps: { - _object: 'originalIssue', - space: 'space' - } - }, - label: tracker.string.Duplicate, - icon: tracker.icon.Duplicate, - keyBinding: [], - input: 'focus', - category: tracker.category.Tracker, - target: tracker.class.Issue, - context: { - mode: ['context', 'browser'], - application: tracker.app.Tracker, - group: 'associate' - } - }, - tracker.action.Duplicate - ) - - createAction( - builder, - { - action: tracker.actionImpl.DeleteMilestone, - label: view.string.Delete, - icon: view.icon.Delete, - keyBinding: ['Meta + Backspace'], - category: tracker.category.Tracker, - input: 'any', - target: tracker.class.Milestone, - context: { mode: ['context', 'browser'], group: 'remove' } - }, - tracker.action.DeleteMilestone - ) - - builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.IgnoreActions, { - actions: [view.action.Delete] - }) - - classPresenter( - builder, - tracker.class.TypeReportedTime, - view.component.NumberPresenter, - tracker.component.ReportedTimeEditor - ) - - const milestoneOptions: ViewOptionsModel = { - groupBy: ['status', 'createdBy', 'modifiedBy'], - orderBy: [ - ['modifiedOn', SortingOrder.Descending], - ['targetDate', SortingOrder.Descending], - ['createdOn', SortingOrder.Descending] - ], - other: [showColorsViewOption] - } - - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: tracker.class.Milestone, - descriptor: view.viewlet.List, - viewOptions: milestoneOptions, - configOptions: { - strict: true, - hiddenKeys: ['targetDate', 'label', 'description'] - }, - config: [ - { - key: 'status', - props: { width: '1rem', kind: 'list', size: 'small', justify: 'center' } - }, - { key: '', presenter: tracker.component.MilestonePresenter, props: { shouldUseMargin: true } }, - { key: '', displayProps: { grow: true } }, - { - key: '', - label: tracker.string.TargetDate, - presenter: tracker.component.MilestoneDatePresenter, - props: { field: 'targetDate' } - } - ] - }, - tracker.viewlet.MilestoneList - ) - - builder.createDoc( - notification.class.NotificationGroup, - core.space.Model, - { - label: tracker.string.Issues, - icon: tracker.icon.Issues, - objectClass: tracker.class.Issue - }, - tracker.ids.TrackerNotificationGroup - ) - - builder.createDoc( - notification.class.NotificationType, - core.space.Model, - { - hidden: false, - generated: false, - label: task.string.AssignedToMe, - group: tracker.ids.TrackerNotificationGroup, - field: 'assignee', - txClasses: [core.class.TxCreateDoc, core.class.TxUpdateDoc], - objectClass: tracker.class.Issue, - onlyOwn: true, - templates: { - textTemplate: '{doc} was assigned to you by {sender}', - htmlTemplate: '

{doc} was assigned to you by {sender}

', - subjectTemplate: '{doc} was assigned to you' - }, - providers: { - [notification.providers.PlatformNotification]: true, - [notification.providers.EmailNotification]: true - } - }, - tracker.ids.AssigneeNotification - ) - - generateClassNotificationTypes( - builder, - tracker.class.Issue, - tracker.ids.TrackerNotificationGroup, - [], - ['comments', 'status', 'priority', 'assignee', 'subIssues', 'blockedBy', 'milestone', 'dueDate'] - ) - - const componentListViewOptions: ViewOptionsModel = { - groupBy: ['lead', 'createdBy', 'modifiedBy'], - orderBy: [ - ['modifiedOn', SortingOrder.Descending], - ['createdOn', SortingOrder.Descending] - ], - other: [showColorsViewOption] - } - - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: tracker.class.Component, - descriptor: view.viewlet.List, - viewOptions: componentListViewOptions, - configOptions: { - strict: true, - hiddenKeys: ['label', 'description'] - }, - config: [ - { - key: '', - presenter: tracker.component.ComponentPresenter, - props: { kind: 'list' } - }, - { key: '', displayProps: { grow: true } }, - { - key: '$lookup.lead', - presenter: tracker.component.LeadPresenter, - displayProps: { - dividerBefore: true, - key: 'lead' - }, - props: { _class: tracker.class.Component, defaultClass: contact.mixin.Employee, shouldShowLabel: false } - } - ] - }, - tracker.viewlet.ComponentList - ) - - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: tracker.class.Project, - descriptor: view.viewlet.List, - viewOptions: { - groupBy: ['createdBy'], - orderBy: [ - ['modifiedOn', SortingOrder.Descending], - ['createdOn', SortingOrder.Descending] - ], - other: [showColorsViewOption] - }, - configOptions: { - strict: true, - hiddenKeys: ['label', 'description'] - }, - config: [ - { - key: '', - props: { kind: 'list' } - }, - { key: '', displayProps: { grow: true } } - ] - }, - tracker.viewlet.ProjectList - ) } diff --git a/models/tracker/src/migration.ts b/models/tracker/src/migration.ts index 72944a0344..2a62558162 100644 --- a/models/tracker/src/migration.ts +++ b/models/tracker/src/migration.ts @@ -34,7 +34,7 @@ import { import { DOMAIN_TASK } from '@hcengineering/model-task' import tags from '@hcengineering/tags' import { Issue, Project, TimeReportDayType, TimeSpendReport, createStatuses } from '@hcengineering/tracker' -import { DOMAIN_TRACKER } from '.' +import { DOMAIN_TRACKER } from './types' import tracker from './plugin' import { DOMAIN_SPACE } from '@hcengineering/model-core' import view from '@hcengineering/view' diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index d4e7da49d7..94f811b58e 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -43,7 +43,8 @@ export default mergeIds(trackerId, tracker, { Unarchive: '' as IntlString, UnarchiveConfirm: '' as IntlString, AllProjects: '' as IntlString, - RemainingTime: '' as IntlString + RemainingTime: '' as IntlString, + MapRelatedIssues: '' as IntlString }, activity: { TxIssueCreated: '' as AnyComponent, @@ -55,7 +56,9 @@ export default mergeIds(trackerId, tracker, { IssueStatistics: '' as AnyComponent, TimeSpendReportPopup: '' as AnyComponent, NotificationIssuePresenter: '' as AnyComponent, - MilestoneFilter: '' as AnyComponent + MilestoneFilter: '' as AnyComponent, + EditRelatedTargets: '' as AnyComponent, + EditRelatedTargetsPopup: '' as AnyComponent }, app: { Tracker: '' as Ref diff --git a/models/tracker/src/presenters.ts b/models/tracker/src/presenters.ts new file mode 100644 index 0000000000..519c1ba085 --- /dev/null +++ b/models/tracker/src/presenters.ts @@ -0,0 +1,144 @@ +// +// 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 { Builder } from '@hcengineering/model' +import core from '@hcengineering/model-core' +import view, { classPresenter } from '@hcengineering/model-view' +import notification from '@hcengineering/notification' +import tracker from './plugin' + +/** + * Define presenters + */ +export function definePresenters (builder: Builder): void { + // + // Issue + // + builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectPresenter, { + presenter: tracker.component.IssuePresenter + }) + + builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.NotificationObjectPresenter, { + presenter: tracker.component.NotificationIssuePresenter + }) + + builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.PreviewPresenter, { + presenter: tracker.component.IssuePreview + }) + + // + // Issue Template + // + builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ObjectPresenter, { + presenter: tracker.component.IssueTemplatePresenter + }) + + // + // Issue Status + // + 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 + }) + + // + // Time Spend Report + // + builder.mixin(tracker.class.TimeSpendReport, core.class.Class, view.mixin.ObjectPresenter, { + presenter: tracker.component.TimeSpendReport + }) + + // + // Type Milestone Status + // + builder.mixin(tracker.class.TypeMilestoneStatus, core.class.Class, view.mixin.AttributePresenter, { + presenter: tracker.component.MilestoneStatusPresenter + }) + + builder.mixin(tracker.class.TypeMilestoneStatus, core.class.Class, view.mixin.AttributeEditor, { + inlineEditor: tracker.component.MilestoneStatusEditor + }) + + // + // Type Issue Priority + // + builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.ObjectPresenter, { + presenter: tracker.component.PriorityPresenter + }) + + builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributePresenter, { + presenter: tracker.component.PriorityRefPresenter + }) + + // + // Project + // + builder.mixin(tracker.class.Project, core.class.Class, view.mixin.ObjectPresenter, { + presenter: tracker.component.ProjectPresenter + }) + + builder.mixin(tracker.class.Project, core.class.Class, view.mixin.SpacePresenter, { + presenter: tracker.component.ProjectSpacePresenter + }) + + // + // Component + // + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ObjectEditor, { + editor: tracker.component.EditComponent + }) + + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ObjectPresenter, { + presenter: tracker.component.ComponentPresenter + }) + + classPresenter( + builder, + tracker.class.Component, + tracker.component.ComponentSelector, + tracker.component.ComponentSelector + ) + + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.AttributeEditor, { + inlineEditor: tracker.component.ComponentSelector + }) + + builder.mixin(tracker.class.Component, core.class.Class, view.mixin.AttributePresenter, { + presenter: tracker.component.ComponentRefPresenter + }) + + /// Milestones + builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ObjectPresenter, { + presenter: tracker.component.MilestonePresenter + }) + + builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.AttributePresenter, { + presenter: tracker.component.MilestoneRefPresenter + }) + + builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ObjectEditor, { + editor: tracker.component.EditMilestone + }) + + classPresenter( + builder, + tracker.class.TypeReportedTime, + view.component.NumberPresenter, + tracker.component.ReportedTimeEditor + ) +} diff --git a/models/tracker/src/types.ts b/models/tracker/src/types.ts new file mode 100644 index 0000000000..42cb4f6139 --- /dev/null +++ b/models/tracker/src/types.ts @@ -0,0 +1,388 @@ +// +// 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 contact, { Employee, Person } from '@hcengineering/contact' +import { + DOMAIN_MODEL, + DateRangeMode, + Domain, + IndexKind, + Markup, + Ref, + RelatedDocument, + Timestamp, + Type +} from '@hcengineering/core' +import { + ArrOf, + Collection, + Hidden, + Index, + Mixin, + Model, + Prop, + ReadOnly, + TypeDate, + TypeMarkup, + TypeNumber, + TypeRef, + TypeString, + UX +} from '@hcengineering/model' +import attachment from '@hcengineering/model-attachment' +import chunter from '@hcengineering/model-chunter' +import core, { TAttachedDoc, TClass, TDoc, TStatus, TType } from '@hcengineering/model-core' +import task, { TSpaceWithStates, TTask } from '@hcengineering/model-task' +import { IntlString, Resource } from '@hcengineering/platform' +import tags, { TagElement } from '@hcengineering/tags' +import { DoneState } from '@hcengineering/task' +import { + Component, + Issue, + IssueChildInfo, + IssueParentInfo, + IssuePriority, + IssueStatus, + IssueTemplate, + IssueTemplateChild, + IssueUpdateFunction, + Milestone, + MilestoneStatus, + Project, + ProjectIssueTargetOptions, + RelatedClassRule, + RelatedIssueTarget, + RelatedSpaceRule, + TimeReportDayType, + TimeSpendReport +} from '@hcengineering/tracker' +import { AnyComponent } from '@hcengineering/ui' +import tracker from './plugin' + +export const DOMAIN_TRACKER = 'tracker' as Domain +/** + * @public + */ + +@Model(tracker.class.IssueStatus, core.class.Status) +@UX(tracker.string.IssueStatuses, undefined, undefined, 'rank', 'name') +export class TIssueStatus extends TStatus implements IssueStatus {} +/** + * @public + */ + +export function TypeIssuePriority (): Type { + return { _class: tracker.class.TypeIssuePriority, label: tracker.string.TypeIssuePriority } +} +/** + * @public + */ + +@Model(tracker.class.TypeIssuePriority, core.class.Type, DOMAIN_MODEL) +export class TTypeIssuePriority extends TType {} +/** + * @public + */ + +export function TypeMilestoneStatus (): Type { + return { _class: tracker.class.TypeMilestoneStatus, label: 'TypeMilestoneStatus' as IntlString } +} +/** + * @public + */ + +@Model(tracker.class.TypeMilestoneStatus, core.class.Type, DOMAIN_MODEL) +export class TTypeMilestoneStatus extends TType {} +/** + * @public + */ + +@Model(tracker.class.Project, task.class.SpaceWithStates) +@UX(tracker.string.Project, tracker.icon.Issues, 'Project', 'name') +export class TProject extends TSpaceWithStates implements Project { + @Prop(TypeString(), tracker.string.ProjectIdentifier) + @Index(IndexKind.FullText) + identifier!: IntlString + + @Prop(TypeNumber(), tracker.string.Number) + @Hidden() + sequence!: number + + @Prop(TypeRef(tracker.class.IssueStatus), tracker.string.DefaultIssueStatus) + defaultIssueStatus!: Ref + + @Prop(TypeRef(contact.mixin.Employee), tracker.string.DefaultAssignee) + defaultAssignee!: Ref + + declare defaultTimeReportDay: TimeReportDayType + + @Prop(Collection(tracker.class.RelatedIssueTarget), tracker.string.RelatedIssue) + relatedIssueTargets!: number +} +/** + * @public + */ + +@Model(tracker.class.RelatedIssueTarget, core.class.Doc, DOMAIN_TRACKER) +@UX(tracker.string.RelatedIssues) +export class TRelatedIssueTarget extends TDoc implements RelatedIssueTarget { + @Prop(TypeRef(tracker.class.Project), tracker.string.Project) + target!: Ref + + rule!: RelatedClassRule | RelatedSpaceRule +} +/** + * @public + */ + +export function TypeReportedTime (): Type { + return { _class: tracker.class.TypeReportedTime, label: core.string.Number } +} +/** + * @public + */ + +@Model(tracker.class.Issue, task.class.Task) +@UX(tracker.string.Issue, tracker.icon.Issue, 'TSK', 'title') +export class TIssue extends TTask implements Issue { + @Prop(TypeRef(tracker.class.Issue), tracker.string.Parent) + declare attachedTo: Ref + + @Prop(TypeString(), tracker.string.Title) + @Index(IndexKind.FullText) + title!: string + + @Prop(TypeMarkup(), tracker.string.Description) + @Index(IndexKind.FullText) + description!: Markup + + @Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status, { + _id: tracker.attribute.IssueStatus, + iconComponent: tracker.activity.StatusIcon + }) + @Index(IndexKind.Indexed) + declare status: Ref + + @Prop(TypeIssuePriority(), tracker.string.Priority, { + iconComponent: tracker.activity.PriorityIcon + }) + @Index(IndexKind.Indexed) + priority!: IssuePriority + + @Prop(TypeNumber(), tracker.string.Number) + @Index(IndexKind.FullText) + @ReadOnly() + declare number: number + + @Prop(TypeRef(contact.class.Person), tracker.string.Assignee) + @Index(IndexKind.Indexed) + declare assignee: Ref | null + + @Prop(TypeRef(tracker.class.Component), tracker.string.Component, { icon: tracker.icon.Component }) + @Index(IndexKind.Indexed) + component!: Ref | null + + @Prop(Collection(tracker.class.Issue), tracker.string.SubIssues) + subIssues!: number + + @Prop(ArrOf(TypeRef(core.class.TypeRelatedDocument)), tracker.string.BlockedBy) + blockedBy!: RelatedDocument[] + + @Prop(ArrOf(TypeRef(core.class.TypeRelatedDocument)), tracker.string.RelatedTo) + @Index(IndexKind.Indexed) + relations!: RelatedDocument[] + + parents!: IssueParentInfo[] + + @Prop(Collection(tags.class.TagReference), tracker.string.Labels) + declare labels: number + + @Prop(TypeRef(task.class.DoneState), task.string.TaskStateDone, { _id: task.attribute.DoneState }) + @Hidden() + declare doneState: Ref | null + + @Prop(TypeRef(tracker.class.Project), tracker.string.Project, { icon: tracker.icon.Issues }) + @Index(IndexKind.Indexed) + @ReadOnly() + declare space: Ref + + @Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.DueDate) + declare dueDate: Timestamp | null + + @Prop(TypeString(), tracker.string.Rank) + @Hidden() + declare rank: string + + @Prop(TypeRef(tracker.class.Milestone), tracker.string.Milestone, { icon: tracker.icon.Milestone }) + @Index(IndexKind.Indexed) + milestone!: Ref | null + + @Prop(TypeNumber(), tracker.string.Estimation) + estimation!: number + + @Prop(TypeReportedTime(), tracker.string.ReportedTime) + @ReadOnly() + reportedTime!: number + + // A fully virtual property with calculated content. + // TODO: Add proper support for this kind of fields + @Prop(TypeNumber(), tracker.string.RemainingTime) + @ReadOnly() + @Hidden() + remainingTime!: number + + @Prop(Collection(tracker.class.TimeSpendReport), tracker.string.TimeSpendReports) + reports!: number + + declare childInfo: IssueChildInfo[] +} +/** + * @public + */ + +@Model(tracker.class.IssueTemplate, core.class.Doc, DOMAIN_TRACKER) +@UX(tracker.string.IssueTemplate, tracker.icon.Issue, 'PROCESS') +export class TIssueTemplate extends TDoc implements IssueTemplate { + @Prop(TypeString(), tracker.string.Title) + @Index(IndexKind.FullText) + title!: string + + @Prop(TypeMarkup(), tracker.string.Description) + @Index(IndexKind.FullText) + description!: Markup + + @Prop(TypeIssuePriority(), tracker.string.Priority) + priority!: IssuePriority + + @Prop(TypeRef(contact.class.Person), tracker.string.Assignee) + assignee!: Ref | null + + @Prop(TypeRef(tracker.class.Component), tracker.string.Component) + component!: Ref | null + + @Prop(ArrOf(TypeRef(tags.class.TagElement)), tracker.string.Labels) + labels?: Ref[] + + declare space: Ref + + @Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.DueDate) + dueDate!: Timestamp | null + + @Prop(TypeRef(tracker.class.Milestone), tracker.string.Milestone) + milestone!: Ref | null + + @Prop(TypeNumber(), tracker.string.Estimation) + estimation!: number + + @Prop(ArrOf(TypeRef(tracker.class.IssueTemplate)), tracker.string.IssueTemplate) + children!: IssueTemplateChild[] + + @Prop(Collection(chunter.class.Comment), tracker.string.Comments) + comments!: number + + @Prop(Collection(attachment.class.Attachment), tracker.string.Attachments) + attachments!: number + + @Prop(ArrOf(TypeRef(core.class.TypeRelatedDocument)), tracker.string.RelatedTo) + relations!: RelatedDocument[] +} +/** + * @public + */ + +@Model(tracker.class.TimeSpendReport, core.class.AttachedDoc, DOMAIN_TRACKER) +@UX(tracker.string.TimeSpendReport, tracker.icon.TimeReport) +export class TTimeSpendReport extends TAttachedDoc implements TimeSpendReport { + @Prop(TypeRef(tracker.class.Issue), tracker.string.Issue) + declare attachedTo: Ref + + @Prop(TypeRef(contact.mixin.Employee), contact.string.Employee) + employee!: Ref + + @Prop(TypeDate(), tracker.string.TimeSpendReportDate) + date!: Timestamp | null + + @Prop(TypeNumber(), tracker.string.TimeSpendReportValue) + value!: number + + @Prop(TypeString(), tracker.string.TimeSpendReportDescription) + description!: string +} +/** + * @public + */ + +@Model(tracker.class.Component, core.class.Doc, DOMAIN_TRACKER) +@UX(tracker.string.Component, tracker.icon.Component, 'COMPONENT', 'label') +export class TComponent extends TDoc implements Component { + @Prop(TypeString(), tracker.string.Title) + @Index(IndexKind.FullText) + label!: string + + @Prop(TypeMarkup(), tracker.string.Description) + description?: Markup + + @Prop(TypeRef(contact.mixin.Employee), tracker.string.ComponentLead) + lead!: Ref | null + + @Prop(Collection(chunter.class.Comment), chunter.string.Comments) + comments!: number + + @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) + attachments?: number + + declare space: Ref +} + +@Mixin(tracker.mixin.ProjectIssueTargetOptions, core.class.Class) +export class TProjectIssueTargetOptions extends TClass implements ProjectIssueTargetOptions { + headerComponent!: AnyComponent + bodyComponent!: AnyComponent + footerComponent!: AnyComponent + + update!: Resource +} +/** + * @public + */ +@Model(tracker.class.Milestone, core.class.Doc, DOMAIN_TRACKER) +@UX(tracker.string.Milestone, tracker.icon.Milestone, '', 'label') +export class TMilestone extends TDoc implements Milestone { + @Prop(TypeString(), tracker.string.Title) + // @Index(IndexKind.FullText) + label!: string + + @Prop(TypeMarkup(), tracker.string.Description) + description?: Markup + + @Prop(TypeMilestoneStatus(), tracker.string.Status) + @Index(IndexKind.Indexed) + status!: MilestoneStatus + + @Prop(Collection(chunter.class.Comment), chunter.string.Comments) + comments!: number + + @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) + attachments?: number + + @Prop(TypeDate(), tracker.string.TargetDate) + targetDate!: Timestamp + + declare space: Ref +} + +@UX(core.string.Number) +@Model(tracker.class.TypeReportedTime, core.class.Type) +export class TTypeReportedTime extends TType {} diff --git a/models/tracker/src/viewlets.ts b/models/tracker/src/viewlets.ts new file mode 100644 index 0000000000..add265f0d1 --- /dev/null +++ b/models/tracker/src/viewlets.ts @@ -0,0 +1,542 @@ +// +// 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 contact from '@hcengineering/contact' +import { SortingOrder } from '@hcengineering/core' +import { Builder } from '@hcengineering/model' +import core from '@hcengineering/model-core' +import task from '@hcengineering/model-task' +import view, { showColorsViewOption } from '@hcengineering/model-view' +import { BuildModelKey, ViewOptionsModel } from '@hcengineering/view' +import tracker from './plugin' +import tags from '@hcengineering/tags' + +export const issuesOptions = (kanban: boolean): ViewOptionsModel => ({ + groupBy: ['status', 'assignee', 'priority', 'component', 'milestone', 'createdBy', 'modifiedBy'], + orderBy: [ + ['status', SortingOrder.Ascending], + ['priority', SortingOrder.Descending], + ['modifiedOn', SortingOrder.Descending], + ['createdOn', SortingOrder.Descending], + ['dueDate', SortingOrder.Ascending], + ['rank', SortingOrder.Ascending] + ], + other: [ + { + key: 'shouldShowSubIssues', + type: 'toggle', + defaultValue: true, + actionTarget: 'query', + action: tracker.function.SubIssueQuery, + label: tracker.string.SubIssues + }, + { + key: 'shouldShowAll', + type: 'toggle', + defaultValue: false, + actionTarget: 'category', + action: view.function.ShowEmptyGroups, + label: view.string.ShowEmptyGroups + }, + ...(!kanban ? [showColorsViewOption] : []) + ] +}) + +export function issueConfig ( + key: string = '', + compact: boolean = false, + milestone: boolean = true, + component: boolean = true +): (BuildModelKey | string)[] { + return [ + { + key: '', + label: tracker.string.Priority, + presenter: tracker.component.PriorityEditor, + props: { type: 'priority', kind: 'list', size: 'small' }, + displayProps: { key: 'priority' } + }, + { + key: '', + label: tracker.string.Identifier, + presenter: tracker.component.IssuePresenter, + displayProps: { key: key + 'issue', fixed: 'left' } + }, + { + key: '', + label: tracker.string.Status, + presenter: tracker.component.StatusEditor, + props: { kind: 'list', size: 'small', justify: 'center' }, + displayProps: { key: key + 'status' } + }, + { + key: '', + label: tracker.string.Title, + presenter: tracker.component.TitlePresenter, + props: compact ? { shouldUseMargin: true, showParent: false } : {}, + displayProps: { key: key + 'title' } + }, + { + key: '', + label: tracker.string.SubIssues, + presenter: tracker.component.SubIssuesSelector, + props: {} + }, + { key: 'comments', displayProps: { key: key + 'comments', suffix: true } }, + { key: 'attachments', displayProps: { key: key + 'attachments', suffix: true } }, + { key: '', displayProps: { grow: true } }, + { + key: 'labels', + presenter: tags.component.LabelsPresenter, + displayProps: { compression: true }, + props: { kind: 'list', full: false } + }, + ...(milestone + ? [ + { + key: '', + label: tracker.string.Milestone, + presenter: tracker.component.MilestoneEditor, + props: { + kind: 'list', + size: 'small', + shouldShowPlaceholder: false + }, + displayProps: { + key: key + 'milestone', + excludeByKey: 'milestone', + compression: true + } + } + ] + : []), + ...(component + ? [ + { + key: '', + label: tracker.string.Component, + presenter: tracker.component.ComponentEditor, + props: { + kind: 'list', + size: 'small', + shouldShowPlaceholder: false + }, + displayProps: { + key: key + 'component', + excludeByKey: 'component', + compression: true + } + } + ] + : []), + { + key: '', + label: tracker.string.DueDate, + presenter: tracker.component.DueDatePresenter, + displayProps: { key: key + 'dueDate', compression: true }, + props: { kind: 'list' } + }, + { + key: '', + label: tracker.string.Estimation, + presenter: tracker.component.EstimationEditor, + props: { kind: 'list', size: 'small' }, + displayProps: { key: key + 'estimation', fixed: 'left', dividerBefore: true, optional: true } + }, + { + key: 'modifiedOn', + presenter: tracker.component.ModificationDatePresenter, + displayProps: { key: key + 'modified', fixed: 'left', dividerBefore: true } + }, + { + key: 'assignee', + presenter: tracker.component.AssigneeEditor, + displayProps: { key: 'assignee', fixed: 'right' }, + props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' } + } + ] +} + +export function defineViewlets (builder: Builder): void { + builder.createDoc( + view.class.ViewletDescriptor, + core.space.Model, + { + label: tracker.string.Board, + icon: task.icon.Kanban, + component: tracker.component.KanbanView + }, + tracker.viewlet.Kanban + ) + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.Issue, + descriptor: view.viewlet.List, + viewOptions: issuesOptions(false), + configOptions: { + strict: true, + hiddenKeys: [ + 'title', + 'blockedBy', + 'relations', + 'description', + 'number', + 'reportedTime', + 'reports', + 'priority', + 'component', + 'milestone', + 'estimation', + 'status', + 'dueDate', + 'attachedTo', + 'createdBy', + 'modifiedBy' + ] + }, + config: issueConfig() + }, + tracker.viewlet.IssueList + ) + + const subIssuesOptions: ViewOptionsModel = { + groupBy: ['status', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'], + orderBy: [ + ['rank', SortingOrder.Ascending], + ['status', SortingOrder.Ascending], + ['priority', SortingOrder.Ascending], + ['modifiedOn', SortingOrder.Descending], + ['createdOn', SortingOrder.Descending], + ['dueDate', SortingOrder.Ascending] + ], + groupDepth: 1, + other: [showColorsViewOption] + } + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.Issue, + descriptor: view.viewlet.List, + viewOptions: subIssuesOptions, + variant: 'subissue', + configOptions: { + strict: true, + hiddenKeys: [ + 'priority', + 'number', + 'status', + 'title', + 'dueDate', + 'milestone', + 'estimation', + 'createdBy', + 'modifiedBy' + ] + }, + config: issueConfig('sub', true, true) + }, + tracker.viewlet.SubIssues + ) + + const milestoneIssueOptions: ViewOptionsModel = { + groupBy: ['status', 'assignee', 'priority', 'component', 'createdBy', 'modifiedBy'], + orderBy: [ + ['rank', SortingOrder.Ascending], + ['status', SortingOrder.Ascending], + ['priority', SortingOrder.Ascending], + ['modifiedOn', SortingOrder.Descending], + ['createdOn', SortingOrder.Descending], + ['dueDate', SortingOrder.Ascending] + ], + groupDepth: 1, + other: [showColorsViewOption] + } + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.Issue, + descriptor: view.viewlet.List, + viewOptions: milestoneIssueOptions, + variant: 'milestone', + configOptions: { + strict: true, + hiddenKeys: [ + 'priority', + 'number', + 'status', + 'title', + 'dueDate', + 'milestone', + 'estimation', + 'createdBy', + 'modifiedBy' + ] + }, + config: issueConfig('sub', true, false, true) + }, + tracker.viewlet.MilestoneIssuesList + ) + + const componentIssueOptions: ViewOptionsModel = { + groupBy: ['status', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'], + orderBy: [ + ['rank', SortingOrder.Ascending], + ['status', SortingOrder.Ascending], + ['priority', SortingOrder.Ascending], + ['modifiedOn', SortingOrder.Descending], + ['createdOn', SortingOrder.Descending], + ['dueDate', SortingOrder.Ascending] + ], + groupDepth: 1, + other: [showColorsViewOption] + } + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.Issue, + descriptor: view.viewlet.List, + viewOptions: componentIssueOptions, + variant: 'component', + configOptions: { + strict: true, + hiddenKeys: [ + 'priority', + 'number', + 'status', + 'title', + 'dueDate', + 'component', + 'estimation', + 'createdBy', + 'modifiedBy' + ] + }, + config: issueConfig('sub', true, true, false) + }, + tracker.viewlet.ComponentIssuesList + ) + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.IssueTemplate, + descriptor: view.viewlet.List, + viewOptions: { + groupBy: ['assignee', 'priority', 'component', 'milestone', 'createdBy', 'modifiedBy'], + orderBy: [ + ['priority', SortingOrder.Ascending], + ['modifiedOn', SortingOrder.Descending], + ['dueDate', SortingOrder.Ascending], + ['rank', SortingOrder.Ascending] + ], + other: [showColorsViewOption] + }, + configOptions: { + strict: true, + hiddenKeys: ['milestone', 'estimation', 'component', 'title', 'description', 'createdBy', 'modifiedBy'] + }, + config: [ + // { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } }, + { + key: '', + presenter: tracker.component.IssueTemplatePresenter, + props: { type: 'issue', shouldUseMargin: true } + }, + // { key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } }, + { + key: '', + presenter: tracker.component.ComponentEditor, + label: tracker.string.Component, + props: { + kind: 'list', + size: 'small', + shouldShowPlaceholder: false + }, + displayProps: { key: 'component', compression: true } + }, + { + key: '', + label: tracker.string.Milestone, + presenter: tracker.component.MilestoneEditor, + props: { + kind: 'list', + size: 'small', + shouldShowPlaceholder: false + }, + displayProps: { key: 'milestone', compression: true } + }, + { + key: '', + label: tracker.string.Estimation, + presenter: tracker.component.TemplateEstimationEditor, + props: { + kind: 'list', + size: 'small' + }, + displayProps: { key: 'estimation', compression: true } + }, + { key: '', displayProps: { grow: true } }, + { + key: 'modifiedOn', + presenter: tracker.component.ModificationDatePresenter, + displayProps: { fixed: 'right', dividerBefore: true } + }, + { + key: 'assignee', + presenter: tracker.component.AssigneeEditor, + props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' } + } + ] + }, + tracker.viewlet.IssueTemplateList + ) + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.Issue, + descriptor: tracker.viewlet.Kanban, + viewOptions: { + ...issuesOptions(true), + groupDepth: 1 + }, + configOptions: { + strict: true + }, + config: ['subIssues', 'priority', 'component', 'dueDate', 'labels', 'estimation', 'attachments', 'comments'] + }, + tracker.viewlet.IssueKanban + ) + + const componentListViewOptions: ViewOptionsModel = { + groupBy: ['lead', 'createdBy', 'modifiedBy'], + orderBy: [ + ['modifiedOn', SortingOrder.Descending], + ['createdOn', SortingOrder.Descending] + ], + other: [showColorsViewOption] + } + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.Component, + descriptor: view.viewlet.List, + viewOptions: componentListViewOptions, + configOptions: { + strict: true, + hiddenKeys: ['label', 'description'] + }, + config: [ + { + key: '', + presenter: tracker.component.ComponentPresenter, + props: { kind: 'list' } + }, + { key: '', displayProps: { grow: true } }, + { + key: '$lookup.lead', + presenter: tracker.component.LeadPresenter, + displayProps: { + dividerBefore: true, + key: 'lead' + }, + props: { _class: tracker.class.Component, defaultClass: contact.mixin.Employee, shouldShowLabel: false } + } + ] + }, + tracker.viewlet.ComponentList + ) + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.Project, + descriptor: view.viewlet.List, + viewOptions: { + groupBy: ['createdBy'], + orderBy: [ + ['modifiedOn', SortingOrder.Descending], + ['createdOn', SortingOrder.Descending] + ], + other: [showColorsViewOption] + }, + configOptions: { + strict: true, + hiddenKeys: ['label', 'description'] + }, + config: [ + { + key: '', + props: { kind: 'list' } + }, + { key: '', displayProps: { grow: true } } + ] + }, + tracker.viewlet.ProjectList + ) + + const milestoneOptions: ViewOptionsModel = { + groupBy: ['status', 'createdBy', 'modifiedBy'], + orderBy: [ + ['modifiedOn', SortingOrder.Descending], + ['targetDate', SortingOrder.Descending], + ['createdOn', SortingOrder.Descending] + ], + other: [showColorsViewOption] + } + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.Milestone, + descriptor: view.viewlet.List, + viewOptions: milestoneOptions, + configOptions: { + strict: true, + hiddenKeys: ['targetDate', 'label', 'description'] + }, + config: [ + { + key: 'status', + props: { width: '1rem', kind: 'list', size: 'small', justify: 'center' } + }, + { key: '', presenter: tracker.component.MilestonePresenter, props: { shouldUseMargin: true } }, + { key: '', displayProps: { grow: true } }, + { + key: '', + label: tracker.string.TargetDate, + presenter: tracker.component.MilestoneDatePresenter, + props: { field: 'targetDate' } + } + ] + }, + tracker.viewlet.MilestoneList + ) +} diff --git a/packages/core/src/operations.ts b/packages/core/src/operations.ts index 7821c818a8..56fbc4223f 100644 --- a/packages/core/src/operations.ts +++ b/packages/core/src/operations.ts @@ -296,10 +296,15 @@ export class TxOperations implements Omit { return new ApplyOperations(this, scope) } - async diffUpdate (doc: Doc, raw: Doc | Data, date: Timestamp, account?: Ref): Promise { + async diffUpdate( + doc: T, + update: T | Data | DocumentUpdate, + date?: Timestamp, + account?: Ref + ): Promise { // We need to update fields if they are different. - const documentUpdate: DocumentUpdate = {} - for (const [k, v] of Object.entries(raw)) { + const documentUpdate: DocumentUpdate = {} + for (const [k, v] of Object.entries(update)) { if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) { continue } @@ -309,7 +314,7 @@ export class TxOperations implements Omit { } } if (Object.keys(documentUpdate).length > 0) { - await this.update(doc, documentUpdate, false, date, account ?? doc.modifiedBy) + await this.update(doc, documentUpdate, false, date ?? Date.now(), account) TxProcessor.applyUpdate(doc, documentUpdate) } return doc diff --git a/packages/presentation/src/components/SpaceCreateCard.svelte b/packages/presentation/src/components/SpaceCreateCard.svelte index d5edbd4649..872f5b2944 100644 --- a/packages/presentation/src/components/SpaceCreateCard.svelte +++ b/packages/presentation/src/components/SpaceCreateCard.svelte @@ -22,6 +22,7 @@ export let label: IntlString export let okAction: () => void + export let okLabel: IntlString | undefined = undefined export let canSave: boolean = false const dispatch = createEventDispatcher() @@ -40,7 +41,7 @@ - + {#if _space} + + {/if} {#if targetSettings?.bodyComponent && currentProject} + import core, { Space, WithLookup } from '@hcengineering/core' + import { SpaceSelector, createQuery, getClient } from '@hcengineering/presentation' + import { RelatedIssueTarget } from '@hcengineering/tracker' + import { Button, Icon, IconArrowRight, IconDelete, Label } from '@hcengineering/ui' + import { FixedColumn } from '@hcengineering/view-resources' + import tracker from '../plugin' + + export let value: Space | undefined + + const targetQuery = createQuery() + + let targets: WithLookup[] = [] + $: targetQuery.query( + tracker.class.RelatedIssueTarget, + {}, + (res) => { + targets = res.toSorted((a, b) => a.rule.kind.localeCompare(b.rule.kind)) + }, + { + lookup: { + target: tracker.class.Project + } + } + ) + const client = getClient() + + $: showCreate = + value !== undefined && + targets.find((it) => it.rule.kind === 'spaceRule' && it.rule.space === value?._id) === undefined + + +
+
+
+ {#each targets as target, i} +
+
+ + {#if target.rule.kind === 'classRule'} + {@const documentClass = client.getHierarchy().getClass(target.rule.ofClass)} + +
+
+ {:else if target.rule.kind === 'spaceRule'} + + {/if} +
+ + + + { + client.update(target, { target: evt.detail || null }) + }} + /> + + {#if target.rule.kind === 'spaceRule'} +
+
+ {/if} +
+
+ {/each} + {#if showCreate && value !== undefined} + {@const space = value._id} +
+ + + + => + + + { + client.createDoc(tracker.class.RelatedIssueTarget, space, { + target: evt.detail || null, + rule: { + kind: 'spaceRule', + space + } + }) + }} + /> + +
+ {/if} +
+ + diff --git a/plugins/tracker-resources/src/components/EditRelatedTargetsPopup.svelte b/plugins/tracker-resources/src/components/EditRelatedTargetsPopup.svelte new file mode 100644 index 0000000000..ef5e7d328f --- /dev/null +++ b/plugins/tracker-resources/src/components/EditRelatedTargetsPopup.svelte @@ -0,0 +1,23 @@ + + + { + dispatch('close') + }} + okLabel={ui.string.Ok} + on:close +> + + diff --git a/plugins/tracker-resources/src/index.ts b/plugins/tracker-resources/src/index.ts index 987c9f3b08..9fca32c172 100644 --- a/plugins/tracker-resources/src/index.ts +++ b/plugins/tracker-resources/src/index.ts @@ -75,6 +75,8 @@ import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte' import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte' import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte' import Statuses from './components/workflow/Statuses.svelte' +import EditRelatedTargets from './components/EditRelatedTargets.svelte' +import EditRelatedTargetsPopup from './components/EditRelatedTargetsPopup.svelte' import { getIssueId, getIssueTitle, @@ -475,7 +477,9 @@ export default async (): Promise => ({ PriorityFilterValuePresenter, StatusFilterValuePresenter, ProjectFilterValuePresenter, - ComponentFilterValuePresenter + ComponentFilterValuePresenter, + EditRelatedTargets, + EditRelatedTargetsPopup }, completion: { IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) => diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index d4a6a51bbb..a81255d92d 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -226,7 +226,6 @@ export default mergeIds(trackerId, tracker, { AddRelatedIssue: '' as IntlString, RelatedIssuesNotFound: '' as IntlString, RelatedIssue: '' as IntlString, - RelatedIssues: '' as IntlString, BlockedIssue: '' as IntlString, BlockingIssue: '' as IntlString, BlockedBySearchPlaceholder: '' as IntlString, @@ -301,7 +300,9 @@ export default mergeIds(trackerId, tracker, { NoStatusFound: '' as IntlString, CreateMissingStatus: '' as IntlString, UnsetParent: '' as IntlString, - PreviousAssigned: '' as IntlString + PreviousAssigned: '' as IntlString, + EditRelatedTargets: '' as IntlString, + RelatedIssueTargetDescription: '' as IntlString }, component: { NopeComponent: '' as AnyComponent, diff --git a/plugins/tracker/src/index.ts b/plugins/tracker/src/index.ts index 805968c72a..82808d2f17 100644 --- a/plugins/tracker/src/index.ts +++ b/plugins/tracker/src/index.ts @@ -55,6 +55,29 @@ export interface Project extends SpaceWithStates, IconProps { defaultTimeReportDay: TimeReportDayType } +export type RelatedIssueKind = 'classRule' | 'spaceRule' + +export interface RelatedClassRule { + kind: 'classRule' + ofClass: Ref> +} + +export interface RelatedSpaceRule { + kind: 'spaceRule' + space: Ref +} + +/** + * @public + * + * If defined, will be used to set a default project for this kind of document's related issues. + */ +export interface RelatedIssueTarget extends Doc { + // Attached to project. + target?: Ref | null + rule: RelatedClassRule | RelatedSpaceRule +} + /** * @public */ @@ -369,7 +392,8 @@ export default plugin(trackerId, { Milestone: '' as Ref>, TypeMilestoneStatus: '' as Ref>>, TimeSpendReport: '' as Ref>, - TypeReportedTime: '' as Ref>> + TypeReportedTime: '' as Ref>>, + RelatedIssueTarget: '' as Ref> }, ids: { NoParent: '' as Ref, @@ -471,7 +495,8 @@ export default plugin(trackerId, { EditWorkflowStatuses: '' as Ref, EditProject: '' as Ref, SetMilestone: '' as Ref, - SetLabels: '' as Ref + SetLabels: '' as Ref, + EditRelatedTargets: '' as Ref }, project: { DefaultProject: '' as Ref @@ -487,7 +512,8 @@ export default plugin(trackerId, { IssueNotificationChanged: '' as IntlString, IssueNotificationChangedProperty: '' as IntlString, IssueNotificationMessage: '' as IntlString, - IssueAssigneedToYou: '' as IntlString + IssueAssigneedToYou: '' as IntlString, + RelatedIssues: '' as IntlString }, mixin: { ProjectIssueTargetOptions: '' as Ref>