diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 8bbf7ab56d..16a532192a 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -412,7 +412,7 @@ export function createModel (builder: Builder): void { { attachTo: recruit.class.Applicant, descriptor: view.viewlet.Table, - config: ['', 'attachedTo', 'state', 'doneState', 'modifiedOn'], + config: ['', '$lookup.attachedTo', 'state', 'doneState', 'modifiedOn'], variant: 'short' }, recruit.viewlet.VacancyApplicationsShort @@ -425,7 +425,7 @@ export function createModel (builder: Builder): void { attachTo: recruit.class.Applicant, descriptor: view.viewlet.Table, config: ['', '$lookup.space.name', '$lookup.space.$lookup.company', 'state', 'comments', 'doneState'], - variant: 'short' + variant: 'embedded' }, recruit.viewlet.VacancyApplicationsEmbeddeed ) @@ -496,9 +496,9 @@ export function createModel (builder: Builder): void { config: [ '', { - key: 'attachedTo', - presenter: contact.component.PersonRefPresenter, - sortingKey: 'attachedTo', + key: '$lookup.attachedTo', + presenter: contact.component.PersonPresenter, + sortingKey: '$lookup.attachedTo.name', label: recruit.string.Talent, props: { _class: recruit.mixin.Candidate @@ -541,10 +541,10 @@ export function createModel (builder: Builder): void { config: [ '', { - key: 'attachedTo', - presenter: contact.component.PersonRefPresenter, + key: '$lookup.attachedTo', + presenter: contact.component.PersonPresenter, label: recruit.string.Talent, - sortingKey: 'attachedTo', + sortingKey: '$lookup.attachedTo.name', props: { _class: recruit.mixin.Candidate } diff --git a/plugins/bitrix-resources/src/components/FieldMappingSynchronizer.svelte b/plugins/bitrix-resources/src/components/FieldMappingSynchronizer.svelte index cf306e44a3..a41dd7ac64 100644 --- a/plugins/bitrix-resources/src/components/FieldMappingSynchronizer.svelte +++ b/plugins/bitrix-resources/src/components/FieldMappingSynchronizer.svelte @@ -13,7 +13,7 @@ import core, { Class, Doc, generateId, Ref, Space, WithLookup } from '@hcengineering/core' import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform' import presentation, { getClient, SpaceSelect } from '@hcengineering/presentation' - import { Button, Expandable, Icon, IconAdd, IconClose, Label } from '@hcengineering/ui' + import { Button, CheckBox, Expandable, Icon, IconAdd, IconClose, Label } from '@hcengineering/ui' import { DropdownLabels } from '@hcengineering/ui' import { EditBox } from '@hcengineering/ui' import { NumberEditor } from '@hcengineering/view-resources' @@ -23,6 +23,10 @@ export let mapping: WithLookup export let bitrixClient: BitrixClient + let syncComments = true + let syncEmails = true + let syncAttachments = true + const client = getClient() $: fieldMapping = (mapping.$lookup?.fields as BitrixFieldMapping[]) ?? [] @@ -69,7 +73,10 @@ state = `processed: ${docsProcessed}/${total ?? 1}` }, extraFilter: filterFields.length === 0 ? undefined : mappedFilter, - syncPeriod + syncPeriod, + syncComments, + syncEmails, + syncAttachments }) } catch (err: any) { state = err.message @@ -200,6 +207,20 @@ +
+
+
Sync comments
+ +
+
+
Sync email
+ +
+
+
Sync Attachments
+ +
+
{#each filterFields as field, pos} {@const fValue = fields[field.field]} diff --git a/plugins/bitrix-resources/src/components/mappings/CreateHRApplicationMapping.svelte b/plugins/bitrix-resources/src/components/mappings/CreateHRApplicationMapping.svelte index b9a3f508de..fa04ccde79 100644 --- a/plugins/bitrix-resources/src/components/mappings/CreateHRApplicationMapping.svelte +++ b/plugins/bitrix-resources/src/components/mappings/CreateHRApplicationMapping.svelte @@ -10,7 +10,14 @@ import { getEmbeddedLabel } from '@hcengineering/platform' import { getClient } from '@hcengineering/presentation' import task from '@hcengineering/task' - import { DropdownLabels, DropdownTextItem } from '@hcengineering/ui' + import { + Button, + DropdownIntlItem, + DropdownLabels, + DropdownLabelsIntl, + DropdownTextItem, + IconAdd + } from '@hcengineering/ui' import { ObjectBox } from '@hcengineering/view-resources' import bitrix from '../../plugin' import recruit from '@hcengineering/recruit' @@ -23,6 +30,7 @@ let stateField = (field?.operation as CreateHRApplication)?.stateField let vacancyField = (field?.operation as CreateHRApplication)?.vacancyField let defaultTemplate = (field?.operation as CreateHRApplication)?.defaultTemplate + let copyTalentFields = (field?.operation as CreateHRApplication)?.copyTalentFields ?? [] const client = getClient() @@ -33,7 +41,8 @@ kind: MappingOperation.CreateHRApplication, stateField, vacancyField, - defaultTemplate + defaultTemplate, + copyTalentFields } }) } else { @@ -44,7 +53,8 @@ kind: MappingOperation.CreateHRApplication, stateField, vacancyField, - defaultTemplate + defaultTemplate, + copyTalentFields } }) } @@ -57,6 +67,12 @@ })) } $: items = getItems(fields) + + $: allAttrs = Array.from(client.getHierarchy().getAllAttributes(recruit.mixin.Candidate).values()) + $: attrs = allAttrs.map((it) => ({ id: it.name, label: it.label } as DropdownIntlItem)) + + $: applicantAllAttrs = Array.from(client.getHierarchy().getAllAttributes(recruit.class.Applicant).values()) + $: applicantAttrs = applicantAllAttrs.map((it) => ({ id: it.name, label: it.label } as DropdownIntlItem))
@@ -71,6 +87,30 @@ docQuery={{ space: recruit.space.VacancyTemplates }} bind:value={defaultTemplate} /> + + {#each copyTalentFields as f, i} +
+ => + +
+ {/each} +
diff --git a/plugins/bitrix/src/sync.ts b/plugins/bitrix/src/sync.ts index 0edb8c8a43..13e65520ab 100644 --- a/plugins/bitrix/src/sync.ts +++ b/plugins/bitrix/src/sync.ts @@ -40,6 +40,7 @@ import { LoginInfo } from './types' import { convert, ConvertResult } from './utils' +import recruit from '@hcengineering/recruit' async function updateDoc (client: ApplyOperations, doc: Doc, raw: Doc | Data, date: Timestamp): Promise { // We need to update fields if they are different. @@ -100,6 +101,8 @@ export async function syncDocument ( resultDoc: ConvertResult, info: LoginInfo, frontUrl: string, + syncAttachments: boolean, + extraDocs: Map>, Doc[]>, monitor?: (doc: ConvertResult) => void ): Promise { const hierarchy = client.getHierarchy() @@ -128,9 +131,8 @@ export async function syncDocument ( } for (const op of resultDoc.postOperations) { - await op(resultDoc.document, existing) + await op(resultDoc, extraDocs, existing) } - const idMapping = new Map, Ref>() // Find all attachment documents to existing. @@ -144,79 +146,81 @@ export async function syncDocument ( await syncClass(applyOp, cl, vals, idMapping, resultDoc.document._id) } - // Sync gmail documents - const emailAccount = resultDoc.extraSync.find( - (it) => - it._class === contact.class.Channel && (it as unknown as Channel).provider === contact.channelProvider.Email - ) - if (resultDoc.gmailDocuments.length > 0 && emailAccount !== undefined) { - const emailReadId = idMapping.get(emailAccount._id) ?? emailAccount._id - await syncClass(applyOp, gmail.class.Message, resultDoc.gmailDocuments, idMapping, emailReadId) - } + if (syncAttachments) { + // Sync gmail documents + const emailAccount = resultDoc.extraSync.find( + (it) => + it._class === contact.class.Channel && (it as unknown as Channel).provider === contact.channelProvider.Email + ) + if (resultDoc.gmailDocuments.length > 0 && emailAccount !== undefined) { + const emailReadId = idMapping.get(emailAccount._id) ?? emailAccount._id + await syncClass(applyOp, gmail.class.Message, resultDoc.gmailDocuments, idMapping, emailReadId) + } - const attachIds = Array.from( - new Set(resultDoc.blobs.map((it) => idMapping.get(it[0].attachedTo) ?? it[0].attachedTo)).values() - ) + const attachIds = Array.from( + new Set(resultDoc.blobs.map((it) => idMapping.get(it[0].attachedTo) ?? it[0].attachedTo)).values() + ) - const existingBlobs = await client.findAll(attachment.class.Attachment, { - attachedTo: { $in: [resultDoc.document._id, ...attachIds] } - }) - for (const [ed, op, upd] of resultDoc.blobs) { - const existing = existingBlobs.find((it) => { - const bdoc = hierarchy.as(it, bitrix.mixin.BitrixSyncDoc) - return bdoc.bitrixId === ed.bitrixId + const existingBlobs = await client.findAll(attachment.class.Attachment, { + attachedTo: { $in: [resultDoc.document._id, ...attachIds] } }) - // For Attachments, just do it once per attachment and assume it is not changed. - if (existing === undefined) { - const attachmentId: Ref = generateId() - try { - const edData = await op() - if (edData === undefined) { - console.error('Failed to retrieve document data', ed.name) - continue - } - const data = new FormData() - data.append('file', edData) + for (const [ed, op, upd] of resultDoc.blobs) { + const existing = existingBlobs.find((it) => { + const bdoc = hierarchy.as(it, bitrix.mixin.BitrixSyncDoc) + return bdoc.bitrixId === ed.bitrixId + }) + // For Attachments, just do it once per attachment and assume it is not changed. + if (existing === undefined) { + const attachmentId: Ref = generateId() + try { + const edData = await op() + if (edData === undefined) { + console.error('Failed to retrieve document data', ed.name) + continue + } + const data = new FormData() + data.append('file', edData) - upd(edData, ed) + upd(edData, ed) - ed.lastModified = edData.lastModified - ed.size = edData.size - ed.type = edData.type + ed.lastModified = edData.lastModified + ed.size = edData.size + ed.type = edData.type - let updated = false - for (const existingObj of existingBlobs) { - if (existingObj.name === ed.name && existingObj.size === ed.size && existingObj.type === ed.type) { - if (!updated) { - await updateAttachedDoc(existingObj, applyOp, ed) - updated = true - } else { - // Remove duplicate attachment - await applyOp.remove(existingObj) + let updated = false + for (const existingObj of existingBlobs) { + if (existingObj.name === ed.name && existingObj.size === ed.size && existingObj.type === ed.type) { + if (!updated) { + await updateAttachedDoc(existingObj, applyOp, ed) + updated = true + } else { + // Remove duplicate attachment + await applyOp.remove(existingObj) + } } } - } - if (!updated) { - // No attachment, send to server - const resp = await fetch(concatLink(frontUrl, '/files'), { - method: 'POST', - headers: { - Authorization: 'Bearer ' + info.token - }, - body: data - }) - if (resp.status === 200) { - const uuid = await resp.text() + if (!updated) { + // No attachment, send to server + const resp = await fetch(concatLink(frontUrl, '/files'), { + method: 'POST', + headers: { + Authorization: 'Bearer ' + info.token + }, + body: data + }) + if (resp.status === 200) { + const uuid = await resp.text() - ed.file = uuid - ed._id = attachmentId as Ref + ed.file = uuid + ed._id = attachmentId as Ref - await updateAttachedDoc(undefined, applyOp, ed) + await updateAttachedDoc(undefined, applyOp, ed) + } } + } catch (err: any) { + console.error(err) } - } catch (err: any) { - console.error(err) } } } @@ -440,6 +444,11 @@ export interface SyncOptions { blobProvider?: (blobRef: { file: string, id: string }) => Promise extraFilter?: Record syncPeriod?: number + + // Override to skip synchronization for few areas + syncComments?: boolean + syncEmails?: boolean + syncAttachments?: boolean } interface SyncOptionsExtra { ownerTypeValues: BitrixOwnerType[] @@ -505,6 +514,11 @@ async function doPerformSync (ops: SyncOptions & SyncOptionsExtra): Promise(tags.class.TagElement, {}) + const extraDocs: Map>, Doc[]> = new Map() + + extraDocs.set(recruit.class.Vacancy, await ops.client.findAll(recruit.class.Vacancy, {})) + extraDocs.set(recruit.class.Applicant, await ops.client.findAll(recruit.class.Applicant, {})) + while (added < ops.limit) { const q: Record = { select: sel, @@ -565,7 +579,7 @@ async function doPerformSync (ops: SyncOptions & SyncOptionsExtra): Promise { - ops.monitor?.(total) - }) + await syncDocument( + ops.client, + existingDoc, + res, + ops.loginInfo, + ops.frontUrl, + ops.mapping.attachments && (ops.syncAttachments ?? true), + extraDocs, + () => { + ops.monitor?.(total) + } + ) if (existingDoc !== undefined) { res.document._id = existingDoc._id as Ref } @@ -702,6 +725,8 @@ async function downloadComments ( loginInfo: LoginInfo monitor: (total: number) => void blobProvider?: ((blobRef: { file: string, id: string }) => Promise) | undefined + syncComments?: boolean + syncEmails?: boolean }, commentFieldKeys: string[], userList: Map>, @@ -712,110 +737,115 @@ async function downloadComments ( if (ownerType === undefined) { throw new Error(`No owner type found for ${entityType}`) } - const commentsData = await ops.bitrixClient.call(BitrixEntityType.Comment + '.list', { - filter: { - ENTITY_ID: res.document.bitrixId, - ENTITY_TYPE: entityType - }, - select: commentFieldKeys, - order: { ID: ops.direction } - }) - for (const it of commentsData.result) { - const c: Comment & BitrixSyncDoc = { - _id: generateId(), - _class: chunter.class.Comment, - message: processComment(it.COMMENT as string), - bitrixId: `${it.ID as string}`, - type: it.ENTITY_TYPE, - attachedTo: res.document._id, - attachedToClass: res.document._class, - collection: 'comments', - space: res.document.space, - modifiedBy: userList.get(it.AUTHOR_ID) ?? core.account.System, - modifiedOn: new Date(it.CREATED ?? new Date().toString()).getTime(), - attachments: 0 - } - if (Object.keys(it.FILES ?? {}).length > 0) { - for (const [, v] of Object.entries(it.FILES as BitrixFiles)) { - c.message += `
Attachment: ${v.name} by ${v.authorName}` - // Direct link, we could download using fetch. - c.attachments = (c.attachments ?? 0) + 1 - res.blobs.push([ - { - _id: generateId(), - _class: attachment.class.Attachment, - attachedTo: c._id, - attachedToClass: c._class, - bitrixId: `attach-${v.id}`, - collection: 'attachments', - file: '', - lastModified: Date.now(), - modifiedBy: userList.get(it.AUTHOR_ID) ?? core.account.System, - modifiedOn: new Date(it.CREATED ?? new Date().toString()).getTime(), - name: v.name, - size: v.size, - space: c.space, - type: 'file' - }, - async (): Promise => { - const blob = await ops.blobProvider?.({ file: v.urlDownload, id: `${v.id}` }) - if (blob !== undefined) { - return new File([blob], v.name) - } - }, - (file: File, attach: Attachment) => { - attach.attachedTo = c._id - attach.type = file.type - attach.size = file.size - attach.name = file.name - } - ]) - } - } - res.extraSync.push(c) - } - - const emailAccount = res.extraSync.find( - (it) => it._class === contact.class.Channel && (it as unknown as Channel).provider === contact.channelProvider.Email - ) - if (emailAccount !== undefined) { - const communications = await ops.bitrixClient.call('crm.activity.list', { - order: { ID: 'DESC' }, + if (ops.syncComments ?? true) { + const commentsData = await ops.bitrixClient.call(BitrixEntityType.Comment + '.list', { filter: { - OWNER_ID: res.document.bitrixId, - OWNER_TYPE_ID: ownerType.ID + ENTITY_ID: res.document.bitrixId, + ENTITY_TYPE: entityType }, - select: ['*', 'COMMUNICATIONS'] + select: commentFieldKeys, + order: { ID: ops.direction } }) - const cr = Array.isArray(communications.result) - ? (communications.result as BitrixActivity[]) - : [communications.result as BitrixActivity] - for (const comm of cr) { - if (comm.PROVIDER_TYPE_ID === 'EMAIL') { - const parser = new DOMParser() - - const c: Message & BitrixSyncDoc = { - _id: generateId(), - _class: gmail.class.Message, - content: comm.DESCRIPTION, - textContent: - parser.parseFromString(comm.DESCRIPTION, 'text/html').textContent?.split('\n').slice(0, 3).join('\n') ?? '', - incoming: comm.DIRECTION === '1', - sendOn: new Date(comm.CREATED ?? new Date().toString()).getTime(), - subject: comm.SUBJECT, - bitrixId: `${comm.ID}`, - from: comm.SETTINGS?.EMAIL_META?.from ?? '', - to: comm.SETTINGS?.EMAIL_META?.to ?? '', - replyTo: comm.SETTINGS?.EMAIL_META?.replyTo ?? comm.SETTINGS?.MESSAGE_HEADERS?.['Reply-To'] ?? '', - messageId: comm.SETTINGS?.MESSAGE_HEADERS?.['Message-Id'] ?? '', - attachedTo: emailAccount._id as unknown as Ref, - attachedToClass: emailAccount._class, - collection: 'items', - space: res.document.space, - modifiedBy: userList.get(comm.AUTHOR_ID) ?? core.account.System, - modifiedOn: new Date(comm.CREATED ?? new Date().toString()).getTime() + for (const it of commentsData.result) { + const c: Comment & BitrixSyncDoc = { + _id: generateId(), + _class: chunter.class.Comment, + message: processComment(it.COMMENT as string), + bitrixId: `${it.ID as string}`, + type: it.ENTITY_TYPE, + attachedTo: res.document._id, + attachedToClass: res.document._class, + collection: 'comments', + space: res.document.space, + modifiedBy: userList.get(it.AUTHOR_ID) ?? core.account.System, + modifiedOn: new Date(it.CREATED ?? new Date().toString()).getTime(), + attachments: 0 + } + if (Object.keys(it.FILES ?? {}).length > 0) { + for (const [, v] of Object.entries(it.FILES as BitrixFiles)) { + c.message += `
Attachment: ${v.name} by ${v.authorName}` + // Direct link, we could download using fetch. + c.attachments = (c.attachments ?? 0) + 1 + res.blobs.push([ + { + _id: generateId(), + _class: attachment.class.Attachment, + attachedTo: c._id, + attachedToClass: c._class, + bitrixId: `attach-${v.id}`, + collection: 'attachments', + file: '', + lastModified: Date.now(), + modifiedBy: userList.get(it.AUTHOR_ID) ?? core.account.System, + modifiedOn: new Date(it.CREATED ?? new Date().toString()).getTime(), + name: v.name, + size: v.size, + space: c.space, + type: 'file' + }, + async (): Promise => { + const blob = await ops.blobProvider?.({ file: v.urlDownload, id: `${v.id}` }) + if (blob !== undefined) { + return new File([blob], v.name) + } + }, + (file: File, attach: Attachment) => { + attach.attachedTo = c._id + attach.type = file.type + attach.size = file.size + attach.name = file.name + } + ]) + } + } + res.extraSync.push(c) + } + } + if (ops.syncEmails ?? true) { + const emailAccount = res.extraSync.find( + (it) => + it._class === contact.class.Channel && (it as unknown as Channel).provider === contact.channelProvider.Email + ) + if (emailAccount !== undefined) { + const communications = await ops.bitrixClient.call('crm.activity.list', { + order: { ID: 'DESC' }, + filter: { + OWNER_ID: res.document.bitrixId, + OWNER_TYPE_ID: ownerType.ID + }, + select: ['*', 'COMMUNICATIONS'] + }) + const cr = Array.isArray(communications.result) + ? (communications.result as BitrixActivity[]) + : [communications.result as BitrixActivity] + for (const comm of cr) { + if (comm.PROVIDER_TYPE_ID === 'EMAIL') { + const parser = new DOMParser() + + const c: Message & BitrixSyncDoc = { + _id: generateId(), + _class: gmail.class.Message, + content: comm.DESCRIPTION, + textContent: + parser.parseFromString(comm.DESCRIPTION, 'text/html').textContent?.split('\n').slice(0, 3).join('\n') ?? + '', + incoming: comm.DIRECTION === '1', + sendOn: new Date(comm.CREATED ?? new Date().toString()).getTime(), + subject: comm.SUBJECT, + bitrixId: `${comm.ID}`, + from: comm.SETTINGS?.EMAIL_META?.from ?? '', + to: comm.SETTINGS?.EMAIL_META?.to ?? '', + replyTo: comm.SETTINGS?.EMAIL_META?.replyTo ?? comm.SETTINGS?.MESSAGE_HEADERS?.['Reply-To'] ?? '', + messageId: comm.SETTINGS?.MESSAGE_HEADERS?.['Message-Id'] ?? '', + attachedTo: emailAccount._id as unknown as Ref, + attachedToClass: emailAccount._class, + collection: 'items', + space: res.document.space, + modifiedBy: userList.get(comm.AUTHOR_ID) ?? core.account.System, + modifiedOn: new Date(comm.CREATED ?? new Date().toString()).getTime() + } + res.gmailDocuments.push(c) } - res.gmailDocuments.push(c) } } } diff --git a/plugins/bitrix/src/types.ts b/plugins/bitrix/src/types.ts index 7a9bb4afd6..55644a4d7e 100644 --- a/plugins/bitrix/src/types.ts +++ b/plugins/bitrix/src/types.ts @@ -1,5 +1,5 @@ import { ChannelProvider } from '@hcengineering/contact' -import { AttachedDoc, Class, Doc, Mixin, Ref } from '@hcengineering/core' +import { AnyAttribute, AttachedDoc, Class, Doc, Mixin, Ref } from '@hcengineering/core' import { ExpertKnowledge, InitialKnowledge, MeaningfullKnowledge } from '@hcengineering/tags' import { KanbanTemplate } from '@hcengineering/task' @@ -281,6 +281,8 @@ export interface CreateHRApplication { stateField: string // Name of status in bitrix. defaultTemplate: Ref + + copyTalentFields?: { candidate: Ref, applicant: Ref }[] } /** diff --git a/plugins/bitrix/src/utils.ts b/plugins/bitrix/src/utils.ts index 738acfa6e6..6fbb47eb9d 100644 --- a/plugins/bitrix/src/utils.ts +++ b/plugins/bitrix/src/utils.ts @@ -7,6 +7,7 @@ import core, { Client, Data, Doc, + DocumentUpdate, Mixin, Ref, RefTo, @@ -16,7 +17,7 @@ import core, { generateId } from '@hcengineering/core' import { Message } from '@hcengineering/gmail' -import recruit, { Candidate, Vacancy } from '@hcengineering/recruit' +import recruit, { Applicant, Candidate, Vacancy } from '@hcengineering/recruit' import tags, { TagCategory, TagElement, TagReference } from '@hcengineering/tags' import task from '@hcengineering/task' import bitrix, { @@ -69,7 +70,11 @@ export interface BitrixSyncRequest { /** * @public */ -export type PostOperation = (doc: BitrixSyncDoc, existing?: Doc) => Promise +export type PostOperation = ( + doc: ConvertResult, + extraDocs: Map>, Doc[]>, + existing?: Doc +) => Promise /** * @public @@ -401,11 +406,12 @@ export async function convert ( const vacancyName = extractValue(operation.vacancyField) const statusName = extractValue(operation.stateField) - postOperations.push(async (doc, existingDoc) => { - const vacancies = await client.findAll(recruit.class.Vacancy, {}) - + postOperations.push(async (doc, extraDocs, existingDoc) => { let vacancyId: Ref | undefined + const vacancies = (extraDocs.get(recruit.class.Vacancy) ?? []) as Vacancy[] + const applications = (extraDocs.get(recruit.class.Applicant) ?? []) as Applicant[] + if (vacancyName !== undefined) { const tName = vacancyName.trim().toLowerCase() const vacancy = vacancies.find((it) => it.name.toLowerCase().trim() === tName) @@ -428,28 +434,48 @@ export async function convert ( document.modifiedBy, refOrgField ) + const vacancy = await client.findOne(recruit.class.Vacancy, { _id: vacancyId }) + if (vacancy != null) { + // We need to put new vacancy so it will be reused. + vacancies.push(vacancy) + } } } else { return } // Check if candidate already have vacancy - const existing = await client.findOne(recruit.class.Applicant, { - attachedTo: (existingDoc?._id ?? doc._id) as unknown as Ref, - space: vacancyId - }) + const existing = applications.find( + (it) => + it.attachedTo === ((existingDoc?._id ?? doc.document._id) as unknown as Ref) && + it.space === vacancyId + ) + + const candidate = doc.mixins[recruit.mixin.Candidate] as Data + const ops = new TxOperations(client, document.modifiedBy) if (statusName != null && statusName !== '') { // Find status for vacancy const states = await client.findAll(task.class.State, { space: vacancyId }) + const update: DocumentUpdate = {} + for (const k of operation.copyTalentFields ?? []) { + const val = (candidate as any)[k.candidate] + if ((existing as any)[k.applicant] !== val) { + ;(update as any)[k.applicant] = val + } + } const state = states.find((it) => it.name.toLowerCase().trim() === statusName.toLowerCase().trim()) - const ops = new TxOperations(client, document.modifiedBy) if (state !== undefined) { - if (existing !== undefined && existing.state !== state?._id) { - await ops.update(existing, { state: state._id }) + if (existing !== undefined) { + if (existing.state !== state?._id) { + update.state = state._id + } + if (Object.keys(update).length > 0) { + await ops.update(existing, update) + } } else { - await createApplication(ops, state, vacancyId, document) + await createApplication(ops, state, vacancyId, { ...document, ...update }) } } } diff --git a/plugins/client-resources/src/connection.ts b/plugins/client-resources/src/connection.ts index 2c695890ba..3b0d93bc2a 100644 --- a/plugins/client-resources/src/connection.ts +++ b/plugins/client-resources/src/connection.ts @@ -86,7 +86,7 @@ class Connection implements ClientConnection { const s = this.websocket if (!(s instanceof Promise)) { - console.log('no ping response from server. Closing socket.', s, (s as any).readyState) + console.log('no ping response from server. Closing socket.', s, (s as any)?.readyState) // Trying to close connection and re-establish it. s?.close() } else { diff --git a/plugins/client-resources/src/index.ts b/plugins/client-resources/src/index.ts index 80bc8cf563..a6fb6c3958 100644 --- a/plugins/client-resources/src/index.ts +++ b/plugins/client-resources/src/index.ts @@ -38,7 +38,7 @@ export default async () => { const url = new URL(`/${token}`, endpoint) console.log('connecting to', url.href) const upgradeHandler: TxHandler = (tx) => { - if (tx._class === core.class.TxWorkspaceEvent) { + if (tx?._class === core.class.TxWorkspaceEvent) { if ((tx as TxWorkspaceEvent).event === WorkspaceEvent.Upgrade) { onUpgrade?.() } diff --git a/plugins/contact-resources/src/components/ChannelFilter.svelte b/plugins/contact-resources/src/components/ChannelFilter.svelte index 02cd14b7e5..da28a444d9 100644 --- a/plugins/contact-resources/src/components/ChannelFilter.svelte +++ b/plugins/contact-resources/src/components/ChannelFilter.svelte @@ -14,14 +14,14 @@ -->
dispatch('changeContent')}>
- {#if objectsPromise} - - {:else} - {#each objects as element} - - {/each} - {/if} +
+ + {/each}