Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TSK-1154: Statuses table support #2974

Merged
merged 1 commit into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/ui/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,23 @@ export function handler<T, EVT = MouseEvent> (target: T, op: (value: T, evt: EVT
op(target, evt)
}
}

/**
* @public
*/
export function tableToCSV (tableId: string, separator = ','): string {
const rows = document.querySelectorAll('table#' + tableId + ' tr')
// Construct csv
const csv: string[] = []
for (let i = 0; i < rows.length; i++) {
const row: string[] = []
const cols = rows[i].querySelectorAll('td, th')
for (let j = 0; j < cols.length; j++) {
let data = (cols[j] as HTMLElement).innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/(\s\s)/gm, ' ')
data = data.replace(/"/g, '""')
row.push('"' + data + '"')
}
csv.push(row.join(separator))
}
return csv.join('\n')
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
BitrixFieldMapping,
CreateHRApplication,
Fields,
MappingOperation
MappingOperation,
getAllAttributes
} from '@hcengineering/bitrix'
import { AnyAttribute } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import task from '@hcengineering/task'
import { createQuery, getClient } from '@hcengineering/presentation'
import InlineAttributeBarEditor from '@hcengineering/presentation/src/components/InlineAttributeBarEditor.svelte'
import recruit from '@hcengineering/recruit'
import task, { DoneStateTemplate, StateTemplate } from '@hcengineering/task'
import {
Button,
DropdownIntlItem,
Expand All @@ -20,7 +23,6 @@
} from '@hcengineering/ui'
import { ObjectBox } from '@hcengineering/view-resources'
import bitrix from '../../plugin'
import recruit from '@hcengineering/recruit'

export let mapping: BitrixEntityMapping
export let fields: Fields = {}
Expand All @@ -31,6 +33,7 @@
let vacancyField = (field?.operation as CreateHRApplication)?.vacancyField
let defaultTemplate = (field?.operation as CreateHRApplication)?.defaultTemplate
let copyTalentFields = (field?.operation as CreateHRApplication)?.copyTalentFields ?? []
let stateMapping = (field?.operation as CreateHRApplication)?.stateMapping ?? []

const client = getClient()

Expand All @@ -42,7 +45,8 @@
stateField,
vacancyField,
defaultTemplate,
copyTalentFields
copyTalentFields,
stateMapping
}
})
} else {
Expand All @@ -54,7 +58,8 @@
stateField,
vacancyField,
defaultTemplate,
copyTalentFields
copyTalentFields,
stateMapping
}
})
}
Expand All @@ -68,49 +73,156 @@
}
$: items = getItems(fields)

$: allAttrs = Array.from(client.getHierarchy().getAllAttributes(recruit.mixin.Candidate).values())
$: allAttrs = Array.from(getAllAttributes(client, 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))

$: sourceStates = Array.from(mapping.bitrixFields[stateField].items?.values() ?? []).map(
(it) => ({ id: it.VALUE, label: it.VALUE } as DropdownTextItem)
)

const statusQuery = createQuery()
const doneQuery = createQuery()

let stateTemplates: StateTemplate[] = []
let doneStateTemplates: DoneStateTemplate[] = []

$: statusQuery.query(task.class.StateTemplate, { attachedTo: defaultTemplate }, (res) => {
stateTemplates = res
})

$: doneQuery.query(task.class.DoneStateTemplate, { attachedTo: defaultTemplate }, (res) => {
doneStateTemplates = res
})

$: stateTitles = [{ id: '', label: 'None' }, ...stateTemplates.map((it) => ({ id: it.name, label: it.name }))]
$: doneStateTitles = [{ id: '', label: 'None' }, ...doneStateTemplates.map((it) => ({ id: it.name, label: it.name }))]
</script>

<div class="flex-col flex-wrap">
<div class="flex-row-center gap-2">
<div class="flex-col w-120">
<DropdownLabels minW0={false} label={getEmbeddedLabel('Vacancy field')} {items} bind:selected={vacancyField} />
<DropdownLabels minW0={false} label={getEmbeddedLabel('State field')} {items} bind:selected={stateField} />
<ObjectBox
label={getEmbeddedLabel('Template')}
searchField={'title'}
_class={task.class.KanbanTemplate}
docQuery={{ space: recruit.space.VacancyTemplates }}
bind:value={defaultTemplate}
/>

{#each copyTalentFields as f, i}
<div class="flex-row-center pattern">
<DropdownLabelsIntl
minW0={false}
label={getEmbeddedLabel('Copy field')}
items={attrs}
bind:selected={f.candidate}
/> =>
<DropdownLabelsIntl
minW0={false}
label={getEmbeddedLabel('Copy field')}
items={applicantAttrs}
bind:selected={f.applicant}
/>
</div>
{/each}
<Button
icon={IconAdd}
size={'small'}
on:click={() => {
copyTalentFields = [...copyTalentFields, { candidate: allAttrs[0]._id, applicant: applicantAllAttrs[0]._id }]
}}
/>
<div class="flex-row-center p-1">
<span class="w-22"> Vacancy: </span>
<DropdownLabels
width={'10rem'}
label={getEmbeddedLabel('Vacancy field')}
{items}
bind:selected={vacancyField}
/>
</div>
<div class="flex-row-center p-1">
<span class="w-22"> State: </span>
<DropdownLabels width={'10rem'} label={getEmbeddedLabel('State field')} {items} bind:selected={stateField} />
</div>
<div class="flex-row-center p-1">
<span class="w-22"> Template: </span>
<ObjectBox
width={'10rem'}
label={getEmbeddedLabel('Template')}
searchField={'title'}
_class={task.class.KanbanTemplate}
docQuery={{ space: recruit.space.VacancyTemplates }}
bind:value={defaultTemplate}
/>
</div>

<div class="mt-2 mb-1 flex-row-center p-1">
<span class="mr-2"> Copy following fields: </span>
<Button
icon={IconAdd}
size={'small'}
on:click={() => {
copyTalentFields = [
...copyTalentFields,
{ candidate: allAttrs[0]._id, applicant: applicantAllAttrs[0]._id }
]
}}
/>
</div>
<div class="flex-col flex-wrap">
{#each copyTalentFields as f, i}
<div class="flex-row-center pattern">
<DropdownLabelsIntl
width={'10rem'}
label={getEmbeddedLabel('Copy field')}
items={attrs}
bind:selected={f.candidate}
/> =>
<DropdownLabelsIntl
width={'10rem'}
label={getEmbeddedLabel('Copy field')}
items={applicantAttrs}
bind:selected={f.applicant}
/>
</div>
{/each}
</div>
<div class="mt-2 mb-1 flex-row-center p-1">
<span class="mr-2"> State mapping: </span>
<Button
icon={IconAdd}
size={'small'}
on:click={() => {
stateMapping = [...stateMapping, { sourceName: '', targetName: '', updateCandidate: [], doneState: '' }]
}}
/>
</div>
<div class="flex-co">
{#each stateMapping as m}
<div class="flex-row-center pattern flex-between flex-wrap">
<DropdownLabels
width={'10rem'}
label={getEmbeddedLabel('Source state')}
items={sourceStates}
kind={m.sourceName !== '' ? 'primary' : 'secondary'}
bind:selected={m.sourceName}
/> =>
<DropdownLabels
width={'10rem'}
kind={m.targetName !== '' ? 'primary' : 'secondary'}
label={getEmbeddedLabel('Final state')}
items={stateTitles}
bind:selected={m.targetName}
/>
<span class="ml-4"> Done state: </span>
<DropdownLabels
width={'10rem'}
kind={m.doneState !== '' ? 'primary' : 'secondary'}
label={getEmbeddedLabel('Done state')}
items={doneStateTitles}
bind:selected={m.doneState}
/>
{#each m.updateCandidate as c}
{@const attribute = allAttrs.find((it) => it.name === c.attr)}
<DropdownLabelsIntl
width={'10rem'}
label={getEmbeddedLabel('Field to fill')}
items={attrs}
bind:selected={c.attr}
/>
{#if attribute}
=>
<InlineAttributeBarEditor
_class={recruit.mixin.Candidate}
key={{ key: 'value', attr: attribute }}
draft
object={c}
/>
{/if}
{/each}
<Button
icon={IconAdd}
size={'small'}
on:click={() => {
m.updateCandidate = [...m.updateCandidate, { attr: allAttrs[0]._id, value: undefined }]
}}
/>
</div>
{/each}
</div>
</div>
</div>
</div>
Expand All @@ -132,4 +244,7 @@
color: var(--caption-color);
}
}
.scroll {
overflow: auto;
}
</style>
12 changes: 5 additions & 7 deletions plugins/bitrix/src/hr.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Organization } from '@hcengineering/contact'
import core, { Account, Client, Doc, Ref, SortingOrder, TxOperations } from '@hcengineering/core'
import recruit, { Vacancy } from '@hcengineering/recruit'
import core, { Account, Client, Data, Doc, Ref, SortingOrder, TxOperations } from '@hcengineering/core'
import recruit, { Applicant, Vacancy } from '@hcengineering/recruit'
import task, { KanbanTemplate, State, calcRank, createKanban } from '@hcengineering/task'

export async function createVacancy (
Expand Down Expand Up @@ -42,7 +42,8 @@ export async function createApplication (
client: TxOperations,
selectedState: State,
_space: Ref<Vacancy>,
doc: Doc
doc: Doc,
data: Data<Applicant>
): Promise<void> {
if (selectedState === undefined) {
throw new Error(`Please select initial state:${_space}`)
Expand All @@ -60,13 +61,10 @@ export async function createApplication (
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)

await client.addCollection(recruit.class.Applicant, _space, doc._id, recruit.mixin.Candidate, 'applications', {
...data,
state: state._id,
doneState: null,
number: (incResult as any).object.sequence,
assignee: null,
rank: calcRank(lastOne, undefined),
startDate: null,
dueDate: null,
createOn: Date.now()
})
}
18 changes: 12 additions & 6 deletions plugins/bitrix/src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import core, {
WithLookup
} from '@hcengineering/core'
import gmail, { Message } from '@hcengineering/gmail'
import recruit from '@hcengineering/recruit'
import tags, { TagElement } from '@hcengineering/tags'
import { deepEqual } from 'fast-equals'
import { BitrixClient } from './client'
Expand All @@ -40,7 +41,6 @@ import {
LoginInfo
} from './types'
import { convert, ConvertResult } from './utils'
import recruit from '@hcengineering/recruit'

async function updateDoc (client: ApplyOperations, doc: Doc, raw: Doc | Data<Doc>, date: Timestamp): Promise<Doc> {
// We need to update fields if they are different.
Expand Down Expand Up @@ -109,6 +109,17 @@ export async function syncDocument (

try {
const applyOp = client.apply('bitrix')

if (existing !== undefined) {
// We need update document id.
resultDoc.document._id = existing._id as Ref<BitrixSyncDoc>
}

// Operations could add more change instructions
for (const op of resultDoc.postOperations) {
await op(resultDoc, extraDocs, existing)
}

// const newDoc = existing === undefined
existing = await updateMainDoc(applyOp)

Expand All @@ -130,9 +141,6 @@ export async function syncDocument (
await applyOp.createDoc(_class, space, data, _id, resultDoc.document.modifiedOn, resultDoc.document.modifiedBy)
}

for (const op of resultDoc.postOperations) {
await op(resultDoc, extraDocs, existing)
}
const idMapping = new Map<Ref<Doc>, Ref<Doc>>()

// Find all attachment documents to existing.
Expand Down Expand Up @@ -322,8 +330,6 @@ export async function syncDocument (

async function updateMainDoc (applyOp: ApplyOperations): Promise<BitrixSyncDoc> {
if (existing !== undefined) {
// We need update doucment id.
resultDoc.document._id = existing._id as Ref<BitrixSyncDoc>
// We need to update fields if they are different.
return (await updateDoc(applyOp, existing, resultDoc.document, resultDoc.document.modifiedOn)) as BitrixSyncDoc
// Go over extra documents.
Expand Down
16 changes: 16 additions & 0 deletions plugins/bitrix/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,19 @@ export interface CreateAttachedField {
referenceField?: string
}

/**
* @public
*/
export interface BitrixStateMapping {
sourceName: string
targetName: string // if empty will not create application

doneState: string // Alternative is to set doneState to value

// Allow to put some values, in case of some statues
updateCandidate: { attr: string, value: any }[]
}

/**
* @public
*/
Expand All @@ -283,6 +296,9 @@ export interface CreateHRApplication {
defaultTemplate: Ref<KanbanTemplate>

copyTalentFields?: { candidate: Ref<AnyAttribute>, applicant: Ref<AnyAttribute> }[]

// We would like to map some of bitrix states to our states, name matching is used to hold values.
stateMapping?: BitrixStateMapping[]
}

/**
Expand Down
Loading