From 681d83a5d39f1a71e07e3cb94e748b21cc39120e Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Tue, 16 Aug 2022 17:19:33 +0700 Subject: [PATCH] Initial Estimations support. (#2251) Signed-off-by: Andrey Sobolev --- models/contact/src/index.ts | 7 +- models/hr/src/index.ts | 1 + models/lead/src/index.ts | 6 +- models/recruit/src/index.ts | 6 +- models/tracker/src/index.ts | 63 ++++++++- models/tracker/src/migration.ts | 9 ++ packages/core/src/classes.ts | 1 + packages/core/src/component.ts | 2 +- packages/core/src/tx.ts | 42 ++++++ packages/model/src/dsl.ts | 9 ++ .../src/components/AttributeBarEditor.svelte | 2 + .../ui/src/components/ProgressCircle.svelte | 2 +- plugins/tracker-assets/assets/icons.svg | 5 + plugins/tracker-assets/lang/en.json | 14 +- plugins/tracker-assets/lang/ru.json | 14 +- plugins/tracker-assets/src/index.ts | 4 +- .../src/components/CreateIssue.svelte | 10 +- .../src/components/issues/IssuesList.svelte | 6 + .../src/components/issues/ListView.svelte | 3 +- .../issues/timereport/EstimationEditor.svelte | 126 ++++++++++++++++++ .../issues/timereport/EstimationPopup.svelte | 92 +++++++++++++ .../EstimationProgressCircle.svelte | 71 ++++++++++ .../timereport/ReportedTimeEditor.svelte | 82 ++++++++++++ .../issues/timereport/ReportsPopup.svelte | 59 ++++++++ .../issues/timereport/TimeSpendReport.svelte | 86 ++++++++++++ .../timereport/TimeSpendReportPopup.svelte | 96 +++++++++++++ .../components/projects/ProjectEditor.svelte | 4 +- .../components/sprints/SprintEditor.svelte | 65 ++++++++- plugins/tracker-resources/src/index.ts | 9 +- plugins/tracker-resources/src/plugin.ts | 18 ++- plugins/tracker-resources/src/utils.ts | 14 +- plugins/tracker/src/index.ts | 33 ++++- plugins/view-resources/src/actions.ts | 12 +- .../src/components/ActionHandler.svelte | 24 ++-- .../src/components/NumberEditor.svelte | 11 +- .../src/components/StringEditor.svelte | 9 +- .../src/components/Table.svelte | 17 ++- .../src/components/ViewletSetting.svelte | 78 +++++++---- plugins/view-resources/src/plugin.ts | 3 +- plugins/view-resources/src/utils.ts | 16 +++ plugins/view/src/index.ts | 2 + .../attachment-resources/src/index.ts | 3 +- server-plugins/gmail-resources/src/index.ts | 4 +- server-plugins/hr-resources/src/index.ts | 8 +- .../notification-resources/src/index.ts | 3 +- server-plugins/tags-resources/src/index.ts | 6 +- server-plugins/task-resources/src/index.ts | 6 +- .../telegram-resources/src/index.ts | 4 +- server-plugins/tracker-resources/src/index.ts | 105 +++++++++++++-- server/core/src/index.ts | 1 - server/core/src/storage.ts | 5 +- server/core/src/utils.ts | 20 --- server/ws/src/__tests__/server.test.ts | 6 +- tests/sanity/tests/tracker.projects.spec.ts | 4 +- 54 files changed, 1172 insertions(+), 136 deletions(-) create mode 100644 plugins/tracker-resources/src/components/issues/timereport/EstimationEditor.svelte create mode 100644 plugins/tracker-resources/src/components/issues/timereport/EstimationPopup.svelte create mode 100644 plugins/tracker-resources/src/components/issues/timereport/EstimationProgressCircle.svelte create mode 100644 plugins/tracker-resources/src/components/issues/timereport/ReportedTimeEditor.svelte create mode 100644 plugins/tracker-resources/src/components/issues/timereport/ReportsPopup.svelte create mode 100644 plugins/tracker-resources/src/components/issues/timereport/TimeSpendReport.svelte create mode 100644 plugins/tracker-resources/src/components/issues/timereport/TimeSpendReportPopup.svelte delete mode 100644 server/core/src/utils.ts diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index f3feee7c8d..d4deda0daa 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -206,6 +206,7 @@ export function createModel (builder: Builder): void { '', { key: '$lookup.contact.$lookup.channels', + label: contact.string.Channel, sortingKey: ['$lookup.contact.$lookup.channels.lastMessage', '$lookup.contact.channels'] }, 'modifiedOn' @@ -232,7 +233,11 @@ export function createModel (builder: Builder): void { 'attachments', 'modifiedOn', { key: '', presenter: view.component.RolePresenter, label: view.string.Role }, - { key: '$lookup.channels', sortingKey: ['$lookup.channels.lastMessage', 'channels'] } + { + key: '$lookup.channels', + label: contact.string.ContactInfo, + sortingKey: ['$lookup.channels.lastMessage', 'channels'] + } ], hiddenKeys: ['name'] }, diff --git a/models/hr/src/index.ts b/models/hr/src/index.ts index 903a9cc023..a08b1b6243 100644 --- a/models/hr/src/index.ts +++ b/models/hr/src/index.ts @@ -342,6 +342,7 @@ export function createModel (builder: Builder): void { '', { key: '$lookup.channels', + label: contact.string.ContactInfo, sortingKey: ['$lookup.channels.lastMessage', 'channels'] }, 'modifiedOn' diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index 1f64077cdf..8a8a426fc7 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -182,7 +182,11 @@ export function createModel (builder: Builder): void { '$lookup._class', 'leads', 'modifiedOn', - { key: '$lookup.channels', sortingKey: ['$lookup.channels.lastMessage', 'channels'] } + { + key: '$lookup.channels', + label: contact.string.ContactInfo, + sortingKey: ['$lookup.channels.lastMessage', 'channels'] + } ], hiddenKeys: ['name'] }, diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 5e8425405c..0ed84278f2 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -283,7 +283,11 @@ export function createModel (builder: Builder): void { } }, 'modifiedOn', - { key: '$lookup.channels', sortingKey: ['$lookup.channels.lastMessage', 'channels'] } + { + key: '$lookup.channels', + label: contact.string.ContactInfo, + sortingKey: ['$lookup.channels.lastMessage', 'channels'] + } ], hiddenKeys: ['name'] }, diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 21cf582883..37398116fb 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -24,6 +24,7 @@ import { Index, Model, Prop, + ReadOnly, TypeDate, TypeMarkup, TypeNumber, @@ -34,11 +35,12 @@ import { import attachment from '@anticrm/model-attachment' import chunter from '@anticrm/model-chunter' import core, { DOMAIN_SPACE, TAttachedDoc, TDoc, TSpace, TType } from '@anticrm/model-core' -import view, { createAction } from '@anticrm/model-view' +import view, { classPresenter, createAction } from '@anticrm/model-view' import workbench, { createNavigateAction } from '@anticrm/model-workbench' import notification from '@anticrm/notification' import { Asset, IntlString } from '@anticrm/platform' import setting from '@anticrm/setting' +import tags from '@anticrm/tags' import task from '@anticrm/task' import { Document, @@ -52,10 +54,10 @@ import { Sprint, SprintStatus, Team, + TimeSpendReport, trackerId } from '@anticrm/tracker' import { KeyBinding } from '@anticrm/view' -import tags from '@anticrm/tags' import tracker from './plugin' import presentation from '@anticrm/model-presentation' @@ -159,6 +161,13 @@ export class TTeam extends TSpace implements Team { defaultIssueStatus!: Ref } +/** + * @public + */ +export function TypeReportedTime (): Type { + return { _class: tracker.class.TypeReportedTime, label: core.string.Number } +} + /** * @public */ @@ -219,8 +228,38 @@ export class TIssue extends TAttachedDoc implements Issue { @Prop(TypeRef(tracker.class.Sprint), tracker.string.Sprint) sprint!: Ref | null + + @Prop(TypeNumber(), tracker.string.Estimation) + estimation!: number + + @Prop(TypeReportedTime(), tracker.string.ReportedTime) + @ReadOnly() + reportedTime!: number + + @Prop(Collection(tracker.class.TimeSpendReport), tracker.string.TimeSpendReports) + reports!: number } +/** + * @public + */ +@Model(tracker.class.TimeSpendReport, core.class.AttachedDoc, DOMAIN_TRACKER) +@UX(tracker.string.TimeSpendReport, tracker.icon.TimeReport, tracker.string.TimeSpendReport) +export class TTimeSpendReport extends TAttachedDoc implements TimeSpendReport { + declare attachedTo: Ref + + @Prop(TypeRef(contact.class.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 */ @@ -321,6 +360,10 @@ export class TSprint extends TDoc implements Sprint { declare space: Ref } +@UX(core.string.Number) +@Model(tracker.class.TypeReportedTime, core.class.Type) +export class TTypeReportedTime extends TType {} + export function createModel (builder: Builder): void { builder.createModel( TTeam, @@ -331,7 +374,9 @@ export function createModel (builder: Builder): void { TTypeIssuePriority, TTypeProjectStatus, TSprint, - TTypeSprintStatus + TTypeSprintStatus, + TTimeSpendReport, + TTypeReportedTime ) builder.createDoc(view.class.Viewlet, core.space.Model, { @@ -357,6 +402,7 @@ export function createModel (builder: Builder): void { presenter: tracker.component.SprintEditor, props: { kind: 'list', size: 'small', shape: 'round', shouldShowPlaceholder: false } }, + { key: '', presenter: tracker.component.EstimationEditor, props: { kind: 'list', size: 'small' } }, { key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter, props: { fixed: 'right' } }, { key: '$lookup.assignee', @@ -474,6 +520,10 @@ export function createModel (builder: Builder): void { presenter: tracker.component.IssuePreview }) + builder.mixin(tracker.class.TimeSpendReport, core.class.Class, view.mixin.AttributePresenter, { + presenter: tracker.component.TimeSpendReport + }) + builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectTitle, { titleProvider: tracker.function.IssueTitleProvider }) @@ -1015,4 +1065,11 @@ export function createModel (builder: Builder): void { }, tracker.action.Relations ) + + classPresenter( + builder, + tracker.class.TypeReportedTime, + view.component.NumberPresenter, + tracker.component.ReportedTimeEditor + ) } diff --git a/models/tracker/src/migration.ts b/models/tracker/src/migration.ts index 2e5c384acd..c21d15b136 100644 --- a/models/tracker/src/migration.ts +++ b/models/tracker/src/migration.ts @@ -317,6 +317,15 @@ async function upgradeProjects (tx: TxOperations): Promise { export const trackerOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { + await client.update( + DOMAIN_TRACKER, + { _class: tracker.class.Issue, reports: { $exists: false } }, + { + reports: 0, + estimation: 0, + reportedTime: 0 + } + ) await Promise.all([migrateIssueProjects(client), migrateParentIssues(client)]) await migrateIssueParentInfo(client) }, diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index 30f813a6de..664955d20e 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -65,6 +65,7 @@ export interface UXObject extends Obj { label: IntlString icon?: Asset hidden?: boolean + readonly?: boolean } /** diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 55632901e0..dea4455fd0 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -80,7 +80,7 @@ export default plugin(coreId, { Type: '' as Ref>>, TypeString: '' as Ref>>, TypeIntlString: '' as Ref>>, - TypeNumber: '' as Ref>>, + TypeNumber: '' as Ref>>, TypeMarkup: '' as Ref>>, TypeBoolean: '' as Ref>>, TypeTimestamp: '' as Ref>>, diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts index 0562da54a1..f092aea13a 100644 --- a/packages/core/src/tx.ts +++ b/packages/core/src/tx.ts @@ -292,6 +292,48 @@ export abstract class TxProcessor implements WithTx { return rawDoc } + static buildDoc2Doc(txes: Tx[]): D | undefined { + let doc: Doc + let createTx = txes.find((tx) => tx._class === core.class.TxCreateDoc) + const collectionTxes = false + if (createTx === undefined) { + const collectionTxes = txes.filter((tx) => tx._class === core.class.TxCollectionCUD) as Array< + TxCollectionCUD + > + createTx = collectionTxes.find((p) => p.tx._class === core.class.TxCreateDoc) + } + if (createTx === undefined) return + doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc) + for (let tx of txes) { + if (collectionTxes) { + tx = TxProcessor.extractTx(tx) + } + if (tx._class === core.class.TxUpdateDoc) { + doc = TxProcessor.updateDoc2Doc(doc, tx as TxUpdateDoc) + } else if (tx._class === core.class.TxMixin) { + const mixinTx = tx as TxMixin + doc = TxProcessor.updateMixin4Doc(doc, mixinTx) + } + } + return doc as D + } + + static extractTx (tx: Tx): Tx { + if (tx._class === core.class.TxCollectionCUD) { + const ctx = tx as TxCollectionCUD + if (ctx.tx._class === core.class.TxCreateDoc) { + const create = ctx.tx as TxCreateDoc + create.attributes.attachedTo = ctx.objectId + create.attributes.attachedToClass = ctx.objectClass + create.attributes.collection = ctx.collection + return create + } + return ctx.tx + } + + return tx + } + protected abstract txCreateDoc (tx: TxCreateDoc): Promise protected abstract txPutBag (tx: TxPutBag): Promise protected abstract txUpdateDoc (tx: TxUpdateDoc): Promise diff --git a/packages/model/src/dsl.ts b/packages/model/src/dsl.ts index a25113c09d..8b5aa79811 100644 --- a/packages/model/src/dsl.ts +++ b/packages/model/src/dsl.ts @@ -158,6 +158,15 @@ export function Hidden () { } } +/** + * @public + */ +export function ReadOnly () { + return function (target: any, propertyKey: string): void { + setAttr(target, propertyKey, 'readonly', true) + } +} + /** * @public */ diff --git a/packages/presentation/src/components/AttributeBarEditor.svelte b/packages/presentation/src/components/AttributeBarEditor.svelte index 6e0dc5b91f..25fea6790b 100644 --- a/packages/presentation/src/components/AttributeBarEditor.svelte +++ b/packages/presentation/src/components/AttributeBarEditor.svelte @@ -92,6 +92,7 @@ type={attribute?.type} {maxWidth} value={getAttribute(client, object, { key: attributeKey, attr: attribute })} + readonly={attribute.readonly ?? false} space={object.space} {onChange} {focus} @@ -105,6 +106,7 @@ type={attribute?.type} {maxWidth} value={getAttribute(client, object, { key: attributeKey, attr: attribute })} + readonly={attribute.readonly ?? false} space={object.space} {onChange} {focus} diff --git a/packages/ui/src/components/ProgressCircle.svelte b/packages/ui/src/components/ProgressCircle.svelte index 279435b690..07fd42f4c1 100644 --- a/packages/ui/src/components/ProgressCircle.svelte +++ b/packages/ui/src/components/ProgressCircle.svelte @@ -27,7 +27,7 @@ if (value < min) value = min const lenghtC: number = Math.PI * 14 - 1 - const procC: number = lenghtC / (max - min) + $: procC = lenghtC / (max - min) $: dashOffset = (value - min) * procC diff --git a/plugins/tracker-assets/assets/icons.svg b/plugins/tracker-assets/assets/icons.svg index 892294980a..bf56650768 100644 --- a/plugins/tracker-assets/assets/icons.svg +++ b/plugins/tracker-assets/assets/icons.svg @@ -162,4 +162,9 @@ + + + + + diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json index 37d863dbb3..b4863e867f 100644 --- a/plugins/tracker-assets/lang/en.json +++ b/plugins/tracker-assets/lang/en.json @@ -196,7 +196,19 @@ "AddToSprint": "Add to Sprint", "NewSprint": "New Sprint", - "CreateSprint": "Create" + "CreateSprint": "Create", + + "Estimation": "Estimation", + "ReportedTime": "Reported Time", + "TimeSpendReports": "Time spend reports", + "TimeSpendReport": "Time spend report", + "TimeSpendReportAdd": "Add time report", + "TimeSpendReportDate": "Date", + "TimeSpendReportValue": "Reported time", + "TimeSpendReportValueTooltip": "Reported time in man days", + "TimeSpendReportDescription": "Description", + "TimeSpendValue": "{value}d", + "SprintPassed": "{from}d/{to}d" }, "status": {} } diff --git a/plugins/tracker-assets/lang/ru.json b/plugins/tracker-assets/lang/ru.json index 0102967d1f..87db254642 100644 --- a/plugins/tracker-assets/lang/ru.json +++ b/plugins/tracker-assets/lang/ru.json @@ -196,7 +196,19 @@ "AddToSprint": "Добавить в Спринт", "NewSprint": "Новый Спринт", - "CreateSprint": "Создать" + "CreateSprint": "Создать", + + "Estimation": "Оценка", + "ReportedTime": "Использовано", + "TimeSpendReports": "Отчеты по времени", + "TimeSpendReport": "Отчет по времени", + "TimeSpendReportAdd": "Добавить завтраченное время", + "TimeSpendReportDate": "Дата", + "TimeSpendReportValue": "Затраченное время", + "TimeSpendReportValueTooltip": "Затраченное время в человеко днях", + "TimeSpendReportDescription": "Описание", + "TimeSpendValue": "{value}d", + "SprintPassed": "{from}d/{to}d" }, "status": {} } diff --git a/plugins/tracker-assets/src/index.ts b/plugins/tracker-assets/src/index.ts index c08adfaa8e..867cf0f942 100644 --- a/plugins/tracker-assets/src/index.ts +++ b/plugins/tracker-assets/src/index.ts @@ -66,7 +66,9 @@ loadMetadata(tracker.icon, { CopyID: `${icons}#copyID`, CopyURL: `${icons}#copyURL`, - CopyBranch: `${icons}#copyBranch` + CopyBranch: `${icons}#copyBranch`, + TimeReport: `${icons}#timeReport`, + Estimation: `${icons}#timeReport` }) addStringsLoader(trackerId, async (lang: string) => await import(`../lang/${lang}.json`)) diff --git a/plugins/tracker-resources/src/components/CreateIssue.svelte b/plugins/tracker-resources/src/components/CreateIssue.svelte index d4a376a6eb..dc7c79a6e1 100644 --- a/plugins/tracker-resources/src/components/CreateIssue.svelte +++ b/plugins/tracker-resources/src/components/CreateIssue.svelte @@ -68,7 +68,10 @@ dueDate: null, comments: 0, subIssues: 0, - parents: [] + parents: [], + reportedTime: 0, + estimation: 0, + reports: 0 } const dispatch = createEventDispatcher() @@ -151,7 +154,10 @@ dueDate: object.dueDate, parents: parentIssue ? [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents] - : [] + : [], + reportedTime: 0, + estimation: 0, + reports: 0 } await client.addCollection( diff --git a/plugins/tracker-resources/src/components/issues/IssuesList.svelte b/plugins/tracker-resources/src/components/issues/IssuesList.svelte index 79fe941377..a24cc41bcd 100644 --- a/plugins/tracker-resources/src/components/issues/IssuesList.svelte +++ b/plugins/tracker-resources/src/components/issues/IssuesList.svelte @@ -184,6 +184,7 @@ shouldShowLabel: true, value: groupByKey ? { [groupByKey]: category } : {}, statuses: groupByKey === 'status' ? statuses : undefined, + issues: groupedIssues[category], size: 'inline', kind: 'list' }} @@ -241,6 +242,7 @@ @@ -255,6 +257,7 @@ @@ -263,6 +266,7 @@ {:else if attributeModel.props?.fixed} @@ -275,6 +279,7 @@ @@ -284,6 +289,7 @@ this={attributeModel.presenter} value={getObjectValue(attributeModel.key, docObject) ?? ''} issueId={docObject._id} + groupBy={groupByKey} {...attributeModel.props} /> diff --git a/plugins/tracker-resources/src/components/issues/ListView.svelte b/plugins/tracker-resources/src/components/issues/ListView.svelte index 556ecd2631..0e567ccc4a 100644 --- a/plugins/tracker-resources/src/components/issues/ListView.svelte +++ b/plugins/tracker-resources/src/components/issues/ListView.svelte @@ -57,7 +57,8 @@ lookup: { assignee: contact.class.Employee, status: tracker.class.IssueStatus, - space: tracker.class.Team + space: tracker.class.Team, + sprint: tracker.class.Sprint } } ) diff --git a/plugins/tracker-resources/src/components/issues/timereport/EstimationEditor.svelte b/plugins/tracker-resources/src/components/issues/timereport/EstimationEditor.svelte new file mode 100644 index 0000000000..e34f84b6fe --- /dev/null +++ b/plugins/tracker-resources/src/components/issues/timereport/EstimationEditor.svelte @@ -0,0 +1,126 @@ + + + +{#if value} + {#if kind === 'list'} +
+
+ +
+ + {#if value.reportedTime > 0} + +
+ {:else} +