diff --git a/.gitignore b/.gitignore index dd04f4bb42..3041f95273 100644 --- a/.gitignore +++ b/.gitignore @@ -86,4 +86,4 @@ dev/tool/report*.csv tests/db_dump .build .format -tools/apm/apm.js \ No newline at end of file +tools/apm/apm.js diff --git a/dev/generator/src/issues.ts b/dev/generator/src/issues.ts index 4b2e74e5b9..01c29b9ba5 100644 --- a/dev/generator/src/issues.ts +++ b/dev/generator/src/issues.ts @@ -37,7 +37,8 @@ const object: AttachedData = { remainingTime: 0, estimation: 0, reports: 0, - childInfo: [] + childInfo: [], + kind: tracker.taskTypes.Issue } export interface IssueOptions { @@ -100,7 +101,8 @@ async function genIssue (client: TxOperations, statuses: Ref[]): Pr estimation: object.estimation, reports: 0, relations: [], - childInfo: [] + childInfo: [], + kind: tracker.taskTypes.Issue } await client.addCollection( tracker.class.Issue, diff --git a/dev/generator/src/recruit.ts b/dev/generator/src/recruit.ts index 53c397cc0b..322bc2208f 100644 --- a/dev/generator/src/recruit.ts +++ b/dev/generator/src/recruit.ts @@ -15,7 +15,7 @@ import core, { import { MinioService } from '@hcengineering/minio' import recruit from '@hcengineering/model-recruit' import { Applicant, Candidate, Vacancy } from '@hcengineering/recruit' -import task, { ProjectType, genRanks } from '@hcengineering/task' +import task, { ProjectType, TaskType, genRanks } from '@hcengineering/task' import faker from 'faker' import jpeg, { BufferRet } from 'jpeg-js' import { AttachmentOptions, addAttachments } from './attachments' @@ -97,13 +97,16 @@ async function genVacansyApplicants ( name: faker.name.title(), description: faker.lorem.sentences(2), shortDescription: faker.lorem.sentences(1), - category: recruit.category.VacancyTypeCategories, + descriptor: recruit.descriptors.VacancyType, private: false, members: [], archived: false, + tasks: [], + // TODO: Fix me. statuses: states.map((s) => { - return { _id: s } - }) + return { _id: s, taskType: '' as Ref } + }), + targetClass: recruit.class.Vacancy } await ctx.with('update', {}, (ctx) => @@ -180,7 +183,8 @@ async function genApplicant ( status: faker.random.arrayElement(states), rank, startDate: null, - dueDate: null + dueDate: null, + kind: recruit.taskTypes.Applicant } // Update or create candidate diff --git a/models/board/src/index.ts b/models/board/src/index.ts index fc711f3c11..5853e75734 100644 --- a/models/board/src/index.ts +++ b/models/board/src/index.ts @@ -516,16 +516,26 @@ export function createModel (builder: Builder): void { }) builder.createDoc( - task.class.ProjectTypeCategory, + task.class.ProjectTypeDescriptor, core.space.Model, { - name: board.string.Boards, + name: board.string.BoardApplication, description: board.string.ManageBoardStatuses, icon: board.component.TemplatesIcon, - attachedToClass: board.class.Board, - statusClass: core.class.Status, - statusCategories: [task.statusCategory.Active, task.statusCategory.Won, task.statusCategory.Lost] + baseClass: board.class.Board }, - board.category.BoardType + board.descriptors.BoardType + ) + builder.createDoc( + task.class.TaskTypeDescriptor, + core.space.Model, + { + baseClass: board.class.Card, + allowCreate: true, + description: board.string.Card, + icon: board.icon.Card, + name: board.string.Card + }, + board.descriptors.Card ) } diff --git a/models/board/src/migration.ts b/models/board/src/migration.ts index 1e8442941f..31d462d5cb 100644 --- a/models/board/src/migration.ts +++ b/models/board/src/migration.ts @@ -13,19 +13,21 @@ // limitations under the License. // +import { boardId } from '@hcengineering/board' import { type Ref, TxOperations } from '@hcengineering/core' import { type MigrateOperation, type MigrationClient, type MigrationUpgradeClient, - createOrUpdate + createOrUpdate, + tryMigrate } from '@hcengineering/model' -import core from '@hcengineering/model-core' -import { createProjectType, createSequence } from '@hcengineering/model-task' -import tags from '@hcengineering/model-tags' +import core, { DOMAIN_SPACE } from '@hcengineering/model-core' +import { createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task' +import tags from '@hcengineering/tags' import task, { type ProjectType } from '@hcengineering/task' -import board from './plugin' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' +import board from './plugin' async function createSpace (tx: TxOperations): Promise { const current = await tx.findOne(core.class.Space, { @@ -54,27 +56,51 @@ async function createDefaultProjectType (tx: TxOperations): Promise { } export const boardOperation: MigrateOperation = { - async migrate (client: MigrationClient): Promise {}, + async migrate (client: MigrationClient): Promise { + await tryMigrate(client, boardId, [ + { + state: 'fix-category-descriptors', + func: async (client) => { + await client.update( + DOMAIN_SPACE, + { _class: task.class.ProjectType, category: 'board:category:BoardType' }, + { + $set: { descriptor: board.descriptors.BoardType }, + $unset: { category: 1 } + } + ) + } + }, + { + state: 'fixTaskStatus', + func: async (client): Promise => { + await fixTaskTypes(client, board.descriptors.BoardType, async () => [ + { + name: 'Card', + descriptor: board.descriptors.Card, + ofClass: board.class.Card, + targetClass: board.class.Card, + statusCategories: [ + task.statusCategory.UnStarted, + task.statusCategory.Active, + task.statusCategory.Won, + task.statusCategory.Lost + ], + statusClass: core.class.Status, + kind: 'task' + } + ]) + } + } + ]) + }, async upgrade (client: MigrationUpgradeClient): Promise { const ops = new TxOperations(client, core.account.System) await createDefaults(ops) diff --git a/models/board/src/plugin.ts b/models/board/src/plugin.ts index 0a54d05957..a1156e9c3e 100644 --- a/models/board/src/plugin.ts +++ b/models/board/src/plugin.ts @@ -18,7 +18,7 @@ import { type Board, boardId } from '@hcengineering/board' import board from '@hcengineering/board-resources/src/plugin' import type { Ref } from '@hcengineering/core' import { type IntlString, mergeIds } from '@hcengineering/platform' -import { type ProjectType, type Sequence } from '@hcengineering/task' +import { type TaskTypeDescriptor, type ProjectType, type Sequence } from '@hcengineering/task' import type { AnyComponent } from '@hcengineering/ui/src/types' import { type Action, type ViewAction, type Viewlet, type ViewletDescriptor } from '@hcengineering/view' @@ -67,5 +67,8 @@ export default mergeIds(boardId, board, { }, actionImpl: { ConvertToCard: '' as ViewAction + }, + descriptors: { + Card: '' as Ref } }) diff --git a/models/contact/src/plugin.ts b/models/contact/src/plugin.ts index ead8c97e1e..ebf5cafc66 100644 --- a/models/contact/src/plugin.ts +++ b/models/contact/src/plugin.ts @@ -54,7 +54,6 @@ export default mergeIds(contactId, contact, { ContactArrayEditor: '' as AnyComponent, EmployeeEditor: '' as AnyComponent, CreateEmployee: '' as AnyComponent, - AccountArrayEditor: '' as AnyComponent, ChannelFilter: '' as AnyComponent, MergePersons: '' as AnyComponent, ChannelPanel: '' as AnyComponent, diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index f27cb85ed5..038a923d43 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -689,16 +689,26 @@ export function createModel (builder: Builder): void { }) builder.createDoc( - task.class.ProjectTypeCategory, + task.class.ProjectTypeDescriptor, core.space.Model, { - name: lead.string.Funnels, + name: lead.string.LeadApplication, description: lead.string.ManageFunnelStatuses, icon: lead.component.TemplatesIcon, - attachedToClass: lead.class.Funnel, - statusClass: core.class.Status, - statusCategories: [task.statusCategory.Active, task.statusCategory.Won, task.statusCategory.Lost] + baseClass: lead.class.Funnel }, - lead.category.FunnelTypeCategory + lead.descriptors.FunnelType + ) + builder.createDoc( + task.class.TaskTypeDescriptor, + core.space.Model, + { + baseClass: lead.class.Lead, + allowCreate: true, + description: lead.string.Lead, + icon: lead.icon.Lead, + name: lead.string.Lead + }, + lead.descriptors.Lead ) } diff --git a/models/lead/src/migration.ts b/models/lead/src/migration.ts index e795f8d92c..29ae04487b 100644 --- a/models/lead/src/migration.ts +++ b/models/lead/src/migration.ts @@ -16,15 +16,14 @@ import { TxOperations } from '@hcengineering/core' import { leadId } from '@hcengineering/lead' import { + tryMigrate, + tryUpgrade, type MigrateOperation, type MigrationClient, - type MigrationUpgradeClient, - tryUpgrade + type MigrationUpgradeClient } from '@hcengineering/model' -import core from '@hcengineering/model-core' -import { createProjectType, createSequence } from '@hcengineering/model-task' -import tracker from '@hcengineering/model-tracker' -import task from '@hcengineering/task' +import core, { DOMAIN_SPACE } from '@hcengineering/model-core' +import task, { createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' import lead from './plugin' @@ -37,42 +36,66 @@ async function createSpace (tx: TxOperations): Promise { tx, { name: 'Default funnel', - category: lead.category.FunnelTypeCategory, - description: '' + descriptor: lead.descriptors.FunnelType, + description: '', + tasks: [] }, [ { - color: PaletteColorIndexes.Coin, - name: 'Incoming', - ofAttribute: lead.attribute.State, - category: task.statusCategory.Active - }, - { - color: PaletteColorIndexes.Arctic, - name: 'Negotation', - ofAttribute: lead.attribute.State, - category: task.statusCategory.Active - }, - { - color: PaletteColorIndexes.Watermelon, - name: 'Offer preparing', - ofAttribute: lead.attribute.State, - category: task.statusCategory.Active - }, - { - color: PaletteColorIndexes.Orange, - name: 'Make a decision', - ofAttribute: lead.attribute.State, - category: task.statusCategory.Active - }, - { - color: PaletteColorIndexes.Ocean, - name: 'Contract conclusion', - ofAttribute: lead.attribute.State, - category: task.statusCategory.Active - }, - { name: 'Won', ofAttribute: lead.attribute.State, category: task.statusCategory.Won }, - { name: 'Lost', ofAttribute: lead.attribute.State, category: task.statusCategory.Lost } + _id: lead.taskType.Lead, + name: 'Lead', + descriptor: lead.descriptors.Lead, + ofClass: lead.class.Lead, + targetClass: lead.class.Lead, + statusClass: core.class.Status, + statusCategories: [ + task.statusCategory.UnStarted, + task.statusCategory.Active, + task.statusCategory.Won, + task.statusCategory.Lost + ], + kind: 'task', + factory: [ + { + color: PaletteColorIndexes.Coin, + name: 'Backlog', + ofAttribute: lead.attribute.State, + category: task.statusCategory.UnStarted + }, + { + color: PaletteColorIndexes.Coin, + name: 'Incoming', + ofAttribute: lead.attribute.State, + category: task.statusCategory.Active + }, + { + color: PaletteColorIndexes.Arctic, + name: 'Negotation', + ofAttribute: lead.attribute.State, + category: task.statusCategory.Active + }, + { + color: PaletteColorIndexes.Watermelon, + name: 'Offer preparing', + ofAttribute: lead.attribute.State, + category: task.statusCategory.Active + }, + { + color: PaletteColorIndexes.Orange, + name: 'Make a decision', + ofAttribute: lead.attribute.State, + category: task.statusCategory.Active + }, + { + color: PaletteColorIndexes.Ocean, + name: 'Contract conclusion', + ofAttribute: lead.attribute.State, + category: task.statusCategory.Active + }, + { name: 'Won', ofAttribute: lead.attribute.State, category: task.statusCategory.Won }, + { name: 'Lost', ofAttribute: lead.attribute.State, category: task.statusCategory.Lost } + ] + } ], lead.template.DefaultFunnel ) @@ -98,24 +121,48 @@ async function createDefaults (tx: TxOperations): Promise { } export const leadOperation: MigrateOperation = { - async migrate (client: MigrationClient): Promise {}, + async migrate (client: MigrationClient): Promise { + await tryMigrate(client, leadId, [ + { + state: 'fix-category-descriptors', + func: async (client) => { + await client.update( + DOMAIN_SPACE, + { _class: task.class.ProjectType, category: 'lead:category:FunnelTypeCategory' }, + { + $set: { descriptor: lead.descriptors.FunnelType }, + $unset: { category: 1 } + } + ) + } + }, + { + state: 'fixTaskStatus', + func: async (client): Promise => { + await fixTaskTypes(client, lead.descriptors.FunnelType, async () => [ + { + name: 'Lead', + descriptor: lead.descriptors.Lead, + ofClass: lead.class.Lead, + targetClass: lead.class.Lead, + statusCategories: [ + task.statusCategory.UnStarted, + task.statusCategory.Active, + task.statusCategory.Won, + task.statusCategory.Lost + ], + statusClass: core.class.Status, + kind: 'task' + } + ]) + } + } + ]) + }, 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 - } - }) - } - } - ]) + await tryUpgrade(client, leadId, []) } } diff --git a/models/lead/src/plugin.ts b/models/lead/src/plugin.ts index 49ce0b10f7..aef8a17b7b 100644 --- a/models/lead/src/plugin.ts +++ b/models/lead/src/plugin.ts @@ -21,7 +21,7 @@ import lead from '@hcengineering/lead-resources/src/plugin' import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' import type { IntlString } from '@hcengineering/platform' import { mergeIds } from '@hcengineering/platform' -import { type ProjectType } from '@hcengineering/task' +import { type TaskTypeDescriptor, type ProjectType } from '@hcengineering/task' import type { AnyComponent } from '@hcengineering/ui/src/types' import { type Action, type ActionCategory, type Viewlet } from '@hcengineering/view' @@ -72,5 +72,8 @@ export default mergeIds(leadId, lead, { LeadCreateNotification: '' as Ref, AssigneeNotification: '' as Ref, LeadChatMessageViewlet: '' as Ref + }, + descriptors: { + Lead: '' as Ref } }) diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index 7f10819853..30303d05de 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -276,9 +276,9 @@ export function createModel (builder: Builder): void { label: notification.string.Notifications, icon: notification.icon.Notifications, component: notification.component.NotificationSettings, - group: 'settings', + group: 'settings-account', secured: false, - order: 2500 + order: 1500 }, notification.ids.NotificationSettings ) diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 8e3fe4a357..c126b262ca 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -316,7 +316,7 @@ export function createModel (builder: Builder): void { label: recruit.string.Applications, createLabel: recruit.string.ApplicationCreateLabel, createComponent: recruit.component.CreateApplication, - category: recruit.category.VacancyTypeCategories, + descriptor: recruit.descriptors.VacancyType, descriptors: [ view.viewlet.Table, view.viewlet.List, @@ -1750,17 +1750,28 @@ export function createModel (builder: Builder): void { ) builder.createDoc( - task.class.ProjectTypeCategory, + task.class.ProjectTypeDescriptor, core.space.Model, { - name: recruit.string.Vacancies, + name: recruit.string.RecruitApplication, description: recruit.string.ManageVacancyStatuses, icon: recruit.component.TemplatesIcon, editor: recruit.component.VacancyTemplateEditor, - attachedToClass: recruit.class.Vacancy, - statusClass: core.class.Status, - statusCategories: [task.statusCategory.Active, task.statusCategory.Won, task.statusCategory.Lost] + baseClass: recruit.class.Vacancy }, - recruit.category.VacancyTypeCategories + recruit.descriptors.VacancyType + ) + + builder.createDoc( + task.class.TaskTypeDescriptor, + core.space.Model, + { + baseClass: recruit.class.Applicant, + allowCreate: true, + description: recruit.string.Application, + icon: recruit.icon.Application, + name: recruit.string.Application + }, + recruit.descriptors.Application ) } diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index b0bc111a33..2e93a63717 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -14,70 +14,67 @@ // import { getCategories } from '@anticrm/skillset' -import core, { type Ref, TxOperations } from '@hcengineering/core' +import core, { TxOperations, type Ref } from '@hcengineering/core' import { + createOrUpdate, + tryMigrate, + tryUpgrade, type MigrateOperation, type MigrationClient, - type MigrationUpgradeClient, - createOrUpdate, - tryUpgrade + type MigrationUpgradeClient } from '@hcengineering/model' import tags, { type TagCategory } from '@hcengineering/model-tags' -import { createProjectType, createSequence } from '@hcengineering/model-task' -import tracker from '@hcengineering/model-tracker' +import { createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task' import { recruitId } from '@hcengineering/recruit' import task, { type ProjectType } from '@hcengineering/task' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' import recruit from './plugin' +import { DOMAIN_SPACE } from '@hcengineering/model-core' export const recruitOperation: MigrateOperation = { - async migrate (client: MigrationClient): Promise {}, + async migrate (client: MigrationClient): Promise { + await tryMigrate(client, recruitId, [ + { + state: 'fix-category-descriptors', + func: async (client) => { + await client.update( + DOMAIN_SPACE, + { _class: task.class.ProjectType, category: 'recruit:category:VacancyTypeCategories' }, + { + $set: { descriptor: recruit.descriptors.VacancyType }, + $unset: { category: 1 } + } + ) + } + }, + { + state: 'fixTaskStatus', + func: async (client): Promise => { + await fixTaskTypes(client, recruit.descriptors.VacancyType, async () => [ + { + name: 'Applicant', + descriptor: recruit.descriptors.Application, + ofClass: recruit.class.Applicant, + targetClass: recruit.class.Applicant, + statusCategories: [ + task.statusCategory.UnStarted, + task.statusCategory.Active, + task.statusCategory.Won, + task.statusCategory.Lost + ], + statusClass: core.class.Status, + kind: 'task' + } + ]) + } + } + ]) + }, async upgrade (client: MigrationUpgradeClient): Promise { const tx = new TxOperations(client, core.account.System) await createDefaults(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 - } - }) - } - }, - { - state: 'wrong-categories', - func: async (client): Promise => { - const ops = new TxOperations(client, core.account.System) - while (true) { - const docs = await ops.findAll( - tags.class.TagElement, - { - targetClass: recruit.mixin.Candidate, - category: { $in: [tracker.category.Other, 'document:category:Other' as Ref] } - }, - { limit: 1000 } - ) - for (const d of docs) { - await ops.update(d, { category: recruit.category.Other }) - } - if (docs.length === 0) { - break - } - } - } - }, { state: 'remove-members', func: async (client): Promise => { @@ -137,36 +134,60 @@ async function createDefaultKanbanTemplate (tx: TxOperations): Promise, TableVacancyList: '' as Ref, ApplicantDashboard: '' as Ref + }, + descriptors: { + Application: '' as Ref } }) diff --git a/models/setting/src/index.ts b/models/setting/src/index.ts index f16cbfe21d..3ca2e06b9f 100644 --- a/models/setting/src/index.ts +++ b/models/setting/src/index.ts @@ -15,23 +15,22 @@ import activity from '@hcengineering/activity' import contact from '@hcengineering/contact' -import { type Account, type Domain, DOMAIN_MODEL, type Ref } from '@hcengineering/core' -import { type Builder, Mixin, Model } from '@hcengineering/model' +import { DOMAIN_MODEL, type Account, type Domain, type Ref } from '@hcengineering/core' +import { Mixin, Model, type Builder } from '@hcengineering/model' import core, { TClass, TConfiguration, TDoc } from '@hcengineering/model-core' import view, { createAction } from '@hcengineering/model-view' import notification from '@hcengineering/notification' import type { Asset, IntlString } from '@hcengineering/platform' import { + settingId, type Editable, type Handler, type Integration, type IntegrationType, type InviteSettings, - settingId, type SettingsCategory, type UserMixin } from '@hcengineering/setting' -import task from '@hcengineering/task' import templates from '@hcengineering/templates' import setting from './plugin' @@ -39,8 +38,8 @@ import workbench from '@hcengineering/model-workbench' import { type AnyComponent } from '@hcengineering/ui/src/types' export { settingId } from '@hcengineering/setting' -export { default } from './plugin' export { settingOperation } from './migration' +export { default } from './plugin' export const DOMAIN_SETTING = 'setting' as Domain @@ -153,6 +152,9 @@ export function createModel (builder: Builder): void { label: setting.string.WorkspaceSetting, icon: setting.icon.Setting, component: setting.component.WorkspaceSettings, + extraComponents: { + navigation: setting.component.WorkspaceSettings + }, group: 'settings', secured: false, order: 2000 @@ -167,9 +169,9 @@ export function createModel (builder: Builder): void { label: setting.string.Integrations, icon: setting.icon.Integrations, component: setting.component.Integrations, - group: 'settings', + group: 'settings-account', secured: false, - order: 3000 + order: 1500 }, setting.ids.Integrations ) @@ -200,21 +202,7 @@ export function createModel (builder: Builder): void { setting.ids.Configure ) builder.createDoc( - setting.class.WorkspaceSettingCategory, - core.space.Model, - { - name: 'statuses', - label: setting.string.ManageProjects, - icon: task.icon.ManageTemplates, - component: setting.component.ManageProjects, - group: 'settings-editor', - secured: false, - order: 4000 - }, - setting.ids.ManageProjects - ) - builder.createDoc( - setting.class.WorkspaceSettingCategory, + setting.class.SettingsCategory, core.space.Model, { name: 'classes', @@ -237,7 +225,8 @@ export function createModel (builder: Builder): void { component: setting.component.EnumSetting, group: 'settings-editor', secured: false, - order: 4600 + order: 4600, + expandable: true }, setting.ids.EnumSetting ) diff --git a/models/task/package.json b/models/task/package.json index cddd74705b..1fa555532f 100644 --- a/models/task/package.json +++ b/models/task/package.json @@ -42,6 +42,7 @@ "@hcengineering/task-resources": "^0.6.0", "@hcengineering/ui": "^0.6.11", "@hcengineering/view": "^0.6.9", - "@hcengineering/workbench": "^0.6.9" + "@hcengineering/setting": "^0.6.11", + "@hcengineering/notification": "^0.6.16" } } diff --git a/models/task/src/index.ts b/models/task/src/index.ts index d65735874b..dbe770f162 100644 --- a/models/task/src/index.ts +++ b/models/task/src/index.ts @@ -16,18 +16,25 @@ import type { Employee, Person } from '@hcengineering/contact' import contact from '@hcengineering/contact' import { - type Class, + ClassifierKind, DOMAIN_MODEL, + DOMAIN_STATUS, + DOMAIN_TX, + IndexKind, + generateId, + type Class, + type Data, type Doc, type Domain, - IndexKind, type Ref, type Status, type StatusCategory, - type Timestamp + type Timestamp, + type TxCreateDoc, + type TxMixin } from '@hcengineering/core' import { - type Builder, + ArrOf, Collection, Hidden, Index, @@ -37,34 +44,50 @@ import { TypeBoolean, TypeDate, TypeMarkup, + TypeRecord, TypeRef, TypeString, - UX + UX, + type Builder, + type MigrationClient } from '@hcengineering/model' import attachment from '@hcengineering/model-attachment' -import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core' -import view, { createAction, template, actionTemplates as viewTemplates } from '@hcengineering/model-view' -import { type IntlString } from '@hcengineering/platform' -import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' +import chunter from '@hcengineering/model-chunter' +import core, { DOMAIN_SPACE, TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core' +import view, { + classPresenter, + createAction, + template, + actionTemplates as viewTemplates +} from '@hcengineering/model-view' +import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform' +import setting from '@hcengineering/setting' import tags from '@hcengineering/tags' import { + calculateStatuses, + findStatusAttr, type KanbanCard, type Project, type ProjectStatus, type ProjectType, - type ProjectTypeCategory, + type ProjectTypeClass, + type ProjectTypeDescriptor, type Sequence, type Task, + type TaskType, + type TaskTypeClass, + type TaskTypeDescriptor, + type TaskTypeKind, type TodoItem } from '@hcengineering/task' -import type { AnyComponent } from '@hcengineering/ui/src/types' +import type { AnyComponent } from '@hcengineering/ui' +import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' import { type ViewAction } from '@hcengineering/view' -import chunter from '@hcengineering/model-chunter' import task from './plugin' -export { taskId } from '@hcengineering/task' -export { createProjectType, createSequence, taskOperation } from './migration' +export { createProjectType, taskId } from '@hcengineering/task' +export { createSequence, taskOperation } from './migration' export { default } from './plugin' export const DOMAIN_TASK = 'task' as Domain @@ -80,6 +103,10 @@ export class TTask extends TAttachedDoc implements Task { @Index(IndexKind.Indexed) status!: Ref + @Prop(TypeRef(task.class.TaskType), task.string.TaskType) + @Index(IndexKind.Indexed) + kind!: Ref + @Prop(TypeString(), task.string.TaskNumber) @Index(IndexKind.FullText) @Hidden() @@ -132,27 +159,91 @@ export class TKanbanCard extends TClass implements KanbanCard { card!: AnyComponent } +@Model(task.class.TaskTypeDescriptor, core.class.Doc, DOMAIN_MODEL) +export class TTaskTypeDescriptor extends TDoc implements TaskTypeDescriptor { + name!: IntlString + description!: IntlString + icon!: Asset + baseClass!: Ref> + + // If specified, will allow to be created by users, system type overwize + allowCreate!: boolean +} + +@Mixin(task.mixin.TaskTypeClass, core.class.Class) +export class TTaskTypeClass extends TClass implements TaskTypeClass { + taskType!: Ref + projectType!: Ref +} + +@Mixin(task.mixin.ProjectTypeClass, core.class.Class) +export class TProjectTypeClass extends TClass implements ProjectTypeClass { + projectType!: Ref +} + @Model(task.class.Project, core.class.Space) export class TProject extends TSpace implements Project { - type!: Ref + @Prop(TypeRef(task.class.ProjectType), task.string.ProjectType) + type!: Ref } @Model(task.class.ProjectType, core.class.Space) export class TProjectType extends TSpace implements ProjectType { - statuses!: ProjectStatus[] shortDescription?: string - category!: Ref + + @Prop(TypeRef(task.class.ProjectTypeDescriptor), getEmbeddedLabel('Descriptor')) + descriptor!: Ref + + @Prop(ArrOf(TypeRef(task.class.TaskType)), getEmbeddedLabel('Tasks')) + tasks!: Ref[] + + @Prop(ArrOf(TypeRecord()), getEmbeddedLabel('Project statuses')) + statuses!: ProjectStatus[] + + @Prop(TypeRef(core.class.Class), getEmbeddedLabel('Target Class')) + targetClass!: Ref> } -@Model(task.class.ProjectTypeCategory, core.class.Doc, DOMAIN_MODEL) -export class TProjectTypeCategory extends TDoc implements ProjectTypeCategory { +@Model(task.class.TaskType, core.class.Doc, DOMAIN_TASK) +export class TTaskType extends TDoc implements TaskType { + @Prop(TypeString(), getEmbeddedLabel('Name')) + name!: string + + @Prop(TypeRef(task.class.TaskTypeDescriptor), getEmbeddedLabel('Descriptor')) + descriptor!: Ref + + @Prop(TypeRef(task.class.ProjectType), getEmbeddedLabel('Task class')) + parent!: Ref // Base class for task + + @Prop(TypeString(), getEmbeddedLabel('Kind')) + kind!: TaskTypeKind + + @Prop(ArrOf(TypeRef(task.class.TaskType)), getEmbeddedLabel('Parent')) + allowedAsChildOf!: Ref[] // In case of specified, task type is for sub-tasks + + @Prop(TypeRef(core.class.Class), getEmbeddedLabel('Task class')) + ofClass!: Ref> // Base class for task + + @Prop(TypeRef(core.class.Class), getEmbeddedLabel('Task target class')) + targetClass!: Ref> // Class or Mixin mixin to hold all user defined attributes. + + @Prop(ArrOf(TypeRef(core.class.Status)), getEmbeddedLabel('Task statuses')) + statuses!: Ref[] + + @Prop(TypeRef(core.class.Class), getEmbeddedLabel('Task status class')) + statusClass!: Ref> + + @Prop(TypeRef(core.class.StatusCategory), getEmbeddedLabel('Task status categories')) + statusCategories!: Ref[] +} + +@Model(task.class.ProjectTypeDescriptor, core.class.Doc, DOMAIN_MODEL) +export class TProjectTypeDescriptor extends TDoc implements ProjectTypeDescriptor { name!: IntlString description!: IntlString icon!: AnyComponent editor?: AnyComponent - attachedToClass!: Ref> - statusClass!: Ref> - statusCategories!: Ref[] + baseClass!: Ref> } @Model(task.class.Sequence, core.class.Doc, DOMAIN_KANBAN) @@ -218,7 +309,19 @@ export const actionTemplates = template({ }) export function createModel (builder: Builder): void { - builder.createModel(TKanbanCard, TSequence, TTask, TTodoItem, TProject, TProjectType, TProjectTypeCategory) + builder.createModel( + TKanbanCard, + TSequence, + TTask, + TTodoItem, + TProject, + TProjectType, + TTaskType, + TProjectTypeDescriptor, + TTaskTypeDescriptor, + TTaskTypeClass, + TProjectTypeClass + ) builder.createDoc( view.class.ViewletDescriptor, @@ -243,6 +346,10 @@ export function createModel (builder: Builder): void { editor: task.component.TaskHeader }) + builder.mixin(task.class.ProjectType, core.class.Class, view.mixin.IgnoreActions, { + actions: [view.action.Open] + }) + builder.createDoc( view.class.ActionCategory, core.space.Model, @@ -312,6 +419,16 @@ export function createModel (builder: Builder): void { presenter: task.component.TodoItemPresenter }) + builder.mixin(task.class.TaskType, core.class.Class, view.mixin.ObjectPresenter, { + presenter: task.component.TaskTypePresenter + }) + + builder.mixin(task.class.ProjectType, core.class.Class, view.mixin.ObjectPresenter, { + presenter: task.component.ProjectTypePresenter + }) + + classPresenter(builder, task.class.TaskType, task.component.TaskTypePresenter, task.component.TaskTypePresenter) + createAction(builder, { label: task.string.MarkAsDone, icon: task.icon.TodoCheck, @@ -370,7 +487,21 @@ export function createModel (builder: Builder): void { core.space.Model, { ofAttribute: task.attribute.State, - label: core.string.Status, + label: task.string.StateBacklog, + icon: task.icon.TaskState, + color: PaletteColorIndexes.Coin, + defaultStatusName: 'Backlog', + order: 0 + }, + task.statusCategory.UnStarted + ) + + builder.createDoc( + core.class.StatusCategory, + core.space.Model, + { + ofAttribute: task.attribute.State, + label: task.string.StateActive, icon: task.icon.TaskState, color: PaletteColorIndexes.Blueberry, defaultStatusName: 'New state', @@ -431,4 +562,252 @@ export function createModel (builder: Builder): void { // }, // task.ids.AssigneedNotification // ) + + builder.mixin(task.mixin.TaskTypeClass, core.class.Class, view.mixin.ObjectPresenter, { + presenter: task.component.TaskTypeClassPresenter + }) + builder.mixin(task.mixin.ProjectTypeClass, core.class.Class, view.mixin.ObjectPresenter, { + presenter: task.component.ProjectTypeClassPresenter + }) + + builder.mixin(task.class.Task, core.class.Class, setting.mixin.Editable, { + value: true + }) + + builder.mixin(task.class.Project, core.class.Class, setting.mixin.Editable, { + value: true + }) + + builder.createDoc( + setting.class.SettingsCategory, + core.space.Model, + { + name: 'statuses', + label: task.string.ManageProjects, + icon: task.icon.ManageTemplates, + component: task.component.ManageProjectsContent, + extraComponents: { + navigation: task.component.ManageProjects, + tools: task.component.ManageProjectsTools + }, + group: 'settings-editor', + secured: false, + order: 6000, + expandable: true + }, + task.ids.ManageProjects + ) +} + +/** + * @public + */ +export type FixTaskData = Omit, 'space' | 'statuses' | 'parent'> & { _id?: TaskType['_id'] } +export interface FixTaskResult { + taskTypes: TaskType[] + projectTypes: ProjectType[] + projects: Project[] +} +/** + * @public + */ +export async function fixTaskTypes ( + client: MigrationClient, + descriptor: Ref, + dataFactory: (t: ProjectType) => Promise +): Promise { + const categoryObj = client.model.findObject(descriptor) + if (categoryObj === undefined) { + throw new Error('category is not found in model') + } + + const projectTypes = await client.find(DOMAIN_SPACE, { + _class: task.class.ProjectType, + descriptor + }) + const baseClassClass = client.hierarchy.getClass(categoryObj.baseClass) + + const resultTaskTypes: TaskType[] = [] + const resultProjects: Project[] = [] + + for (const t of projectTypes) { + t.tasks = [...(t.tasks ?? [])] + if (t.targetClass === undefined) { + const targetProjectClassId: Ref> = generateId() + t.targetClass = targetProjectClassId + + await client.create>(DOMAIN_TX, { + _id: generateId(), + objectId: targetProjectClassId, + _class: core.class.TxCreateDoc, + objectClass: core.class.Class, + objectSpace: core.space.Model, + modifiedBy: core.account.ConfigUser, + modifiedOn: Date.now(), + space: core.space.Model, + attributes: { + extends: categoryObj.baseClass, + kind: ClassifierKind.MIXIN, + label: baseClassClass.label, + icon: baseClassClass.icon + } + }) + + await client.create, ProjectTypeClass>>(DOMAIN_TX, { + _class: core.class.TxMixin, + _id: generateId(), + space: core.space.Model, + modifiedBy: core.account.ConfigUser, + modifiedOn: Date.now(), + objectId: targetProjectClassId, + objectClass: core.class.Class, + objectSpace: core.space.Model, + mixin: task.mixin.ProjectTypeClass, + attributes: { + projectType: t._id + } + }) + await client.update( + DOMAIN_SPACE, + { + _id: t._id + }, + { $set: { targetClass: targetProjectClassId } } + ) + } + + const dataTypes = await dataFactory(t) + + const projects = await client.find(DOMAIN_SPACE, { type: t._id }) + resultProjects.push(...projects) + + for (const data of dataTypes) { + const taskTypeId: Ref = data._id ?? generateId() + const descr = client.model.getObject(data.descriptor) + + const statuses = await client.find(DOMAIN_STATUS, { + _id: { $in: t.statuses.map((it) => it._id) }, + _class: data.statusClass + }) + + const dStatuses = [...t.statuses.map((it) => it._id)] + const statusAttr = findStatusAttr(client.hierarchy, data.ofClass) + // Ensure we have at leas't one item in every category. + for (const c of data.statusCategories) { + const cat = await client.model.findOne(core.class.StatusCategory, { _id: c }) + const st = statuses.find((it) => it.category === c) + if (st === undefined) { + // We need to add new status into missing category + const statusId: Ref = generateId() + await client.create(DOMAIN_STATUS, { + _id: statusId, + _class: data.statusClass, + category: c, + modifiedBy: core.account.ConfigUser, + modifiedOn: Date.now(), + name: cat?.defaultStatusName ?? 'New state', + space: task.space.Statuses, + ofAttribute: statusAttr._id + }) + dStatuses.push(statusId) + + await client.update( + DOMAIN_SPACE, + { + _id: t._id + }, + { $push: { statuses: { _id: statusId } } } + ) + t.statuses.push({ _id: statusId, taskType: taskTypeId }) + } + } + const taskType: TaskType = { + ...data, + parent: t._id, + _id: taskTypeId, + _class: task.class.TaskType, + space: t._id, + statuses: dStatuses, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + kind: 'both', + icon: data.icon ?? descr.icon + } + + const ofClassClass = client.hierarchy.getClass(data.ofClass) + + taskType.icon = ofClassClass.icon + + // Create target class for custom field. + const targetClassId: Ref> = generateId() + taskType.targetClass = targetClassId + + await client.create>(DOMAIN_TX, { + _id: generateId(), + objectId: targetClassId, + _class: core.class.TxCreateDoc, + objectClass: core.class.Class, + objectSpace: core.space.Model, + modifiedBy: core.account.ConfigUser, + modifiedOn: Date.now(), + space: core.space.Model, + attributes: { + extends: data.ofClass, + kind: ClassifierKind.MIXIN, + label: getEmbeddedLabel(data.name), + icon: ofClassClass.icon + } + }) + + await client.create, TaskTypeClass>>(DOMAIN_TX, { + _class: core.class.TxMixin, + _id: generateId(), + space: core.space.Model, + modifiedBy: core.account.ConfigUser, + modifiedOn: Date.now(), + objectId: targetClassId, + objectClass: core.class.Class, + objectSpace: core.space.Model, + mixin: task.mixin.TaskTypeClass, + attributes: { + taskType: taskTypeId, + projectType: t._id + } + }) + + await client.create(DOMAIN_TASK, taskType) + resultTaskTypes.push(taskType) + + await client.update( + DOMAIN_SPACE, + { + _id: t._id + }, + { $push: { tasks: taskTypeId } } + ) + t.tasks.push(taskTypeId) + // Update kind and target classId + await client.update( + DOMAIN_TASK, + { space: { $in: projects.map((it) => it._id) }, _class: data.ofClass }, + { $set: { kind: taskTypeId } } + ) + } + + // We need to fix project statuses field, for proper icon calculation. + } + for (const t of projectTypes) { + const ttypes = await client.find(DOMAIN_TASK, { _id: { $in: t.tasks } }) + const newStatuses = calculateStatuses(t, new Map(ttypes.map((it) => [it._id, it])), []) + await client.update( + DOMAIN_SPACE, + { _id: t._id }, + { + $set: { + statuses: newStatuses + } + } + ) + } + return { taskTypes: resultTaskTypes, projectTypes, projects: resultProjects } } diff --git a/models/task/src/migration.ts b/models/task/src/migration.ts index 95afff3843..d8500e281f 100644 --- a/models/task/src/migration.ts +++ b/models/task/src/migration.ts @@ -13,46 +13,20 @@ // limitations under the License. // +import { TxOperations, type Class, type Doc, type Ref } from '@hcengineering/core' import { - type Attribute, - type Class, - DOMAIN_STATUS, - DOMAIN_TX, - type Data, - type Doc, - type Ref, - type Space, - type Status, - TxOperations, - generateId, - toIdMap -} from '@hcengineering/core' -import { - type MigrateOperation, - type MigrationClient, - type MigrationUpgradeClient, createOrUpdate, tryMigrate, - tryUpgrade + tryUpgrade, + type MigrateOperation, + type MigrationClient, + type MigrationUpgradeClient } from '@hcengineering/model' import core, { DOMAIN_SPACE } from '@hcengineering/model-core' import tags from '@hcengineering/model-tags' -import { - type Project, - type ProjectStatus, - type ProjectType, - type ProjectTypeCategory, - type Task, - createState, - taskId -} from '@hcengineering/task' -import view, { type Filter } from '@hcengineering/view' -import { DOMAIN_KANBAN, DOMAIN_TASK } from '.' +import { taskId } from '@hcengineering/task' import task from './plugin' -type ProjectData = Omit, 'statuses' | 'private' | 'members' | 'archived'> -type OldStatus = Status & { rank: string } - /** * @public */ @@ -65,47 +39,6 @@ export async function createSequence (tx: TxOperations, _class: Ref>) } } -/** - * @public - */ -export async function createProjectType ( - client: TxOperations, - data: ProjectData, - states: Data[], - _id: Ref, - stateClass: Ref> = core.class.Status -): Promise> { - const current = await client.findOne(task.class.ProjectType, { _id }) - if (current !== undefined) { - return current._id - } - - const statuses: Ref[] = [] - for (const st of states) { - statuses.push(await createState(client, stateClass, st)) - } - - const tmpl = await client.createDoc( - task.class.ProjectType, - core.space.Model, - { - description: data.description, - shortDescription: data.shortDescription, - category: data.category, - statuses: statuses.map((p) => { - return { _id: p } - }), - name: data.name, - private: false, - members: [], - archived: false - }, - _id - ) - - return tmpl -} - async function createDefaultSequence (tx: TxOperations): Promise { const current = await tx.findOne(core.class.Space, { _id: task.space.Sequence @@ -151,469 +84,9 @@ async function createDefaults (tx: TxOperations): Promise { await createDefaultStatesSpace(tx) } -async function renameState (client: MigrationClient): Promise { - const toUpdate = await client.find(DOMAIN_TASK, { state: { $exists: true } }) - if (toUpdate.length > 0) { - for (const doc of toUpdate) { - await client.update( - DOMAIN_TX, - { objectId: doc._id }, - { $rename: { 'attributes.state': 'attributes.status', 'operations.state': 'operations.status' } } - ) - await client.update( - DOMAIN_TX, - { 'tx.objectId': doc._id }, - { $rename: { 'tx.attributes.state': 'tx.attributes.status', 'tx.operations.state': 'tx.operations.status' } } - ) - } - await client.update(DOMAIN_TASK, { _id: { $in: toUpdate.map((p) => p._id) } }, { $rename: { state: 'status' } }) - } -} - -async function renameStatePrefs (client: MigrationUpgradeClient): Promise { - const txop = new TxOperations(client, core.account.System) - const prefs = await client.findAll(view.class.ViewletPreference, {}) - for (const pref of prefs) { - let update = false - const config = pref.config - for (let index = 0; index < config.length; index++) { - const conf = config[index] - if (typeof conf === 'string') { - if (conf === 'state') { - config[index] = 'status' - update = true - } else if (conf === '$lookup.state') { - config[index] = '$lookup.status' - update = true - } - } else if (conf.key === 'state') { - conf.key = 'status' - update = true - } - } - if (update) { - await txop.update(pref, { - config - }) - } - } - const res = await client.findAll(view.class.FilteredView, { filters: /"key":"state"/ as any }) - if (res.length > 0) { - for (const doc of res) { - const filters = JSON.parse(doc.filters) as Filter[] - for (const filter of filters) { - if (filter.key.key === 'state') { - filter.key.key = 'status' - } - } - await txop.update(doc, { - filters: JSON.stringify(filters) - }) - } - } -} - -async function fixStatusAttributes (client: MigrationClient): Promise { - const spaces = await client.find(DOMAIN_SPACE, {}) - const map = toIdMap(spaces) - const oldStatuses = await client.find(DOMAIN_STATUS, { ofAttribute: { $exists: false } }) - for (const oldStatus of oldStatuses) { - const space = map.get(oldStatus.space) - if (space !== undefined) { - try { - let ofAttribute = task.attribute.State - if (space._class === ('recruit:class:Vacancy' as Ref>)) { - ofAttribute = 'recruit:attribute:State' as Ref> - } - if (space._class === ('lead:class:Funnel' as Ref>)) { - ofAttribute = 'lead:attribute:State' as Ref> - } - if (space._class === ('board:class:Board' as Ref>)) { - ofAttribute = 'board:attribute:State' as Ref> - } - if (space._class === ('tracker:class:Project' as Ref>)) { - ofAttribute = 'tracker:attribute:IssueStatus' as Ref> - } - if (ofAttribute !== oldStatus.ofAttribute) { - await client.update(DOMAIN_STATUS, { _id: oldStatus._id }, { ofAttribute }) - } - } catch (err) { - console.log(err) - } - } - } -} - -function getTemplateOfAttribute (space: Ref): Ref> { - let ofAttribute = task.attribute.State - if (space === ('recruit:space:VacancyTemplates' as Ref)) { - ofAttribute = 'recruit:attribute:State' as Ref> - } - if (space === ('lead:space:FunnelTemplates' as Ref)) { - ofAttribute = 'lead:attribute:State' as Ref> - } - if (space === ('board:space:BoardTemplates' as Ref)) { - ofAttribute = 'board:attribute:State' as Ref> - } - return ofAttribute -} - -async function fixStatusDoneAttributes (client: MigrationClient): Promise { - const oldStatuses = await client.find(DOMAIN_STATUS, {}) - for (const oldStatus of oldStatuses) { - if (!oldStatus.ofAttribute.includes('DoneState')) continue - const ofAttribute = oldStatus.ofAttribute.replace('DoneState', 'State') - await client.update(DOMAIN_STATUS, { _id: oldStatus._id }, { ofAttribute }) - } -} - -async function removeDoneStatuses (client: MigrationClient): Promise { - const tasks = await client.find(DOMAIN_TASK, { doneState: { $exists: true } }) - for (const task of tasks) { - if ((task as any).doneState != null) { - await client.update(DOMAIN_TASK, { _id: task._id }, { status: (task as any).doneState, isDone: true }) - } - await client.update(DOMAIN_TASK, { _id: task._id }, { $unset: { doneState: '' } }) - } - await client.update( - DOMAIN_TX, - { 'tx.operations.doneState': { $ne: null } }, - { $rename: { 'tx.operations.doneState': 'tx.operations.status' } } - ) - await client.update( - DOMAIN_TX, - { 'tx.attributes.doneState': { $ne: null } }, - { $rename: { 'tx.attributes.doneState': 'tx.attributes.status' } } - ) - await client.update( - DOMAIN_TX, - { 'tx.operations.doneState': { $exists: true } }, - { $unset: { 'tx.operations.doneState': '' } } - ) - await client.update( - DOMAIN_TX, - { 'tx.attributes.doneState': { $exists: true } }, - { $unset: { 'tx.attributes.doneState': '' } } - ) - - // we need join doneStates to states for all projects - const classes = client.hierarchy.getDescendants(task.class.Project) - const projects = await client.find(DOMAIN_SPACE, { _class: { $in: classes }, doneStates: { $exists: true } }) - for (const project of projects) { - await client.update( - DOMAIN_SPACE, - { _id: project._id }, - { states: (project as any).states.concat((project as any).doneStates) } - ) - await client.update(DOMAIN_SPACE, { _id: project._id }, { $unset: { doneStates: '' } }) - } -} - -async function removeStateClass (client: MigrationClient): Promise { - await client.update( - DOMAIN_STATUS, - { _class: 'task:class:State' as Ref> }, - { _class: core.class.Status, category: task.statusCategory.Active } - ) - await client.update(DOMAIN_TX, { objectClass: 'task:class:State' }, { objectClass: core.class.Status }) - await client.update( - DOMAIN_STATUS, - { _class: 'task:class:WonState' as Ref> }, - { _class: core.class.Status, category: task.statusCategory.Won } - ) - await client.update(DOMAIN_TX, { objectClass: 'task:class:WonState' }, { objectClass: core.class.Status }) - await client.update( - DOMAIN_STATUS, - { _class: 'task:class:LostState' as Ref> }, - { _class: core.class.Status, category: task.statusCategory.Lost } - ) - await client.update(DOMAIN_TX, { objectClass: 'task:class:LostState' }, { objectClass: core.class.Status }) - await client.update( - DOMAIN_STATUS, - { _class: 'task:class:DoneState' as Ref> }, - { _class: core.class.Status } - ) - await client.update(DOMAIN_TX, { objectClass: 'task:class:DoneState' }, { objectClass: core.class.Status }) -} - -async function migrateTemplatesToTypes (client: MigrationClient): Promise { - interface KanbanTemplate extends Doc { - title: string - description?: string - shortDescription?: string - } - - interface KanbanTemplateSpace extends Space { - attachedToClass: Ref> - } - - interface StateTemplate extends Doc, Status { - attachedTo: Ref - rank: string - } - - const classes = client.hierarchy.getDescendants(task.class.Project) - const templates = await client.find(DOMAIN_KANBAN, { - _class: 'task:class:KanbanTemplate' as Ref> - }) - for (const template of templates) { - const used = await client.find(DOMAIN_SPACE, { templateId: template._id }) - if (used.length === 0) { - await client.delete(DOMAIN_KANBAN, template._id) - continue - } - const space = ( - await client.find(DOMAIN_SPACE, { _id: template.space as Ref }) - )[0] - if (space === undefined) continue - const states = await client.find( - DOMAIN_KANBAN, - { _class: 'task:class:StateTemplate' as Ref>, attachedTo: template._id }, - { sort: { rank: 1 } } - ) - const wonStates = await client.find( - DOMAIN_KANBAN, - { _class: 'task:class:WonStateTemplate' as Ref>, attachedTo: template._id }, - { sort: { rank: 1 } } - ) - const lostStates = await client.find( - DOMAIN_KANBAN, - { _class: 'task:class:LostStateTemplate' as Ref>, attachedTo: template._id }, - { sort: { rank: 1 } } - ) - - const statuses: ProjectStatus[] = [] - const currentStates = await client.find(DOMAIN_STATUS, {}) - for (const st of states) { - const exists = currentStates.find((p) => p.name.toLocaleLowerCase() === st.name.toLocaleLowerCase()) - if (exists !== undefined) { - statuses.push({ _id: exists._id, color: st.color }) - } else { - const id = generateId() - await client.create(DOMAIN_STATUS, { - ofAttribute: getTemplateOfAttribute(st.space), - name: st.name, - _id: id, - space: task.space.Statuses, - modifiedOn: st.modifiedOn, - modifiedBy: st.modifiedBy, - _class: core.class.Status, - color: st.color ?? 9, - createdBy: st.createdBy, - createdOn: st.createdOn, - category: task.statusCategory.Active - }) - statuses.push({ _id: id, color: st.color }) - } - } - - for (const st of wonStates) { - const exists = currentStates.find((p) => p.name.toLocaleLowerCase() === st.name.toLocaleLowerCase()) - if (exists !== undefined) { - statuses.push({ _id: exists._id, color: st.color }) - } else { - const id = generateId() - await client.create(DOMAIN_STATUS, { - ofAttribute: getTemplateOfAttribute(st.space), - name: st.name, - _id: id, - space: task.space.Statuses, - modifiedOn: st.modifiedOn, - modifiedBy: st.modifiedBy, - _class: core.class.Status, - color: st.color ?? 15, - createdBy: st.createdBy, - createdOn: st.createdOn, - category: task.statusCategory.Won - }) - statuses.push({ _id: id, color: st.color }) - } - } - - for (const st of lostStates) { - const exists = currentStates.find((p) => p.name.toLocaleLowerCase() === st.name.toLocaleLowerCase()) - if (exists !== undefined) { - statuses.push({ _id: exists._id, color: st.color }) - } else { - const id = generateId() - await client.create(DOMAIN_STATUS, { - ofAttribute: getTemplateOfAttribute(st.space), - name: st.name, - _id: id, - space: task.space.Statuses, - modifiedOn: st.modifiedOn, - modifiedBy: st.modifiedBy, - _class: core.class.Status, - color: st.color ?? 0, - createdBy: st.createdBy, - createdOn: st.createdOn, - category: task.statusCategory.Lost - }) - statuses.push({ _id: id, color: st.color }) - } - } - - const category = await getProjectTypeCategory(client, space.attachedToClass) - await client.create(DOMAIN_SPACE, { - name: template.title, - description: template.description ?? '', - shortDescription: template.shortDescription, - category: category ?? (template.space as Ref as Ref), - private: false, - members: [], - archived: false, - _id: template._id as Ref as Ref, - space: core.space.Space, - modifiedOn: template.modifiedOn, - modifiedBy: template.modifiedBy, - _class: task.class.ProjectType, - statuses - }) - await client.delete(DOMAIN_KANBAN, template._id) - } - - // we should found all projects without types and has templateID we should just rename it to type (if it exists) - const projectsWithTemplate = await client.find(DOMAIN_SPACE, { - _class: { $in: classes }, - type: { $exists: false }, - templateId: { $exists: true } - }) - for (const project of projectsWithTemplate) { - await client.update(DOMAIN_SPACE, { _id: project._id }, { type: (project as any).templateId }) - } - - // we should remove all state templates - const stateClasses = [ - 'task:class:StateTemplate' as Ref>, - 'task:class:WonStateTemplate' as Ref>, - 'task:class:LostStateTemplate' as Ref> - ] - const states = await client.find(DOMAIN_KANBAN, { _class: { $in: stateClasses } }) - for (const st of states) { - await client.delete(DOMAIN_KANBAN, st._id) - } -} - -async function getProjectTypeCategory ( - client: MigrationClient, - _class: Ref> -): Promise | undefined> { - let clazz = _class - while (true) { - const res = await client.model.findAll(task.class.ProjectTypeCategory, { attachedToClass: clazz }) - if (res[0] !== undefined) return res[0]._id - const parent = client.hierarchy.getClass(clazz) - if (parent.extends === undefined) return - clazz = parent.extends - } -} - -async function migrateProjectTypes (client: MigrationClient): Promise { - const classes = client.hierarchy.getDescendants(task.class.Project) - // we should found all projects without types and group by class and then by statuses and create new type - const projects = await client.find(DOMAIN_SPACE, { _class: { $in: classes }, type: { $exists: false } }) - const projectsByCategory = new Map, Project[]>() - for (const project of projects) { - const category = await getProjectTypeCategory(client, project._class) - if (category === undefined) continue - const arr = projectsByCategory.get(category) ?? [] - arr.push(project) - projectsByCategory.set(category, arr) - } - for (const [category, projects] of projectsByCategory) { - const gouped = group(projects) - for (const gr of gouped) { - await client.create(DOMAIN_SPACE, { - category, - name: gr.projects[0].name, - description: '', - private: false, - members: [], - archived: false, - _id: gr.type, - space: core.space.Space, - modifiedOn: Date.now(), - modifiedBy: core.account.System, - _class: task.class.ProjectType, - statuses: gr.statuses.map((p) => { - return { _id: p } - }) - }) - - await client.update(DOMAIN_SPACE, { _id: { $in: gr.projects.map((p) => p._id) } }, { type: gr.type }) - } - } - - // we need remove states from all projects - const allProjects = await client.find(DOMAIN_SPACE, { _class: { $in: classes } }) - await Promise.all( - allProjects.map(async (project) => { - await client.update( - DOMAIN_SPACE, - { _id: project._id }, - { $unset: { states: '', templateId: '', doneStates: '' } } - ) - }) - ) -} - -interface ProjectTypeGroup { - statuses: Ref[] - projects: Project[] - type: Ref -} - -function group (projects: Project[]): ProjectTypeGroup[] { - const map = new Map() - - for (const project of projects) { - const ids = getIds((project as any).states ?? []) - const obj: ProjectTypeGroup = map.get(ids) ?? { - statuses: (project as any).states ?? [], - projects: [], - type: generateId() - } - obj.projects.push(project) - map.set(ids, obj) - } - return Array.from(map.values()) -} - -function getIds (states: Ref[]): string { - return states.join(',') -} - export const taskOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await tryMigrate(client, taskId, [ - { - state: 'fixStatusAttributes', - func: fixStatusAttributes - }, - { - state: 'renameState', - func: renameState - }, - { - state: 'removeDoneStatuses', - func: removeDoneStatuses - }, - { - state: 'fixStatusDoneAttributes', - func: fixStatusDoneAttributes - }, - { - state: 'removeStateClass', - func: removeStateClass - }, - { - state: 'migrateTemplatesToTypes', - func: migrateTemplatesToTypes - }, - { - state: 'migrateProjectTypes', - func: migrateProjectTypes - }, { state: 'projectTypeSpace', func: async (client) => { @@ -640,11 +113,6 @@ export const taskOperation: MigrateOperation = { task.category.TaskTag ) - await tryUpgrade(client, taskId, [ - { - state: 'renameStatePrefs', - func: renameStatePrefs - } - ]) + await tryUpgrade(client, taskId, []) } } diff --git a/models/task/src/plugin.ts b/models/task/src/plugin.ts index a8d397cd88..c0de8d6984 100644 --- a/models/task/src/plugin.ts +++ b/models/task/src/plugin.ts @@ -14,8 +14,9 @@ // limitations under the License. // +import {} from '@hcengineering/notification' import type { Ref, Space } from '@hcengineering/core' -import { mergeIds } from '@hcengineering/platform' +import { mergeIds, type IntlString } from '@hcengineering/platform' import { type TagCategory } from '@hcengineering/tags' import { taskId } from '@hcengineering/task' import task from '@hcengineering/task-resources/src/plugin' @@ -59,7 +60,14 @@ export default mergeIds(taskId, task, { StatusSelector: '' as AnyComponent, TemplatesIcon: '' as AnyComponent, TypesView: '' as AnyComponent, - StateIconPresenter: '' as AnyComponent + StateIconPresenter: '' as AnyComponent, + TaskTypePresenter: '' as AnyComponent, + ProjectTypePresenter: '' as AnyComponent, + TaskTypeClassPresenter: '' as AnyComponent, + ProjectTypeClassPresenter: '' as AnyComponent, + ManageProjects: '' as AnyComponent, + ManageProjectsTools: '' as AnyComponent, + ManageProjectsContent: '' as AnyComponent }, space: { TasksPublic: '' as Ref @@ -67,5 +75,10 @@ export default mergeIds(taskId, task, { viewlet: { TableIssue: '' as Ref, KanbanIssue: '' as Ref + }, + string: { + ManageProjects: '' as IntlString, + StateBacklog: '' as IntlString, + StateActive: '' as IntlString } }) diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 3c94010807..ab96d2faca 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -211,6 +211,7 @@ function defineFilters (builder: Builder): void { // builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, { filters: [ + 'kind', 'status', 'priority', 'space', @@ -359,7 +360,8 @@ function defineApplication ( ['all', tracker.string.All, {}], ['active', tracker.string.Active, {}], ['backlog', tracker.string.Backlog, {}] - ] + ], + allProjectsTypes: true } }, { @@ -368,7 +370,6 @@ function defineApplication ( icon: view.icon.Archive, label: tracker.string.AllProjects, position: 'bottom', - visibleIf: workbench.function.IsOwner, spaceClass: tracker.class.Project, componentProps: { _class: tracker.class.Project, @@ -686,22 +687,27 @@ export function createModel (builder: Builder): void { ) builder.createDoc( - task.class.ProjectTypeCategory, + task.class.ProjectTypeDescriptor, core.space.Model, { - name: tracker.string.Projects, + name: tracker.string.TrackerApplication, description: tracker.string.ManageWorkflowStatuses, icon: task.component.TemplatesIcon, - attachedToClass: tracker.class.Project, - statusClass: tracker.class.IssueStatus, - statusCategories: [ - tracker.issueStatusCategory.Backlog, - tracker.issueStatusCategory.Unstarted, - tracker.issueStatusCategory.Started, - tracker.issueStatusCategory.Completed, - tracker.issueStatusCategory.Canceled - ] + baseClass: tracker.class.Project }, - tracker.category.ProjectTypeCategory + tracker.descriptors.ProjectType + ) + + builder.createDoc( + task.class.TaskTypeDescriptor, + core.space.Model, + { + baseClass: tracker.class.Issue, + allowCreate: true, + description: tracker.string.Issue, + icon: tracker.icon.Issue, + name: tracker.string.Issue + }, + tracker.descriptors.Issue ) } diff --git a/models/tracker/src/migration.ts b/models/tracker/src/migration.ts index cff9771d5f..3adb25162f 100644 --- a/models/tracker/src/migration.ts +++ b/models/tracker/src/migration.ts @@ -13,31 +13,20 @@ // limitations under the License. // -import core, { - DOMAIN_TX, - type Data, - type Ref, - SortingOrder, - type Status, - type TxCollectionCUD, - type TxCreateDoc, - TxOperations, - type TxUpdateDoc, - toIdMap -} from '@hcengineering/core' +import core, { SortingOrder, TxOperations, generateId, type Data, type Ref, type Status } from '@hcengineering/core' import { + createOrUpdate, + tryMigrate, type MigrateOperation, type MigrationClient, - type MigrationUpgradeClient, - createOrUpdate, - tryMigrate + type MigrationUpgradeClient } from '@hcengineering/model' -import { DOMAIN_TASK, createProjectType } from '@hcengineering/model-task' +import { DOMAIN_SPACE } from '@hcengineering/model-core' +import { createProjectType, fixTaskTypes } from '@hcengineering/model-task' import tags from '@hcengineering/tags' -import { type Issue, TimeReportDayType, type TimeSpendReport } from '@hcengineering/tracker' -import view from '@hcengineering/view' +import task, { type TaskType } from '@hcengineering/task' +import { TimeReportDayType } from '@hcengineering/tracker' import tracker from './plugin' -import { DOMAIN_TRACKER } from './types' async function createDefaultProject (tx: TxOperations): Promise { const current = await tx.findOne(tracker.class.Project, { @@ -48,14 +37,12 @@ async function createDefaultProject (tx: TxOperations): Promise { objectId: tracker.project.DefaultProject }) - // Create new if not deleted by customers. - if (current === undefined && currentDeleted === undefined) { + if ((await tx.findOne(task.class.ProjectType, { _id: tracker.ids.ClassingProjectType })) === undefined) { const categories = await tx.findAll( core.class.StatusCategory, { ofAttribute: tracker.attribute.IssueStatus }, { sort: { order: SortingOrder.Ascending } } ) - const states: Omit, 'rank'>[] = [] for (const category of categories) { @@ -65,22 +52,85 @@ async function createDefaultProject (tx: TxOperations): Promise { category: category._id }) } + await createProjectType( + tx, + { + name: 'Classic project', + descriptor: tracker.descriptors.ProjectType, + description: '', + tasks: [] + }, + [ + { + _id: tracker.taskTypes.Issue, + descriptor: tracker.descriptors.Issue, + name: 'Issue', + factory: states, + ofClass: tracker.class.Issue, + targetClass: tracker.class.Issue, + statusCategories: categories.map((it) => it._id), + statusClass: core.class.Status, + kind: 'both', + allowedAsChildOf: [tracker.taskTypes.Issue] + } + ], + tracker.ids.ClassingProjectType + ) + } - const typeId = await createProjectType( + if ((await tx.findOne(task.class.ProjectType, { _id: tracker.ids.BaseProjectType })) === undefined) { + const issueId: Ref = generateId() + const baseCategories = [ + task.statusCategory.UnStarted, + task.statusCategory.Active, + task.statusCategory.Won, + task.statusCategory.Lost + ] + const categories = await tx.findAll( + core.class.StatusCategory, + { _id: { $in: baseCategories } }, + { sort: { order: SortingOrder.Ascending } } + ) + const states: Omit, 'rank'>[] = [] + + for (const category of categories) { + states.push({ + ofAttribute: tracker.attribute.IssueStatus, + name: category.defaultStatusName, + category: category._id + }) + } + await createProjectType( tx, { name: 'Base project', - category: tracker.category.ProjectTypeCategory, - description: '' + descriptor: tracker.descriptors.ProjectType, + description: '', + tasks: [] }, - states, - tracker.ids.BaseProjectType, - tracker.class.IssueStatus + [ + { + _id: issueId, + name: 'Issue', + descriptor: tracker.descriptors.Issue, + factory: states, + ofClass: tracker.class.Issue, + targetClass: tracker.class.Issue, + statusCategories: baseCategories, + statusClass: core.class.Status, + kind: 'both', + allowedAsChildOf: [issueId] + } + ], + tracker.ids.BaseProjectType ) + } + // Create new if not deleted by customers. + if (current === undefined && currentDeleted === undefined) { const state = await tx.findOne( tracker.class.IssueStatus, - { space: typeId }, + { space: tracker.ids.DefaultProjectType }, { sort: { rank: SortingOrder.Ascending } } ) if (state !== undefined) { @@ -98,7 +148,7 @@ async function createDefaultProject (tx: TxOperations): Promise { defaultIssueStatus: state._id, defaultTimeReportDay: TimeReportDayType.PreviousWorkDay, defaultAssignee: undefined, - type: typeId + type: tracker.ids.DefaultProjectType }, tracker.project.DefaultProject ) @@ -123,184 +173,55 @@ async function createDefaults (tx: TxOperations): Promise { ) } -async function fixIconsWithEmojis (tx: TxOperations): Promise { - const projectsWithWrongIcon = await tx.findAll(tracker.class.Project, { icon: tracker.component.IconWithEmoji }) - const promises = [] - for (const project of projectsWithWrongIcon) { - promises.push(tx.update(project, { icon: view.ids.IconWithEmoji })) - } - await Promise.all(promises) -} - -async function fixSpentTime (client: MigrationClient): Promise { - const issues = await client.find(DOMAIN_TASK, { reportedTime: { $gt: 0 } }) - for (const issue of issues) { - const childInfo = issue.childInfo - for (const child of childInfo ?? []) { - child.reportedTime = child.reportedTime * 8 - } - await client.update(DOMAIN_TASK, { _id: issue._id }, { reportedTime: issue.reportedTime * 8, childInfo }) - } - const reports = await client.find(DOMAIN_TRACKER, {}) - for (const report of reports) { - await client.update(DOMAIN_TRACKER, { _id: report._id }, { value: report.value * 8 }) - } - const createTxes = await client.find>(DOMAIN_TX, { - 'tx.objectClass': tracker.class.TimeSpendReport, - 'tx._class': core.class.TxCreateDoc, - 'tx.attributes.value': { $exists: true } - }) - for (const tx of createTxes) { - await client.update( - DOMAIN_TX, - { _id: tx._id }, - { 'tx.attributes.value': (tx.tx as TxCreateDoc).attributes.value * 8 } - ) - } - const updateTxes = await client.find>(DOMAIN_TX, { - 'tx.objectClass': tracker.class.TimeSpendReport, - 'tx._class': core.class.TxUpdateDoc, - 'tx.operations.value': { $exists: true } - }) - for (const tx of updateTxes) { - const val = (tx.tx as TxUpdateDoc).operations.value - if (val !== undefined) { - await client.update(DOMAIN_TX, { _id: tx._id }, { 'tx.operations.value': val * 8 }) - } - } -} - -async function fixEstimation (client: MigrationClient): Promise { - const issues = await client.find(DOMAIN_TASK, { estimation: { $gt: 0 } }) - for (const issue of issues) { - const childInfo = issue.childInfo - for (const child of childInfo ?? []) { - child.estimation = child.estimation * 8 - } - await client.update(DOMAIN_TASK, { _id: issue._id }, { estimation: issue.estimation * 8, childInfo }) - } - const createTxes = await client.find>(DOMAIN_TX, { - 'tx.objectClass': tracker.class.Issue, - 'tx._class': core.class.TxCreateDoc, - 'tx.attributes.estimation': { $gt: 0 } - }) - for (const tx of createTxes) { - await client.update( - DOMAIN_TX, - { _id: tx._id }, - { 'tx.attributes.estimation': (tx.tx as TxCreateDoc).attributes.estimation * 8 } - ) - } - const updateTxes = await client.find>(DOMAIN_TX, { - 'tx.objectClass': tracker.class.Issue, - 'tx._class': core.class.TxUpdateDoc, - 'tx.operations.estimation': { $exists: true } - }) - for (const tx of updateTxes) { - const val = (tx.tx as TxUpdateDoc).operations.estimation - if (val !== undefined) { - await client.update(DOMAIN_TX, { _id: tx._id }, { 'tx.operations.estimation': val * 8 }) - } - } -} - -async function fixRemainingTime (client: MigrationClient): Promise { - while (true) { - const issues = await client.find( - DOMAIN_TASK, - { _class: tracker.class.Issue, remainingTime: { $exists: false } }, - { limit: 1000 } - ) - for (const issue of issues) { - await client.update( - DOMAIN_TASK, - { _id: issue._id }, - { remainingTime: Math.max(0, issue.estimation - issue.reportedTime) } - ) - } - if (issues.length === 0) { - break - } - } - await client.update( - DOMAIN_TASK, - { _class: { $ne: tracker.class.Issue }, remainingTime: { $exists: true } }, - { $unset: { remainingTime: '' } } - ) -} - -async function fixParentsSpace (client: MigrationClient): Promise { - while (true) { - const issues = await client.find( - DOMAIN_TASK, - { _class: tracker.class.Issue, 'parents.space': { $exists: false }, parents: { $exists: true, $ne: [] } }, - { limit: 1000 } - ) - - const parentIds = new Set>() - for (const i of issues) { - for (const p of i.parents ?? []) { - parentIds.add(p.parentId) +async function fixTrackerTaskTypes (client: MigrationClient): Promise { + await fixTaskTypes(client, tracker.descriptors.ProjectType, async (t) => { + const typeId: Ref = generateId() + return [ + { + _id: typeId, + name: 'Issue', + descriptor: tracker.descriptors.Issue, + ofClass: tracker.class.Issue, + targetClass: tracker.class.Issue, + statusCategories: [ + tracker.issueStatusCategory.Backlog, + tracker.issueStatusCategory.Unstarted, + tracker.issueStatusCategory.Started, + tracker.issueStatusCategory.Completed, + tracker.issueStatusCategory.Canceled + ], + statusClass: tracker.class.IssueStatus, + kind: 'task', + allowedAsChildOf: [typeId] } - } - - const parentIssues = toIdMap( - await client.find(DOMAIN_TASK, { _class: tracker.class.Issue, _id: { $in: Array.from(parentIds) } }) - ) - - for (const issue of issues) { - await client.update( - DOMAIN_TASK, - { _id: issue._id }, - { parents: issue.parents.map((it) => ({ ...it, space: parentIssues.get(it.parentId)?.space ?? it.space })) } - ) - } - if (issues.length === 0) { - break - } - } - await client.update( - DOMAIN_TASK, - { _class: { $ne: tracker.class.Issue }, remainingTime: { $exists: true } }, - { $unset: { remainingTime: '' } } - ) -} - -async function moveIssues (client: MigrationClient): Promise { - const docs = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Issue }) - if (docs.length > 0) { - await client.move(DOMAIN_TRACKER, { _class: tracker.class.Issue }, DOMAIN_TASK) - } + ] + }) } export const trackerOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await tryMigrate(client, 'tracker', [ { - state: 'moveIssues', - func: moveIssues + state: 'fix-category-descriptors', + func: async (client) => { + await client.update( + DOMAIN_SPACE, + { _class: task.class.ProjectType, category: 'tracker:category:ProjectTypeCategory' }, + { + $set: { descriptor: tracker.descriptors.ProjectType }, + $unset: { category: 1 } + } + ) + } }, { - state: 'reportTimeDayToHour', - func: fixSpentTime - }, - { - state: 'estimationDayToHour', - func: fixEstimation - }, - { - state: 'fixRemainingTime', - func: fixRemainingTime - }, - { - state: 'fixParentsSpace', - func: fixParentsSpace + state: 'fixTaskTypes', + func: fixTrackerTaskTypes } ]) }, async upgrade (client: MigrationUpgradeClient): Promise { const tx = new TxOperations(client, core.account.System) await createDefaults(tx) - await fixIconsWithEmojis(tx) } } diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index 1cd9f61d83..a0e324c192 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -13,18 +13,18 @@ // See the License for the specific language governing permissions and // limitations under the License. // +import { type DocUpdateMessageViewlet, type TxViewlet } from '@hcengineering/activity' +import { type ChatMessageViewlet } from '@hcengineering/chunter' import { type Doc, type Ref } from '@hcengineering/core' import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation' -import { type IntlString, type Resource, mergeIds } from '@hcengineering/platform' -import { type ProjectType } from '@hcengineering/task' +import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' +import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform' +import { type ProjectType, type TaskTypeDescriptor } from '@hcengineering/task' import { trackerId } from '@hcengineering/tracker' import tracker from '@hcengineering/tracker-resources/src/plugin' import type { AnyComponent } from '@hcengineering/ui/src/types' import { type Action, type ViewAction, type Viewlet } from '@hcengineering/view' import { type Application } from '@hcengineering/workbench' -import { type DocUpdateMessageViewlet, type TxViewlet } from '@hcengineering/activity' -import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' -import { type ChatMessageViewlet } from '@hcengineering/chunter' export default mergeIds(trackerId, tracker, { string: { @@ -59,7 +59,8 @@ export default mergeIds(trackerId, tracker, { MilestoneFilter: '' as AnyComponent, EditRelatedTargets: '' as AnyComponent, EditRelatedTargetsPopup: '' as AnyComponent, - IssueSearchIcon: '' as AnyComponent + IssueSearchIcon: '' as AnyComponent, + MembersArrayEditor: '' as AnyComponent }, app: { Tracker: '' as Ref @@ -85,7 +86,9 @@ export default mergeIds(trackerId, tracker, { IssueChatMessageViewlet: '' as Ref, IssueTemplateChatMessageViewlet: '' as Ref, ComponentChatMessageViewlet: '' as Ref, - MilestoneChatMessageViewlet: '' as Ref + MilestoneChatMessageViewlet: '' as Ref, + ClassingProjectType: '' as Ref, + DefaultProjectType: '' as Ref }, completion: { IssueQuery: '' as Resource, @@ -106,5 +109,8 @@ export default mergeIds(trackerId, tracker, { DeleteProject: '' as Ref>>, DeleteProjectClean: '' as Ref>>, DeleteIssue: '' as Ref>> + }, + descriptors: { + Issue: '' as Ref } }) diff --git a/models/tracker/src/types.ts b/models/tracker/src/types.ts index bf3c9a54b2..aac243f985 100644 --- a/models/tracker/src/types.ts +++ b/models/tracker/src/types.ts @@ -69,7 +69,7 @@ import chunter from '@hcengineering/chunter' export const DOMAIN_TRACKER = 'tracker' as Domain @Model(tracker.class.IssueStatus, core.class.Status) -@UX(tracker.string.IssueStatuses, undefined, undefined, 'rank', 'name') +@UX(tracker.string.IssueStatus, undefined, undefined, 'rank', 'name') export class TIssueStatus extends TStatus implements IssueStatus {} /** * @public @@ -120,7 +120,7 @@ export class TProject extends TTaskProject implements Project { declare defaultTimeReportDay: TimeReportDayType - @Prop(Collection(tracker.class.RelatedIssueTarget), tracker.string.RelatedIssue) + @Prop(Collection(tracker.class.RelatedIssueTarget), tracker.string.RelatedIssues) relatedIssueTargets!: number } /** diff --git a/models/tracker/src/viewlets.ts b/models/tracker/src/viewlets.ts index 5d604aeba4..9ad3ec976a 100644 --- a/models/tracker/src/viewlets.ts +++ b/models/tracker/src/viewlets.ts @@ -26,6 +26,7 @@ import tags from '@hcengineering/tags' export const issuesOptions = (kanban: boolean): ViewOptionsModel => ({ groupBy: [ 'status', + 'kind', 'assignee', 'priority', 'component', @@ -38,6 +39,7 @@ export const issuesOptions = (kanban: boolean): ViewOptionsModel => ({ ], orderBy: [ ['status', SortingOrder.Ascending], + ['kind', SortingOrder.Ascending], ['priority', SortingOrder.Ascending], ['modifiedOn', SortingOrder.Descending], ['createdOn', SortingOrder.Descending], @@ -95,6 +97,13 @@ export function issueConfig ( props: { kind: 'list', size: 'small', justify: 'center' }, displayProps: { key: key + 'status' } }, + // { + // key: 'kind', + // label: task.string.TaskType, + // presenter: task.component.TaskTypePresenter, + // props: { kind: 'list', size: 'small', justify: 'center' }, + // displayProps: { key: key + 'kind' } + // }, { key: '', label: tracker.string.Title, @@ -230,9 +239,10 @@ export function defineViewlets (builder: Builder): void { ) const subIssuesOptions: ViewOptionsModel = { - groupBy: ['status', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'], + groupBy: ['status', 'kind', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'], orderBy: [ ['rank', SortingOrder.Ascending], + ['kind', SortingOrder.Ascending], ['status', SortingOrder.Ascending], ['priority', SortingOrder.Ascending], ['modifiedOn', SortingOrder.Descending], @@ -508,8 +518,9 @@ export function defineViewlets (builder: Builder): void { attachTo: tracker.class.Project, descriptor: view.viewlet.List, viewOptions: { - groupBy: ['createdBy'], + groupBy: ['type', 'createdBy'], orderBy: [ + ['type', SortingOrder.Descending], ['modifiedOn', SortingOrder.Descending], ['createdOn', SortingOrder.Descending] ], @@ -524,7 +535,26 @@ export function defineViewlets (builder: Builder): void { key: '', props: { kind: 'list' } }, - { key: '', displayProps: { grow: true } } + { key: '', displayProps: { grow: true } }, + { + key: '', + presenter: tracker.component.MembersArrayEditor, + sortingKey: 'members', + props: { readonly: true, kind: 'list' } + }, + { + key: 'type', + props: { kind: 'list' } + }, + { + key: 'defaultAssignee', + props: { kind: 'list' } + }, + { + key: 'modifiedOn', + presenter: tracker.component.ModificationDatePresenter, + displayProps: { fixed: 'right', dividerBefore: true } + } ] }, tracker.viewlet.ProjectList diff --git a/packages/core/src/hierarchy.ts b/packages/core/src/hierarchy.ts index f8bbd9c25a..3597a4bd13 100644 --- a/packages/core/src/hierarchy.ts +++ b/packages/core/src/hierarchy.ts @@ -427,7 +427,11 @@ export class Hierarchy { this.classifierProperties.delete(attribute.attributeOf) } - getAllAttributes (clazz: Ref, to?: Ref): Map { + getAllAttributes ( + clazz: Ref, + to?: Ref, + traverse?: (name: string, attr: AnyAttribute) => void + ): Map { const result = new Map() let ancestors = this.getAncestors(clazz) if (to !== undefined) { @@ -448,6 +452,7 @@ export class Hierarchy { const attributes = this.attributes.get(cls) if (attributes !== undefined) { for (const [name, attr] of attributes) { + traverse?.(name, attr) result.set(name, attr) } } diff --git a/packages/platform-rig/profiles/package/eslint.config.json b/packages/platform-rig/profiles/package/eslint.config.json index f91dabca82..5fc789658b 100644 --- a/packages/platform-rig/profiles/package/eslint.config.json +++ b/packages/platform-rig/profiles/package/eslint.config.json @@ -7,5 +7,5 @@ "@typescript-eslint/array-type": "off", "@typescript-eslint/promise-function-async": "off", "@typescript-eslint/consistent-type-imports": "off" - } + } } diff --git a/packages/platform-rig/profiles/ui/eslint.config.json b/packages/platform-rig/profiles/ui/eslint.config.json index f2dfc00197..9397f5f415 100644 --- a/packages/platform-rig/profiles/ui/eslint.config.json +++ b/packages/platform-rig/profiles/ui/eslint.config.json @@ -19,6 +19,14 @@ }, "plugins": ["@typescript-eslint", "import"], "ignorePatterns": ["*.json", "node_modules/*", ".eslintrc.js"], + "settings": { + "import/resolver": { + "node": { + "extensions": [".ts"], + "moduleDirectory": ["src", "node_modules"] + } + } + }, "overrides": [ { "files": ["**/*.svelte"], @@ -30,13 +38,13 @@ "@typescript-eslint/array-type": "off", "@typescript-eslint/promise-function-async": "off", "@typescript-eslint/consistent-type-imports": "off", - "import/first": "off", - "import/no-duplicates": "off", + "import/first": "warn", + "import/no-duplicates": "warn", "import/no-mutable-exports": "off", - "import/no-unresolved": "off", - "no-multiple-empty-lines": "off", + "import/no-unresolved": "warn", + "no-multiple-empty-lines": "warn", "no-undef-init": "off", - "no-use-before-define": "off", + "no-use-before-define": "warn", // This need to be enabled eventually "@typescript-eslint/explicit-function-return-type": "warn", "@typescript-eslint/strict-boolean-expressions":"warn", diff --git a/packages/platform/src/i18n.ts b/packages/platform/src/i18n.ts index f996a5bfdb..eaf74cde80 100644 --- a/packages/platform/src/i18n.ts +++ b/packages/platform/src/i18n.ts @@ -21,7 +21,7 @@ import type { IntlString, Plugin } from './platform' import { Severity, Status, unknownError } from './status' import { getMetadata } from './metadata' -import platform from './platform' +import platform, { _EmbeddedId } from './platform' /** * @public @@ -119,7 +119,7 @@ export async function translate

> ( } else { try { const id = _parseId(message) - if (id.component === 'embedded') { + if (id.component === _EmbeddedId) { return id.name } const translation = (await getTranslation(id, locale)) ?? message diff --git a/packages/presentation/src/components/AttributeEditor.svelte b/packages/presentation/src/components/AttributeEditor.svelte index 9cd1986659..4b1712bf16 100644 --- a/packages/presentation/src/components/AttributeEditor.svelte +++ b/packages/presentation/src/components/AttributeEditor.svelte @@ -17,9 +17,9 @@ // import core from '@hcengineering/core' import type { Class, Doc, Ref } from '@hcengineering/core' import { getResource } from '@hcengineering/platform' - import type { AnySvelteComponent } from '@hcengineering/ui' + import type { AnySvelteComponent, EditStyle } from '@hcengineering/ui' import view from '@hcengineering/view' - import { getAttribute, KeyedAttribute, updateAttribute } from '../attributes' + import { KeyedAttribute, getAttribute, updateAttribute } from '../attributes' import { getAttributePresenterClass, getClient } from '../utils' export let _class: Ref> @@ -29,6 +29,7 @@ export let focus: boolean = false export let editable = true export let focusIndex = -1 + export let editKind: EditStyle | undefined = undefined const client = getClient() const hierarchy = client.getHierarchy() @@ -42,7 +43,9 @@ $: if (presenterClass !== undefined) { const typeClass = hierarchy.getClass(presenterClass.attrClass) const editorMixin = hierarchy.as(typeClass, view.mixin.AttributeEditor) - editor = getResource(editorMixin.inlineEditor) + if (editorMixin.inlineEditor !== undefined) { + editor = getResource(editorMixin.inlineEditor) + } } function onChange (value: any) { @@ -67,6 +70,7 @@ {onChange} {focus} {focusIndex} + {editKind} /> {/await} {/if} diff --git a/packages/presentation/src/components/Card.svelte b/packages/presentation/src/components/Card.svelte index 01fa03f89d..f8f8c89292 100644 --- a/packages/presentation/src/components/Card.svelte +++ b/packages/presentation/src/components/Card.svelte @@ -15,10 +15,17 @@ --> { - space = evt.detail - dispatch('change', space) - }} - on:space + on:object {findDefaultSpace} /> diff --git a/packages/theme/styles/_layouts.scss b/packages/theme/styles/_layouts.scss index 0ced03b22e..6478e5ebe0 100644 --- a/packages/theme/styles/_layouts.scss +++ b/packages/theme/styles/_layouts.scss @@ -692,6 +692,7 @@ input.search { .min-w-60 { min-width: 15rem; } .min-w-80 { min-width: 20rem; } .min-w-100 { min-width: 25rem; } +.min-w-144 { min-width: 25rem; } .min-w-168 { min-width: 42rem; } .min-w-min { min-width: min-content; } .min-h-0 { min-height: 0; } @@ -841,6 +842,7 @@ a.no-line { .uppercase { text-transform: uppercase; } .lower { text-transform: lowercase; } .text-left { text-align: left; } +.text-right { text-align: right !important; } .text-center { text-align: center; } .leading-16px { line-height: 16px; } .leading-3 { line-height: .75rem; } diff --git a/packages/ui/src/components/DropdownLabels.svelte b/packages/ui/src/components/DropdownLabels.svelte index 6a63dbc3fb..9c743395b1 100644 --- a/packages/ui/src/components/DropdownLabels.svelte +++ b/packages/ui/src/components/DropdownLabels.svelte @@ -97,7 +97,9 @@ }} > - {#if Array.isArray(selectedItem)} + {#if $$slots.content} + + {:else if Array.isArray(selectedItem)} {#if selectedItem.length > 0} {#each selectedItem as seleceted, i} {seleceted.label} diff --git a/packages/ui/src/components/DropdownLabelsPopup.svelte b/packages/ui/src/components/DropdownLabelsPopup.svelte index 92dbd4b9cc..0d4d75d947 100644 --- a/packages/ui/src/components/DropdownLabelsPopup.svelte +++ b/packages/ui/src/components/DropdownLabelsPopup.svelte @@ -107,7 +107,7 @@ /> {/if} -

+
diff --git a/packages/ui/src/components/EditBox.svelte b/packages/ui/src/components/EditBox.svelte index bdc2ae9195..2b24872209 100644 --- a/packages/ui/src/components/EditBox.svelte +++ b/packages/ui/src/components/EditBox.svelte @@ -147,7 +147,7 @@ class:flex-grow={fullSize} class:w-full={focusable || fullSize} class:uppercase - on:click={() => { + on:click|stopPropagation={() => { input.focus() }} use:resizeObserver={(element) => { @@ -181,7 +181,9 @@ on:change on:keydown on:keypress - on:blur + on:blur={() => { + dispatch('blur', value) + }} /> {:else if format === 'number'} { + dispatch('blur', value) + }} /> {:else} { + dispatch('blur', value) + }} /> {/if}
diff --git a/packages/ui/src/components/Label.svelte b/packages/ui/src/components/Label.svelte index c982384cfb..3480344e33 100644 --- a/packages/ui/src/components/Label.svelte +++ b/packages/ui/src/components/Label.svelte @@ -20,7 +20,7 @@ export let label: IntlString export let params: Record = {} - let _value: string | undefined = undefined + let _value: string | undefined $: if (label !== undefined) { translate(label, params ?? {}, $themeStore.language) diff --git a/packages/ui/src/components/ListView.svelte b/packages/ui/src/components/ListView.svelte index fd5478b057..fc2a646299 100644 --- a/packages/ui/src/components/ListView.svelte +++ b/packages/ui/src/components/ListView.svelte @@ -21,6 +21,7 @@ export let addClass: string | undefined = undefined export let noScroll: boolean = false export let kind: 'default' | 'thin' = 'default' + export let updateOnMouse = true const refs: HTMLElement[] = [] @@ -75,10 +76,14 @@ class="list-item{addClass ? ` ${addClass}` : ''}" class:selection={row === selection} on:mouseover={mouseAttractor(() => { - onRow(row) + if (updateOnMouse) { + onRow(row) + } })} on:mouseenter={mouseAttractor(() => { - onRow(row) + if (updateOnMouse) { + onRow(row) + } })} on:focus={() => {}} bind:this={refs[row]} @@ -100,6 +105,9 @@ margin: 0 0.5rem; min-width: 0; border-radius: 0.25rem; + &:hover { + background-color: var(--theme-popup-divider); + } } &.thin { .list-item { diff --git a/packages/ui/src/components/Scroller.svelte b/packages/ui/src/components/Scroller.svelte index 8c75430482..d818aa4fa2 100644 --- a/packages/ui/src/components/Scroller.svelte +++ b/packages/ui/src/components/Scroller.svelte @@ -19,10 +19,10 @@ import { closeTooltip, tooltipstore } from '../tooltips' import type { FadeOptions } from '../types' import { defaultSP } from '../types' - import IconUpOutline from './icons/UpOutline.svelte' + import { DelayedCaller } from '../utils' import IconDownOutline from './icons/DownOutline.svelte' import HalfUpDown from './icons/HalfUpDown.svelte' - import { DelayedCaller } from '../utils' + import IconUpOutline from './icons/UpOutline.svelte' export let padding: string | undefined = undefined export let autoscroll: boolean = false diff --git a/packages/ui/src/components/calendar/DatePresenter.svelte b/packages/ui/src/components/calendar/DatePresenter.svelte index 6ced13b297..39fe9f914e 100644 --- a/packages/ui/src/components/calendar/DatePresenter.svelte +++ b/packages/ui/src/components/calendar/DatePresenter.svelte @@ -14,7 +14,7 @@ --> + + + + + + + + + + + diff --git a/packages/ui/src/components/internal/LangSelector.svelte b/packages/ui/src/components/internal/LangSelector.svelte index 89c7aded4c..8c389d03db 100644 --- a/packages/ui/src/components/internal/LangSelector.svelte +++ b/packages/ui/src/components/internal/LangSelector.svelte @@ -15,9 +15,8 @@
- +
diff --git a/plugins/board-resources/src/components/UserBoxList.svelte b/plugins/board-resources/src/components/UserBoxList.svelte index 65abaa0c4d..4ddc0e121b 100644 --- a/plugins/board-resources/src/components/UserBoxList.svelte +++ b/plugins/board-resources/src/components/UserBoxList.svelte @@ -1,8 +1,7 @@
- +
diff --git a/plugins/lead/src/index.ts b/plugins/lead/src/index.ts index 4bd1f3792e..7ecccb8308 100644 --- a/plugins/lead/src/index.ts +++ b/plugins/lead/src/index.ts @@ -19,7 +19,7 @@ import type { Attribute, Class, Doc, Ref, Status, Timestamp } from '@hcengineeri import { Mixin } from '@hcengineering/core' import type { Asset, IntlString, Plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' -import type { Project, ProjectTypeCategory, Task } from '@hcengineering/task' +import type { Project, ProjectTypeDescriptor, Task, TaskType } from '@hcengineering/task' /** * @public @@ -82,8 +82,11 @@ const lead = plugin(leadId, { LeadApplication: '' as Asset, CreateCustomer: '' as Asset }, - category: { - FunnelTypeCategory: '' as Ref + descriptors: { + FunnelType: '' as Ref + }, + taskType: { + Lead: '' as Ref } }) diff --git a/plugins/login-resources/src/components/Form.svelte b/plugins/login-resources/src/components/Form.svelte index bdebb1c3a4..d46a4f19d3 100644 --- a/plugins/login-resources/src/components/Form.svelte +++ b/plugins/login-resources/src/components/Form.svelte @@ -20,13 +20,12 @@ StylishEdit, Label, Button, - deviceOptionsStore as deviceInfo + deviceOptionsStore as deviceInfo, + themeStore } from '@hcengineering/ui' import StatusControl from './StatusControl.svelte' - import { OK, Status, Severity } from '@hcengineering/platform' + import { OK, Status, Severity, translate } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform' - import { translate } from '@hcengineering/platform' - import { themeStore } from '@hcengineering/ui' import login from '../plugin' import { onMount } from 'svelte' diff --git a/plugins/login-resources/src/components/LoginApp.svelte b/plugins/login-resources/src/components/LoginApp.svelte index 10fa933da3..56228b0172 100644 --- a/plugins/login-resources/src/components/LoginApp.svelte +++ b/plugins/login-resources/src/components/LoginApp.svelte @@ -14,11 +14,10 @@ // limitations under the License. --> -
-
+
+
-
-
{#each preferencesGroups as preferenceGroup} { currentPreferenceGroup = preferenceGroup group = undefined + const loc = getCurrentResolvedLocation() + loc.path.length = 4 + navigate(loc) }} /> {/each} @@ -96,6 +104,10 @@ on:click={() => { group = gr._id currentPreferenceGroup = undefined + const loc = getCurrentResolvedLocation() + loc.path[4] = group + loc.path.length = 5 + navigate(loc) }} /> {/each} @@ -103,8 +115,6 @@
- -
{#if group} diff --git a/plugins/recruit-resources/src/components/CandidateCard.svelte b/plugins/recruit-resources/src/components/CandidateCard.svelte index 358bd78dd8..e8563c2a97 100644 --- a/plugins/recruit-resources/src/components/CandidateCard.svelte +++ b/plugins/recruit-resources/src/components/CandidateCard.svelte @@ -15,13 +15,12 @@
- +
diff --git a/plugins/recruit-resources/src/components/VacancyCard.svelte b/plugins/recruit-resources/src/components/VacancyCard.svelte index f6ecbd2903..f1257dd9d8 100644 --- a/plugins/recruit-resources/src/components/VacancyCard.svelte +++ b/plugins/recruit-resources/src/components/VacancyCard.svelte @@ -23,7 +23,7 @@ import { NavLink } from '@hcengineering/view-resources' import recruit from '../plugin' import VacancyIcon from './icons/Vacancy.svelte' - import notification from '@hcengineering/notification' + import chunter from '@hcengineering/chunter' export let vacancy: WithLookup | undefined export let disabled: boolean = false @@ -97,7 +97,7 @@