mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-14 12:25:17 +00:00
TSK-736, TSK-759, TSK-733 (#2691)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
3998ad4d55
commit
496e0cff38
@ -117,6 +117,7 @@
|
||||
"libphonenumber-js": "^1.9.46",
|
||||
"@hcengineering/setting": "^0.6.2",
|
||||
"@hcengineering/minio": "^0.6.0",
|
||||
"@hcengineering/openai": "^0.6.0"
|
||||
"@hcengineering/openai": "^0.6.0",
|
||||
"@hcengineering/tracker": "^0.6.1"
|
||||
}
|
||||
}
|
||||
|
127
dev/tool/src/clean.ts
Normal file
127
dev/tool/src/clean.ts
Normal file
@ -0,0 +1,127 @@
|
||||
//
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// 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 attachment from '@hcengineering/attachment'
|
||||
import contact from '@hcengineering/contact'
|
||||
import core, { BackupClient, Client as CoreClient, DOMAIN_TX, TxOperations, WorkspaceId } from '@hcengineering/core'
|
||||
import { MinioService } from '@hcengineering/minio'
|
||||
import { getWorkspaceDB } from '@hcengineering/mongo'
|
||||
import recruit from '@hcengineering/recruit'
|
||||
import { connect } from '@hcengineering/server-tool'
|
||||
import tracker from '@hcengineering/tracker'
|
||||
import { MongoClient } from 'mongodb'
|
||||
|
||||
export async function cleanWorkspace (
|
||||
mongoUrl: string,
|
||||
workspaceId: WorkspaceId,
|
||||
minio: MinioService,
|
||||
elasticUrl: string,
|
||||
transactorUrl: string,
|
||||
opt: { recruit: boolean, tracker: boolean, removeTx: boolean }
|
||||
): Promise<void> {
|
||||
const connection = (await connect(transactorUrl, workspaceId, undefined, {
|
||||
mode: 'backup',
|
||||
model: 'upgrade'
|
||||
})) as unknown as CoreClient & BackupClient
|
||||
try {
|
||||
const ops = new TxOperations(connection, core.account.System)
|
||||
|
||||
const hierarchy = ops.getHierarchy()
|
||||
|
||||
const attachments = await ops.findAll(attachment.class.Attachment, {})
|
||||
|
||||
const contacts = await ops.findAll(contact.class.Contact, {})
|
||||
|
||||
const files = new Set(
|
||||
attachments.map((it) => it.file).concat(contacts.map((it) => it.avatar).filter((it) => it) as string[])
|
||||
)
|
||||
|
||||
const minioList = await minio.list(workspaceId)
|
||||
const toClean: string[] = []
|
||||
for (const mv of minioList) {
|
||||
if (!files.has(mv.name)) {
|
||||
toClean.push(mv.name)
|
||||
}
|
||||
}
|
||||
await minio.remove(workspaceId, toClean)
|
||||
// connection.loadChunk(DOMAIN_BLOB, idx = )
|
||||
|
||||
if (opt.recruit) {
|
||||
const contacts = await ops.findAll(recruit.mixin.Candidate, {})
|
||||
console.log('removing Talents', contacts.length)
|
||||
const filter = contacts.filter((it) => !hierarchy.isDerived(it._class, contact.class.Employee))
|
||||
|
||||
while (filter.length > 0) {
|
||||
const part = filter.splice(0, 100)
|
||||
const op = ops.apply('')
|
||||
for (const c of part) {
|
||||
await op.remove(c)
|
||||
}
|
||||
const t = Date.now()
|
||||
console.log('remove:', part.map((it) => it.name).join(', '))
|
||||
await op.commit()
|
||||
const t2 = Date.now()
|
||||
console.log('remove time:', t2 - t, filter.length)
|
||||
}
|
||||
|
||||
// const vacancies = await ops.findAll(recruit.class.Vacancy, {})
|
||||
// console.log('removing vacancies', vacancies.length)
|
||||
// for (const c of vacancies) {
|
||||
// console.log('Remove', c.name)
|
||||
// await ops.remove(c)
|
||||
// }
|
||||
}
|
||||
|
||||
if (opt.tracker) {
|
||||
const issues = await ops.findAll(tracker.class.Issue, {})
|
||||
console.log('removing Issues', issues.length)
|
||||
|
||||
while (issues.length > 0) {
|
||||
const part = issues.splice(0, 5)
|
||||
const op = ops.apply('')
|
||||
for (const c of part) {
|
||||
await op.remove(c)
|
||||
}
|
||||
const t = Date.now()
|
||||
await op.commit()
|
||||
const t2 = Date.now()
|
||||
console.log('remove time:', t2 - t, issues.length)
|
||||
}
|
||||
}
|
||||
|
||||
const client = new MongoClient(mongoUrl)
|
||||
try {
|
||||
await client.connect()
|
||||
const db = getWorkspaceDB(client, workspaceId)
|
||||
|
||||
if (opt.removeTx) {
|
||||
const txes = await db.collection(DOMAIN_TX).find({}).toArray()
|
||||
|
||||
for (const tx of txes) {
|
||||
if (tx._class === core.class.TxRemoveDoc) {
|
||||
// We need to remove all update and create operations for document
|
||||
await db.collection(DOMAIN_TX).deleteMany({ objectId: tx.objectId })
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await client.close()
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.trace(err)
|
||||
} finally {
|
||||
await connection.close()
|
||||
}
|
||||
}
|
@ -51,6 +51,7 @@ import { MigrateOperation } from '@hcengineering/model'
|
||||
import { openAIConfigDefaults } from '@hcengineering/openai'
|
||||
import { rebuildElastic } from './elastic'
|
||||
import { openAIConfig } from './openai'
|
||||
import { cleanWorkspace } from './clean'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -428,5 +429,25 @@ export function devTool (
|
||||
console.log(decodeToken(token))
|
||||
})
|
||||
|
||||
program
|
||||
.command('clean-workspace <workspace>')
|
||||
.description('set user role')
|
||||
.option('--recruit', 'Clean recruit', false)
|
||||
.option('--tracker', 'Clean tracker', false)
|
||||
.option('--removedTx', 'Clean removed transactions', false)
|
||||
.action(async (workspace: string, cmd: { recruit: boolean, tracker: boolean, removeTx: boolean }) => {
|
||||
const { mongodbUri, minio } = prepareTools()
|
||||
return await withDatabase(mongodbUri, async (db) => {
|
||||
await cleanWorkspace(
|
||||
mongodbUri,
|
||||
getWorkspaceId(workspace, productId),
|
||||
minio,
|
||||
getElasticUrl(),
|
||||
transactorUrl,
|
||||
cmd
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
program.parse(process.argv)
|
||||
}
|
||||
|
@ -226,6 +226,7 @@ export function createModel (builder: Builder): void {
|
||||
const candidatesId = 'candidates'
|
||||
const archiveId = 'archive'
|
||||
const assignedId = 'assigned'
|
||||
const organizationsId = 'organizations'
|
||||
|
||||
builder.createDoc(
|
||||
workbench.class.Application,
|
||||
@ -246,6 +247,13 @@ export function createModel (builder: Builder): void {
|
||||
createItemLabel: recruit.string.VacancyCreateLabel,
|
||||
position: 'vacancy'
|
||||
},
|
||||
{
|
||||
id: organizationsId,
|
||||
component: recruit.component.Organizations,
|
||||
icon: contact.icon.Company,
|
||||
label: recruit.string.Organizations,
|
||||
position: 'vacancy'
|
||||
},
|
||||
{
|
||||
id: candidatesId,
|
||||
component: workbench.component.SpecialView,
|
||||
@ -403,6 +411,32 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
recruit.viewlet.TableVacancy
|
||||
)
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: recruit.mixin.VacancyList,
|
||||
descriptor: view.viewlet.Table,
|
||||
config: [
|
||||
'',
|
||||
{
|
||||
key: '@vacancies',
|
||||
label: recruit.string.Vacancies
|
||||
},
|
||||
{
|
||||
key: '@applications',
|
||||
label: recruit.string.Applications
|
||||
},
|
||||
'$lookup.channels',
|
||||
{
|
||||
key: '@applications.modifiedOn',
|
||||
label: core.string.Modified
|
||||
}
|
||||
],
|
||||
hiddenKeys: ['name', 'space', 'modifiedOn']
|
||||
},
|
||||
recruit.viewlet.TableVacancyList
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
|
@ -77,6 +77,7 @@ export default mergeIds(recruitId, recruit, {
|
||||
Applications: '' as AnyComponent,
|
||||
SkillsView: '' as AnyComponent,
|
||||
Vacancies: '' as AnyComponent,
|
||||
Organizations: '' as AnyComponent,
|
||||
|
||||
CreateReview: '' as AnyComponent,
|
||||
Reviews: '' as AnyComponent,
|
||||
@ -104,6 +105,7 @@ export default mergeIds(recruitId, recruit, {
|
||||
TableApplicant: '' as Ref<Viewlet>,
|
||||
TableApplicantMatch: '' as Ref<Viewlet>,
|
||||
CalendarReview: '' as Ref<Viewlet>,
|
||||
TableReview: '' as Ref<Viewlet>
|
||||
TableReview: '' as Ref<Viewlet>,
|
||||
TableVacancyList: '' as Ref<Viewlet>
|
||||
}
|
||||
})
|
||||
|
@ -30,7 +30,7 @@
|
||||
fname.length > maxLenght ? fname.substr(0, (maxLenght - 1) / 2) + '...' + fname.substr(-(maxLenght - 1) / 2) : fname
|
||||
|
||||
function iconLabel (name: string): string {
|
||||
const parts = name.split('.')
|
||||
const parts = `${name}`.split('.')
|
||||
const ext = parts[parts.length - 1]
|
||||
return ext.substring(0, 4).toUpperCase()
|
||||
}
|
||||
|
@ -23,7 +23,8 @@
|
||||
core.class.TypeNumber,
|
||||
core.class.EnumOf,
|
||||
core.class.Collection,
|
||||
core.class.ArrOf
|
||||
core.class.ArrOf,
|
||||
core.class.RefTo
|
||||
])
|
||||
|
||||
function addMapping (evt: MouseEvent, kind: MappingOperation): void {
|
||||
@ -63,6 +64,12 @@
|
||||
action: (_: any, evt: MouseEvent) => {
|
||||
addMapping(evt, MappingOperation.DownloadAttachment)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: getEmbeddedLabel('Add Field reference mapping'),
|
||||
action: (_: any, evt: MouseEvent) => {
|
||||
addMapping(evt, MappingOperation.FindReference)
|
||||
}
|
||||
}
|
||||
] as Action[]
|
||||
</script>
|
||||
|
@ -8,6 +8,7 @@
|
||||
import CreateChannelMapping from './mappings/CreateChannelMapping.svelte'
|
||||
import CreateTagMapping from './mappings/CreateTagMapping.svelte'
|
||||
import DownloadAttachmentMapping from './mappings/DownloadAttachmentMapping.svelte'
|
||||
import FindReferenceMapping from './mappings/FindReferenceMapping.svelte'
|
||||
|
||||
export let mapping: BitrixEntityMapping
|
||||
export let fields: Fields = {}
|
||||
@ -42,5 +43,7 @@
|
||||
<CreateChannelMapping {mapping} {fields} {attribute} {field} bind:this={op} />
|
||||
{:else if _kind === MappingOperation.DownloadAttachment}
|
||||
<DownloadAttachmentMapping {mapping} {fields} {attribute} {field} bind:this={op} />
|
||||
{:else if _kind === MappingOperation.FindReference}
|
||||
<FindReferenceMapping {mapping} {fields} {attribute} {field} bind:this={op} />
|
||||
{/if}
|
||||
</Card>
|
||||
|
@ -7,6 +7,7 @@
|
||||
import CreateChannelMappingPresenter from './mappings/CreateChannelMappingPresenter.svelte'
|
||||
import CreateTagMappingPresenter from './mappings/CreateTagMappingPresenter.svelte'
|
||||
import DownloadAttachmentPresenter from './mappings/DownloadAttachmentPresenter.svelte'
|
||||
import FindReferencePresenter from './mappings/FindReferencePresenter.svelte'
|
||||
|
||||
export let mapping: BitrixEntityMapping
|
||||
export let value: BitrixFieldMapping
|
||||
@ -37,6 +38,8 @@
|
||||
<CreateChannelMappingPresenter {mapping} {value} />
|
||||
{:else if kind === MappingOperation.DownloadAttachment}
|
||||
<DownloadAttachmentPresenter {mapping} {value} />
|
||||
{:else if kind === MappingOperation.FindReference}
|
||||
<FindReferencePresenter {mapping} {value} />
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
|
@ -58,7 +58,7 @@
|
||||
})
|
||||
.map((it) => ({
|
||||
id: it[0],
|
||||
label: `${it[1].formLabel ?? it[1].title}${it[0].startsWith('UF_') ? ' *' : ''} - ${it[0]}`
|
||||
label: `${it[1].formLabel ?? it[1].title}${it[0].startsWith('UF_') ? ' *' : ''} - ${it[0]} - ${it[1].type}`
|
||||
}))
|
||||
</script>
|
||||
|
||||
|
@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
BitrixEntityMapping,
|
||||
BitrixEntityType,
|
||||
BitrixFieldMapping,
|
||||
Fields,
|
||||
FindReferenceOperation,
|
||||
MappingOperation,
|
||||
mappingTypes
|
||||
} from '@hcengineering/bitrix'
|
||||
import core, { AnyAttribute } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { DropdownTextItem } from '@hcengineering/ui'
|
||||
import DropdownLabels from '@hcengineering/ui/src/components/DropdownLabels.svelte'
|
||||
import bitrix from '../../plugin'
|
||||
|
||||
export let mapping: BitrixEntityMapping
|
||||
export let fields: Fields = {}
|
||||
export let attribute: AnyAttribute
|
||||
export let field: BitrixFieldMapping | undefined
|
||||
|
||||
let findField: string = (field?.operation as FindReferenceOperation)?.field ?? ''
|
||||
let referenceType = (field?.operation as FindReferenceOperation)?.referenceType ?? BitrixEntityType.Company
|
||||
let referenceClass = (field?.operation as FindReferenceOperation)?.referenceClass ?? core.class.Doc
|
||||
|
||||
const client = getClient()
|
||||
|
||||
export async function save (): Promise<void> {
|
||||
if (field !== undefined) {
|
||||
await client.update(field, {
|
||||
operation: {
|
||||
kind: MappingOperation.FindReference,
|
||||
field: findField,
|
||||
referenceType,
|
||||
referenceClass
|
||||
}
|
||||
})
|
||||
} else {
|
||||
await client.addCollection(bitrix.class.FieldMapping, mapping.space, mapping._id, mapping._class, 'fields', {
|
||||
ofClass: attribute.attributeOf,
|
||||
attributeName: attribute.name,
|
||||
operation: {
|
||||
kind: MappingOperation.FindReference,
|
||||
field: findField,
|
||||
referenceType,
|
||||
referenceClass
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getItems (fields: Fields): DropdownTextItem[] {
|
||||
return Object.entries(fields)
|
||||
.filter((it) => it[1].type === 'crm_company' || it[1].type === 'user' || it[1].type === 'crm')
|
||||
.map((it) => ({
|
||||
id: it[0],
|
||||
label: `${it[1].formLabel ?? it[1].title}${it[0].startsWith('UF_') ? ' *' : ''} - ${it[0]} - ${it[1].type}`
|
||||
}))
|
||||
}
|
||||
$: items = getItems(fields)
|
||||
|
||||
let classes: DropdownTextItem[] = []
|
||||
|
||||
client.findAll(core.class.Class, {}).then((res) => {
|
||||
classes = res.map((it) => ({ id: it._id, label: it._id }))
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex-col flex-wrap">
|
||||
<div class="pattern flex-row-center gap-2">
|
||||
<DropdownLabels minW0={false} label={bitrix.string.FieldMapping} {items} bind:selected={findField} />
|
||||
|
||||
<DropdownLabels
|
||||
minW0={false}
|
||||
label={bitrix.string.FieldMapping}
|
||||
items={[...mappingTypes, { id: '#', label: 'None' }]}
|
||||
bind:selected={referenceType}
|
||||
/>
|
||||
|
||||
<DropdownLabels minW0={false} label={bitrix.string.FieldMapping} items={classes} bind:selected={referenceClass} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.pattern {
|
||||
margin: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
border: 1px dashed var(--accent-color);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
|
||||
// text-transform: uppercase;
|
||||
color: var(--accent-color);
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { BitrixEntityMapping, BitrixFieldMapping, FindReferenceOperation } from '@hcengineering/bitrix'
|
||||
|
||||
export let mapping: BitrixEntityMapping
|
||||
export let value: BitrixFieldMapping
|
||||
|
||||
$: op = value.operation as FindReferenceOperation
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
<div class="pattern flex-row-center gap-2">
|
||||
{#if mapping.bitrixFields}
|
||||
{op.field ? mapping.bitrixFields[op.field]?.formLabel ?? mapping.bitrixFields[op.field]?.title : op.field ?? ''}
|
||||
=> {op.referenceType} === {op.referenceClass}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.pattern {
|
||||
margin: 0.1rem;
|
||||
padding: 0.3rem;
|
||||
flex-shrink: 0;
|
||||
border: 1px dashed var(--accent-color);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
|
||||
// text-transform: uppercase;
|
||||
color: var(--accent-color);
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -35,7 +35,8 @@
|
||||
"@hcengineering/chunter": "^0.6.2",
|
||||
"@hcengineering/attachment": "^0.6.1",
|
||||
"fast-equals": "^2.0.3",
|
||||
"qs": "~6.11.0"
|
||||
"qs": "~6.11.0",
|
||||
"@hcengineering/gmail": "^0.6.3"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/anticrm",
|
||||
"publishConfig": {
|
||||
|
@ -532,6 +532,26 @@ async function doPerformSync (ops: SyncOptions & SyncOptionsExtra): Promise<Bitr
|
||||
|
||||
added++
|
||||
const total = result.total
|
||||
|
||||
if (res.syncRequests.length > 0) {
|
||||
for (const r of res.syncRequests) {
|
||||
const m = ops.allMappings.find((it) => it.type === r.type)
|
||||
if (m !== undefined) {
|
||||
const [d] = await doPerformSync({
|
||||
...ops,
|
||||
mapping: m,
|
||||
extraFilter: { ID: r.bitrixId },
|
||||
monitor: (total) => {
|
||||
console.log('total', total)
|
||||
}
|
||||
})
|
||||
if (d !== undefined) {
|
||||
r.update(d._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await syncDocument(ops.client, existingDoc, res, ops.loginInfo, ops.frontUrl, () => {
|
||||
ops.monitor?.(total)
|
||||
})
|
||||
@ -660,7 +680,7 @@ async function downloadComments (
|
||||
_id: generateId(),
|
||||
_class: chunter.class.Comment,
|
||||
message: processComment(it.COMMENT as string),
|
||||
bitrixId: it.ID,
|
||||
bitrixId: `${it.ID as string}`,
|
||||
type: it.ENTITY_TYPE,
|
||||
attachedTo: res.document._id,
|
||||
attachedToClass: res.document._class,
|
||||
@ -738,7 +758,7 @@ async function downloadComments (
|
||||
_id: generateId(),
|
||||
_class: chunter.class.Comment,
|
||||
message,
|
||||
bitrixId: comm.ID,
|
||||
bitrixId: `${comm.ID}`,
|
||||
type: 'email',
|
||||
attachedTo: res.document._id,
|
||||
attachedToClass: res.document._class,
|
||||
@ -770,12 +790,16 @@ async function synchronizeUsers (
|
||||
): Promise<void> {
|
||||
let totalUsers = 1
|
||||
let next = 0
|
||||
|
||||
const employees = new Map((await ops.client.findAll(contact.class.Employee, {})).map((it) => [it._id, it]))
|
||||
|
||||
while (userList.size < totalUsers) {
|
||||
const users = await ops.bitrixClient.call('user.search', { start: next })
|
||||
next = users.next
|
||||
totalUsers = users.total
|
||||
for (const u of users.result) {
|
||||
let accountId = allEmployee.find((it) => it.email === u.EMAIL)?._id
|
||||
const account = allEmployee.find((it) => it.email === u.EMAIL)
|
||||
let accountId = account?._id
|
||||
if (accountId === undefined) {
|
||||
const employeeId = await ops.client.createDoc(contact.class.Employee, contact.space.Contacts, {
|
||||
name: combineName(u.NAME, u.LAST_NAME),
|
||||
@ -790,6 +814,26 @@ async function synchronizeUsers (
|
||||
employee: employeeId,
|
||||
role: AccountRole.User
|
||||
})
|
||||
await ops.client.createMixin<Doc, BitrixSyncDoc>(
|
||||
employeeId,
|
||||
contact.class.Employee,
|
||||
contact.space.Contacts,
|
||||
bitrix.mixin.BitrixSyncDoc,
|
||||
{
|
||||
type: 'employee',
|
||||
bitrixId: `${u.ID as string}`,
|
||||
syncTime: Date.now()
|
||||
}
|
||||
)
|
||||
} else if (account != null) {
|
||||
const emp = employees.get(account.employee)
|
||||
if (emp !== undefined && !ops.client.getHierarchy().hasMixin(emp, bitrix.mixin.BitrixSyncDoc)) {
|
||||
await ops.client.createMixin<Doc, BitrixSyncDoc>(emp._id, emp._class, emp.space, bitrix.mixin.BitrixSyncDoc, {
|
||||
type: 'employee',
|
||||
bitrixId: `${u.ID as string}`,
|
||||
syncTime: Date.now()
|
||||
})
|
||||
}
|
||||
}
|
||||
userList.set(u.ID, accountId)
|
||||
}
|
||||
|
@ -172,7 +172,8 @@ export enum MappingOperation {
|
||||
CopyValue,
|
||||
CreateTag, // Create tag
|
||||
CreateChannel, // Create channel
|
||||
DownloadAttachment
|
||||
DownloadAttachment,
|
||||
FindReference
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
@ -235,6 +236,21 @@ export interface DownloadAttachmentOperation {
|
||||
fields: { field: string }[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface FindReferenceOperation {
|
||||
kind: MappingOperation.FindReference
|
||||
|
||||
field: string
|
||||
|
||||
// If missing will trigger sync for this kind with extraFilter ID={referenceID}
|
||||
// If defined will be used to synchronize
|
||||
referenceType?: BitrixEntityType | null
|
||||
|
||||
referenceClass: Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -242,7 +258,12 @@ export interface BitrixFieldMapping extends AttachedDoc {
|
||||
ofClass: Ref<Class<Doc>> // Specify mixin if applicable
|
||||
attributeName: string
|
||||
|
||||
operation: CopyValueOperation | CreateTagOperation | CreateChannelOperation | DownloadAttachmentOperation
|
||||
operation:
|
||||
| CopyValueOperation
|
||||
| CreateTagOperation
|
||||
| CreateChannelOperation
|
||||
| DownloadAttachmentOperation
|
||||
| FindReferenceOperation
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,14 +14,16 @@ import core, {
|
||||
WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import tags, { TagCategory, TagElement, TagReference } from '@hcengineering/tags'
|
||||
import {
|
||||
import bitrix, {
|
||||
BitrixEntityMapping,
|
||||
BitrixEntityType,
|
||||
BitrixFieldMapping,
|
||||
BitrixSyncDoc,
|
||||
CopyValueOperation,
|
||||
CreateChannelOperation,
|
||||
CreateTagOperation,
|
||||
DownloadAttachmentOperation,
|
||||
FindReferenceOperation,
|
||||
MappingOperation
|
||||
} from '.'
|
||||
|
||||
@ -48,16 +50,26 @@ export function collectFields (fieldMapping: BitrixFieldMapping[]): string[] {
|
||||
return fields
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface BitrixSyncRequest {
|
||||
type: BitrixEntityType
|
||||
bitrixId: string
|
||||
update: (doc: Ref<Doc>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ConvertResult {
|
||||
document: BitrixSyncDoc // Document we should achive
|
||||
document: BitrixSyncDoc // Document we should sync
|
||||
rawData: any
|
||||
mixins: Record<Ref<Mixin<Doc>>, Data<Doc>> // Mixins of document we will achive
|
||||
extraDocs: Doc[] // Extra documents we will achive, etc.
|
||||
extraSync: (AttachedDoc & BitrixSyncDoc)[] // Extra documents we will achive, etc.
|
||||
mixins: Record<Ref<Mixin<Doc>>, Data<Doc>> // Mixins of document we will sync
|
||||
extraDocs: Doc[] // Extra documents we will sync, etc.
|
||||
extraSync: (AttachedDoc & BitrixSyncDoc)[] // Extra documents we will sync, etc.
|
||||
blobs: [Attachment & BitrixSyncDoc, () => Promise<File | undefined>, (file: File, attach: Attachment) => void][]
|
||||
syncRequests: BitrixSyncRequest[]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,6 +112,8 @@ export async function convert (
|
||||
][] = []
|
||||
const mixins: Record<Ref<Mixin<Doc>>, Data<Doc>> = {}
|
||||
|
||||
const syncRequests: BitrixSyncRequest[] = []
|
||||
|
||||
const extractValue = (field?: string, alternatives?: string[]): any | undefined => {
|
||||
if (field !== undefined) {
|
||||
let lval = rawDocument[field]
|
||||
@ -125,9 +139,9 @@ export async function convert (
|
||||
}
|
||||
} else if (bfield.type === 'file') {
|
||||
if (Array.isArray(lval) && bfield.isMultiple) {
|
||||
return lval.map((it) => ({ id: it.id, file: it.downloadUrl }))
|
||||
return lval.map((it) => ({ id: `${it.id as string}`, file: it.downloadUrl }))
|
||||
} else if (lval != null) {
|
||||
return [{ id: lval.id, file: lval.downloadUrl }]
|
||||
return [{ id: `${lval.id as string}`, file: lval.downloadUrl }]
|
||||
}
|
||||
} else if (bfield.type === 'string' || bfield.type === 'url' || bfield.type === 'crm_company') {
|
||||
if (bfield.isMultiple && Array.isArray(lval)) {
|
||||
@ -164,6 +178,8 @@ export async function convert (
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return lval
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -311,10 +327,41 @@ export async function convert (
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getFindValue = async (
|
||||
attr: AnyAttribute,
|
||||
operation: FindReferenceOperation
|
||||
): Promise<{ ref?: Ref<Doc>, bitrixId: string } | undefined> => {
|
||||
const lval = extractValue(operation.field)
|
||||
if (lval != null) {
|
||||
const bid = Array.isArray(lval) ? lval[0] : lval
|
||||
const doc = await client.findOne(operation.referenceClass, {
|
||||
[bitrix.mixin.BitrixSyncDoc + '.bitrixId']: bid
|
||||
})
|
||||
if (doc !== undefined) {
|
||||
return { ref: doc._id, bitrixId: bid }
|
||||
} else {
|
||||
return { bitrixId: bid }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setValue = (value: any, attr: AnyAttribute): void => {
|
||||
if (value !== undefined) {
|
||||
if (hierarchy.isMixin(attr.attributeOf)) {
|
||||
mixins[attr.attributeOf] = {
|
||||
...mixins[attr.attributeOf],
|
||||
[attr.name]: value
|
||||
}
|
||||
} else {
|
||||
;(document as any)[attr.name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const f of fields) {
|
||||
const attr = hierarchy.getAttribute(f.ofClass, f.attributeName)
|
||||
if (attr === undefined) {
|
||||
console.trace('Attribue not found', f)
|
||||
console.trace('Attribute not found', f)
|
||||
continue
|
||||
}
|
||||
let value: any
|
||||
@ -333,7 +380,7 @@ export async function convert (
|
||||
for (const blobRef of blobRefs) {
|
||||
const attachDoc: Attachment & BitrixSyncDoc = {
|
||||
_id: generateId(),
|
||||
bitrixId: blobRef.id,
|
||||
bitrixId: `${blobRef.id}`,
|
||||
file: '', // Empty since not uploaded yet.
|
||||
name: blobRef.id,
|
||||
size: -1,
|
||||
@ -379,20 +426,34 @@ export async function convert (
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
if (value !== undefined) {
|
||||
if (hierarchy.isMixin(attr.attributeOf)) {
|
||||
mixins[attr.attributeOf] = {
|
||||
...mixins[attr.attributeOf],
|
||||
[attr.name]: value
|
||||
case MappingOperation.FindReference: {
|
||||
const ret = await getFindValue(attr, f.operation)
|
||||
if (ret?.ref !== undefined) {
|
||||
value = ret.ref
|
||||
} else if (ret !== undefined && f.operation.referenceType != null) {
|
||||
syncRequests.push({
|
||||
bitrixId: `${ret.bitrixId}`,
|
||||
type: f.operation.referenceType,
|
||||
update: (newRef: Ref<Doc>) => {
|
||||
setValue(newRef, attr)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
;(document as any)[attr.name] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
setValue(value, attr)
|
||||
}
|
||||
|
||||
return { document, mixins, extraSync: newExtraSyncDocs, extraDocs: newExtraDocs, blobs, rawData: rawDocument }
|
||||
return {
|
||||
document,
|
||||
mixins,
|
||||
extraSync: newExtraSyncDocs,
|
||||
extraDocs: newExtraDocs,
|
||||
blobs,
|
||||
rawData: rawDocument,
|
||||
syncRequests
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,7 +45,7 @@
|
||||
export let timeReports: Map<Ref<Employee>, EmployeeReports>
|
||||
|
||||
$: month = getStartDate(currentDate.getFullYear(), currentDate.getMonth()) // getMonth(currentDate, currentDate.getMonth())
|
||||
$: wDays = weekDays(month.getUTCFullYear(), month.getUTCMonth())
|
||||
$: wDays = weekDays(month.getFullYear(), month.getMonth())
|
||||
|
||||
function getDateRange (request: Request): string {
|
||||
const ds = getRequestDates(request, types, month.getFullYear(), month.getMonth())
|
||||
|
@ -80,7 +80,7 @@
|
||||
{@const startDate = getStartDate(currentDate.getFullYear(), value)}
|
||||
<th class="fixed last-row" class:today={isToday(startDate)}>
|
||||
<span class="flex-center">
|
||||
{weekDays(startDate.getUTCFullYear(), startDate.getUTCMonth())}
|
||||
{weekDays(startDate.getFullYear(), startDate.getMonth())}
|
||||
</span>
|
||||
</th>
|
||||
{/each}
|
||||
|
@ -86,9 +86,17 @@ export function tzDateEqual (tzDate: TzDate, tzDate2: TzDate): boolean {
|
||||
* @public
|
||||
*/
|
||||
export function weekDays (year: number, month: number): number {
|
||||
return new Array(32 - new Date(year, month, 32).getDate())
|
||||
.fill(1)
|
||||
.filter((id, index) => ![0, 6].includes(new Date(year, month, index + 1).getDay())).length
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate()
|
||||
let days = 0
|
||||
|
||||
for (let i = 1; i <= daysInMonth; i++) {
|
||||
const dayOfWeek = new Date(year, month, i).getDay()
|
||||
|
||||
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
|
||||
days++
|
||||
}
|
||||
}
|
||||
return days
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -104,7 +104,8 @@
|
||||
"Match": "Match",
|
||||
"PerformMatch": "Match",
|
||||
"MoveApplication": "Move to another vacancy",
|
||||
"SearchVacancy": "Search vacancy..."
|
||||
"SearchVacancy": "Search vacancy...",
|
||||
"Organizations": "Companies"
|
||||
},
|
||||
"status": {
|
||||
"TalentRequired": "Please select talent",
|
||||
|
@ -106,7 +106,8 @@
|
||||
"Match": "Совпадение",
|
||||
"PerformMatch": "Сопоставить",
|
||||
"MoveApplication": "Поменять Вакансию",
|
||||
"SearchVacancy": "Найти вакансию..."
|
||||
"SearchVacancy": "Найти вакансию...",
|
||||
"Organizations": "Компании"
|
||||
},
|
||||
"status": {
|
||||
"TalentRequired": "Пожалуйста выберите таланта",
|
||||
|
@ -26,7 +26,7 @@
|
||||
export let disableClick = false
|
||||
|
||||
const client = getClient()
|
||||
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
|
||||
const shortLabel = value && client.getHierarchy().getClass(value._class).shortLabel
|
||||
</script>
|
||||
|
||||
{#if value && shortLabel}
|
||||
|
@ -52,7 +52,7 @@
|
||||
template = result[0]
|
||||
if (!changed || descriptionBox?.isEmptyContent()) {
|
||||
changed = false
|
||||
fullDescription = template.description ?? fullDescription
|
||||
fullDescription = template?.description ?? fullDescription
|
||||
}
|
||||
})
|
||||
|
||||
|
285
plugins/recruit-resources/src/components/Organizations.svelte
Normal file
285
plugins/recruit-resources/src/components/Organizations.svelte
Normal file
@ -0,0 +1,285 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Organization } from '@hcengineering/contact'
|
||||
import core, { Doc, DocumentQuery, Ref } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Applicant, Vacancy } from '@hcengineering/recruit'
|
||||
import {
|
||||
Button,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
Icon,
|
||||
IconAdd,
|
||||
Label,
|
||||
Loading,
|
||||
SearchEdit,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import view, { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import {
|
||||
FilterBar,
|
||||
FilterButton,
|
||||
getViewOptions,
|
||||
setActiveViewletId,
|
||||
TableBrowser,
|
||||
ViewletSettingButton
|
||||
} from '@hcengineering/view-resources'
|
||||
import recruit from '../plugin'
|
||||
import CreateVacancy from './CreateVacancy.svelte'
|
||||
import VacancyListApplicationsPopup from './organizations/VacancyListApplicationsPopup.svelte'
|
||||
import VacancyListCountPresenter from './organizations/VacancyListCountPresenter.svelte'
|
||||
import VacancyPopup from './organizations/VacancyPopup.svelte'
|
||||
|
||||
let search: string = ''
|
||||
let searchQuery: DocumentQuery<Doc> = {}
|
||||
let resultQuery: DocumentQuery<Doc> = {}
|
||||
|
||||
$: searchQuery = search === '' ? {} : { $search: search }
|
||||
|
||||
type CountInfo = {
|
||||
count: number
|
||||
modifiedOn: number
|
||||
vacancies: Ref<Vacancy>[]
|
||||
}
|
||||
let applications: Map<Ref<Organization>, CountInfo> = new Map<Ref<Organization>, CountInfo>()
|
||||
|
||||
let vacancies: Map<Ref<Organization>, CountInfo> = new Map<Ref<Organization>, CountInfo>()
|
||||
let vmap: Map<Ref<Vacancy>, Vacancy> | undefined
|
||||
|
||||
const vacancyQuery = createQuery()
|
||||
|
||||
let applicationsList: Applicant[] = []
|
||||
let vacanciesList: Vacancy[] = []
|
||||
|
||||
$: vacancyQuery.query(recruit.class.Vacancy, { company: { $exists: true }, archived: false }, (res) => {
|
||||
vacanciesList = res
|
||||
vmap = new Map(res.map((it) => [it._id, it]))
|
||||
})
|
||||
|
||||
$: vacancies = vacanciesList.reduce<Map<Ref<Organization>, CountInfo>>((map, it) => {
|
||||
const ifo = map.get(it.company as Ref<Organization>) ?? {
|
||||
count: 0,
|
||||
modifiedOn: 0,
|
||||
vacancies: []
|
||||
}
|
||||
map.set(it.company as Ref<Organization>, {
|
||||
count: ifo.count + 1,
|
||||
modifiedOn: Math.max(ifo.modifiedOn, it.modifiedOn),
|
||||
vacancies: [...ifo.vacancies, it._id]
|
||||
})
|
||||
return map
|
||||
}, new Map())
|
||||
|
||||
const applicantQuery = createQuery()
|
||||
$: applicantQuery.query(
|
||||
recruit.class.Applicant,
|
||||
{
|
||||
doneState: null
|
||||
},
|
||||
(res) => {
|
||||
applicationsList = res
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
_id: 1,
|
||||
modifiedOn: 1,
|
||||
space: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$: {
|
||||
const _applications = new Map<Ref<Organization>, CountInfo>()
|
||||
for (const d of applicationsList) {
|
||||
const vacancy = vmap?.get(d.space)
|
||||
if (vacancy?.company !== undefined) {
|
||||
const v = _applications.get(vacancy.company) ?? {
|
||||
count: 0,
|
||||
modifiedOn: 0,
|
||||
vacancies: []
|
||||
}
|
||||
if (!v.vacancies.includes(vacancy._id)) {
|
||||
v.vacancies.push(vacancy._id)
|
||||
}
|
||||
v.count++
|
||||
v.modifiedOn = Math.max(v.modifiedOn, d.modifiedOn)
|
||||
_applications.set(vacancy.company, v)
|
||||
}
|
||||
}
|
||||
applications = _applications
|
||||
}
|
||||
|
||||
function showCreateDialog () {
|
||||
showPopup(CreateVacancy, { space: recruit.space.CandidatesPublic }, 'top')
|
||||
}
|
||||
const applicationSorting = (a: Doc, b: Doc) =>
|
||||
(applications?.get(b._id as Ref<Organization>)?.count ?? 0) -
|
||||
(applications?.get(a._id as Ref<Organization>)?.count ?? 0) ?? 0
|
||||
|
||||
const vacancySorting = (a: Doc, b: Doc) =>
|
||||
(vacancies?.get(b._id as Ref<Organization>)?.count ?? 0) -
|
||||
(vacancies?.get(a._id as Ref<Organization>)?.count ?? 0) ?? 0
|
||||
|
||||
const modifiedSorting = (a: Doc, b: Doc) =>
|
||||
(applications?.get(b._id as Ref<Organization>)?.modifiedOn ?? b.modifiedOn) -
|
||||
(applications?.get(a._id as Ref<Organization>)?.modifiedOn ?? a.modifiedOn)
|
||||
|
||||
$: replacedKeys = new Map<string, BuildModelKey>([
|
||||
[
|
||||
'@vacancies',
|
||||
{
|
||||
key: '',
|
||||
presenter: VacancyListCountPresenter,
|
||||
label: recruit.string.Vacancies,
|
||||
props: {
|
||||
values: vacancies,
|
||||
label: recruit.string.Vacancies,
|
||||
icon: recruit.icon.Vacancy,
|
||||
component: VacancyPopup
|
||||
},
|
||||
sortingKey: '@vacancies',
|
||||
sortingFunction: vacancySorting
|
||||
}
|
||||
],
|
||||
[
|
||||
'@applications',
|
||||
{
|
||||
key: '',
|
||||
presenter: VacancyListCountPresenter,
|
||||
label: recruit.string.Applications,
|
||||
props: {
|
||||
values: applications,
|
||||
label: recruit.string.Applications,
|
||||
component: VacancyListApplicationsPopup,
|
||||
icon: recruit.icon.Application,
|
||||
resultQuery: {
|
||||
doneState: null
|
||||
}
|
||||
},
|
||||
sortingKey: '@applications',
|
||||
sortingFunction: applicationSorting
|
||||
}
|
||||
],
|
||||
[
|
||||
'@applications.modifiedOn',
|
||||
{
|
||||
key: '',
|
||||
presenter: recruit.component.VacancyModifiedPresenter,
|
||||
label: core.string.Modified,
|
||||
props: { applications },
|
||||
sortingKey: 'modifiedOn',
|
||||
sortingFunction: modifiedSorting
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let descr: Viewlet | undefined
|
||||
let loading = true
|
||||
|
||||
const preferenceQuery = createQuery()
|
||||
let preference: ViewletPreference | undefined
|
||||
|
||||
client
|
||||
.findOne<Viewlet>(view.class.Viewlet, {
|
||||
attachTo: recruit.mixin.VacancyList,
|
||||
descriptor: view.viewlet.Table
|
||||
})
|
||||
.then((res) => {
|
||||
descr = res
|
||||
if (res !== undefined) {
|
||||
setActiveViewletId(res._id)
|
||||
preferenceQuery.query(
|
||||
view.class.ViewletPreference,
|
||||
{
|
||||
attachedTo: res._id
|
||||
},
|
||||
(res) => {
|
||||
preference = res[0]
|
||||
loading = false
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function createConfig (descr: Viewlet, preference: ViewletPreference | undefined): (string | BuildModelKey)[] {
|
||||
const base = preference?.config ?? descr.config
|
||||
const result: (string | BuildModelKey)[] = []
|
||||
for (const key of base) {
|
||||
if (typeof key === 'string') {
|
||||
result.push(replacedKeys.get(key) ?? key)
|
||||
} else {
|
||||
result.push(replacedKeys.get(key.key) ?? key)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
$: twoRows = $deviceInfo.twoRows
|
||||
|
||||
$: viewOptions = getViewOptions(descr)
|
||||
</script>
|
||||
|
||||
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
|
||||
<div class:ac-header-full={!twoRows} class:flex-between={twoRows}>
|
||||
<div class="ac-header__wrap-title mr-3">
|
||||
<div class="ac-header__icon"><Icon icon={recruit.icon.Vacancy} size={'small'} /></div>
|
||||
<span class="ac-header__title"><Label label={recruit.string.Organizations} /></span>
|
||||
<div class="ml-4"><FilterButton _class={recruit.class.Vacancy} /></div>
|
||||
</div>
|
||||
<SearchEdit
|
||||
bind:value={search}
|
||||
on:change={(e) => {
|
||||
search = e.detail
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="ac-header-full" class:secondRow={twoRows}>
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
label={recruit.string.VacancyCreateLabel}
|
||||
size={'small'}
|
||||
kind={'primary'}
|
||||
on:click={showCreateDialog}
|
||||
/>
|
||||
<ViewletSettingButton bind:viewOptions viewlet={descr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FilterBar
|
||||
_class={recruit.mixin.VacancyList}
|
||||
{viewOptions}
|
||||
query={searchQuery}
|
||||
on:change={(e) => (resultQuery = e.detail)}
|
||||
/>
|
||||
|
||||
{#if descr}
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else}
|
||||
<TableBrowser
|
||||
_class={recruit.mixin.VacancyList}
|
||||
config={createConfig(descr, preference)}
|
||||
options={descr.options}
|
||||
query={{
|
||||
...resultQuery,
|
||||
_id: { $in: Array.from(vacancies.keys()) }
|
||||
}}
|
||||
showNotification
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
@ -0,0 +1,52 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
|
||||
import recruit, { Applicant } from '@hcengineering/recruit'
|
||||
import task from '@hcengineering/task'
|
||||
import { Table } from '@hcengineering/view-resources'
|
||||
|
||||
export let value: Ref<Space>[]
|
||||
export let resultQuery: DocumentQuery<Doc>
|
||||
|
||||
const options: FindOptions<Applicant> = {
|
||||
lookup: {
|
||||
state: task.class.State,
|
||||
space: core.class.Space,
|
||||
doneState: task.class.DoneState,
|
||||
attachedTo: recruit.mixin.Candidate
|
||||
},
|
||||
limit: 10
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="popup-table">
|
||||
<Table
|
||||
_class={recruit.class.Applicant}
|
||||
config={['', 'attachedTo', 'state', 'doneState', 'modifiedOn']}
|
||||
{options}
|
||||
query={{ ...(resultQuery ?? {}), space: { $in: value } }}
|
||||
loadingProps={{ length: 0 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.popup-table {
|
||||
overflow: auto;
|
||||
// width: 70rem;
|
||||
// max-width: 70rem !important;
|
||||
max-height: 30rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,50 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Organization } from '@hcengineering/contact'
|
||||
import { Doc, DocumentQuery, Ref } from '@hcengineering/core'
|
||||
import { Asset, IntlString } from '@hcengineering/platform'
|
||||
import { Vacancy, VacancyList } from '@hcengineering/recruit'
|
||||
import { AnySvelteComponent, Icon, tooltip } from '@hcengineering/ui'
|
||||
|
||||
export let value: VacancyList
|
||||
export let values:
|
||||
| Map<Ref<Organization>, { count: number; modifiedOn: number; vacancies: Ref<Vacancy>[] }>
|
||||
| undefined
|
||||
export let resultQuery: DocumentQuery<Doc>
|
||||
|
||||
export let label: IntlString
|
||||
export let component: AnySvelteComponent
|
||||
export let icon: Asset
|
||||
|
||||
$: countValue = values?.get(value._id)
|
||||
$: count = countValue?.count ?? 0
|
||||
</script>
|
||||
|
||||
{#if countValue && count > 0}
|
||||
<div
|
||||
class="sm-tool-icon"
|
||||
use:tooltip={{
|
||||
component,
|
||||
props: { value: countValue.vacancies, resultQuery }
|
||||
}}
|
||||
>
|
||||
<div class="icon">
|
||||
<Icon {icon} size={'small'} />
|
||||
</div>
|
||||
|
||||
{values?.get(value._id)?.count ?? 0}
|
||||
</div>
|
||||
{/if}
|
@ -0,0 +1,45 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
|
||||
import recruit, { Vacancy } from '@hcengineering/recruit'
|
||||
import { Table } from '@hcengineering/view-resources'
|
||||
|
||||
export let value: Ref<Vacancy>[]
|
||||
export let resultQuery: DocumentQuery<Doc>
|
||||
|
||||
const options: FindOptions<Vacancy> = {
|
||||
limit: 10
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="popup-table">
|
||||
<Table
|
||||
_class={recruit.class.Vacancy}
|
||||
config={['', 'modifiedOn']}
|
||||
{options}
|
||||
query={{ ...(resultQuery ?? {}), _id: { $in: value } }}
|
||||
loadingProps={{ length: 0 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.popup-table {
|
||||
overflow: auto;
|
||||
// width: 70rem;
|
||||
// max-width: 70rem !important;
|
||||
max-height: 30rem;
|
||||
}
|
||||
</style>
|
@ -27,7 +27,7 @@
|
||||
const client = getClient()
|
||||
let shortLabel = ''
|
||||
|
||||
const label = client.getHierarchy().getClass(value._class).shortLabel
|
||||
const label = client.getHierarchy().getClass(value?._class)?.shortLabel
|
||||
|
||||
if (label !== undefined) {
|
||||
translate(label, {}).then((r) => {
|
||||
|
@ -63,6 +63,7 @@ import { objectIdProvider, objectLinkProvider, getApplicationTitle } from './uti
|
||||
import VacancyList from './components/VacancyList.svelte'
|
||||
import VacancyTemplateEditor from './components/VacancyTemplateEditor.svelte'
|
||||
import MatchVacancy from './components/MatchVacancy.svelte'
|
||||
import Organizations from './components/Organizations.svelte'
|
||||
|
||||
import { MoveApplicant } from './actionImpl'
|
||||
|
||||
@ -287,6 +288,7 @@ export default async (): Promise<Resources> => ({
|
||||
VacancyPresenter,
|
||||
SkillsView,
|
||||
Vacancies,
|
||||
Organizations,
|
||||
VacancyItemPresenter,
|
||||
VacancyCountPresenter,
|
||||
VacancyModifiedPresenter,
|
||||
|
@ -78,6 +78,7 @@ export default mergeIds(recruitId, recruit, {
|
||||
Location: '' as IntlString,
|
||||
Title: '' as IntlString,
|
||||
Vacancies: '' as IntlString,
|
||||
Organizations: '' as IntlString,
|
||||
|
||||
CopyLink: '' as IntlString,
|
||||
CopyId: '' as IntlString,
|
||||
|
@ -142,13 +142,22 @@ class TServerStorage implements ServerStorage {
|
||||
return adapter
|
||||
}
|
||||
|
||||
private async routeTx (ctx: MeasureContext, ...txes: Tx[]): Promise<TxResult> {
|
||||
private async routeTx (ctx: MeasureContext, removedDocs: Map<Ref<Doc>, Doc>, ...txes: Tx[]): Promise<TxResult> {
|
||||
let part: TxCUD<Doc>[] = []
|
||||
let lastDomain: Domain | undefined
|
||||
const result: TxResult[] = []
|
||||
const processPart = async (): Promise<void> => {
|
||||
if (part.length > 0) {
|
||||
// Find all deleted documents
|
||||
|
||||
const adapter = this.getAdapter(lastDomain as Domain)
|
||||
const toDelete = part.filter((it) => it._class === core.class.TxRemoveDoc).map((it) => it.objectId)
|
||||
|
||||
const toDeleteDocs = await adapter.load(lastDomain as Domain, toDelete)
|
||||
for (const ddoc of toDeleteDocs) {
|
||||
removedDocs.set(ddoc._id, ddoc)
|
||||
}
|
||||
|
||||
const r = await adapter.tx(...part)
|
||||
if (Array.isArray(r)) {
|
||||
result.push(...r)
|
||||
@ -345,62 +354,6 @@ class TServerStorage implements ServerStorage {
|
||||
)
|
||||
}
|
||||
|
||||
private async buildRemovedDoc (ctx: MeasureContext, rawTxes: Tx[], findAll: ServerStorage['findAll']): Promise<Doc[]> {
|
||||
const removeObjectIds: Ref<Doc>[] = []
|
||||
const removeAttachObjectIds: Ref<AttachedDoc>[] = []
|
||||
|
||||
const removeTxes = rawTxes
|
||||
.map((it) => TxProcessor.extractTx(it) as TxRemoveDoc<Doc>)
|
||||
.filter((it) => this.hierarchy.isDerived(it._class, core.class.TxRemoveDoc))
|
||||
|
||||
for (const rtx of removeTxes) {
|
||||
const isAttached = this.hierarchy.isDerived(rtx.objectClass, core.class.AttachedDoc)
|
||||
if (isAttached) {
|
||||
removeAttachObjectIds.push(rtx.objectId as Ref<AttachedDoc>)
|
||||
} else {
|
||||
removeObjectIds.push(rtx.objectId)
|
||||
}
|
||||
}
|
||||
|
||||
const txes =
|
||||
removeObjectIds.length > 0
|
||||
? await findAll<TxCUD<Doc>>(
|
||||
ctx,
|
||||
core.class.TxCUD,
|
||||
{
|
||||
objectId: { $in: removeObjectIds }
|
||||
},
|
||||
{ sort: { modifiedOn: 1 } }
|
||||
)
|
||||
: []
|
||||
const result: Doc[] = []
|
||||
|
||||
const txesAttach =
|
||||
removeAttachObjectIds.length > 0
|
||||
? await findAll<TxCollectionCUD<Doc, AttachedDoc>>(
|
||||
ctx,
|
||||
core.class.TxCollectionCUD,
|
||||
{ 'tx.objectId': { $in: removeAttachObjectIds } },
|
||||
{ sort: { modifiedOn: 1 } }
|
||||
)
|
||||
: []
|
||||
|
||||
for (const rtx of removeTxes) {
|
||||
const isAttached = this.hierarchy.isDerived(rtx.objectClass, core.class.AttachedDoc)
|
||||
|
||||
const objTxex = isAttached
|
||||
? txesAttach.filter((tx) => tx.tx.objectId === rtx.objectId)
|
||||
: txes.filter((it) => it.objectId === rtx.objectId)
|
||||
|
||||
const doc = TxProcessor.buildDoc2Doc(objTxex)
|
||||
if (doc !== undefined) {
|
||||
result.push(doc)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private async processRemove (
|
||||
ctx: MeasureContext,
|
||||
txes: Tx[],
|
||||
@ -409,11 +362,6 @@ class TServerStorage implements ServerStorage {
|
||||
): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
|
||||
const objects = await this.buildRemovedDoc(ctx, txes, findAll)
|
||||
for (const obj of objects) {
|
||||
removedMap.set(obj._id, obj)
|
||||
}
|
||||
|
||||
for (const tx of txes) {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (!this.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)) {
|
||||
@ -532,7 +480,7 @@ class TServerStorage implements ServerStorage {
|
||||
return result
|
||||
}
|
||||
|
||||
private async proccessDerived (
|
||||
private async processDerived (
|
||||
ctx: MeasureContext,
|
||||
txes: Tx[],
|
||||
triggerFx: Effects,
|
||||
@ -594,11 +542,11 @@ class TServerStorage implements ServerStorage {
|
||||
): Promise<Tx[]> {
|
||||
derived.sort((a, b) => a.modifiedOn - b.modifiedOn)
|
||||
|
||||
await ctx.with('derived-route-tx', {}, (ctx) => this.routeTx(ctx, ...derived))
|
||||
await ctx.with('derived-route-tx', {}, (ctx) => this.routeTx(ctx, removedMap, ...derived))
|
||||
|
||||
const nestedTxes: Tx[] = []
|
||||
if (derived.length > 0) {
|
||||
nestedTxes.push(...(await this.proccessDerived(ctx, derived, triggerFx, findAll, removedMap)))
|
||||
nestedTxes.push(...(await this.processDerived(ctx, derived, triggerFx, findAll, removedMap)))
|
||||
}
|
||||
|
||||
const res = [...derived, ...nestedTxes]
|
||||
@ -650,14 +598,15 @@ class TServerStorage implements ServerStorage {
|
||||
)
|
||||
await ctx.with('domain-tx', {}, async () => await this.getAdapter(DOMAIN_TX).tx(...txToStore))
|
||||
|
||||
await ctx.with('apply', {}, (ctx) => this.routeTx(ctx, ...tx))
|
||||
const removedMap = new Map<Ref<Doc>, Doc>()
|
||||
await ctx.with('apply', {}, (ctx) => this.routeTx(ctx, removedMap, ...tx))
|
||||
|
||||
// send transactions
|
||||
if (broadcast) {
|
||||
this.options?.broadcast?.(tx)
|
||||
}
|
||||
// invoke triggers and store derived objects
|
||||
const derived = await this.proccessDerived(ctx, tx, triggerFx, cacheFind, new Map<Ref<Doc>, Doc>())
|
||||
const derived = await this.processDerived(ctx, tx, triggerFx, cacheFind, removedMap)
|
||||
|
||||
// index object
|
||||
for (const _tx of tx) {
|
||||
@ -680,6 +629,7 @@ class TServerStorage implements ServerStorage {
|
||||
const _class = txClass(tx)
|
||||
const cacheFind = createCacheFindAll(this)
|
||||
const objClass = txObjectClass(tx)
|
||||
const removedDocs = new Map<Ref<Doc>, Doc>()
|
||||
return await ctx.with('tx', { _class, objClass }, async (ctx) => {
|
||||
if (tx.space !== core.space.DerivedTx && !this.hierarchy.isDerived(tx._class, core.class.TxApplyIf)) {
|
||||
await ctx.with('domain-tx', { _class, objClass }, async () => await this.getAdapter(DOMAIN_TX).tx(tx))
|
||||
@ -711,13 +661,13 @@ class TServerStorage implements ServerStorage {
|
||||
const atx = await this.getAdapter(DOMAIN_TX)
|
||||
await atx.tx(...applyIf.txes)
|
||||
})
|
||||
derived = await this.processDerivedTxes(applyIf.txes, ctx, triggerFx, cacheFind, new Map<Ref<Doc>, Doc>())
|
||||
derived = await this.processDerivedTxes(applyIf.txes, ctx, triggerFx, cacheFind, removedDocs)
|
||||
}
|
||||
} else {
|
||||
// store object
|
||||
result = await ctx.with('route-tx', { _class, objClass }, (ctx) => this.routeTx(ctx, tx))
|
||||
result = await ctx.with('route-tx', { _class, objClass }, (ctx) => this.routeTx(ctx, removedDocs, tx))
|
||||
// invoke triggers and store derived objects
|
||||
derived = await this.proccessDerived(ctx, [tx], triggerFx, cacheFind, new Map<Ref<Doc>, Doc>())
|
||||
derived = await this.processDerived(ctx, [tx], triggerFx, cacheFind, removedDocs)
|
||||
}
|
||||
|
||||
// index object
|
||||
|
Loading…
Reference in New Issue
Block a user