From 5d24970bf67ba40e628fc25a02e4c8eb00189bd8 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Thu, 6 Jan 2022 18:38:40 +0700 Subject: [PATCH] Candidate mixins (#745) Signed-off-by: Andrey Sobolev --- .vscode/launch.json | 2 +- dev/generator/src/recruit.ts | 27 +-- dev/generator/src/utils.ts | 4 +- dev/tool/src/importer.ts | 22 ++- dev/tool/src/upgrade.ts | 10 +- models/demo/src/creation.ts | 25 ++- models/lead/src/index.ts | 2 +- models/recruit/src/index.ts | 40 ++-- models/recruit/src/migration.ts | 175 +++++++++++++++++- models/recruit/src/plugin.ts | 16 +- packages/core/src/tx.ts | 7 +- packages/model/src/migration.ts | 5 +- packages/theme/styles/_layouts.scss | 5 + packages/ui/src/colors.ts | 41 ++++ packages/ui/src/index.ts | 1 + plugins/activity-assets/assets/icons.svg | 9 +- .../src/components/EditContact.svelte | 168 +++++++++-------- .../src/components/EditPerson.svelte | 23 ++- .../src/components/Customers.svelte | 2 +- plugins/recruit-assets/lang/en.json | 11 +- .../src/components/Candidates.svelte | 122 ++++++++++++ .../src/components/CreateApplication.svelte | 29 ++- .../src/components/CreateCandidate.svelte | 57 +++--- .../src/components/CreateCandidates.svelte | 57 ------ .../src/components/EditApplication.svelte | 2 +- .../src/components/EditCandidate.svelte | 42 +++-- plugins/recruit-resources/src/index.ts | 8 +- plugins/recruit-resources/src/plugin.ts | 17 +- plugins/recruit/src/index.ts | 6 +- .../src/components/CreateTask.svelte | 2 - 30 files changed, 645 insertions(+), 292 deletions(-) create mode 100644 packages/ui/src/colors.ts create mode 100644 plugins/recruit-resources/src/components/Candidates.svelte delete mode 100644 plugins/recruit-resources/src/components/CreateCandidates.svelte diff --git a/.vscode/launch.json b/.vscode/launch.json index dc42481c62..0131da5761 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -51,7 +51,7 @@ "name": "Debug tool", "type": "node", "request": "launch", - "args": ["src/index.ts", "restore-workspace", "ws1", "../../temp/ws1/"], + "args": ["src/index.ts", "upgrade-workspace", "ws1"], "env": { "MINIO_ACCESS_KEY":"minioadmin", "MINIO_SECRET_KEY":"minioadmin", diff --git a/dev/generator/src/recruit.ts b/dev/generator/src/recruit.ts index b2a262a07f..d5879ef56b 100644 --- a/dev/generator/src/recruit.ts +++ b/dev/generator/src/recruit.ts @@ -1,10 +1,10 @@ -import contact, { Employee, EmployeeAccount } from '@anticrm/contact' +import contact, { Employee, EmployeeAccount, Person } from '@anticrm/contact' import core, { AttachedData, Data, generateId, MeasureContext, - MeasureMetricsContext, metricsToString, Ref, + MeasureMetricsContext, metricsToString, MixinUpdate, Ref, TxOperations } from '@anticrm/core' import recruit from '@anticrm/model-recruit' @@ -160,7 +160,7 @@ async function genApplicant ( // Update or create candidate await findOrUpdateAttached(ctx, client, vacancyId, recruit.class.Applicant, applicantId, applicant, { attachedTo: candidateId, - attachedClass: recruit.class.Candidate, + attachedClass: recruit.mixin.Candidate, collection: 'applications' }) @@ -222,23 +222,28 @@ async function genCandidate ( minio.putObject(dbName, imgId, jpegImageData.data, jpegImageData.data.length, { 'Content-Type': 'image/jpeg' }) ) } - const candidate: Data = { + const candidate: Data = { name: fName + ',' + lName, city: faker.address.city(), - title: faker.name.title(), channels: [{ provider: contact.channelProvider.Email, value: faker.internet.email(fName, lName) }], + avatar: imgId + } + + const candidateMixin: MixinUpdate = { + title: faker.name.title(), onsite: faker.datatype.boolean(), remote: faker.datatype.boolean(), - avatar: imgId, source: faker.lorem.lines(1) } + const candidateId = (options.random ? `candidate-${generateId()}-${i}` : `candidate-genid-${i}`) as Ref candidates.push(candidateId) // Update or create candidate - await ctx.with('find-update', {}, () => - findOrUpdate(ctx, client, recruit.space.CandidatesPublic, recruit.class.Candidate, candidateId, candidate) - ) + await ctx.with('find-update', {}, async () => { + await findOrUpdate(ctx, client, recruit.space.CandidatesPublic, contact.class.Person, candidateId, candidate) + await client.updateMixin(candidateId, contact.class.Person, recruit.space.CandidatesPublic, recruit.mixin.Candidate, candidateMixin) + }) await ctx.with('add-comment', {}, () => addComments( @@ -246,7 +251,7 @@ async function genCandidate ( client, recruit.space.CandidatesPublic, candidateId, - recruit.class.Candidate, + contact.class.Person, 'comments' ) ) @@ -260,7 +265,7 @@ async function genCandidate ( dbName, recruit.space.CandidatesPublic, candidateId, - recruit.class.Candidate, + contact.class.Person, 'attachments' ) ) diff --git a/dev/generator/src/utils.ts b/dev/generator/src/utils.ts index 37dde35d0c..81a04f0740 100644 --- a/dev/generator/src/utils.ts +++ b/dev/generator/src/utils.ts @@ -1,11 +1,13 @@ import { AttachedData, AttachedDoc, Class, Data, Doc, DocumentUpdate, MeasureContext, Ref, Space, TxOperations } from '@anticrm/core' -export async function findOrUpdate (ctx: MeasureContext, client: TxOperations, space: Ref, _class: Ref>, objectId: Ref, data: Data): Promise { +export async function findOrUpdate (ctx: MeasureContext, client: TxOperations, space: Ref, _class: Ref>, objectId: Ref, data: Data): Promise { const existingObj = await client.findOne(_class, { _id: objectId, space }) if (existingObj !== undefined) { await client.updateDoc(_class, space, objectId, data) + return false } else { await client.createDoc(_class, space, data, objectId) + return true } } export async function findOrUpdateAttached (ctx: MeasureContext, client: TxOperations, space: Ref, _class: Ref>, objectId: Ref, data: AttachedData, attached: {attachedTo: Ref, attachedClass: Ref>, collection: string}): Promise { diff --git a/dev/tool/src/importer.ts b/dev/tool/src/importer.ts index 4ec4223d7f..2bb30a9337 100644 --- a/dev/tool/src/importer.ts +++ b/dev/tool/src/importer.ts @@ -16,8 +16,8 @@ import attachment, { Attachment } from '@anticrm/attachment' import chunter, { Comment } from '@anticrm/chunter' -import contact, { ChannelProvider } from '@anticrm/contact' -import core, { AttachedData, AttachedDoc, Class, Data, Doc, DocumentUpdate, Ref, SortingOrder, Space, TxOperations, TxResult } from '@anticrm/core' +import contact, { ChannelProvider, Person } from '@anticrm/contact' +import core, { AttachedData, AttachedDoc, Class, Data, Doc, DocumentUpdate, Ref, SortingOrder, Space, TxOperations, TxResult, MixinData } from '@anticrm/core' import recruit from '@anticrm/model-recruit' import { Applicant, Candidate, Vacancy } from '@anticrm/recruit' import task, { calcRank, DoneState, genRanks, Kanban, State } from '@anticrm/task' @@ -156,7 +156,7 @@ export async function importXml ( lastModified: stats.mtime.getTime() }, { attachedTo: candId, - attachedClass: recruit.class.Candidate, + attachedClass: contact.class.Person, collection: 'attachments' }) @@ -227,7 +227,7 @@ async function createApplicant (vacancyId: Ref, candidateId: Ref>, vacancyId: Ref}> { @@ -259,9 +259,13 @@ async function createCandidate (_name: string, pos: number, len: number, c: any, const { sourceFields, telegram, linkedin, github } = parseSocials(c) - const data: Data = { + const data: Data = { name: names.slice(1).join(' ') + ',' + names[0], city: get(c, _.city) ?? '', + channels: [] + } + + const candidateData: MixinData = { title: [ get(c, _.vacancyKind), get(c, _.area) @@ -270,8 +274,7 @@ async function createCandidate (_name: string, pos: number, len: number, c: any, get(c, _.socialContacted), get(c, _.socialChannel), sourceFields.filter(onlyUniq).join(', ') - ].filter(p => p !== undefined && p.trim().length > 0).filter(onlyUniq).join('/'), - channels: [] + ].filter(p => p !== undefined && p.trim().length > 0).filter(onlyUniq).join('/') } pushChannel(c, data, _.email, contact.channelProvider.Email) @@ -296,14 +299,15 @@ async function createCandidate (_name: string, pos: number, len: number, c: any, if (github !== undefined) { data.channels.push({ provider: contact.channelProvider.GitHub, value: github }) } - await findOrUpdate(client, recruit.space.CandidatesPublic, recruit.class.Candidate, candId, data) + await findOrUpdate(client, recruit.space.CandidatesPublic, contact.class.Person, candId, data) + await client.updateMixin(candId, contact.class.Person, recruit.space.CandidatesPublic, recruit.mixin.Candidate, candidateData) const commentId = (candId + '.description.comment') as Ref if (commentData.length > 0) { await findOrUpdateAttached(client, recruit.space.CandidatesPublic, chunter.class.Comment, commentId, { message: commentData.join('\n
') - }, { attachedTo: candId, attachedClass: recruit.class.Candidate, collection: 'comments' }) + }, { attachedTo: candId, attachedClass: recruit.mixin.Candidate, collection: 'comments' }) } } diff --git a/dev/tool/src/upgrade.ts b/dev/tool/src/upgrade.ts index 17641b0e21..ec9c1f51d0 100644 --- a/dev/tool/src/upgrade.ts +++ b/dev/tool/src/upgrade.ts @@ -3,7 +3,7 @@ import { DocumentQuery, Domain, FindOptions, - isOperator, SortingOrder + isOperator, Ref, SortingOrder } from '@anticrm/core' import { MigrationClient, MigrateUpdate, MigrationResult } from '@anticrm/model' import { Db, Document, Filter, Sort, UpdateFilter } from 'mongodb' @@ -87,4 +87,12 @@ export class MigrateClientImpl implements MigrationClient { await this.db.collection(sourceDomain).deleteMany(q) return result } + + async create (domain: Domain, doc: T): Promise { + await this.db.collection(domain).insertOne(doc as Document) + } + + async delete (domain: Domain, _id: Ref): Promise { + await this.db.collection(domain).deleteOne({ _id }) + } } diff --git a/models/demo/src/creation.ts b/models/demo/src/creation.ts index c9144cb142..d9821a4eda 100644 --- a/models/demo/src/creation.ts +++ b/models/demo/src/creation.ts @@ -39,12 +39,11 @@ export async function createDeps (client: Client): Promise { account.employee ) - await tx.createDoc( - recruit.class.Candidate, + const u1 = await tx.createDoc( + contact.class.Person, recruit.space.CandidatesPublic, { name: 'P.,Andrey', - title: 'Chief Architect', city: 'Monte Carlo', channels: [ { @@ -55,12 +54,15 @@ export async function createDeps (client: Client): Promise { } ) - await tx.createDoc( - recruit.class.Candidate, + await tx.createMixin(u1, contact.class.Person, recruit.space.CandidatesPublic, recruit.mixin.Candidate, { + title: 'Chief Architect' + }) + + const u2 = await tx.createDoc( + contact.class.Person, recruit.space.CandidatesPublic, { name: 'M.,Marina', - title: 'Chief Designer', city: 'Los Angeles', channels: [ { @@ -70,13 +72,15 @@ export async function createDeps (client: Client): Promise { ] } ) + await tx.createMixin(u2, contact.class.Person, recruit.space.CandidatesPublic, recruit.mixin.Candidate, { + title: 'Chief Designer' + }) - await tx.createDoc( - recruit.class.Candidate, + const u3 = await tx.createDoc( + contact.class.Person, recruit.space.CandidatesPublic, { name: 'P.,Alex', - title: 'Frontend Engineer', city: 'Krasnodar, Russia', channels: [ { @@ -86,4 +90,7 @@ export async function createDeps (client: Client): Promise { ] } ) + await tx.createMixin(u3, contact.class.Person, recruit.space.CandidatesPublic, recruit.mixin.Candidate, { + title: 'Frontend Engineer' + }) } diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index ed55e79d90..26c435854a 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -54,7 +54,7 @@ export class TLead extends TTask implements Lead { } @Mixin(lead.mixin.Customer, contact.class.Contact) -@UX('Customer' as IntlString, contact.icon.Person) // <-- Use general customer icons here. +@UX('Customer' as IntlString, lead.icon.LeadApplication) export class TCustomer extends TPerson implements Customer { @Prop(Collection(lead.class.Lead), 'Leads' as IntlString) leads?: number diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index c7e690188a..3c3e588672 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -15,7 +15,7 @@ import type { Employee } from '@anticrm/contact' import { Doc, FindOptions, Ref, Timestamp } from '@anticrm/core' -import { Builder, Collection, Model, Prop, TypeBoolean, TypeDate, TypeRef, TypeString, UX } from '@anticrm/model' +import { Builder, Collection, Mixin, Model, Prop, TypeBoolean, TypeDate, TypeRef, TypeString, UX } from '@anticrm/model' import attachment from '@anticrm/model-attachment' import chunter from '@anticrm/model-chunter' import contact, { TPerson } from '@anticrm/model-contact' @@ -50,8 +50,8 @@ export class TVacancy extends TSpaceWithStates implements Vacancy { @UX(recruit.string.CandidatePools, recruit.icon.RecruitApplication) export class TCandidates extends TSpace implements Candidates {} -@Model(recruit.class.Candidate, contact.class.Person) -@UX('Candidate' as IntlString, contact.icon.Person) +@Mixin(recruit.mixin.Candidate, contact.class.Person) +@UX('Candidate' as IntlString, recruit.icon.RecruitApplication) export class TCandidate extends TPerson implements Candidate { @Prop(TypeString(), 'Title' as IntlString) title?: string @@ -73,7 +73,7 @@ export class TCandidate extends TPerson implements Candidate { @UX('Application' as IntlString, recruit.icon.Application, 'APP' as IntlString, 'number') export class TApplicant extends TTask implements Applicant { // We need to declare, to provide property with label - @Prop(TypeRef(recruit.class.Candidate), 'Candidate' as IntlString) + @Prop(TypeRef(recruit.mixin.Candidate), 'Candidate' as IntlString) declare attachedTo: Ref @Prop(Collection(attachment.class.Attachment), 'Attachments' as IntlString) @@ -96,13 +96,6 @@ export function createModel (builder: Builder): void { } }) - builder.mixin(recruit.class.Candidates, core.class.Class, workbench.mixin.SpaceView, { - view: { - class: recruit.class.Candidate, - createItemDialog: recruit.component.CreateCandidate - } - }) - builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.AttributeEditor, { editor: recruit.component.Applications }) @@ -122,12 +115,15 @@ export function createModel (builder: Builder): void { addSpaceLabel: recruit.string.CreateVacancy, createComponent: recruit.component.CreateVacancy, component: recruit.component.EditVacancy - }, + } + ], + specials: [ { + id: 'candidates', + component: recruit.component.Candidates, + icon: contact.icon.Person, label: recruit.string.Candidates, - spaceClass: recruit.class.Candidates, - addSpaceLabel: recruit.string.CreateCandidates, - createComponent: recruit.component.CreateCandidates + position: 'bottom' } ] } @@ -148,7 +144,7 @@ export function createModel (builder: Builder): void { ) builder.createDoc(view.class.Viewlet, core.space.Model, { - attachTo: recruit.class.Candidate, + attachTo: recruit.mixin.Candidate, descriptor: view.viewlet.Table, open: contact.component.EditContact, // eslint-disable-next-line @typescript-eslint/consistent-type-assertions @@ -176,7 +172,7 @@ export function createModel (builder: Builder): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions options: { lookup: { - attachedTo: recruit.class.Candidate, + attachedTo: recruit.mixin.Candidate, state: task.class.State, assignee: contact.class.Employee, doneState: task.class.DoneState @@ -202,7 +198,7 @@ export function createModel (builder: Builder): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions options: { lookup: { - attachedTo: recruit.class.Candidate, + attachedTo: recruit.mixin.Candidate, state: task.class.State } } as FindOptions, // TODO: fix @@ -216,7 +212,7 @@ export function createModel (builder: Builder): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions options: { lookup: { - attachedTo: recruit.class.Candidate, + attachedTo: recruit.mixin.Candidate, state: task.class.State, assignee: contact.class.Employee, doneState: task.class.DoneState @@ -239,7 +235,7 @@ export function createModel (builder: Builder): void { card: recruit.component.KanbanCard }) - builder.mixin(recruit.class.Candidate, core.class.Class, view.mixin.ObjectEditor, { + builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ObjectEditor, { editor: recruit.component.EditCandidate }) @@ -267,12 +263,12 @@ export function createModel (builder: Builder): void { ) builder.createDoc(view.class.ActionTarget, core.space.Model, { - target: recruit.class.Candidate, + target: recruit.mixin.Candidate, action: recruit.action.CreateApplication }) builder.createDoc(view.class.ActionTarget, core.space.Model, { - target: recruit.class.Candidate, + target: recruit.mixin.Candidate, action: task.action.CreateTask }) diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index a3f344cb80..3e0dd9992a 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -13,7 +13,26 @@ // limitations under the License. // +import { Person } from '@anticrm/contact' +import core, { AttachedDoc, Class, Doc, DOMAIN_TX, MixinData, Ref, TxCollectionCUD, TxCreateDoc, TxMixin, TxUpdateDoc } from '@anticrm/core' import { MigrateOperation, MigrationClient, MigrationResult, MigrationUpgradeClient } from '@anticrm/model' +import contact, { DOMAIN_CONTACT } from '@anticrm/model-contact' +import recruit, { Candidate } from '@anticrm/recruit' + +function toCandidateData (c: Pick | undefined): MixinData { + if (c === undefined) { + return {} + } + const result: MixinData = { + onsite: c.onsite, + title: c.title, + remote: c.remote, + source: c.source + } + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + Object.keys(result).forEach(key => (result as any)[key] == null && delete (result as any)[key]) + return result +} // eslint-disable-next-line @typescript-eslint/no-unused-vars function logInfo (msg: string, result: MigrationResult): void { @@ -22,6 +41,160 @@ function logInfo (msg: string, result: MigrationResult): void { } } export const recruitOperation: MigrateOperation = { - async migrate (client: MigrationClient): Promise {}, + async migrate (client: MigrationClient): Promise { + // Move all candidates to mixins. + await client.update(DOMAIN_CONTACT, { + _class: 'recruit:class:Candidate' as Ref> + }, { + $rename: { + title: `${recruit.mixin.Candidate}.title`, + applications: `${recruit.mixin.Candidate}.applications`, + remote: `${recruit.mixin.Candidate}.remote`, + source: `${recruit.mixin.Candidate}.source`, + onsite: `${recruit.mixin.Candidate}.onsite` + } + }) + + await client.update(DOMAIN_CONTACT, { + _class: 'recruit:class:Candidate' as Ref> + }, { + _class: contact.class.Person + }) + + // Migrate Create operations. + await client.update(DOMAIN_TX, { + _class: core.class.TxCreateDoc, + objectClass: 'recruit:class:Candidate' as Ref> + }, { + // objectClass: contact.class.Person, + $rename: { + 'attributes.title': `attributes.${recruit.mixin.Candidate}.title`, + 'attributes.applications': `attributes.${recruit.mixin.Candidate}.applications`, + 'attributes.remote': `attributes.${recruit.mixin.Candidate}.remote`, + 'attributes.onsite': `attributes.${recruit.mixin.Candidate}.onsite`, + 'attributes.source': `attributes.${recruit.mixin.Candidate}.source` + } + }) + + await migrateCreateCandidateToPersonAndMixin(client) + + // Migrate update operations. + await client.update(DOMAIN_TX, { + _class: core.class.TxUpdateDoc, + objectClass: 'recruit:class:Candidate' as Ref> + }, { + $rename: { + 'operations.title': `operations.${recruit.mixin.Candidate}.title`, + 'operations.applications': `operations.${recruit.mixin.Candidate}.applications`, + 'operations.remote': `operations.${recruit.mixin.Candidate}.remote`, + 'operations.onsite': `operations.${recruit.mixin.Candidate}.onsite`, + 'operations.source': `operations.${recruit.mixin.Candidate}.source` + } + }) + await migrateUpdateCandidateToPersonAndMixin(client) + + await migrateTxCollectionCandidateToPerson(client) + + await client.update(DOMAIN_TX, { + _class: core.class.TxRemoveDoc, + objectClass: 'recruit:class:Candidate' as Ref> + }, { + objectClass: contact.class.Person + }) + }, async upgrade (client: MigrationUpgradeClient): Promise {} } +async function migrateUpdateCandidateToPersonAndMixin (client: MigrationClient): Promise { + const updateCandidates = await client.find(DOMAIN_TX, { + _class: core.class.TxUpdateDoc, + objectClass: 'recruit:class:Candidate' as Ref> + }) as TxUpdateDoc[] + console.log('Processing update candidate operations:', updateCandidates.length) + for (const c of updateCandidates) { + const mixinOp: TxMixin = { + _class: core.class.TxMixin, + _id: (c._id + '.m') as Ref>, + objectId: c.objectId, + objectClass: contact.class.Person, + mixin: recruit.mixin.Candidate, + attributes: toCandidateData((c.operations as any)[recruit.mixin.Candidate] as unknown as Candidate), + objectSpace: c.objectSpace, + modifiedBy: c.modifiedBy, + modifiedOn: c.modifiedOn, + space: c.space + } + if (Object.keys(mixinOp.attributes).length > 0) { + try { + await client.create(DOMAIN_TX, mixinOp) + } catch (ex) { + // Ignore if existing + } + } + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (c.operations as any)[recruit.mixin.Candidate] + + if (Object.keys(c.operations).length === 0) { + // Delete existing transaction, since there is nothing to change inside. + await client.delete(DOMAIN_TX, c._id) + } else { + await client.update(DOMAIN_TX, { _id: c._id }, { + $set: { objectClass: contact.class.Person }, + $unset: { [`operations.${recruit.mixin.Candidate}`]: '' } + }) + } + } +} +async function migrateTxCollectionCandidateToPerson (client: MigrationClient): Promise { + const collectionCandidates = await client.find(DOMAIN_TX, { + _class: core.class.TxCollectionCUD, + objectClass: 'recruit:class:Candidate' as Ref> + }) as TxCollectionCUD[] + console.log('Processing collection candidate operations:', collectionCandidates.length) + + for (const c of collectionCandidates) { + if (c.tx.objectClass === recruit.class.Applicant) { + await client.update(DOMAIN_TX, { _id: c._id }, { + objectClass: recruit.mixin.Candidate + }) + } else { + await client.update(DOMAIN_TX, { _id: c._id }, { + objectClass: contact.class.Person + }) + } + } +} + +async function migrateCreateCandidateToPersonAndMixin (client: MigrationClient): Promise { + const createCandidates = await client.find(DOMAIN_TX, { + _class: core.class.TxCreateDoc, + objectClass: 'recruit:class:Candidate' as Ref>, + [`attributes.${recruit.mixin.Candidate}`]: { $exists: true } + }) as TxCreateDoc[] + console.log('Processing create candidate operations:', createCandidates.length) + for (const c of createCandidates) { + const mixinOp: TxMixin = { + _class: core.class.TxMixin, + _id: (c._id + '.m') as Ref>, + objectId: c.objectId, + objectClass: contact.class.Person, + mixin: recruit.mixin.Candidate, + attributes: toCandidateData((c.attributes as any)[recruit.mixin.Candidate] as unknown as Candidate), + objectSpace: c.objectSpace, + modifiedBy: c.modifiedBy, + modifiedOn: c.modifiedOn, + space: c.space + } + if (Object.keys(mixinOp.attributes).length > 0) { + try { + await client.create(DOMAIN_TX, mixinOp) + } catch (ex) { + // Ignore if existing + } + } + + await client.update(DOMAIN_TX, { _id: c._id }, { + $set: { objectClass: contact.class.Person }, + $unset: { [`attributes.${recruit.mixin.Candidate}`]: '' } + }) + } +} diff --git a/models/recruit/src/plugin.ts b/models/recruit/src/plugin.ts index ffc11144ce..e526f61cb9 100644 --- a/models/recruit/src/plugin.ts +++ b/models/recruit/src/plugin.ts @@ -13,15 +13,15 @@ // limitations under the License. // -import type { Client, Doc, Ref, Space } from '@anticrm/core' +import type { Client, Doc, Ref } from '@anticrm/core' import type { IntlString, Resource, Status } from '@anticrm/platform' import { mergeIds } from '@anticrm/platform' import { recruitId } from '@anticrm/recruit' import recruit from '@anticrm/recruit-resources/src/plugin' +import { KanbanTemplate } from '@anticrm/task' import type { AnyComponent } from '@anticrm/ui' import type { Action } from '@anticrm/view' import { Application } from '@anticrm/workbench' -import { KanbanTemplate } from '@anticrm/task' export default mergeIds(recruitId, recruit, { app: { @@ -36,17 +36,13 @@ export default mergeIds(recruitId, recruit, { string: { RecruitApplication: '' as IntlString, Vacancies: '' as IntlString, - CandidatePools: '' as IntlString, - Candidates: '' as IntlString, - Vacancy: '' as IntlString + CandidatePools: '' as IntlString }, validator: { ApplicantValidator: '' as Resource<(doc: T, client: Client) => Promise> }, component: { CreateVacancy: '' as AnyComponent, - CreateCandidates: '' as AnyComponent, - CreateCandidate: '' as AnyComponent, CreateApplication: '' as AnyComponent, EditCandidate: '' as AnyComponent, KanbanCard: '' as AnyComponent, @@ -55,10 +51,8 @@ export default mergeIds(recruitId, recruit, { EditVacancy: '' as AnyComponent, EditApplication: '' as AnyComponent, TemplatesIcon: '' as AnyComponent, - Applications: '' as AnyComponent - }, - space: { - CandidatesPublic: '' as Ref + Applications: '' as AnyComponent, + Candidates: '' as AnyComponent }, template: { DefaultVacancy: '' as Ref diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts index 94928ca7f1..044d266f89 100644 --- a/packages/core/src/tx.ts +++ b/packages/core/src/tx.ts @@ -70,6 +70,11 @@ export interface TxBulkWrite extends Tx { txes: TxCUD[] } +/** + * @public + */ +export type MixinData = Omit & PushOptions> & IncOptions> + /** * @public */ @@ -426,7 +431,7 @@ export class TxOperations implements Storage { objectClass: Ref>, objectSpace: Ref, mixin: Ref>, - attributes: MixinUpdate + attributes: MixinData ): Promise { const tx = this.txFactory.createTxMixin(objectId, objectClass, objectSpace, mixin, attributes) return this.storage.tx(tx) diff --git a/packages/model/src/migration.ts b/packages/model/src/migration.ts index 5982cfcc83..f381da6011 100644 --- a/packages/model/src/migration.ts +++ b/packages/model/src/migration.ts @@ -1,4 +1,4 @@ -import { Client, Doc, DocumentQuery, Domain, FindOptions, IncOptions, ObjQueryType, PushOptions } from '@anticrm/core' +import { Client, Doc, DocumentQuery, Domain, FindOptions, IncOptions, ObjQueryType, PushOptions, Ref } from '@anticrm/core' /** * @public @@ -43,6 +43,9 @@ export interface MigrationClient { // Move documents per domain move: (sourceDomain: Domain, query: DocumentQuery, targetDomain: Domain) => Promise + + create: (domain: Domain, doc: T) => Promise + delete: (domain: Domain, _id: Ref) => Promise } /** diff --git a/packages/theme/styles/_layouts.scss b/packages/theme/styles/_layouts.scss index e5c472f692..af31561d8b 100644 --- a/packages/theme/styles/_layouts.scss +++ b/packages/theme/styles/_layouts.scss @@ -118,6 +118,11 @@ p:last-child { margin-block-end: 0; } align-items: center; flex-wrap: nowrap; } +.flex-row-streach { + display: flex; + align-items: stretch; + flex-wrap: nowrap; +} .flex-row-top { display: flex; align-items: flex-start; diff --git a/packages/ui/src/colors.ts b/packages/ui/src/colors.ts new file mode 100644 index 0000000000..6dab113a6d --- /dev/null +++ b/packages/ui/src/colors.ts @@ -0,0 +1,41 @@ + +const blackColors: string[] = [ + '#A5D179', + '#77C07B', + '#60B96E', + '#45AEA3', + '#46CBDE', + '#47BDF6', + '#5AADF6', + '#73A6CD', + '#B977CB', + '#7C6FCD', + '#6F7BC5', + '#F28469' +] + +/** + * @public + */ +export function getPlatformColor (hash: number): string { + return blackColors[Math.abs(hash) % blackColors.length] +} + +/** + * @public + */ +export function getPlatformColorForText (text: string): string { + return getPlatformColor(hashCode(text)) +} + +/** + * @public + */ +export function getPlatformColorCount (): number { + return blackColors.length +} + +function hashCode (str: string): number { + return str.split('').reduce((prevHash, currVal) => + (((prevHash << 5) - prevHash) + currVal.charCodeAt(0)) | 0, 0) +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 5140ab0f82..8056ad0e3c 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -157,3 +157,4 @@ addStringsLoader(uiId, async (lang: string) => { }) export { default } from './plugin' +export * from './colors' diff --git a/plugins/activity-assets/assets/icons.svg b/plugins/activity-assets/assets/icons.svg index 7354ce8524..22075d5191 100644 --- a/plugins/activity-assets/assets/icons.svg +++ b/plugins/activity-assets/assets/icons.svg @@ -1,7 +1,8 @@ - - - - + + + + + diff --git a/plugins/contact-resources/src/components/EditContact.svelte b/plugins/contact-resources/src/components/EditContact.svelte index 4a7cd8e178..1cba555416 100644 --- a/plugins/contact-resources/src/components/EditContact.svelte +++ b/plugins/contact-resources/src/components/EditContact.svelte @@ -25,10 +25,9 @@ getClient, KeyedAttribute } from '@anticrm/presentation' - import { ActionIcon, AnyComponent, Component, Label } from '@anticrm/ui' - + import { AnyComponent, Component, getPlatformColorForText, Label } from '@anticrm/ui' import view from '@anticrm/view' - import { createEventDispatcher } from 'svelte' + import { createEventDispatcher, onDestroy } from 'svelte' import contact from '../plugin' export let _id: Ref @@ -58,14 +57,16 @@ let mixins: Mixin[] = [] let selectedMixin: Mixin | undefined - + $: if (object && prevSelected !== object._class) { prevSelected = object._class selectedClass = objectClass._id selectedMixin = undefined const h = client.getHierarchy() - mixins = h.getDescendants(contact.class.Contact) - .filter((m) => h.getClass(m).kind === ClassifierKind.MIXIN && h.hasMixin(object, m)).map(m => h.getClass(m) as Mixin) + mixins = h + .getDescendants(contact.class.Contact) + .filter((m) => h.getClass(m).kind === ClassifierKind.MIXIN && h.hasMixin(object, m)) + .map((m) => h.getClass(m) as Mixin) } const dispatch = createEventDispatcher() @@ -109,7 +110,10 @@ return editorMixin.editor } - async function getEditorOrDefault (_class: Ref> | undefined, defaultClass: Ref>): Promise { + async function getEditorOrDefault ( + _class: Ref> | undefined, + defaultClass: Ref> + ): Promise { const editor = _class !== undefined ? await getEditor(_class) : undefined if (editor !== undefined) { return editor @@ -125,6 +129,34 @@ } $: icon = (objectClass?.icon ?? contact.class.Person) as Asset + + function getStyle (id: Ref>, selected: boolean): string { + const color = getPlatformColorForText(id as string) + return ` + background: ${color + (selected ? 'ff' : '33')}; + border: 1px solid ${color + (selected ? '0f' : '66')}; + ` + } + + let mainEditor: HTMLElement + let prevEditor: HTMLElement + let maxHeight = 0 + const observer = new ResizeObserver(() => { + const curHeight = mainEditor.clientHeight + maxHeight = Math.max(maxHeight, curHeight) + }) + + $: if (mainEditor != null) { + if (prevEditor != null) { + observer.unobserve(prevEditor) + } + prevEditor = mainEditor + observer.observe(mainEditor) + } + + onDestroy(() => { + observer.disconnect() + }) {#if object !== undefined} @@ -138,32 +170,13 @@ dispatch('close') }} > -
-
- {#if mixins.length > 0} -
- { - selectedClass = objectClass._id - selectedMixin = undefined - }} /> -
- {#each mixins as mixin} -
- { - selectedClass = mixin._id - selectedMixin = mixin - }} /> -
- {/each} - {/if} -
-
- {#if keys} - - {/if} -
+
+ {#if keys} + + {/if}
- {#await getEditorOrDefault(selectedClass, object._class) then is} +
+ {#await getEditorOrDefault(selectedClass, object._class) then is} - {/await} + {/await} +
+ {#if mixins.length > 0} +
+
{ selectedClass = objectClass._id; selectedMixin = undefined }}> +
+ {#each mixins as mixin} +
{ selectedClass = mixin._id; selectedMixin = mixin }}> +
+ {/each} +
+ {/if} {#each collectionKeys as collection}
{#await getCollectionEditor(collection) then is} @@ -183,53 +213,36 @@ {/await}
{/each} - - {/if} diff --git a/plugins/contact-resources/src/components/EditPerson.svelte b/plugins/contact-resources/src/components/EditPerson.svelte index c0b2a89d7a..6c31a0bbde 100644 --- a/plugins/contact-resources/src/components/EditPerson.svelte +++ b/plugins/contact-resources/src/components/EditPerson.svelte @@ -64,17 +64,22 @@ {#if object !== undefined} -
+
-
- -
-
- +
+
+ +
+
+ +
+ +
+
{#if !object.channels || object.channels.length === 0} @@ -127,4 +132,10 @@ margin-left: 0.5rem; } } + + .separator { + margin: 1rem 0; + height: 1px; + background-color: var(--theme-card-divider); + } diff --git a/plugins/lead-resources/src/components/Customers.svelte b/plugins/lead-resources/src/components/Customers.svelte index dfcf2195c5..9f1972ce5f 100644 --- a/plugins/lead-resources/src/components/Customers.svelte +++ b/plugins/lead-resources/src/components/Customers.svelte @@ -32,7 +32,7 @@
- + { resultQuery = {} } } />
diff --git a/plugins/recruit-assets/lang/en.json b/plugins/recruit-assets/lang/en.json index 035fdd55f0..d24135fc16 100644 --- a/plugins/recruit-assets/lang/en.json +++ b/plugins/recruit-assets/lang/en.json @@ -7,14 +7,19 @@ "VacancyName": "Vacancy Title *", "VacancyDescription": "Vacancy Description", "CreateVacancy": "Create Vacancy", - "CreateCandidate": "Create Candidate", "MakePrivate": "Make Private", "Vacancy": "Vacancy", - "CreateCandidates": "Create pool", "CandidatesName": "Pool name *", "MakePrivateDescription": "Only members can see it", "CreateAnApplication": "Create an application", - "NoApplicationsForCandidate": "There are no applications for this candidate." + "NoApplicationsForCandidate": "There are no applications for this candidate.", + "CreateApplication": "Create Application", + "SelectVacancy": "Select vacancy", + "Candidate": "Candidate", + "AssignRecruiter": "Assigned recruiter", + "UnAssignRecruiter": "Unassigned recruiter", + "Recruiters": "Recruiters", + "Create": "Create" }, "status": { "CandidateRequired": "Please select candidate" diff --git a/plugins/recruit-resources/src/components/Candidates.svelte b/plugins/recruit-resources/src/components/Candidates.svelte new file mode 100644 index 0000000000..e5c959d718 --- /dev/null +++ b/plugins/recruit-resources/src/components/Candidates.svelte @@ -0,0 +1,122 @@ + + + + +
+
+
+ + +
+
+ + { resultQuery = {} } } /> +
+ +
+
+ + {#await tableDescriptor then descr} + {#if descr} + + {/if} + {/await} + + + + diff --git a/plugins/recruit-resources/src/components/CreateApplication.svelte b/plugins/recruit-resources/src/components/CreateApplication.svelte index 57251f757a..a2b0152d07 100644 --- a/plugins/recruit-resources/src/components/CreateApplication.svelte +++ b/plugins/recruit-resources/src/components/CreateApplication.svelte @@ -13,7 +13,7 @@ // limitations under the License. --> { dispatch('close') @@ -135,15 +144,15 @@ {#if !preserveCandidate} - + {/if} diff --git a/plugins/recruit-resources/src/components/CreateCandidate.svelte b/plugins/recruit-resources/src/components/CreateCandidate.svelte index 3682da9eee..87fd30c87b 100644 --- a/plugins/recruit-resources/src/components/CreateCandidate.svelte +++ b/plugins/recruit-resources/src/components/CreateCandidate.svelte @@ -14,28 +14,21 @@ --> - - { dispatch('close') }} -> - - - - - diff --git a/plugins/recruit-resources/src/components/EditApplication.svelte b/plugins/recruit-resources/src/components/EditApplication.svelte index 7b8fa72303..0fb8a75195 100644 --- a/plugins/recruit-resources/src/components/EditApplication.svelte +++ b/plugins/recruit-resources/src/components/EditApplication.svelte @@ -30,7 +30,7 @@ const candidateQuery = createQuery() $: if (object !== undefined) { - candidateQuery.query(recruit.class.Candidate, { _id: object.attachedTo as Ref }, (result) => { + candidateQuery.query(recruit.mixin.Candidate, { _id: object.attachedTo as Ref }, (result) => { candidate = result[0] }) } diff --git a/plugins/recruit-resources/src/components/EditCandidate.svelte b/plugins/recruit-resources/src/components/EditCandidate.svelte index c3e65fe159..b017f3cb43 100644 --- a/plugins/recruit-resources/src/components/EditCandidate.svelte +++ b/plugins/recruit-resources/src/components/EditCandidate.svelte @@ -14,7 +14,7 @@ // limitations under the License. --> {#if object !== undefined} -
+
-
- -
-
- -
-
- +
+
+ +
+
+ +
+
+ +
-
+
{#if !object.channels || object.channels.length === 0} => ({ }, component: { CreateVacancy, - CreateCandidates, - CreateCandidate, CreateApplication, EditCandidate, EditApplication, @@ -73,6 +70,7 @@ export default async (): Promise => ({ ApplicationsPresenter, EditVacancy, TemplatesIcon, - Applications + Applications, + Candidates } }) diff --git a/plugins/recruit-resources/src/plugin.ts b/plugins/recruit-resources/src/plugin.ts index 15595b558d..10f63ed15a 100644 --- a/plugins/recruit-resources/src/plugin.ts +++ b/plugins/recruit-resources/src/plugin.ts @@ -13,6 +13,7 @@ // limitations under the License. // +import { Ref, Space } from '@anticrm/core' import type { IntlString, StatusCode } from '@anticrm/platform' import { mergeIds } from '@anticrm/platform' import recruit, { recruitId } from '@anticrm/recruit' @@ -29,13 +30,23 @@ export default mergeIds(recruitId, recruit, { VacancyDescription: '' as IntlString, MakePrivate: '' as IntlString, MakePrivateDescription: '' as IntlString, - CreateCandidates: '' as IntlString, CandidatesName: '' as IntlString, CandidatesDescription: '' as IntlString, - CreateCandidate: '' as IntlString, CreateAnApplication: '' as IntlString, NoApplicationsForCandidate: '' as IntlString, FirstName: '' as IntlString, - LastName: '' as IntlString + LastName: '' as IntlString, + Candidates: '' as IntlString, + CreateApplication: '' as IntlString, + Vacancy: '' as IntlString, + SelectVacancy: '' as IntlString, + Candidate: '' as IntlString, + AssignRecruiter: '' as IntlString, + Recruiters: '' as IntlString, + UnAssignRecruiter: '' as IntlString, + Create: '' as IntlString + }, + space: { + CandidatesPublic: '' as Ref } }) diff --git a/plugins/recruit/src/index.ts b/plugins/recruit/src/index.ts index dc95f0e956..b497efa494 100644 --- a/plugins/recruit/src/index.ts +++ b/plugins/recruit/src/index.ts @@ -14,7 +14,7 @@ // import type { Person } from '@anticrm/contact' -import type { Class, Ref, Space, Timestamp } from '@anticrm/core' +import type { Class, Mixin, Ref, Space, Timestamp } from '@anticrm/core' import type { Asset, Plugin } from '@anticrm/platform' import { plugin } from '@anticrm/platform' import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@anticrm/task' @@ -65,10 +65,12 @@ export const recruitId = 'recruit' as Plugin const recruit = plugin(recruitId, { class: { Applicant: '' as Ref>, - Candidate: '' as Ref>, Candidates: '' as Ref>, Vacancy: '' as Ref> }, + mixin: { + Candidate: '' as Ref> + }, icon: { RecruitApplication: '' as Asset, Vacancy: '' as Asset, diff --git a/plugins/task-resources/src/components/CreateTask.svelte b/plugins/task-resources/src/components/CreateTask.svelte index c16896f408..87290a66d1 100644 --- a/plugins/task-resources/src/components/CreateTask.svelte +++ b/plugins/task-resources/src/components/CreateTask.svelte @@ -94,8 +94,6 @@ } - -