diff --git a/dev/tool/src/workspace.ts b/dev/tool/src/workspace.ts index e02b6a4974..8b76dc50f5 100644 --- a/dev/tool/src/workspace.ts +++ b/dev/tool/src/workspace.ts @@ -16,7 +16,7 @@ import contact from '@anticrm/contact' import core, { DOMAIN_TX, Tx } from '@anticrm/core' -import builder, { migrateOperations } from '@anticrm/model-all' +import builder, { migrateOperations, createDeps } from '@anticrm/model-all' import { existsSync } from 'fs' import { mkdir, open, readFile, writeFile } from 'fs/promises' import { Client } from 'minio' @@ -39,6 +39,10 @@ export async function initWorkspace ( transactorUrl: string, minio: Client ): Promise { + if (txes.some(tx => tx.objectSpace !== core.space.Model)) { + throw Error('Model txes must target only core.space.Model') + } + const client = new MongoClient(mongoUrl) try { await client.connect() @@ -48,17 +52,13 @@ export async function initWorkspace ( await db.dropDatabase() console.log('creating model...') - const model = txes.filter((tx) => tx.objectSpace === core.space.Model) + const model = txes const result = await db.collection(DOMAIN_TX).insertMany(model as Document[]) console.log(`${result.insertedCount} model transactions inserted.`) console.log('creating data...') - const data = txes.filter((tx) => tx.objectSpace !== core.space.Model) - const connection = await connect(transactorUrl, dbName) - for (const tx of data) { - await connection.tx(tx) - } + await createDeps(connection) await connection.close() console.log('create minio bucket') @@ -79,6 +79,10 @@ export async function upgradeWorkspace ( transactorUrl: string, minio: Client ): Promise { + if (txes.some(tx => tx.objectSpace !== core.space.Model)) { + throw Error('Model txes must target only core.space.Model') + } + const client = new MongoClient(mongoUrl) try { await client.connect() @@ -94,7 +98,7 @@ export async function upgradeWorkspace ( console.log(`${result.deletedCount} transactions deleted.`) console.log('creating model...') - const model = txes.filter((tx) => tx.objectSpace === core.space.Model) + const model = txes const insert = await db.collection(DOMAIN_TX).insertMany(model as Document[]) console.log(`${insert.insertedCount} model transactions inserted.`) diff --git a/models/all/src/creation.ts b/models/all/src/creation.ts new file mode 100644 index 0000000000..15df4b84b4 --- /dev/null +++ b/models/all/src/creation.ts @@ -0,0 +1,27 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Client } from '@anticrm/core' +import { createDeps as createTaskDeps } from '@anticrm/model-task' +import { createDeps as createLeadDeps } from '@anticrm/model-lead' +import { createDeps as createRecruitDeps } from '@anticrm/model-recruit' +import { createDeps as createDemoDeps } from '@anticrm/model-demo' + +export async function createDeps (client: Client): Promise { + await createTaskDeps(client) + await createLeadDeps(client) + await createRecruitDeps(client) + await createDemoDeps(client) +} diff --git a/models/all/src/index.ts b/models/all/src/index.ts index ebb6efa156..2b39253392 100644 --- a/models/all/src/index.ts +++ b/models/all/src/index.ts @@ -59,3 +59,5 @@ export default builder // Export upgrade procedures export { migrateOperations } from './migration' + +export { createDeps } from './creation' diff --git a/models/demo/src/creation.ts b/models/demo/src/creation.ts new file mode 100644 index 0000000000..c9144cb142 --- /dev/null +++ b/models/demo/src/creation.ts @@ -0,0 +1,89 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { TxOperations } from '@anticrm/core' +import type { Client } from '@anticrm/core' +import contact from '@anticrm/model-contact' +import recruit from '@anticrm/model-recruit' + +export async function createDeps (client: Client): Promise { + const account = await client.findOne(contact.class.EmployeeAccount, { email: 'rosamund@hc.engineering' }) + + if (account === undefined) { + throw Error('Failed to find EmployeeAccount') + } + + const tx = new TxOperations(client, account._id) + + // Create missing Employee + await tx.createDoc( + contact.class.Employee, + contact.space.Employee, + { + name: 'Chen,Rosamund', + city: 'Mountain View', + channels: [] + }, + account.employee + ) + + await tx.createDoc( + recruit.class.Candidate, + recruit.space.CandidatesPublic, + { + name: 'P.,Andrey', + title: 'Chief Architect', + city: 'Monte Carlo', + channels: [ + { + provider: contact.channelProvider.Email, + value: 'andrey@hc.engineering' + } + ] + } + ) + + await tx.createDoc( + recruit.class.Candidate, + recruit.space.CandidatesPublic, + { + name: 'M.,Marina', + title: 'Chief Designer', + city: 'Los Angeles', + channels: [ + { + provider: contact.channelProvider.Email, + value: 'marina@hc.engineering' + } + ] + } + ) + + await tx.createDoc( + recruit.class.Candidate, + recruit.space.CandidatesPublic, + { + name: 'P.,Alex', + title: 'Frontend Engineer', + city: 'Krasnodar, Russia', + channels: [ + { + provider: contact.channelProvider.Email, + value: 'alex@hc.engineering' + } + ] + } + ) +} diff --git a/models/demo/src/index.ts b/models/demo/src/index.ts index b40232cca9..3819a92f0b 100644 --- a/models/demo/src/index.ts +++ b/models/demo/src/index.ts @@ -15,60 +15,19 @@ // import { Employee, EmployeeAccount } from '@anticrm/contact' -import core, { generateId, Ref } from '@anticrm/core' +import core, { generateId } from '@anticrm/core' import { Builder } from '@anticrm/model' import contact from '@anticrm/model-contact' -import recruit from '@anticrm/model-recruit' export function createDemo (builder: Builder): void { - const rosamund = generateId() - const account: Ref = generateId() - - builder.createDoc(contact.class.Employee, contact.space.Employee, { - name: 'Chen,Rosamund', - city: 'Mountain View', - channels: [] - }, rosamund, account) + const rosamund = generateId() + const account = generateId() builder.createDoc(contact.class.EmployeeAccount, core.space.Model, { email: 'rosamund@hc.engineering', - employee: rosamund as Ref, + employee: rosamund, name: 'Chen,Rosamund' }, account, account) - - builder.createDoc(recruit.class.Candidate, recruit.space.CandidatesPublic, { - name: 'P.,Andrey', - title: 'Chief Architect', - city: 'Monte Carlo', - channels: [ - { - provider: contact.channelProvider.Email, - value: 'andrey@hc.engineering' - } - ] - }, undefined, account) - - builder.createDoc(recruit.class.Candidate, recruit.space.CandidatesPublic, { - name: 'M.,Marina', - title: 'Chief Designer', - city: 'Los Angeles', - channels: [ - { - provider: contact.channelProvider.Email, - value: 'marina@hc.engineering' - } - ] - }, undefined, account) - - builder.createDoc(recruit.class.Candidate, recruit.space.CandidatesPublic, { - name: 'P.,Alex', - title: 'Frontend Engineer', - city: 'Krasnodar, Russia', - channels: [ - { - provider: contact.channelProvider.Email, - value: 'alex@hc.engineering' - } - ] - }, undefined, account) } + +export { createDeps } from './creation' diff --git a/models/lead/src/creation.ts b/models/lead/src/creation.ts new file mode 100644 index 0000000000..d26321a495 --- /dev/null +++ b/models/lead/src/creation.ts @@ -0,0 +1,61 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { TxOperations } from '@anticrm/core' +import type { Client, Ref } from '@anticrm/core' +import task, { createKanban } from '@anticrm/task' +import type { KanbanTemplate } from '@anticrm/task' +import { createKanbanTemplate } from '@anticrm/model-task' + +import lead from './plugin' + +export async function createDeps (client: Client): Promise { + const tx = new TxOperations(client, core.account.System) + + await tx.createDoc( + task.class.Sequence, + task.space.Sequence, + { + attachedTo: lead.class.Lead, + sequence: 0 + } + ) + const defaultTmpl = await createDefaultKanbanTemplate(tx) + await createKanban(tx, lead.space.DefaultFunnel, defaultTmpl) +} + +const defaultKanban = { + states: [ + { color: '#7C6FCD', title: 'Incoming' }, + { color: '#6F7BC5', title: 'Negotation' }, + { color: '#77C07B', title: 'Offer preparing' }, + { color: '#A5D179', title: 'Make a decision' }, + { color: '#F28469', title: 'Contract conclusion' }, + { color: '#7C6FCD', title: 'Done' } + ], + doneStates: [ + { isWon: true, title: 'Won' }, + { isWon: false, title: 'Lost' } + ] +} + +const createDefaultKanbanTemplate = async (client: TxOperations): Promise> => + await createKanbanTemplate(client, { + kanbanId: lead.template.DefaultFunnel, + space: lead.space.FunnelTemplates, + title: 'Default funnel', + states: defaultKanban.states, + doneStates: defaultKanban.doneStates + }) diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index 534033781c..9d46b0dade 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -16,9 +16,8 @@ // To help typescript locate view plugin properly import type { Contact, Employee } from '@anticrm/contact' -import type { Class, Data, Doc, FindOptions, Ref, Space } from '@anticrm/core' +import type { Doc, FindOptions, Ref } from '@anticrm/core' import type { Funnel, Lead } from '@anticrm/lead' -import { createKanban } from '@anticrm/lead' import { Builder, Collection, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model' import attachment from '@anticrm/model-attachment' import chunter from '@anticrm/model-chunter' @@ -28,7 +27,6 @@ import task, { TSpaceWithStates, TTask } from '@anticrm/model-task' import view from '@anticrm/model-view' import workbench from '@anticrm/model-workbench' import type { IntlString } from '@anticrm/platform' -import { createDefaultKanbanTemplate } from '@anticrm/task' import type {} from '@anticrm/view' import lead from './plugin' @@ -146,11 +144,6 @@ export function createModel (builder: Builder): void { presenter: lead.component.LeadPresenter }) - builder.createDoc(task.class.Sequence, task.space.Sequence, { - attachedTo: lead.class.Lead, - sequence: 0 - }) - builder.createDoc( task.class.KanbanTemplateSpace, core.space.Model, @@ -164,29 +157,8 @@ export function createModel (builder: Builder): void { }, lead.space.FunnelTemplates ) - - createKanban(lead.space.DefaultFunnel, async (_class, space, data, id) => { - builder.createDoc(_class, space, data, id) - return await Promise.resolve() - }).catch((err) => console.error(err)) - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - createDefaultKanbanTemplate(async ( - props: { - id?: Ref - space: Ref - class: Ref> - }, - attrs: Data - ): Promise => { - builder.createDoc( - props.class, - props.space, - attrs, - props.id - ) - }) } -export { leadOperation } from './migration' export { default } from './plugin' +export { leadOperation } from './migration' +export { createDeps } from './creation' diff --git a/models/lead/src/migration.ts b/models/lead/src/migration.ts index c87303c6ce..b3ed34415b 100644 --- a/models/lead/src/migration.ts +++ b/models/lead/src/migration.ts @@ -14,13 +14,9 @@ // limitations under the License. // -import { Doc, TxOperations } from '@anticrm/core' import { MigrateOperation, MigrationClient, MigrationResult, MigrationUpgradeClient } from '@anticrm/model' -import core from '@anticrm/model-core' -import task, { DOMAIN_TASK } from '@anticrm/model-task' -import { createDefaultKanbanTemplate, createKanban } from '@anticrm/lead' -import lead from './plugin' +// eslint-disable-next-line @typescript-eslint/no-unused-vars function logInfo (msg: string, result: MigrationResult): void { if (result.updated > 0) { console.log(`Lead: Migrate ${msg} ${result.updated}`) @@ -28,46 +24,6 @@ function logInfo (msg: string, result: MigrationResult): void { } export const leadOperation: MigrateOperation = { - async migrate (client: MigrationClient): Promise { - // Update done states for tasks - logInfo('lead done states', await client.update(DOMAIN_TASK, { _class: lead.class.Lead, doneState: { $exists: false } }, { doneState: null })) - }, - async upgrade (client: MigrationUpgradeClient): Promise { - console.log('Lead: Performing model upgrades') - - const ops = new TxOperations(client, core.account.System) - if (await client.findOne(task.class.Kanban, { attachedTo: lead.space.DefaultFunnel }) === undefined) { - console.info('Lead: Create kanban for default funnel.') - await createKanban(lead.space.DefaultFunnel, async (_class, space, data, id) => { - const doc = await ops.findOne(_class, { _id: id }) - if (doc === undefined) { - await ops.createDoc(_class, space, data, id) - } else { - await ops.updateDoc(_class, space, id, data) - } - }).catch((err) => console.error(err)) - } else { - console.log('Lead: => default funnel Kanban is ok') - } - - if (await client.findOne(task.class.Sequence, { attachedTo: lead.class.Lead }) === undefined) { - console.info('Lead: Create sequence for default task project.') - // We need to create sequence - await ops.createDoc(task.class.Sequence, task.space.Sequence, { - attachedTo: lead.class.Lead, - sequence: 0 - }) - } else { - console.log('Lead: => sequence is ok') - } - - if (await client.findOne(core.class.TxCreateDoc, { objectId: lead.template.DefaultFunnel }) === undefined) { - await createDefaultKanbanTemplate(async ( - props, - attrs - ): Promise => { - await ops.createDoc(props.class, props.space, attrs, props.id) - }) - } - } + async migrate (client: MigrationClient): Promise {}, + async upgrade (client: MigrationUpgradeClient): Promise {} } diff --git a/models/lead/src/plugin.ts b/models/lead/src/plugin.ts index 5ccd96af8f..8b74debc69 100644 --- a/models/lead/src/plugin.ts +++ b/models/lead/src/plugin.ts @@ -19,7 +19,7 @@ import { leadId } from '@anticrm/lead' import lead from '@anticrm/lead-resources/src/plugin' import type { IntlString } from '@anticrm/platform' import { mergeIds } from '@anticrm/platform' -import '@anticrm/task' +import { KanbanTemplate } from '@anticrm/task' import type { AnyComponent } from '@anticrm/ui' import { Application } from '@anticrm/workbench' @@ -42,5 +42,8 @@ export default mergeIds(leadId, lead, { }, space: { DefaultFunnel: '' as Ref + }, + template: { + DefaultFunnel: '' as Ref } }) diff --git a/models/recruit/src/creation.ts b/models/recruit/src/creation.ts new file mode 100644 index 0000000000..ea6741e7a4 --- /dev/null +++ b/models/recruit/src/creation.ts @@ -0,0 +1,61 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { Ref, TxOperations } from '@anticrm/core' +import type { Client } from '@anticrm/core' +import { createKanbanTemplate } from '@anticrm/model-task' + +import recruit from './plugin' +import task from '@anticrm/task' +import type { KanbanTemplate } from '@anticrm/task' + +export async function createDeps (client: Client): Promise { + const tx = new TxOperations(client, core.account.System) + + await tx.createDoc( + task.class.Sequence, + task.space.Sequence, + { + attachedTo: recruit.class.Applicant, + sequence: 0 + } + ) + await createDefaultKanbanTemplate(tx) +} + +const defaultKanban = { + states: [ + { color: '#7C6FCD', title: 'HR Interview' }, + { color: '#6F7BC5', title: 'Technical Interview' }, + { color: '#77C07B', title: 'Test task' }, + { color: '#A5D179', title: 'Offer' } + ], + doneStates: [ + { isWon: true, title: 'Won' }, + { isWon: false, title: 'Lost' } + ] +} + +/** + * @public + */ +export const createDefaultKanbanTemplate = async (client: TxOperations): Promise> => + await createKanbanTemplate(client, { + kanbanId: recruit.template.DefaultVacancy, + space: recruit.space.VacancyTemplates, + title: 'Default vacancy', + states: defaultKanban.states, + doneStates: defaultKanban.doneStates + }) diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index e71487200a..c7e690188a 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -14,7 +14,7 @@ // import type { Employee } from '@anticrm/contact' -import { Class, Data, Doc, FindOptions, Ref, Space, Timestamp } from '@anticrm/core' +import { Doc, FindOptions, Ref, Timestamp } from '@anticrm/core' import { Builder, Collection, Model, Prop, TypeBoolean, TypeDate, TypeRef, TypeString, UX } from '@anticrm/model' import attachment from '@anticrm/model-attachment' import chunter from '@anticrm/model-chunter' @@ -25,7 +25,6 @@ import view from '@anticrm/model-view' import workbench from '@anticrm/model-workbench' import type { IntlString } from '@anticrm/platform' import { Applicant, Candidate, Candidates, Vacancy } from '@anticrm/recruit' -import { createDefaultKanbanTemplate } from '@anticrm/task' import recruit from './plugin' @Model(recruit.class.Vacancy, task.class.SpaceWithStates) @@ -277,11 +276,6 @@ export function createModel (builder: Builder): void { action: task.action.CreateTask }) - builder.createDoc(task.class.Sequence, task.space.Sequence, { - attachedTo: recruit.class.Applicant, - sequence: 0 - }) - builder.createDoc( task.class.KanbanTemplateSpace, core.space.Model, @@ -295,24 +289,8 @@ export function createModel (builder: Builder): void { }, recruit.space.VacancyTemplates ) - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - createDefaultKanbanTemplate(async ( - props: { - id?: Ref - space: Ref - class: Ref> - }, - attrs: Data - ): Promise => { - builder.createDoc( - props.class, - props.space, - attrs, - props.id - ) - }) } -export { recruitOperation } from './migration' export { default } from './plugin' +export { recruitOperation } from './migration' +export { createDeps } from './creation' diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index f7b3da9a6d..a3f344cb80 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -13,61 +13,15 @@ // limitations under the License. // -import core, { TxOperations } from '@anticrm/core' import { MigrateOperation, MigrationClient, MigrationResult, MigrationUpgradeClient } from '@anticrm/model' -import contact, { DOMAIN_CONTACT } from '@anticrm/model-contact' -import { DOMAIN_TASK } from '@anticrm/model-task' -import { createDefaultKanbanTemplate } from '@anticrm/recruit' -import recruit from './plugin' +// eslint-disable-next-line @typescript-eslint/no-unused-vars function logInfo (msg: string, result: MigrationResult): void { if (result.updated > 0) { console.log(`Recruit: Migrate ${msg} ${result.updated}`) } } export const recruitOperation: MigrateOperation = { - async migrate (client: MigrationClient): Promise { - logInfo( - 'done for Applicants', - await client.update( - DOMAIN_TASK, - { _class: recruit.class.Applicant, doneState: { $exists: false } }, - { doneState: null } - ) - ) - - logInfo( - '$move employee => assignee', - await client.update( - DOMAIN_TASK, - { _class: recruit.class.Applicant, employee: { $exists: true } }, - { $rename: { employee: 'assignee' } } - ) - ) - - const employees = (await client.find(DOMAIN_CONTACT, { _class: contact.class.Employee })).map((emp) => emp._id) - - // update assignee to unassigned if there is no employee exists. - logInfo( - 'applicants wrong assignee', - await client.update( - DOMAIN_TASK, - { _class: recruit.class.Applicant, assignee: { $not: { $in: employees } } }, - { assignee: null } - ) - ) - }, - async upgrade (client: MigrationUpgradeClient): Promise { - console.log('Recruit: Performing model upgrades') - - const ops = new TxOperations(client, core.account.System) - if (await client.findOne(core.class.TxCreateDoc, { objectId: recruit.template.DefaultVacancy }) === undefined) { - await createDefaultKanbanTemplate(async ( - props, - attrs - ): Promise => { - await ops.createDoc(props.class, props.space, attrs, props.id) - }) - } - } + async migrate (client: MigrationClient): Promise {}, + async upgrade (client: MigrationUpgradeClient): Promise {} } diff --git a/models/recruit/src/plugin.ts b/models/recruit/src/plugin.ts index 31293ae2cb..ffc11144ce 100644 --- a/models/recruit/src/plugin.ts +++ b/models/recruit/src/plugin.ts @@ -21,7 +21,7 @@ import recruit from '@anticrm/recruit-resources/src/plugin' import type { AnyComponent } from '@anticrm/ui' import type { Action } from '@anticrm/view' import { Application } from '@anticrm/workbench' -import '@anticrm/task' +import { KanbanTemplate } from '@anticrm/task' export default mergeIds(recruitId, recruit, { app: { @@ -59,5 +59,8 @@ export default mergeIds(recruitId, recruit, { }, space: { CandidatesPublic: '' as Ref + }, + template: { + DefaultVacancy: '' as Ref } }) diff --git a/models/task/src/creation.ts b/models/task/src/creation.ts new file mode 100644 index 0000000000..3da92d2a66 --- /dev/null +++ b/models/task/src/creation.ts @@ -0,0 +1,117 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import core, { TxOperations } from '@anticrm/core' +import type { Client, Ref, Space } from '@anticrm/core' +import { createKanban, genRanks } from '@anticrm/task' +import type { DoneStateTemplate, KanbanTemplate, StateTemplate } from '@anticrm/task' + +import task from './plugin' + +export async function createDeps (client: Client): Promise { + const tx = new TxOperations(client, core.account.System) + + await tx.createDoc( + task.class.Sequence, + task.space.Sequence, + { + attachedTo: task.class.Issue, + sequence: 0 + } + ) + const defaultTmpl = await createDefaultKanbanTemplate(tx) + await createKanban(tx, task.space.TasksPublic, defaultTmpl) +} + +const defaultKanban = { + states: [ + { color: '#7C6FCD', title: 'Open' }, + { color: '#6F7BC5', title: 'In Progress' }, + { color: '#77C07B', title: 'Under review' }, + { color: '#A5D179', title: 'Done' }, + { color: '#F28469', title: 'Invalid' } + ], + doneStates: [ + { isWon: true, title: 'Won' }, + { isWon: false, title: 'Lost' } + ] +} + +/** + * @public + */ +export interface KanbanTemplateData { + kanbanId: Ref + space: Ref + title: KanbanTemplate['title'] + states: Pick[] + doneStates: (Pick & { isWon: boolean })[] +} + +/** + * @public + */ +export async function createKanbanTemplate (client: TxOperations, data: KanbanTemplateData): Promise> { + const tmpl = await client.createDoc( + task.class.KanbanTemplate, + data.space, + { + doneStatesC: 0, + statesC: 0, + title: data.title + }, + data.kanbanId + ) + + 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', + { + rank: doneStateRanks[i], + title: st.title + })) + ) + + 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', + { + rank: stateRanks[i], + title: st.title, + color: st.color + })) + ) + + return tmpl +} + +const createDefaultKanbanTemplate = async (client: TxOperations): Promise> => + await createKanbanTemplate(client, { + kanbanId: task.template.DefaultProject, + space: task.space.ProjectTemplates, + title: 'Default project', + states: defaultKanban.states, + doneStates: defaultKanban.doneStates + }) diff --git a/models/task/src/index.ts b/models/task/src/index.ts index f675b8d770..37ed159bb2 100644 --- a/models/task/src/index.ts +++ b/models/task/src/index.ts @@ -19,7 +19,7 @@ import type { ActionTarget } from '@anticrm/view' import attachment from '@anticrm/model-attachment' import type { Employee } from '@anticrm/contact' import contact from '@anticrm/contact' -import { Arr, Class, Data, Doc, Domain, DOMAIN_MODEL, FindOptions, Ref, Space, Timestamp } from '@anticrm/core' +import { Arr, Class, Doc, Domain, DOMAIN_MODEL, FindOptions, Ref, Space, Timestamp } from '@anticrm/core' import { Builder, Collection, Implements, Mixin, Model, Prop, TypeBoolean, TypeDate, TypeRef, TypeString, UX } from '@anticrm/model' import chunter from '@anticrm/model-chunter' import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@anticrm/model-core' @@ -45,7 +45,6 @@ import type { Task, TodoItem } from '@anticrm/task' -import { createProjectKanban, createDefaultKanbanTemplate } from '@anticrm/task' import task from './plugin' import { AnyComponent } from '@anticrm/ui' @@ -306,11 +305,6 @@ export function createModel (builder: Builder): void { editor: task.component.EditIssue }) - builder.createDoc(task.class.Sequence, task.space.Sequence, { - attachedTo: task.class.Issue, - sequence: 0 - }) - builder.createDoc(view.class.Viewlet, core.space.Model, { attachTo: task.class.Issue, descriptor: task.viewlet.Kanban, @@ -361,11 +355,6 @@ export function createModel (builder: Builder): void { task.space.ProjectTemplates ) - createProjectKanban(task.space.TasksPublic, async (_class, space, data, id) => { - builder.createDoc(_class, space, data, id) - return await Promise.resolve() - }).catch((err) => console.error(err)) - builder.createDoc( view.class.Action, core.space.Model, @@ -514,23 +503,7 @@ export function createModel (builder: Builder): void { done: true } }) - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - createDefaultKanbanTemplate(async ( - props: { - id?: Ref - space: Ref - class: Ref> - }, - attrs: Data - ): Promise => { - builder.createDoc( - props.class, - props.space, - attrs, - props.id - ) - }) } export { taskOperation } from './migration' +export { createDeps, createKanbanTemplate } from './creation' diff --git a/models/task/src/migration.ts b/models/task/src/migration.ts index a2fc9a1862..3554799f33 100644 --- a/models/task/src/migration.ts +++ b/models/task/src/migration.ts @@ -13,284 +13,21 @@ // limitations under the License. // -import { AttachedDoc, Class, Client, Doc, Domain, DOMAIN_TX, Ref, Space, TxCUD, TxOperations } from '@anticrm/core' import { MigrateOperation, - MigrateUpdate, MigrationClient, MigrationResult, MigrationUpgradeClient } from '@anticrm/model' -import core from '@anticrm/model-core' -import { createDefaultKanbanTemplate, createProjectKanban, KanbanTemplate, DocWithRank, genRanks } from '@anticrm/task' -import { DOMAIN_TASK, DOMAIN_STATE, DOMAIN_KANBAN } from '.' -import task from './plugin' +// eslint-disable-next-line @typescript-eslint/no-unused-vars function logInfo (msg: string, result: MigrationResult): void { if (result.updated > 0) { console.log(`Task: Migrate ${msg} ${result.updated}`) } } -async function migrateClass ( - client: MigrationClient, - domain: Domain, - from: Ref>, - to: Ref>, - extraOps: MigrateUpdate = {}, - txExtraOps: MigrateUpdate> = {} -): Promise { - logInfo(`${from} => ${to}: `, await client.update(domain, { _class: from }, { ...extraOps, _class: to })) - logInfo( - `${from} => ${to} Transactions`, - await client.update>(DOMAIN_TX, { objectClass: from }, { ...txExtraOps, objectClass: to }) - ) -} export const taskOperation: MigrateOperation = { - async migrate (client: MigrationClient): Promise { - // Since we should not have Task class instances, we convert them all to Issue. - await migrateClass(client, DOMAIN_TASK, task.class.Task, task.class.Issue) - await migrateClass(client, DOMAIN_STATE, 'core:class:State' as Ref>, task.class.State) - await migrateClass(client, DOMAIN_STATE, 'core:class:WonState' as Ref>, task.class.WonState) - await migrateClass(client, DOMAIN_STATE, 'core:class:LostState' as Ref>, task.class.LostState) - await migrateClass(client, DOMAIN_KANBAN, 'view:class:Kanban' as Ref>, task.class.Kanban) - await migrateClass( - client, - DOMAIN_KANBAN, - 'view:class:Sequence' as Ref>, - task.class.Sequence, - { space: task.space.Sequence }, - { objectSpace: task.space.Sequence } - ) - - // Update attached to for task - await client.update( - DOMAIN_KANBAN, - { _class: task.class.Sequence, attachedTo: task.class.Task }, - { attachedTo: task.class.Issue } - ) - - await migrateClass(client, DOMAIN_KANBAN, 'view:class:KanbanTemplate' as Ref>, task.class.KanbanTemplate) - await migrateClass(client, DOMAIN_KANBAN, 'view:class:StateTemplate' as Ref>, task.class.StateTemplate) - await migrateClass( - client, - DOMAIN_KANBAN, - 'view:class:DoneStateTemplate' as Ref>, - task.class.DoneStateTemplate - ) - await migrateClass( - client, - DOMAIN_KANBAN, - 'view:class:WonStateTemplate' as Ref>, - task.class.WonStateTemplate - ) - await migrateClass( - client, - DOMAIN_KANBAN, - 'view:class:LostStateTemplate' as Ref>, - task.class.LostStateTemplate - ) - - await client.move( - 'recruit' as Domain, - { - _class: 'recruit:class:Applicant' as Ref> - }, - DOMAIN_TASK - ) - - await client.move( - 'lead' as Domain, - { - _class: 'lead:class:Lead' as Ref> - }, - DOMAIN_TASK - ) - - // Update done states for tasks - await client.update(DOMAIN_TASK, { _class: task.class.Issue, doneState: { $exists: false } }, { doneState: null }) - }, - async upgrade (client: MigrationUpgradeClient): Promise { - console.log('Task: Performing model upgrades') - - const ops = new TxOperations(client, core.account.System) - if ((await client.findOne(task.class.Sequence, { attachedTo: task.class.Issue })) === undefined) { - console.info('Task: Create sequence for default task project.') - // We need to create sequence - await ops.createDoc(task.class.Sequence, task.space.Sequence, { - attachedTo: task.class.Issue, - sequence: 0 - }) - } else { - console.log('Task: => sequence is ok') - } - if ((await client.findOne(task.class.Kanban, { attachedTo: task.space.TasksPublic })) === undefined) { - console.info('Task: Create kanban for default task project.') - await createProjectKanban(task.space.TasksPublic, async (_class, space, data, id) => { - const doc = await ops.findOne(_class, { _id: id }) - if (doc === undefined) { - await ops.createDoc(_class, space, data, id) - } else { - await ops.updateDoc(_class, space, id, data) - } - }).catch((err) => console.error(err)) - } else { - console.log('Task: => public project Kanban is ok') - } - - if (await client.findOne(core.class.TxCreateDoc, { objectId: task.template.DefaultProject }) === undefined) { - await createDefaultKanbanTemplate(async ( - props, - attrs - ): Promise => { - await ops.createDoc(props.class, props.space, attrs, props.id) - }) - } - - console.log('View: Performing model upgrades') - - await createMissingDoneStates(client, ops) - await updateRankItems({ client, ops, _class: task.class.State, extractOrder: (kanban) => kanban.states }) - await updateRankItems({ client, ops, _class: task.class.DoneState, extractOrder: (kanban) => kanban.doneStates }) - await updateRankItems({ client, ops, _class: task.class.Task, extractOrder: (kanban) => kanban.order }) - await updateTemplateRankItems({ client, ops, _class: task.class.StateTemplate, extractOrder: (kanban) => kanban.states }) - await updateTemplateRankItems({ client, ops, _class: task.class.DoneStateTemplate, extractOrder: (kanban) => kanban.doneStates }) - } -} - -async function createMissingDoneStates (client: Client, ops: TxOperations): Promise { - const spacesWithStates = await client.findAll(task.class.SpaceWithStates, {}) - const doneStates = await client.findAll(task.class.DoneState, {}) - const spaceIdsWithDoneStates = new Set(doneStates.map(x => x.space)) - const outdatedSpaces = spacesWithStates.filter((space) => !spaceIdsWithDoneStates.has(space._id)) - - const pairRanks = [...genRanks(2)] - - await Promise.all( - outdatedSpaces - .map(async (space) => { - console.log(`Creating done states for space: ${space._id}`) - try { - await Promise.all([ - ops.createDoc(task.class.WonState, space._id, { - title: 'Won', - rank: pairRanks[0] - }), - ops.createDoc(task.class.LostState, space._id, { - title: 'Lost', - rank: pairRanks[1] - }) - ]) - } catch (e) { - console.error(e) - } - })) -} - -async function updateRankItems ({ - client, - ops, - _class, - extractOrder -}: { - client: Client - ops: TxOperations - _class: Ref> - extractOrder: (kanban: any) => Ref[] -}): Promise { - const allItems = await client.findAll(_class, {}) - const unorderedItems = allItems - .filter((item) => item.rank === undefined) - const groupedUnsortedItems = new Map, T[]>() - - unorderedItems.forEach((item) => { - const existing = groupedUnsortedItems.get(item.space) ?? [] - groupedUnsortedItems.set(item.space, [...existing, item]) - }) - - for (const [space, items] of groupedUnsortedItems.entries()) { - const kanban = await client.findOne(task.class.Kanban, { attachedTo: space }) - - if (kanban === undefined) { - console.error(`Failed to find kanban attached to space '${space}'`) - continue - } - - const order = extractOrder(kanban) - - if (order === undefined) { - console.error(`Kanban doesn't contain items order: ${kanban._id}`) - continue - } - - const orderedItems = order - .map((id) => items.find(x => x._id === id)) - .filter((items): items is T => items !== undefined) - const ranks = genRanks(orderedItems.length) - - for (const item of orderedItems) { - const rank = ranks.next().value - - if (rank === undefined) { - console.error('Failed to generate rank') - break - } - - await ops.updateDoc(item._class as Ref>, item.space, item._id, { rank }) - } - } -} - -async function updateTemplateRankItems ({ - client, - ops, - _class, - extractOrder -}: { - client: Client - ops: TxOperations - _class: Ref> - extractOrder: (kanban: any) => Ref[] -}): Promise { - const allItems = await client.findAll(_class, {}) - const unorderedItems = allItems - .filter((state) => state.rank === undefined) - const groupedUnsortedItems = new Map, T[]>() - - unorderedItems.forEach((item) => { - const existing = groupedUnsortedItems.get(item.attachedTo) ?? [] - groupedUnsortedItems.set(item.attachedTo, [...existing, item]) - }) - - for (const [attachedTo, items] of groupedUnsortedItems.entries()) { - const kanban = await client.findOne(task.class.KanbanTemplate, { _id: attachedTo as Ref }) - - if (kanban === undefined) { - console.error(`Failed to find kanban '${attachedTo}'`) - continue - } - - const order = extractOrder(kanban) - - if (order === undefined) { - console.error(`Kanban doesn't contain items order: ${kanban._id}`) - continue - } - - const orderedItems = order - .map((id) => items.find(x => x._id === id)) - .filter((items): items is T => items !== undefined) - const ranks = genRanks(orderedItems.length) - - for (const item of orderedItems) { - const rank = ranks.next().value - - if (rank === undefined) { - console.error('Failed to generate rank') - break - } - - await ops.updateDoc(item._class as Ref>, item.space, item._id, { rank }) - } - } + async migrate (client: MigrationClient): Promise {}, + async upgrade (client: MigrationUpgradeClient): Promise {} } diff --git a/models/task/src/plugin.ts b/models/task/src/plugin.ts index 03487e5895..9708ac9d4c 100644 --- a/models/task/src/plugin.ts +++ b/models/task/src/plugin.ts @@ -17,7 +17,7 @@ import type { Doc, Ref, Space } from '@anticrm/core' import type { IntlString, Resource } from '@anticrm/platform' import { mergeIds } from '@anticrm/platform' -import task, { taskId } from '@anticrm/task' +import task, { KanbanTemplate, taskId } from '@anticrm/task' import type { AnyComponent } from '@anticrm/ui' import { Application } from '@anticrm/workbench' import type { Action } from '@anticrm/view' @@ -67,5 +67,8 @@ export default mergeIds(taskId, task, { }, space: { TasksPublic: '' as Ref + }, + template: { + DefaultProject: '' as Ref } }) diff --git a/plugins/lead/src/index.ts b/plugins/lead/src/index.ts index a337c07068..55be881894 100644 --- a/plugins/lead/src/index.ts +++ b/plugins/lead/src/index.ts @@ -15,10 +15,10 @@ // import type { Contact } from '@anticrm/contact' -import { Class, Data, Doc, Ref, Space } from '@anticrm/core' +import type { Class, Ref } from '@anticrm/core' import type { Asset, Plugin } from '@anticrm/platform' import { plugin } from '@anticrm/platform' -import task, { CreateFn, genRanks, createKanbanTemplate, DoneState, Kanban, KanbanTemplate, KanbanTemplateSpace, SpaceWithStates, State, Task } from '@anticrm/task' +import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@anticrm/task' /** * @public @@ -53,97 +53,7 @@ const lead = plugin(leadId, { }, space: { FunnelTemplates: '' as Ref - }, - template: { - DefaultFunnel: '' as Ref } }) export default lead - -const defaultKanban = { - states: [ - { color: '#7C6FCD', title: 'Incoming' }, - { color: '#6F7BC5', title: 'Negotation' }, - { color: '#77C07B', title: 'Offer preparing' }, - { color: '#A5D179', title: 'Make a decision' }, - { color: '#F28469', title: 'Contract conclusion' }, - { color: '#7C6FCD', title: 'Done' } - ], - doneStates: [ - { isWon: true, title: 'Won' }, - { isWon: false, title: 'Lost' } - ] -} - -/** - * @public - */ -export async function createKanban ( - funnelId: Ref, - factory: (_class: Ref>, space: Ref, data: Data, id: Ref) => Promise -): Promise { - const { states, doneStates } = defaultKanban - const stateRank = genRanks(states.length) - for (const st of states) { - const sid = (funnelId + '.state.' + st.title.toLowerCase().replace(' ', '_')) as Ref - const rank = stateRank.next().value - - if (rank === undefined) { - throw Error('Failed to generate rank') - } - - await factory( - task.class.State, - funnelId, - { - title: st.title, - color: st.color, - rank - }, - sid - ) - } - - const doneStateRank = genRanks(doneStates.length) - for (const st of doneStates) { - const rank = doneStateRank.next().value - - if (rank === undefined) { - throw Error('Failed to generate rank') - } - - const sid = (funnelId + '.done-state.' + st.title.toLowerCase().replace(' ', '_')) as Ref - await factory( - st.isWon ? task.class.WonState : task.class.LostState, - funnelId, - { - title: st.title, - rank - }, - sid - ) - } - - await factory( - task.class.Kanban, - funnelId, - { - attachedTo: funnelId - }, - (funnelId + '.kanban') as Ref - ) -} - -/** - * @public - */ -export const createDefaultKanbanTemplate = async (create: CreateFn): Promise => { - await createKanbanTemplate(create)({ - kanbanId: lead.template.DefaultFunnel, - space: lead.space.FunnelTemplates, - title: 'Default funnel', - states: defaultKanban.states, - doneStates: defaultKanban.doneStates - }) -} diff --git a/plugins/recruit/src/index.ts b/plugins/recruit/src/index.ts index b9ed02f7bf..dc95f0e956 100644 --- a/plugins/recruit/src/index.ts +++ b/plugins/recruit/src/index.ts @@ -17,7 +17,7 @@ import type { Person } from '@anticrm/contact' import type { Class, Ref, Space, Timestamp } from '@anticrm/core' import type { Asset, Plugin } from '@anticrm/platform' import { plugin } from '@anticrm/platform' -import { CreateFn, createKanbanTemplate, KanbanTemplate, KanbanTemplateSpace, SpaceWithStates, Task } from '@anticrm/task' +import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@anticrm/task' /** * @public @@ -79,36 +79,7 @@ const recruit = plugin(recruitId, { }, space: { VacancyTemplates: '' as Ref - }, - template: { - DefaultVacancy: '' as Ref } }) export default recruit - -const defaultKanban = { - states: [ - { color: '#7C6FCD', title: 'HR Interview' }, - { color: '#6F7BC5', title: 'Technical Interview' }, - { color: '#77C07B', title: 'Test task' }, - { color: '#A5D179', title: 'Offer' } - ], - doneStates: [ - { isWon: true, title: 'Won' }, - { isWon: false, title: 'Lost' } - ] -} - -/** - * @public - */ -export const createDefaultKanbanTemplate = async (create: CreateFn): Promise => { - await createKanbanTemplate(create)({ - kanbanId: recruit.template.DefaultVacancy, - space: recruit.space.VacancyTemplates, - title: 'Default vacancy', - states: defaultKanban.states, - doneStates: defaultKanban.doneStates - }) -} diff --git a/plugins/task/src/index.ts b/plugins/task/src/index.ts index 72ea43c40a..b82e99e808 100644 --- a/plugins/task/src/index.ts +++ b/plugins/task/src/index.ts @@ -17,8 +17,6 @@ import type { Employee } from '@anticrm/contact' import { AttachedDoc, Class, - Client, - Data, Doc, Interface, Mixin, @@ -223,91 +221,17 @@ const task = plugin(taskId, { space: { ProjectTemplates: '' as Ref, Sequence: '' as Ref - }, - template: { - DefaultProject: '' as Ref } }) + export default task - -const defaultKanban = { - states: [ - { color: '#7C6FCD', title: 'Open' }, - { color: '#6F7BC5', title: 'In Progress' }, - { color: '#77C07B', title: 'Under review' }, - { color: '#A5D179', title: 'Done' }, - { color: '#F28469', title: 'Invalid' } - ], - doneStates: [ - { isWon: true, title: 'Won' }, - { isWon: false, title: 'Lost' } - ] -} - -/** - * @public - */ -export async function createProjectKanban ( - projectId: Ref, - factory: (_class: Ref>, space: Ref, data: Data, id: Ref) => Promise -): Promise { - const { states, doneStates } = defaultKanban - const stateRank = genRanks(states.length) - for (const st of states) { - const rank = stateRank.next().value - - if (rank === undefined) { - throw Error('Failed to generate rank') - } - - const sid = (projectId + '.state.' + st.title.toLowerCase().replace(' ', '_')) as Ref - await factory( - task.class.State, - projectId, - { - title: st.title, - color: st.color, - rank - }, - sid - ) - } - - const doneStateRank = genRanks(doneStates.length) - for (const st of doneStates) { - const rank = doneStateRank.next().value - - if (rank === undefined) { - throw Error('Failed to generate rank') - } - - const sid = (projectId + '.done-state.' + st.title.toLowerCase().replace(' ', '_')) as Ref - await factory( - st.isWon ? task.class.WonState : task.class.LostState, - projectId, - { - title: st.title, - rank - }, - sid - ) - } - - await factory( - task.class.Kanban, - projectId, - { - attachedTo: projectId - }, - (projectId + '.kanban') as Ref - ) -} +export * from './utils' /** * @public */ export async function createKanban ( - client: Client & TxOperations, + client: TxOperations, attachedTo: Ref, templateId?: Ref ): Promise> { @@ -367,85 +291,3 @@ export async function createKanban ( attachedTo }) } - -export * from './utils' - -/** - * @public - */ -export type CreateFn = ( - props: { - id?: Ref - space: Ref - class: Ref> - }, - attrs: Data -) => Promise - -/** - * @public - */ -export interface KanbanTemplateData { - kanbanId: Ref - space: Ref - title: KanbanTemplate['title'] - states: Pick[] - doneStates: (Pick & { isWon: boolean })[] -} - -/** - * @public - */ -export const createKanbanTemplate = (create: CreateFn) => - async (data: KanbanTemplateData) => { - await create( - { - id: data.kanbanId, - space: data.space, - class: task.class.KanbanTemplate - }, - { - doneStatesC: data.doneStates.length, - statesC: data.states.length, - title: data.title - } - ) - - const doneStateRanks = [...genRanks(data.doneStates.length)] - await Promise.all(data.doneStates.map((st, i) => create({ - space: data.space, - class: st.isWon ? task.class.WonStateTemplate : task.class.LostStateTemplate - }, { - attachedTo: data.kanbanId, - attachedToClass: task.class.KanbanTemplate, - collection: 'doneStatesC', - rank: doneStateRanks[i], - title: st.title - }))) - - const stateRanks = [...genRanks(data.states.length)] - await Promise.all(data.states.map((st, i) => create({ - space: data.space, - class: task.class.StateTemplate - }, { - attachedTo: data.kanbanId, - attachedToClass: task.class.KanbanTemplate, - collection: 'statesC', - rank: stateRanks[i], - title: st.title, - color: st.color - }))) - } - -/** - * @public - */ -export const createDefaultKanbanTemplate = async (create: CreateFn): Promise => { - await createKanbanTemplate(create)({ - kanbanId: task.template.DefaultProject, - space: task.space.ProjectTemplates, - title: 'Default project', - states: defaultKanban.states, - doneStates: defaultKanban.doneStates - }) -}