diff --git a/models/board/src/migration.ts b/models/board/src/migration.ts index f7f7cfa919..a4f551d538 100644 --- a/models/board/src/migration.ts +++ b/models/board/src/migration.ts @@ -52,7 +52,8 @@ async function createSpace (tx: TxOperations): Promise { icon: board.component.TemplatesIcon, private: false, archived: false, - members: [] + members: [], + attachedToClass: board.class.Board }, board.space.BoardTemplates ) @@ -108,6 +109,13 @@ async function createDefaults (tx: TxOperations): Promise { ) } +async function fixTemplateSpace (tx: TxOperations): Promise { + const templateSpace = await tx.findOne(task.class.KanbanTemplateSpace, { _id: board.space.BoardTemplates }) + if (templateSpace !== undefined && templateSpace?.attachedToClass === undefined) { + await tx.update(templateSpace, { attachedToClass: board.class.Board }) + } +} + async function migrateLabels (client: MigrationClient): Promise {} export const boardOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { @@ -116,5 +124,6 @@ export const boardOperation: MigrateOperation = { async upgrade (client: MigrationUpgradeClient): Promise { const ops = new TxOperations(client, core.account.System) await createDefaults(ops) + await fixTemplateSpace(ops) } } diff --git a/models/lead/src/migration.ts b/models/lead/src/migration.ts index 6012488b6d..e8eda401c8 100644 --- a/models/lead/src/migration.ts +++ b/models/lead/src/migration.ts @@ -53,7 +53,8 @@ async function createSpace (tx: TxOperations): Promise { icon: lead.component.TemplatesIcon, private: false, members: [], - archived: false + archived: false, + attachedToClass: lead.class.Funnel }, lead.space.FunnelTemplates ) @@ -94,10 +95,18 @@ async function createDefaultKanban (tx: TxOperations): Promise { await createKanban(tx, lead.space.DefaultFunnel, defaultTmpl) } +async function fixTemplateSpace (tx: TxOperations): Promise { + const templateSpace = await tx.findOne(task.class.KanbanTemplateSpace, { _id: lead.space.FunnelTemplates }) + if (templateSpace !== undefined && templateSpace?.attachedToClass === undefined) { + await tx.update(templateSpace, { attachedToClass: lead.class.Funnel }) + } +} + async function createDefaults (tx: TxOperations): Promise { await createSpace(tx) await createSequence(tx, lead.class.Lead) await createDefaultKanban(tx) + await fixTemplateSpace(tx) } export const leadOperation: MigrateOperation = { diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index 4a52cb8c61..331a7f449d 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -27,6 +27,14 @@ export const recruitOperation: MigrateOperation = { async upgrade (client: MigrationUpgradeClient): Promise { const tx = new TxOperations(client, core.account.System) await createDefaults(tx) + await fixTemplateSpace(tx) + } +} + +async function fixTemplateSpace (tx: TxOperations): Promise { + const templateSpace = await tx.findOne(task.class.KanbanTemplateSpace, { _id: recruit.space.VacancyTemplates }) + if (templateSpace !== undefined && templateSpace?.attachedToClass === undefined) { + await tx.update(templateSpace, { attachedToClass: recruit.class.Vacancy }) } } @@ -148,7 +156,8 @@ async function createSpaces (tx: TxOperations): Promise { editor: recruit.component.VacancyTemplateEditor, private: false, members: [], - archived: false + archived: false, + attachedToClass: recruit.class.Vacancy }, recruit.space.VacancyTemplates ) diff --git a/models/server-task/src/index.ts b/models/server-task/src/index.ts index 9939adf9a3..8542db0306 100644 --- a/models/server-task/src/index.ts +++ b/models/server-task/src/index.ts @@ -14,7 +14,31 @@ // import { Builder } from '@hcengineering/model' +import serverCore from '@hcengineering/server-core' +import core from '@hcengineering/core/lib/component' +import serverTask from '@hcengineering/server-task' +import task from '@hcengineering/task' export { serverTaskId } from '@hcengineering/server-task' -export function createModel (builder: Builder): void {} +export function createModel (builder: Builder): void { + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverTask.trigger.OnTemplateStateUpdate, + txMatch: { + _class: core.class.TxUpdateDoc, + objectClass: { + $in: [task.class.StateTemplate, task.class.LostStateTemplate, task.class.WonStateTemplate] + } + } + }) + + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverTask.trigger.OnTemplateStateCreate, + txMatch: { + _class: core.class.TxCreateDoc, + objectClass: { + $in: [task.class.StateTemplate, task.class.LostStateTemplate, task.class.WonStateTemplate] + } + } + }) +} diff --git a/models/task/src/index.ts b/models/task/src/index.ts index 97a7741a97..d2ef3bac6d 100644 --- a/models/task/src/index.ts +++ b/models/task/src/index.ts @@ -149,7 +149,9 @@ export class TKanban extends TDoc implements Kanban { } @Model(task.class.SpaceWithStates, core.class.Space) -export class TSpaceWithStates extends TSpace {} +export class TSpaceWithStates extends TSpace { + templateId!: Ref +} @Model(task.class.KanbanTemplateSpace, core.class.Space) export class TKanbanTemplateSpace extends TSpace implements KanbanTemplateSpace { @@ -157,13 +159,16 @@ export class TKanbanTemplateSpace extends TSpace implements KanbanTemplateSpace description!: IntlString icon!: AnyComponent editor!: AnyComponent + attachedToClass!: Ref> } -@Model(task.class.StateTemplate, core.class.AttachedDoc, DOMAIN_KANBAN) -export class TStateTemplate extends TAttachedDoc implements StateTemplate { +@Model(task.class.StateTemplate, core.class.Doc, DOMAIN_KANBAN) +export class TStateTemplate extends TDoc implements StateTemplate { // We attach to attribute, so we could distinguish between ofAttribute!: Ref> + attachedTo!: Ref + @Prop(TypeString(), task.string.StateTemplateTitle) name!: string @@ -173,11 +178,13 @@ export class TStateTemplate extends TAttachedDoc implements StateTemplate { declare rank: string } -@Model(task.class.DoneStateTemplate, core.class.AttachedDoc, DOMAIN_KANBAN) -export class TDoneStateTemplate extends TAttachedDoc implements DoneStateTemplate { +@Model(task.class.DoneStateTemplate, core.class.Doc, DOMAIN_KANBAN) +export class TDoneStateTemplate extends TDoc implements DoneStateTemplate { // We attach to attribute, so we could distinguish between ofAttribute!: Ref> + attachedTo!: Ref + @Prop(TypeString(), task.string.StateTemplateTitle) name!: string diff --git a/models/task/src/migration.ts b/models/task/src/migration.ts index 0d8401a930..05cffc2db3 100644 --- a/models/task/src/migration.ts +++ b/models/task/src/migration.ts @@ -76,25 +76,20 @@ export async function createKanbanTemplate ( const doneStateRanks = [...genRanks(data.doneStates.length)] await Promise.all( data.doneStates.map((st, i) => - client.addCollection( - st.isWon ? task.class.WonStateTemplate : task.class.LostStateTemplate, - data.space, - data.kanbanId, - task.class.KanbanTemplate, - 'doneStatesC', - { - ofAttribute: task.attribute.DoneState, - rank: doneStateRanks[i], - name: st.name - } - ) + client.createDoc(st.isWon ? task.class.WonStateTemplate : task.class.LostStateTemplate, data.space, { + rank: doneStateRanks[i], + ofAttribute: task.attribute.DoneState, + name: st.name, + attachedTo: data.kanbanId + }) ) ) const stateRanks = [...genRanks(data.states.length)] await Promise.all( data.states.map((st, i) => - client.addCollection(task.class.StateTemplate, data.space, data.kanbanId, task.class.KanbanTemplate, 'statesC', { + client.createDoc(task.class.StateTemplate, data.space, { + attachedTo: data.kanbanId, ofAttribute: task.attribute.State, rank: stateRanks[i], name: st.name, diff --git a/plugins/board-resources/src/utils/BoardUtils.ts b/plugins/board-resources/src/utils/BoardUtils.ts index ab172e125b..814c4c96fd 100644 --- a/plugins/board-resources/src/utils/BoardUtils.ts +++ b/plugins/board-resources/src/utils/BoardUtils.ts @@ -30,7 +30,8 @@ export async function createBoard ( description, private: false, archived: false, - members: [getCurrentAccount()._id] + members: [getCurrentAccount()._id], + templateId }) await Promise.all([createKanban(client, boardRef, templateId)]) diff --git a/plugins/recruit-resources/src/components/CreateVacancy.svelte b/plugins/recruit-resources/src/components/CreateVacancy.svelte index 3ce5e2b7ff..efa07e9d46 100644 --- a/plugins/recruit-resources/src/components/CreateVacancy.svelte +++ b/plugins/recruit-resources/src/components/CreateVacancy.svelte @@ -184,7 +184,8 @@ archived: false, number: (incResult as any).object.sequence, company, - members: [getCurrentAccount()._id] + members: [getCurrentAccount()._id], + templateId }, objectId ) diff --git a/plugins/setting-resources/src/components/statuses/ManageTemplates.svelte b/plugins/setting-resources/src/components/statuses/ManageTemplates.svelte index e3764cc153..00bc83b797 100644 --- a/plugins/setting-resources/src/components/statuses/ManageTemplates.svelte +++ b/plugins/setting-resources/src/components/statuses/ManageTemplates.svelte @@ -32,7 +32,6 @@ if (template === undefined) { return } - const hierarchy = client.getHierarchy() showPopup( MessageBox, @@ -43,15 +42,7 @@ undefined, async (result) => { if (result && template !== undefined) { - const collection = hierarchy.isDerived(state._class, task.class.DoneStateTemplate) ? 'doneStatesC' : 'statesC' - await client.removeCollection( - state._class, - template.space, - state._id, - template._id, - template._class, - collection - ) + await client.removeDoc(state._class, template.space, state._id) } } ) diff --git a/plugins/setting-resources/src/components/statuses/Templates.svelte b/plugins/setting-resources/src/components/statuses/Templates.svelte index 920cc06055..ab2c545c7b 100644 --- a/plugins/setting-resources/src/components/statuses/Templates.svelte +++ b/plugins/setting-resources/src/components/statuses/Templates.svelte @@ -57,19 +57,13 @@ title: 'New Template' }) - await client.addCollection( - task.class.StateTemplate, - space as Ref as Ref, - template, - task.class.KanbanTemplate, - 'statesC', - { - ofAttribute: task.attribute.State, - name: 'New State', - color: 9, - rank: [...genRanks(1)][0] - } - ) + await client.createDoc(task.class.StateTemplate, space as Ref as Ref, { + attachedTo: template, + ofAttribute: task.attribute.State, + name: 'New State', + color: 9, + rank: [...genRanks(1)][0] + }) const ranks = [...genRanks(2)] const doneStates = [ @@ -87,18 +81,12 @@ await Promise.all( doneStates.map(async (ds) => { - await client.addCollection( - ds.class, - space as Ref as Ref, - template, - task.class.KanbanTemplate, - 'doneStatesC', - { - ofAttribute: task.attribute.DoneState, - name: ds.name, - rank: ds.rank - } - ) + await client.createDoc(ds.class, space as Ref as Ref, { + attachedTo: template, + ofAttribute: task.attribute.DoneState, + name: ds.name, + rank: ds.rank + }) }) ) } diff --git a/plugins/task-resources/src/components/kanban/KanbanTemplateEditor.svelte b/plugins/task-resources/src/components/kanban/KanbanTemplateEditor.svelte index 36125bdbca..6acec6f69e 100644 --- a/plugins/task-resources/src/components/kanban/KanbanTemplateEditor.svelte +++ b/plugins/task-resources/src/components/kanban/KanbanTemplateEditor.svelte @@ -104,17 +104,19 @@ if (hierarchy.isDerived(_class, task.class.DoneState)) { const targetClass = _class === task.class.WonState ? task.class.WonStateTemplate : task.class.LostStateTemplate - await client.addCollection(targetClass, kanban.space, kanban._id, kanban._class, 'doneStatesC', { + await client.createDoc(targetClass, kanban.space, { ofAttribute: task.attribute.DoneState, name: 'New Done State', - rank: calcRank(lastOne, undefined) + rank: calcRank(lastOne, undefined), + attachedTo: kanban._id }) } else { - await client.addCollection(task.class.StateTemplate, kanban.space, kanban._id, kanban._class, 'statesC', { + await client.createDoc(task.class.StateTemplate, kanban.space, { name: 'New State', ofAttribute: task.attribute.DoneState, color: 9, - rank: calcRank(lastOne, undefined) + rank: calcRank(lastOne, undefined), + attachedTo: kanban._id }) } } diff --git a/plugins/task/src/index.ts b/plugins/task/src/index.ts index 3884040c6a..4d2049f5b9 100644 --- a/plugins/task/src/index.ts +++ b/plugins/task/src/index.ts @@ -45,7 +45,9 @@ export interface DocWithRank extends Doc { /** * @public */ -export interface SpaceWithStates extends Space {} +export interface SpaceWithStates extends Space { + templateId?: Ref +} // S T A T E @@ -123,12 +125,16 @@ export interface Sequence extends Doc { /** * @public */ -export interface StateTemplate extends AttachedDoc, State {} +export interface StateTemplate extends Doc, State { + attachedTo: Ref +} /** * @public */ -export interface DoneStateTemplate extends AttachedDoc, DoneState {} +export interface DoneStateTemplate extends Doc, DoneState { + attachedTo: Ref +} /** * @public @@ -159,6 +165,7 @@ export interface KanbanTemplateSpace extends Space { description: IntlString icon: AnyComponent editor?: AnyComponent + attachedToClass: Ref> } /** diff --git a/server-plugins/task-resources/src/index.ts b/server-plugins/task-resources/src/index.ts index a3d41cc587..ad6cba2f87 100644 --- a/server-plugins/task-resources/src/index.ts +++ b/server-plugins/task-resources/src/index.ts @@ -13,7 +13,95 @@ // limitations under the License. // +import { Class, Doc, Ref, Space, Tx, TxCreateDoc, TxProcessor, TxUpdateDoc } from '@hcengineering/core' +import { TriggerControl } from '@hcengineering/server-core' +import core from '@hcengineering/core/lib/component' +import task, { KanbanTemplateSpace, KanbanTemplate, StateTemplate, State, DoneState } from '@hcengineering/task' + +/** + * @public + */ +export async function OnTemplateStateUpdate (tx: Tx, control: TriggerControl): Promise { + const actualTx = tx as TxUpdateDoc + const txes = await control.findAll(core.class.TxCollectionCUD, { + 'tx.objectId': actualTx.objectId + }) + const createTxes = await control.findAll(core.class.TxCreateDoc, { objectId: actualTx.objectId }) + const updateTxes = await control.findAll(core.class.TxUpdateDoc, { objectId: actualTx.objectId }) + const prevDoc = TxProcessor.buildDoc2Doc( + [...createTxes, ...txes, ...updateTxes].filter((t) => t._id !== tx._id) + ) as StateTemplate + if (prevDoc === undefined) return [] + const templateSpace = ( + await control.findAll(task.class.KanbanTemplateSpace, { + _id: actualTx.objectSpace as Ref + }) + )[0] as KanbanTemplateSpace + if (templateSpace === undefined) return [] + const template = (await control.findAll(task.class.KanbanTemplate, { _id: prevDoc.attachedTo }))[0] as KanbanTemplate + const classToChange = getClassToChangeOrCreate(actualTx.objectClass) + const objectWithStatesToChange = await control.findAll(templateSpace.attachedToClass, { templateId: template._id }) + const ids = Array.from(objectWithStatesToChange.map((x) => x._id)) as Array> + const newDoc = TxProcessor.buildDoc2Doc([...createTxes, ...txes, ...updateTxes]) as StateTemplate + const statesToChange = Array.from( + await control.findAll(classToChange, { space: { $in: ids }, name: prevDoc.name }) + ) as Array + return statesToChange.map((it) => { + const newAttributes = it._class === task.class.State ? { color: newDoc.color, rank: newDoc.rank } : {} + return control.txFactory.createTxUpdateDoc(it._class, it.space, it._id, { + name: newDoc.name, + ...newAttributes + }) + }) +} + +/** + * @public + */ +export async function OnTemplateStateCreate (tx: Tx, control: TriggerControl): Promise { + const actualTx = tx as TxCreateDoc + const templateSpace = ( + await control.findAll(task.class.KanbanTemplateSpace, { + _id: actualTx.objectSpace as Ref + }) + )[0] as KanbanTemplateSpace + if (templateSpace === undefined) return [] + const template = ( + await control.findAll(task.class.KanbanTemplate, { _id: actualTx.attributes.attachedTo }) + )[0] as KanbanTemplate + const classToChange = getClassToChangeOrCreate(actualTx.objectClass) + const objectWithStatesToChange = await control.findAll(templateSpace.attachedToClass, { templateId: template._id }) + const ids = Array.from(objectWithStatesToChange.map((x) => x._id)) as Array> + const doc = TxProcessor.createDoc2Doc(actualTx) + const ofAttribute = classToChange === task.class.State ? task.attribute.State : task.attribute.DoneState + return ids.map((it) => { + const newAttributes = classToChange === task.class.State ? { color: doc.color, rank: doc.rank } : {} + return control.txFactory.createTxCreateDoc(classToChange, it, { + ofAttribute, + name: doc.name, + ...newAttributes + }) + }) +} + +function getClassToChangeOrCreate (check: Ref>): Ref> { + let classToChange = task.class.State + switch (check) { + case task.class.WonStateTemplate: + classToChange = task.class.WonState + break + case task.class.LostStateTemplate: + classToChange = task.class.LostState + break + } + return classToChange +} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ - function: {} + function: {}, + trigger: { + OnTemplateStateUpdate, + OnTemplateStateCreate + } }) diff --git a/server-plugins/task/src/index.ts b/server-plugins/task/src/index.ts index 7a6c5d24c9..79c2ad0efe 100644 --- a/server-plugins/task/src/index.ts +++ b/server-plugins/task/src/index.ts @@ -16,6 +16,7 @@ import type { Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import { Presenter } from '@hcengineering/server-notification' +import { TriggerFunc } from '@hcengineering/server-core' /** * @public @@ -29,5 +30,9 @@ export default plugin(serverTaskId, { function: { IssueHTMLPresenter: '' as Resource, IssueTextPresenter: '' as Resource + }, + trigger: { + OnTemplateStateUpdate: '' as Resource, + OnTemplateStateCreate: '' as Resource } })