Introduce createDeps (#702)

Signed-off-by: Ilya Sumbatyants <ilya.sumb@gmail.com>
This commit is contained in:
Ilya Sumbatyants 2021-12-22 16:04:07 +07:00 committed by GitHub
parent ae34d53746
commit d5c2c07b9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 410 additions and 788 deletions

View File

@ -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<void> {
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<void> {
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.`)

View File

@ -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<void> {
await createTaskDeps(client)
await createLeadDeps(client)
await createRecruitDeps(client)
await createDemoDeps(client)
}

View File

@ -59,3 +59,5 @@ export default builder
// Export upgrade procedures
export { migrateOperations } from './migration'
export { createDeps } from './creation'

View File

@ -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<void> {
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'
}
]
}
)
}

View File

@ -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<EmployeeAccount> = generateId()
builder.createDoc(contact.class.Employee, contact.space.Employee, {
name: 'Chen,Rosamund',
city: 'Mountain View',
channels: []
}, rosamund, account)
const rosamund = generateId<Employee>()
const account = generateId<EmployeeAccount>()
builder.createDoc<EmployeeAccount>(contact.class.EmployeeAccount, core.space.Model, {
email: 'rosamund@hc.engineering',
employee: rosamund as Ref<Employee>,
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'

View File

@ -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<void> {
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<Ref<KanbanTemplate>> =>
await createKanbanTemplate(client, {
kanbanId: lead.template.DefaultFunnel,
space: lead.space.FunnelTemplates,
title: 'Default funnel',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
})

View File

@ -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 <T extends Doc>(
props: {
id?: Ref<T>
space: Ref<Space>
class: Ref<Class<T>>
},
attrs: Data<T>
): Promise<void> => {
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'

View File

@ -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<void> {
// 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<void> {
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<Doc>(_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<void> => {
await ops.createDoc(props.class, props.space, attrs, props.id)
})
}
}
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {}
}

View File

@ -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<Space>
},
template: {
DefaultFunnel: '' as Ref<KanbanTemplate>
}
})

View File

@ -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<void> {
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<Ref<KanbanTemplate>> =>
await createKanbanTemplate(client, {
kanbanId: recruit.template.DefaultVacancy,
space: recruit.space.VacancyTemplates,
title: 'Default vacancy',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
})

View File

@ -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 <T extends Doc>(
props: {
id?: Ref<T>
space: Ref<Space>
class: Ref<Class<T>>
},
attrs: Data<T>
): Promise<void> => {
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'

View File

@ -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<void> {
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<void> {
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<void> => {
await ops.createDoc(props.class, props.space, attrs, props.id)
})
}
}
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {}
}

View File

@ -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<Space>
},
template: {
DefaultVacancy: '' as Ref<KanbanTemplate>
}
})

117
models/task/src/creation.ts Normal file
View File

@ -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<void> {
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<KanbanTemplate>
space: Ref<Space>
title: KanbanTemplate['title']
states: Pick<StateTemplate, 'title' | 'color'>[]
doneStates: (Pick<DoneStateTemplate, 'title'> & { isWon: boolean })[]
}
/**
* @public
*/
export async function createKanbanTemplate (client: TxOperations, data: KanbanTemplateData): Promise<Ref<KanbanTemplate>> {
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<Ref<KanbanTemplate>> =>
await createKanbanTemplate(client, {
kanbanId: task.template.DefaultProject,
space: task.space.ProjectTemplates,
title: 'Default project',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
})

View File

@ -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 <T extends Doc>(
props: {
id?: Ref<T>
space: Ref<Space>
class: Ref<Class<T>>
},
attrs: Data<T>
): Promise<void> => {
builder.createDoc(
props.class,
props.space,
attrs,
props.id
)
})
}
export { taskOperation } from './migration'
export { createDeps, createKanbanTemplate } from './creation'

View File

@ -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<T extends Doc> (
client: MigrationClient,
domain: Domain,
from: Ref<Class<Doc>>,
to: Ref<Class<T>>,
extraOps: MigrateUpdate<T> = {},
txExtraOps: MigrateUpdate<TxCUD<Doc>> = {}
): Promise<void> {
logInfo(`${from} => ${to}: `, await client.update<Doc>(domain, { _class: from }, { ...extraOps, _class: to }))
logInfo(
`${from} => ${to} Transactions`,
await client.update<TxCUD<Doc>>(DOMAIN_TX, { objectClass: from }, { ...txExtraOps, objectClass: to })
)
}
export const taskOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
// 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<Class<Doc>>, task.class.State)
await migrateClass(client, DOMAIN_STATE, 'core:class:WonState' as Ref<Class<Doc>>, task.class.WonState)
await migrateClass(client, DOMAIN_STATE, 'core:class:LostState' as Ref<Class<Doc>>, task.class.LostState)
await migrateClass(client, DOMAIN_KANBAN, 'view:class:Kanban' as Ref<Class<Doc>>, task.class.Kanban)
await migrateClass(
client,
DOMAIN_KANBAN,
'view:class:Sequence' as Ref<Class<Doc>>,
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<Class<Doc>>, task.class.KanbanTemplate)
await migrateClass(client, DOMAIN_KANBAN, 'view:class:StateTemplate' as Ref<Class<Doc>>, task.class.StateTemplate)
await migrateClass(
client,
DOMAIN_KANBAN,
'view:class:DoneStateTemplate' as Ref<Class<Doc>>,
task.class.DoneStateTemplate
)
await migrateClass(
client,
DOMAIN_KANBAN,
'view:class:WonStateTemplate' as Ref<Class<Doc>>,
task.class.WonStateTemplate
)
await migrateClass(
client,
DOMAIN_KANBAN,
'view:class:LostStateTemplate' as Ref<Class<Doc>>,
task.class.LostStateTemplate
)
await client.move(
'recruit' as Domain,
{
_class: 'recruit:class:Applicant' as Ref<Class<Doc>>
},
DOMAIN_TASK
)
await client.move(
'lead' as Domain,
{
_class: 'lead:class:Lead' as Ref<Class<Doc>>
},
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<void> {
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<Doc>(_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<void> => {
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<void> {
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<T extends DocWithRank> ({
client,
ops,
_class,
extractOrder
}: {
client: Client
ops: TxOperations
_class: Ref<Class<T>>
extractOrder: (kanban: any) => Ref<T>[]
}): Promise<void> {
const allItems = await client.findAll(_class, {})
const unorderedItems = allItems
.filter((item) => item.rank === undefined)
const groupedUnsortedItems = new Map<Ref<Space>, 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<Class<DocWithRank>>, item.space, item._id, { rank })
}
}
}
async function updateTemplateRankItems<T extends DocWithRank & AttachedDoc> ({
client,
ops,
_class,
extractOrder
}: {
client: Client
ops: TxOperations
_class: Ref<Class<T>>
extractOrder: (kanban: any) => Ref<T>[]
}): Promise<void> {
const allItems = await client.findAll(_class, {})
const unorderedItems = allItems
.filter((state) => state.rank === undefined)
const groupedUnsortedItems = new Map<Ref<Doc>, 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<KanbanTemplate> })
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<Class<DocWithRank>>, item.space, item._id, { rank })
}
}
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {}
}

View File

@ -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<Space>
},
template: {
DefaultProject: '' as Ref<KanbanTemplate>
}
})

View File

@ -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<KanbanTemplateSpace>
},
template: {
DefaultFunnel: '' as Ref<KanbanTemplate>
}
})
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<Funnel>,
factory: <T extends Doc>(_class: Ref<Class<T>>, space: Ref<Space>, data: Data<T>, id: Ref<T>) => Promise<void>
): Promise<void> {
const { states, doneStates } = defaultKanban
const stateRank = genRanks(states.length)
for (const st of states) {
const sid = (funnelId + '.state.' + st.title.toLowerCase().replace(' ', '_')) as Ref<State>
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<DoneState>
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<Kanban>
)
}
/**
* @public
*/
export const createDefaultKanbanTemplate = async (create: CreateFn): Promise<void> => {
await createKanbanTemplate(create)({
kanbanId: lead.template.DefaultFunnel,
space: lead.space.FunnelTemplates,
title: 'Default funnel',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
})
}

View File

@ -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<KanbanTemplateSpace>
},
template: {
DefaultVacancy: '' as Ref<KanbanTemplate>
}
})
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<void> => {
await createKanbanTemplate(create)({
kanbanId: recruit.template.DefaultVacancy,
space: recruit.space.VacancyTemplates,
title: 'Default vacancy',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
})
}

View File

@ -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<KanbanTemplateSpace>,
Sequence: '' as Ref<Space>
},
template: {
DefaultProject: '' as Ref<KanbanTemplate>
}
})
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<Project>,
factory: <T extends Doc>(_class: Ref<Class<T>>, space: Ref<Space>, data: Data<T>, id: Ref<T>) => Promise<void>
): Promise<void> {
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<State>
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<DoneState>
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<Kanban>
)
}
export * from './utils'
/**
* @public
*/
export async function createKanban (
client: Client & TxOperations,
client: TxOperations,
attachedTo: Ref<Space>,
templateId?: Ref<KanbanTemplate>
): Promise<Ref<Kanban>> {
@ -367,85 +291,3 @@ export async function createKanban (
attachedTo
})
}
export * from './utils'
/**
* @public
*/
export type CreateFn = <T extends Doc>(
props: {
id?: Ref<T>
space: Ref<Space>
class: Ref<Class<T>>
},
attrs: Data<T>
) => Promise<void>
/**
* @public
*/
export interface KanbanTemplateData {
kanbanId: Ref<KanbanTemplate>
space: Ref<Space>
title: KanbanTemplate['title']
states: Pick<StateTemplate, 'title' | 'color'>[]
doneStates: (Pick<DoneStateTemplate, 'title'> & { 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<void> => {
await createKanbanTemplate(create)({
kanbanId: task.template.DefaultProject,
space: task.space.ProjectTemplates,
title: 'Default project',
states: defaultKanban.states,
doneStates: defaultKanban.doneStates
})
}