mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-21 07:46:24 +00:00
parent
ae09613d5f
commit
122cf92165
@ -112,6 +112,7 @@
|
|||||||
"csv-parse": "~5.1.0",
|
"csv-parse": "~5.1.0",
|
||||||
"@anticrm/lead": "~0.6.0",
|
"@anticrm/lead": "~0.6.0",
|
||||||
"email-addresses": "^5.0.0",
|
"email-addresses": "^5.0.0",
|
||||||
"libphonenumber-js": "^1.9.46"
|
"libphonenumber-js": "^1.9.46",
|
||||||
|
"@anticrm/setting": "~0.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ import { updateClasses } from './classes'
|
|||||||
import { CustomCustomer, FieldType } from './types'
|
import { CustomCustomer, FieldType } from './types'
|
||||||
import { filled } from './utils'
|
import { filled } from './utils'
|
||||||
|
|
||||||
import { parse } from 'csv-parse'
|
import { parseCSV } from './parseCSV'
|
||||||
|
|
||||||
const names = {
|
const names = {
|
||||||
orgName: 'Название компании',
|
orgName: 'Название компании',
|
||||||
@ -125,31 +125,6 @@ async function updateStates<T extends State | DoneState> (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseCSV (csvData: string): Promise<any[]> {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
parse(
|
|
||||||
csvData,
|
|
||||||
{
|
|
||||||
delimiter: ';',
|
|
||||||
columns: true,
|
|
||||||
quote: '"',
|
|
||||||
bom: true,
|
|
||||||
cast: true,
|
|
||||||
autoParse: true,
|
|
||||||
castDate: false,
|
|
||||||
skipEmptyLines: true,
|
|
||||||
skipRecordsWithEmptyValues: true
|
|
||||||
},
|
|
||||||
(err, records) => {
|
|
||||||
if (err !== undefined) {
|
|
||||||
console.error(err)
|
|
||||||
reject(err)
|
|
||||||
}
|
|
||||||
resolve(records)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
export async function importLead (transactorUrl: string, dbName: string, csvFile: string): Promise<void> {
|
export async function importLead (transactorUrl: string, dbName: string, csvFile: string): Promise<void> {
|
||||||
const connection = await connect(transactorUrl, dbName)
|
const connection = await connect(transactorUrl, dbName)
|
||||||
|
|
27
dev/tool/src/csv/parseCSV.ts
Normal file
27
dev/tool/src/csv/parseCSV.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { parse } from 'csv-parse'
|
||||||
|
|
||||||
|
export async function parseCSV (csvData: string): Promise<any[]> {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
parse(
|
||||||
|
csvData,
|
||||||
|
{
|
||||||
|
delimiter: ';',
|
||||||
|
columns: true,
|
||||||
|
quote: '"',
|
||||||
|
bom: true,
|
||||||
|
cast: true,
|
||||||
|
autoParse: true,
|
||||||
|
castDate: false,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
skipRecordsWithEmptyValues: true
|
||||||
|
},
|
||||||
|
(err, records) => {
|
||||||
|
if (err !== undefined) {
|
||||||
|
console.error(err)
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
resolve(records)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
760
dev/tool/src/csv/talant-importer.ts
Normal file
760
dev/tool/src/csv/talant-importer.ts
Normal file
@ -0,0 +1,760 @@
|
|||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
import contact, { ChannelProvider, combineName, Contact, EmployeeAccount, Person } from '@anticrm/contact'
|
||||||
|
import core, {
|
||||||
|
AnyAttribute,
|
||||||
|
BackupClient,
|
||||||
|
BlobData,
|
||||||
|
Class,
|
||||||
|
ClassifierKind,
|
||||||
|
Client,
|
||||||
|
Data,
|
||||||
|
Doc,
|
||||||
|
DOMAIN_BLOB,
|
||||||
|
Enum,
|
||||||
|
EnumOf,
|
||||||
|
generateId,
|
||||||
|
Ref,
|
||||||
|
Timestamp,
|
||||||
|
TxOperations,
|
||||||
|
WithLookup
|
||||||
|
} from '@anticrm/core'
|
||||||
|
import { Asset, getEmbeddedLabel, IntlString } from '@anticrm/platform'
|
||||||
|
import recruit, { Candidate } from '@anticrm/recruit'
|
||||||
|
import { connect } from '@anticrm/server-tool'
|
||||||
|
import setting from '@anticrm/setting'
|
||||||
|
import { readFile } from 'fs/promises'
|
||||||
|
import { parseCSV } from './parseCSV'
|
||||||
|
import { FieldType } from './types'
|
||||||
|
import { filled } from './utils'
|
||||||
|
import got from 'got'
|
||||||
|
import mimetypes from 'mime-types'
|
||||||
|
import attachment, { Attachment } from '@anticrm/attachment'
|
||||||
|
import { generateToken } from '@anticrm/server-token'
|
||||||
|
import { recognize, updateContacts, updateSkills } from '../recruit'
|
||||||
|
import { ReconiDocument } from '@anticrm/rekoni'
|
||||||
|
import { findOrUpdateAttached } from '../utils'
|
||||||
|
|
||||||
|
const names = {
|
||||||
|
status: 'Status',
|
||||||
|
fullName: 'Lead Name',
|
||||||
|
firstName: 'First Name',
|
||||||
|
middleName: 'Middle Name',
|
||||||
|
lastName: 'Last Name',
|
||||||
|
birthDay: 'Date of birth',
|
||||||
|
created: 'Created',
|
||||||
|
kasource: 'Source',
|
||||||
|
workEmail: 'Work E-mail',
|
||||||
|
newsletterEmail: 'Newsletters email',
|
||||||
|
otherEmail: 'Other E-mail',
|
||||||
|
homeEmail: 'Home E-mail',
|
||||||
|
telegram: 'Telegram account',
|
||||||
|
vkAccount: 'VK account',
|
||||||
|
skypeID: 'Skype ID',
|
||||||
|
viberContact: 'Viber contact',
|
||||||
|
otherContact: 'Other Contact',
|
||||||
|
responsible: 'Responsible',
|
||||||
|
resume: 'Резюме',
|
||||||
|
profile: 'Ссылка на профайл',
|
||||||
|
liveCountry: 'Страна проживания',
|
||||||
|
phone: 'Телефон',
|
||||||
|
phoneNumber: 'Номер телефона',
|
||||||
|
githubPortfolio: 'Ссылка на гитхаб/портфолио',
|
||||||
|
webSite: 'Веб-сайт',
|
||||||
|
linkedRu1: 'Линкедин',
|
||||||
|
englishLevel: 'Уровень английского',
|
||||||
|
movedFromTrello: 'Полностью перенесен из Трелло',
|
||||||
|
bot: 'С кем общался (бот)',
|
||||||
|
research: 'Ресеч ',
|
||||||
|
region: 'Регион',
|
||||||
|
mainExperience: 'Опыт работы по основному стеку с года:',
|
||||||
|
mainStack: 'Основной стек (выпадающий список)',
|
||||||
|
extraStack: 'Доп.стек',
|
||||||
|
education: 'Образование',
|
||||||
|
relocationPreferences: 'Предпочтения по релокации',
|
||||||
|
relocationIgnore: 'Какую компанию не предлагать! (или страну)',
|
||||||
|
relocationWant: 'Какие страны/города рассматривает',
|
||||||
|
language: 'Язык общения',
|
||||||
|
languageExtra: 'Владение другими языками',
|
||||||
|
seed: 'Посев',
|
||||||
|
actualization: 'Актуализация (дата)',
|
||||||
|
unsubscribe: 'Отказался от рассылки',
|
||||||
|
companyName: 'Название компании (новое поле)'
|
||||||
|
}
|
||||||
|
|
||||||
|
const kaDetails = 'recruit:mixin:KADetails' as Ref<Class<Doc>>
|
||||||
|
const portfolioDetails = 'recruit:mixin:KAPortfolio' as Ref<Class<Doc>>
|
||||||
|
const techDetails = 'recruit:mixin:TechDetails' as Ref<Class<Doc>>
|
||||||
|
const locationDetails = 'recruit:mixin:CountryDetails' as Ref<Class<Doc>>
|
||||||
|
|
||||||
|
const fieldMapping: Record<string, FieldType> = {
|
||||||
|
[names.fullName]: {
|
||||||
|
name: 'fullName',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: contact.class.Person
|
||||||
|
},
|
||||||
|
[names.middleName]: {
|
||||||
|
name: 'middleName',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: contact.class.Person
|
||||||
|
},
|
||||||
|
Language: {
|
||||||
|
name: 'language',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'KAPrefferedLanguage',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.language,
|
||||||
|
sourceClass: contact.class.Person
|
||||||
|
},
|
||||||
|
[names.languageExtra]: {
|
||||||
|
name: 'language_extra',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: contact.class.Person
|
||||||
|
},
|
||||||
|
|
||||||
|
[names.created]: { name: 'created', type: core.class.TypeDate, label: core.string.Date, sourceClass: kaDetails },
|
||||||
|
[names.status]: {
|
||||||
|
name: 'lead_status',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'KAStatus',
|
||||||
|
label: core.string.Enum,
|
||||||
|
sourceClass: kaDetails
|
||||||
|
},
|
||||||
|
[names.kasource]: {
|
||||||
|
name: 'kasource',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'KASource',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.kasource,
|
||||||
|
sourceClass: kaDetails
|
||||||
|
},
|
||||||
|
[names.responsible]: {
|
||||||
|
name: 'responsible',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'KAResponsible',
|
||||||
|
label: core.string.Enum,
|
||||||
|
sourceClass: kaDetails
|
||||||
|
},
|
||||||
|
[names.movedFromTrello]: {
|
||||||
|
name: 'trello_moved',
|
||||||
|
type: core.class.TypeBoolean,
|
||||||
|
label: core.string.Boolean,
|
||||||
|
sourceClass: kaDetails
|
||||||
|
},
|
||||||
|
[names.bot]: {
|
||||||
|
name: 'bot',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'KABot',
|
||||||
|
label: core.string.Enum,
|
||||||
|
sourceClass: kaDetails
|
||||||
|
},
|
||||||
|
Ресерч: {
|
||||||
|
name: 'research',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'KAResearch',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.research,
|
||||||
|
sourceClass: kaDetails
|
||||||
|
},
|
||||||
|
[names.seed]: {
|
||||||
|
name: 'seed',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'KASeed',
|
||||||
|
label: core.string.Enum,
|
||||||
|
sourceClass: kaDetails
|
||||||
|
},
|
||||||
|
[names.actualization]: {
|
||||||
|
name: 'actualization',
|
||||||
|
type: core.class.TypeDate,
|
||||||
|
label: core.string.Date,
|
||||||
|
sourceClass: kaDetails
|
||||||
|
},
|
||||||
|
'Unsubscribe / Отказ от рассылки': {
|
||||||
|
name: 'unsubscribe',
|
||||||
|
type: core.class.TypeBoolean,
|
||||||
|
label: core.string.Boolean,
|
||||||
|
sourceClass: kaDetails
|
||||||
|
},
|
||||||
|
[names.companyName]: {
|
||||||
|
name: 'company_name',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.Boolean,
|
||||||
|
sourceClass: kaDetails
|
||||||
|
},
|
||||||
|
|
||||||
|
[names.resume]: {
|
||||||
|
name: 'resume',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: portfolioDetails
|
||||||
|
},
|
||||||
|
[names.profile]: {
|
||||||
|
name: 'profile_link',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: portfolioDetails
|
||||||
|
},
|
||||||
|
[names.githubPortfolio]: {
|
||||||
|
name: 'github_portfolio',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: portfolioDetails
|
||||||
|
},
|
||||||
|
[names.vkAccount]: {
|
||||||
|
name: 'vk_account',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: portfolioDetails
|
||||||
|
},
|
||||||
|
[names.viberContact]: {
|
||||||
|
name: 'viberContact',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: portfolioDetails
|
||||||
|
},
|
||||||
|
[names.otherContact]: {
|
||||||
|
name: 'otherContact',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: portfolioDetails
|
||||||
|
},
|
||||||
|
|
||||||
|
'Основной Стек': {
|
||||||
|
name: 'main_stack',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'KAMainTechStack',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.mainStack,
|
||||||
|
sourceClass: techDetails
|
||||||
|
},
|
||||||
|
[names.englishLevel]: {
|
||||||
|
name: 'english_level',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'KAEnglishLevel',
|
||||||
|
label: core.string.Enum,
|
||||||
|
sourceClass: techDetails
|
||||||
|
},
|
||||||
|
[names.mainExperience]: {
|
||||||
|
name: 'main_experience',
|
||||||
|
type: core.class.TypeNumber,
|
||||||
|
label: core.string.Number,
|
||||||
|
sourceClass: techDetails
|
||||||
|
},
|
||||||
|
[names.extraStack]: {
|
||||||
|
name: 'extra_stack',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: techDetails
|
||||||
|
},
|
||||||
|
[names.education]: {
|
||||||
|
name: 'education',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: techDetails
|
||||||
|
},
|
||||||
|
|
||||||
|
Region: {
|
||||||
|
name: 'region',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'KARegion',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.region,
|
||||||
|
sourceClass: locationDetails
|
||||||
|
},
|
||||||
|
[names.relocationPreferences]: {
|
||||||
|
name: 'relocation_preferences',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'KARelocationPreferences',
|
||||||
|
label: core.string.Enum,
|
||||||
|
sourceClass: locationDetails
|
||||||
|
},
|
||||||
|
[names.relocationIgnore]: {
|
||||||
|
name: 'relocation_ignore',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: locationDetails
|
||||||
|
},
|
||||||
|
[names.relocationWant]: {
|
||||||
|
name: 'relocation_want',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
sourceClass: locationDetails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAttr (client: TxOperations, _class: Ref<Class<Doc>>, name: string): AnyAttribute | undefined {
|
||||||
|
try {
|
||||||
|
return client.getHierarchy().getAttribute(_class, name)
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTalantClasses (
|
||||||
|
client: TxOperations,
|
||||||
|
records: any[],
|
||||||
|
fieldMapping: Record<string, FieldType>,
|
||||||
|
refClass: Ref<Class<Doc>> = recruit.mixin.Candidate
|
||||||
|
): Promise<void> {
|
||||||
|
const allAttrs = client.getHierarchy().getAllAttributes(refClass)
|
||||||
|
for (const [k, v] of Object.entries(fieldMapping)) {
|
||||||
|
if (v.type === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let attr = v.sourceClass === undefined ? allAttrs.get(v.name) : getAttr(client, v.sourceClass, v.name)
|
||||||
|
if (attr === undefined) {
|
||||||
|
try {
|
||||||
|
if (!client.getHierarchy().isDerived(v.type, core.class.Type)) {
|
||||||
|
// Skip channels mapping
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} catch (any) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Create attr
|
||||||
|
const data: Data<AnyAttribute> = {
|
||||||
|
attributeOf: v.sourceClass ?? refClass,
|
||||||
|
name: v.name,
|
||||||
|
label: getEmbeddedLabel(k),
|
||||||
|
isCustom: true,
|
||||||
|
type: {
|
||||||
|
_class: v.type,
|
||||||
|
label: v.label ?? core.string.String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (client.getHierarchy().isDerived(v.type, core.class.EnumOf)) {
|
||||||
|
;(data.type as EnumOf).of = `recruit:class:${(v as any).enumName as string}` as Ref<Enum>
|
||||||
|
}
|
||||||
|
const attrId = ((v.sourceClass ?? refClass) + '.' + v.name) as Ref<AnyAttribute>
|
||||||
|
await client.createDoc(core.class.Attribute, core.space.Model, data, attrId)
|
||||||
|
attr = await client.findOne(core.class.Attribute, { _id: attrId })
|
||||||
|
}
|
||||||
|
if (attr === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Check update Enum/Values
|
||||||
|
if (client.getHierarchy().isDerived(attr.type._class, core.class.EnumOf)) {
|
||||||
|
const enumName = (v as any).enumName as string
|
||||||
|
const enumId = `recruit:class:${enumName}` as Ref<Enum>
|
||||||
|
let enumClass = await client.findOne(core.class.Enum, { _id: enumId })
|
||||||
|
if (enumClass === undefined) {
|
||||||
|
await client.createDoc(
|
||||||
|
core.class.Enum,
|
||||||
|
core.space.Model,
|
||||||
|
{
|
||||||
|
name: enumName,
|
||||||
|
enumValues: []
|
||||||
|
},
|
||||||
|
enumId
|
||||||
|
)
|
||||||
|
enumClass = client.getModel().getObject(enumId)
|
||||||
|
}
|
||||||
|
// Check values
|
||||||
|
const mapv = (v?: string): string =>
|
||||||
|
(v?.toString() ?? '').trim().length === 0 ? 'не задано' : (v?.toString() ?? '').trim()
|
||||||
|
const values = records
|
||||||
|
.map((it) => it[v.fName ?? k])
|
||||||
|
.map(mapv)
|
||||||
|
.filter((it, idx, arr) => arr.indexOf(it) === idx)
|
||||||
|
for (const v of values) {
|
||||||
|
if (!enumClass.enumValues.includes(v)) {
|
||||||
|
await client.update(enumClass, {
|
||||||
|
$push: { enumValues: v }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importTalants (
|
||||||
|
transactorUrl: string,
|
||||||
|
dbName: string,
|
||||||
|
csvFile: string,
|
||||||
|
rekoniUrl: string
|
||||||
|
): Promise<void> {
|
||||||
|
const connection = (await connect(transactorUrl, dbName, undefined, {
|
||||||
|
mode: 'backup'
|
||||||
|
})) as unknown as Client & BackupClient
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('loading cvs document...')
|
||||||
|
|
||||||
|
const csvData = await readFile(csvFile, 'utf-8')
|
||||||
|
const records: any[] = await parseCSV(csvData)
|
||||||
|
const uniqKeys: string[] = []
|
||||||
|
const filledFields = records.map((it) => filled(it, uniqKeys))
|
||||||
|
// console.log(filledFields)
|
||||||
|
|
||||||
|
const client = new TxOperations(connection, 'core:account:recruit-importer' as Ref<EmployeeAccount>)
|
||||||
|
|
||||||
|
await createMissingClass(client, kaDetails, getEmbeddedLabel('KA Details'))
|
||||||
|
await createMissingClass(client, portfolioDetails, getEmbeddedLabel('Portfolio'))
|
||||||
|
await createMissingClass(client, techDetails, getEmbeddedLabel('Technology'))
|
||||||
|
await createMissingClass(client, locationDetails, getEmbeddedLabel('Language & Relocations'))
|
||||||
|
|
||||||
|
await updateTalantClasses(client, records, fieldMapping)
|
||||||
|
await createTalants(client, filledFields, connection, rekoniUrl, generateToken('anticrm@hc.engineering', dbName))
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
await connection.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function createMissingClass (
|
||||||
|
client: TxOperations,
|
||||||
|
ref: Ref<Class<Doc>>,
|
||||||
|
label: IntlString,
|
||||||
|
icon?: Asset
|
||||||
|
): Promise<void> {
|
||||||
|
const cl = await client.findOne(core.class.Class, { _id: ref })
|
||||||
|
if (cl === undefined) {
|
||||||
|
await client.createDoc(
|
||||||
|
core.class.Mixin,
|
||||||
|
core.space.Model,
|
||||||
|
{
|
||||||
|
extends: recruit.mixin.Candidate,
|
||||||
|
label,
|
||||||
|
kind: ClassifierKind.MIXIN,
|
||||||
|
icon
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
await client.createMixin(ref, core.class.Mixin, core.space.Model, setting.mixin.UserMixin, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KaDetailsTalant extends Candidate {
|
||||||
|
created: Timestamp
|
||||||
|
lead_status: any
|
||||||
|
kasource: any
|
||||||
|
responsible: any
|
||||||
|
trello_moved: boolean
|
||||||
|
bot: any
|
||||||
|
research: any
|
||||||
|
seed: any
|
||||||
|
actualization: Timestamp
|
||||||
|
unsubscribe: boolean
|
||||||
|
company_name: string
|
||||||
|
}
|
||||||
|
export interface PortfolioDetailsTalant extends Candidate {
|
||||||
|
resume: string
|
||||||
|
profile_link: string
|
||||||
|
github_portfolio: string
|
||||||
|
vk_account: string
|
||||||
|
viberContact: string
|
||||||
|
otherContact: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TechDetailsTalant extends Candidate {
|
||||||
|
main_stack: any
|
||||||
|
english_level: any
|
||||||
|
main_experience: number
|
||||||
|
extra_stack: string
|
||||||
|
education: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationDetailsTalant extends Candidate {
|
||||||
|
region: any
|
||||||
|
relocation_preferences: any
|
||||||
|
relocation_ignore: string
|
||||||
|
relocation_want: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomPerson extends Person {
|
||||||
|
fullName: string
|
||||||
|
middleName: string
|
||||||
|
language?: string
|
||||||
|
language_extra?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTalants (
|
||||||
|
client: TxOperations,
|
||||||
|
filledFields: any[],
|
||||||
|
connection: Client & BackupClient,
|
||||||
|
rekoniUrl: string,
|
||||||
|
token: string
|
||||||
|
): Promise<void> {
|
||||||
|
for (const record of filledFields) {
|
||||||
|
const candidateId = `imported-${record.ID as string}` as Ref<Candidate>
|
||||||
|
const leadName = record[names.fullName]
|
||||||
|
const candidate = await client.findOne(recruit.mixin.Candidate, { _id: candidateId })
|
||||||
|
console.log('processing', leadName)
|
||||||
|
const dataKa = {
|
||||||
|
created: new Date(record[names.created]).getTime(),
|
||||||
|
lead_status: record[names.status],
|
||||||
|
kasource: record[names.kasource],
|
||||||
|
responsible: record[names.responsible],
|
||||||
|
trello_moved: record[names.movedFromTrello] === 'yes',
|
||||||
|
bot: record[names.bot],
|
||||||
|
research: record[names.research],
|
||||||
|
seed: record[names.seed],
|
||||||
|
actualization: new Date(record[names.actualization]).getTime(),
|
||||||
|
unsubscribe: record[names.unsubscribe] === 'yes',
|
||||||
|
company_name: record[names.companyName]
|
||||||
|
}
|
||||||
|
const dataPortfolio = {
|
||||||
|
resume: record[names.resume],
|
||||||
|
profile_link: record[names.profile],
|
||||||
|
github_portfolio: record[names.githubPortfolio],
|
||||||
|
vk_account: record[names.vkAccount],
|
||||||
|
viberContact: record[names.viberContact],
|
||||||
|
otherContact: record[names.otherContact]
|
||||||
|
}
|
||||||
|
const dataTechStack = {
|
||||||
|
main_stack: record[names.mainStack],
|
||||||
|
english_level: record[names.englishLevel],
|
||||||
|
main_experience: record[names.mainExperience],
|
||||||
|
extra_stack: record[names.extraStack],
|
||||||
|
education: record[names.education]
|
||||||
|
}
|
||||||
|
const dataLocationDetails = {
|
||||||
|
region: record[names.region],
|
||||||
|
relocation_preferences: record[names.relocationPreferences],
|
||||||
|
relocation_ignore: record[names.relocationIgnore],
|
||||||
|
relocation_want: record[names.relocationWant]
|
||||||
|
}
|
||||||
|
if (candidate !== undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const cdata = {
|
||||||
|
name: combineName(record[names.firstName], record[names.lastName]),
|
||||||
|
city: record[names.liveCountry],
|
||||||
|
birthday: record[names.birthDay] !== '' ? new Date(record[names.birthDay]).getTime() : undefined,
|
||||||
|
fullName: record[names.fullName],
|
||||||
|
middleName: record[names.middleName],
|
||||||
|
language: record[names.language],
|
||||||
|
language_extra: record[names.languageExtra]
|
||||||
|
}
|
||||||
|
await client.createDoc<CustomPerson>(
|
||||||
|
contact.class.Person,
|
||||||
|
contact.space.Contacts,
|
||||||
|
cdata,
|
||||||
|
candidateId as unknown as Ref<CustomPerson>
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.createMixin<Contact, Candidate>(
|
||||||
|
candidateId,
|
||||||
|
contact.class.Person,
|
||||||
|
contact.space.Contacts,
|
||||||
|
recruit.mixin.Candidate,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
await client.createMixin<Contact, KaDetailsTalant>(
|
||||||
|
candidateId,
|
||||||
|
contact.class.Person,
|
||||||
|
contact.space.Contacts,
|
||||||
|
kaDetails,
|
||||||
|
dataKa
|
||||||
|
)
|
||||||
|
await client.createMixin<Contact, PortfolioDetailsTalant>(
|
||||||
|
candidateId,
|
||||||
|
contact.class.Person,
|
||||||
|
contact.space.Contacts,
|
||||||
|
portfolioDetails,
|
||||||
|
dataPortfolio
|
||||||
|
)
|
||||||
|
await client.createMixin<Contact, TechDetailsTalant>(
|
||||||
|
candidateId,
|
||||||
|
contact.class.Person,
|
||||||
|
contact.space.Contacts,
|
||||||
|
techDetails,
|
||||||
|
dataTechStack
|
||||||
|
)
|
||||||
|
await client.createMixin<Contact, LocationDetailsTalant>(
|
||||||
|
candidateId,
|
||||||
|
contact.class.Person,
|
||||||
|
contact.space.Contacts,
|
||||||
|
locationDetails,
|
||||||
|
dataLocationDetails
|
||||||
|
)
|
||||||
|
|
||||||
|
function getValid (...names: string[]): string | undefined {
|
||||||
|
for (const o of names) {
|
||||||
|
const v = record[o]
|
||||||
|
if (v !== undefined && typeof v === 'string' && v.trim().length > 0) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateChannel (value: string | undefined, provider: Ref<ChannelProvider>): Promise<void> {
|
||||||
|
if (value === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const channels = await client.findAll(contact.class.Channel, { attachedTo: candidateId })
|
||||||
|
const emailPr = channels.find((it) => it.value === value)
|
||||||
|
if (emailPr === undefined) {
|
||||||
|
await client.addCollection(
|
||||||
|
contact.class.Channel,
|
||||||
|
contact.space.Contacts,
|
||||||
|
candidateId,
|
||||||
|
contact.class.Person,
|
||||||
|
'channels',
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
provider
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateChannel(
|
||||||
|
getValid(record, names.workEmail, names.homeEmail, names.newsletterEmail, names.otherEmail),
|
||||||
|
contact.channelProvider.Email
|
||||||
|
)
|
||||||
|
await updateChannel(getValid(record, names.webSite), contact.channelProvider.Homepage)
|
||||||
|
await updateChannel(getValid(record, names.phone, names.phoneNumber), contact.channelProvider.Phone)
|
||||||
|
await updateChannel(getValid(record, names.telegram), contact.channelProvider.Telegram)
|
||||||
|
|
||||||
|
const ghval = getValid(record, names.githubPortfolio)
|
||||||
|
if (ghval?.includes('https://github.com') ?? false) {
|
||||||
|
await updateChannel(ghval, contact.channelProvider.GitHub)
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = getValid(record, names.profile)
|
||||||
|
if (profile?.includes('linkedin.com') ?? false) {
|
||||||
|
await updateChannel(profile, contact.channelProvider.LinkedIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resume = record[names.resume] as string
|
||||||
|
|
||||||
|
if (resume !== undefined) {
|
||||||
|
const resumes = resume.split(',')
|
||||||
|
for (const r of resumes) {
|
||||||
|
try {
|
||||||
|
const url = (r ?? '').trim()
|
||||||
|
if (url.startsWith('http')) {
|
||||||
|
const lastpos = url.lastIndexOf('/')
|
||||||
|
const fname = url.substring(lastpos + 1)
|
||||||
|
|
||||||
|
const buffer = await got(url).buffer()
|
||||||
|
const blobId = (candidateId + '_' + generateId()) as Ref<BlobData>
|
||||||
|
const type = mimetypes.contentType(fname)
|
||||||
|
const data: BlobData = {
|
||||||
|
_id: blobId,
|
||||||
|
space: contact.space.Contacts,
|
||||||
|
modifiedBy: client.txFactory.account,
|
||||||
|
modifiedOn: Date.now(),
|
||||||
|
_class: core.class.BlobData,
|
||||||
|
name: fname,
|
||||||
|
size: buffer.length,
|
||||||
|
type: type !== false ? type : 'unknown',
|
||||||
|
base64Data: buffer.toString('base64')
|
||||||
|
}
|
||||||
|
await connection.upload(DOMAIN_BLOB, [data])
|
||||||
|
|
||||||
|
await client.addCollection(
|
||||||
|
attachment.class.Attachment,
|
||||||
|
contact.space.Contacts,
|
||||||
|
candidateId,
|
||||||
|
contact.class.Person,
|
||||||
|
'attachments',
|
||||||
|
{
|
||||||
|
file: blobId,
|
||||||
|
name: fname,
|
||||||
|
size: buffer.length,
|
||||||
|
type: type !== false ? type : 'unknown',
|
||||||
|
lastModified: Date.now()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const doc = await client.findOne(recruit.mixin.Candidate, { _id: candidateId })
|
||||||
|
if (doc === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (type === 'application/pdf') {
|
||||||
|
const document = await recognize(rekoniUrl, data.base64Data, token)
|
||||||
|
if (document !== undefined) {
|
||||||
|
if (document.title !== undefined) {
|
||||||
|
await client.update(doc, { title: document.title })
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateAvatar(doc, document, connection, client)
|
||||||
|
|
||||||
|
// Update contact
|
||||||
|
await updateContacts(client, doc, document)
|
||||||
|
|
||||||
|
// Update skills
|
||||||
|
await updateSkills(client, doc, document)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateAvatar (
|
||||||
|
c: WithLookup<Candidate>,
|
||||||
|
document: ReconiDocument,
|
||||||
|
connection: Client & BackupClient,
|
||||||
|
client: TxOperations
|
||||||
|
): Promise<void> {
|
||||||
|
if (document.format !== 'headhunter' && document.format !== 'podbor') {
|
||||||
|
// Only update avatar for this kind of resume formats.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
c.avatar === undefined &&
|
||||||
|
document.avatar !== undefined &&
|
||||||
|
document.avatarName !== undefined &&
|
||||||
|
document.avatarFormat !== undefined
|
||||||
|
) {
|
||||||
|
const attachId = `${c._id}.${document.avatarName}` as Ref<Attachment>
|
||||||
|
// Upload new avatar for candidate
|
||||||
|
const data = Buffer.from(document.avatar, 'base64')
|
||||||
|
|
||||||
|
const bdata: BlobData = {
|
||||||
|
_id: attachId as unknown as Ref<BlobData>,
|
||||||
|
space: contact.space.Contacts,
|
||||||
|
modifiedBy: client.txFactory.account,
|
||||||
|
modifiedOn: Date.now(),
|
||||||
|
_class: core.class.BlobData,
|
||||||
|
name: document.avatarName,
|
||||||
|
size: data.length,
|
||||||
|
type: document.avatarFormat,
|
||||||
|
base64Data: document.avatar
|
||||||
|
}
|
||||||
|
await connection.upload(DOMAIN_BLOB, [bdata])
|
||||||
|
|
||||||
|
await findOrUpdateAttached<Attachment>(
|
||||||
|
client,
|
||||||
|
contact.space.Contacts,
|
||||||
|
attachment.class.Photo,
|
||||||
|
attachId,
|
||||||
|
{
|
||||||
|
name: document.avatarName,
|
||||||
|
file: attachId,
|
||||||
|
type: document.avatarFormat,
|
||||||
|
size: data.length,
|
||||||
|
lastModified: Date.now()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attachedTo: c._id,
|
||||||
|
attachedClass: contact.class.Person,
|
||||||
|
collection: 'photos'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await client.update(c, { avatar: attachId })
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,7 @@ export interface FieldType {
|
|||||||
name: string
|
name: string
|
||||||
type?: Ref<Class<Doc>>
|
type?: Ref<Class<Doc>>
|
||||||
label?: IntlString
|
label?: IntlString
|
||||||
|
sourceClass?: Ref<Class<Doc>>
|
||||||
enumName?: string
|
enumName?: string
|
||||||
fName?: string
|
fName?: string
|
||||||
}
|
}
|
@ -36,11 +36,12 @@ import toolPlugin, { prepareTools, version } from '@anticrm/server-tool'
|
|||||||
import { program } from 'commander'
|
import { program } from 'commander'
|
||||||
import { Db, MongoClient } from 'mongodb'
|
import { Db, MongoClient } from 'mongodb'
|
||||||
import { exit } from 'process'
|
import { exit } from 'process'
|
||||||
|
import { removeDuplicates } from './csv/duplicates'
|
||||||
|
import { importLead } from './csv/lead-importer'
|
||||||
|
import { importLead2 } from './csv/lead-importer2'
|
||||||
|
import { importTalants } from './csv/talant-importer'
|
||||||
import { rebuildElastic } from './elastic'
|
import { rebuildElastic } from './elastic'
|
||||||
import { importXml } from './importer'
|
import { importXml } from './importer'
|
||||||
import { removeDuplicates } from './leads/duplicates'
|
|
||||||
import { importLead } from './leads/lead-importer'
|
|
||||||
import { importLead2 } from './leads/lead-importer2'
|
|
||||||
import { updateCandidates } from './recruit'
|
import { updateCandidates } from './recruit'
|
||||||
import { clearTelegramHistory } from './telegram'
|
import { clearTelegramHistory } from './telegram'
|
||||||
import { diffWorkspace, dumpWorkspace, restoreWorkspace } from './workspace'
|
import { diffWorkspace, dumpWorkspace, restoreWorkspace } from './workspace'
|
||||||
@ -374,6 +375,18 @@ program
|
|||||||
return await importLead2(transactorUrl, workspace, fileName)
|
return await importLead2(transactorUrl, workspace, fileName)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('import-talant-csv <workspace> <fileName>')
|
||||||
|
.description('Import Talant csv')
|
||||||
|
.action(async (workspace, fileName, cmd) => {
|
||||||
|
const rekoniUrl = process.env.REKONI_URL
|
||||||
|
if (rekoniUrl === undefined) {
|
||||||
|
console.log('Please provide REKONI_URL environment variable')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
return await importTalants(transactorUrl, workspace, fileName, rekoniUrl)
|
||||||
|
})
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('lead-duplicates <workspace>')
|
.command('lead-duplicates <workspace>')
|
||||||
.description('Find and remove duplicate organizations.')
|
.description('Find and remove duplicate organizations.')
|
||||||
|
@ -30,7 +30,7 @@ import { ElasticTool } from './elastic'
|
|||||||
import { findOrUpdateAttached } from './utils'
|
import { findOrUpdateAttached } from './utils'
|
||||||
import { readMinioData } from './workspace'
|
import { readMinioData } from './workspace'
|
||||||
|
|
||||||
async function recognize (rekoniUrl: string, data: string, token: string): Promise<ReconiDocument | undefined> {
|
export async function recognize (rekoniUrl: string, data: string, token: string): Promise<ReconiDocument | undefined> {
|
||||||
const { body }: { body?: ReconiDocument } = await got.post(rekoniUrl + '/recognize?format=pdf', {
|
const { body }: { body?: ReconiDocument } = await got.post(rekoniUrl + '/recognize?format=pdf', {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Bearer ' + token,
|
Authorization: 'Bearer ' + token,
|
||||||
@ -45,14 +45,14 @@ async function recognize (rekoniUrl: string, data: string, token: string): Promi
|
|||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUndef (value?: string): boolean {
|
export function isUndef (value?: string): boolean {
|
||||||
if (value == null || value.trim().length === 0) {
|
if (value == null || value.trim().length === 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addChannel (
|
export async function addChannel (
|
||||||
client: TxOperations,
|
client: TxOperations,
|
||||||
channels: Channel[],
|
channels: Channel[],
|
||||||
c: Candidate,
|
c: Candidate,
|
||||||
@ -149,7 +149,7 @@ export async function updateCandidates (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSkills (client: TxOperations, c: Candidate, document: ReconiDocument): Promise<void> {
|
export async function updateSkills (client: TxOperations, c: Candidate, document: ReconiDocument): Promise<void> {
|
||||||
const skills = await client.findAll(tags.class.TagReference, { attachedTo: c._id })
|
const skills = await client.findAll(tags.class.TagReference, { attachedTo: c._id })
|
||||||
const namedSkills = new Set(Array.from(skills.map((it) => it.title.toLowerCase())))
|
const namedSkills = new Set(Array.from(skills.map((it) => it.title.toLowerCase())))
|
||||||
|
|
||||||
@ -185,7 +185,11 @@ async function updateSkills (client: TxOperations, c: Candidate, document: Recon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function updateContacts (client: TxOperations, c: WithLookup<Candidate>, document: ReconiDocument): Promise<void> {
|
export async function updateContacts (
|
||||||
|
client: TxOperations,
|
||||||
|
c: WithLookup<Candidate>,
|
||||||
|
document: ReconiDocument
|
||||||
|
): Promise<void> {
|
||||||
const channels = await client.findAll(contact.class.Channel, { attachedTo: c._id })
|
const channels = await client.findAll(contact.class.Channel, { attachedTo: c._id })
|
||||||
await addChannel(client, channels, c, contact.channelProvider.Email, document.email)
|
await addChannel(client, channels, c, contact.channelProvider.Email, document.email)
|
||||||
await addChannel(client, channels, c, contact.channelProvider.GitHub, document.github)
|
await addChannel(client, channels, c, contact.channelProvider.GitHub, document.github)
|
||||||
|
@ -29,7 +29,18 @@ import {
|
|||||||
} from '@anticrm/contact'
|
} from '@anticrm/contact'
|
||||||
import type { Class, Domain, Ref, Timestamp } from '@anticrm/core'
|
import type { Class, Domain, Ref, Timestamp } from '@anticrm/core'
|
||||||
import { DOMAIN_MODEL, IndexKind } from '@anticrm/core'
|
import { DOMAIN_MODEL, IndexKind } from '@anticrm/core'
|
||||||
import { Builder, Collection, Index, Model, Prop, TypeRef, TypeString, TypeTimestamp, UX } from '@anticrm/model'
|
import {
|
||||||
|
Builder,
|
||||||
|
Collection,
|
||||||
|
Index,
|
||||||
|
Model,
|
||||||
|
Prop,
|
||||||
|
TypeDate,
|
||||||
|
TypeRef,
|
||||||
|
TypeString,
|
||||||
|
TypeTimestamp,
|
||||||
|
UX
|
||||||
|
} from '@anticrm/model'
|
||||||
import attachment from '@anticrm/model-attachment'
|
import attachment from '@anticrm/model-attachment'
|
||||||
import chunter from '@anticrm/model-chunter'
|
import chunter from '@anticrm/model-chunter'
|
||||||
import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
|
import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
|
||||||
@ -94,7 +105,10 @@ export class TChannel extends TAttachedDoc implements Channel {
|
|||||||
|
|
||||||
@Model(contact.class.Person, contact.class.Contact)
|
@Model(contact.class.Person, contact.class.Contact)
|
||||||
@UX(contact.string.Person, contact.icon.Person, undefined, 'name')
|
@UX(contact.string.Person, contact.icon.Person, undefined, 'name')
|
||||||
export class TPerson extends TContact implements Person {}
|
export class TPerson extends TContact implements Person {
|
||||||
|
@Prop(TypeDate(), contact.string.Birthday)
|
||||||
|
birthday?: Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
@Model(contact.class.Member, core.class.AttachedDoc, DOMAIN_CONTACT)
|
@Model(contact.class.Member, core.class.AttachedDoc, DOMAIN_CONTACT)
|
||||||
@UX(contact.string.Member, contact.icon.Person, undefined, 'name')
|
@UX(contact.string.Member, contact.icon.Person, undefined, 'name')
|
||||||
|
@ -65,7 +65,8 @@ export default mergeIds(contactId, contact, {
|
|||||||
GitHub: '' as IntlString,
|
GitHub: '' as IntlString,
|
||||||
Facebook: '' as IntlString,
|
Facebook: '' as IntlString,
|
||||||
TypeLabel: '' as IntlString,
|
TypeLabel: '' as IntlString,
|
||||||
Homepage: '' as IntlString
|
Homepage: '' as IntlString,
|
||||||
|
Birthday: '' as IntlString
|
||||||
},
|
},
|
||||||
completion: {
|
completion: {
|
||||||
PersonQuery: '' as Resource<ObjectSearchFactory>,
|
PersonQuery: '' as Resource<ObjectSearchFactory>,
|
||||||
|
@ -732,7 +732,7 @@ export function createModel (builder: Builder): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, {
|
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, {
|
||||||
filters: ['status', 'priority', 'assignee', 'project', 'dueDate', 'modifiedOn']
|
filters: ['status', 'priority', 'assignee', 'project', 'sprint', 'dueDate', 'modifiedOn']
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.createDoc(
|
builder.createDoc(
|
||||||
|
@ -68,6 +68,7 @@
|
|||||||
"KickEmployeeDescr": "Are you sure you want to kick the employee out of the workspace? This action cannot be undone",
|
"KickEmployeeDescr": "Are you sure you want to kick the employee out of the workspace? This action cannot be undone",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"CreateEmployee": "Create an employee",
|
"CreateEmployee": "Create an employee",
|
||||||
"Inactive": "Inactive"
|
"Inactive": "Inactive",
|
||||||
|
"Birthday": "Birthday"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -68,6 +68,7 @@
|
|||||||
"KickEmployeeDescr": "Вы действительно хотите выгнать сотрудника из рабочего пространства? Это действие нельзя отменить",
|
"KickEmployeeDescr": "Вы действительно хотите выгнать сотрудника из рабочего пространства? Это действие нельзя отменить",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"CreateEmployee": "Создать сотрудника",
|
"CreateEmployee": "Создать сотрудника",
|
||||||
"Inactive": "Не активный"
|
"Inactive": "Не активный",
|
||||||
|
"Birthday": "День рождения"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -76,7 +76,9 @@ export interface Contact extends Doc {
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface Person extends Contact {}
|
export interface Person extends Contact {
|
||||||
|
birthday?: Timestamp | null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
|
@ -168,6 +168,7 @@
|
|||||||
<table class="antiTable">
|
<table class="antiTable">
|
||||||
<thead class="scroller-thead">
|
<thead class="scroller-thead">
|
||||||
<tr class="scroller-thead__tr">
|
<tr class="scroller-thead__tr">
|
||||||
|
<!-- <th>Field name</th> -->
|
||||||
<th>
|
<th>
|
||||||
<div class="antiTable-cells">
|
<div class="antiTable-cells">
|
||||||
<Label label={settings.string.Attribute} />
|
<Label label={settings.string.Attribute} />
|
||||||
@ -197,6 +198,9 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<!-- <td class='select-text'>
|
||||||
|
{attr.name}
|
||||||
|
</td> -->
|
||||||
<td>
|
<td>
|
||||||
<div class="antiTable-cells__firstCell">
|
<div class="antiTable-cells__firstCell">
|
||||||
<Label label={attr.label} />
|
<Label label={attr.label} />
|
||||||
@ -207,7 +211,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="select-text">
|
||||||
<Label label={attr.type.label} />
|
<Label label={attr.type.label} />
|
||||||
{#if attrType !== undefined}
|
{#if attrType !== undefined}
|
||||||
: <Label label={attrType} />
|
: <Label label={attrType} />
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
const data: Data<AnyAttribute> = {
|
const data: Data<AnyAttribute> = {
|
||||||
attributeOf: _class,
|
attributeOf: _class,
|
||||||
name: name + generateId(),
|
name: name.trim().replace('/', '').replace(' ', '') + '_' + generateId(),
|
||||||
label: getEmbeddedLabel(name),
|
label: getEmbeddedLabel(name),
|
||||||
isCustom: true,
|
isCustom: true,
|
||||||
type
|
type
|
||||||
|
@ -16,10 +16,6 @@
|
|||||||
import { WithLookup } from '@anticrm/core'
|
import { WithLookup } from '@anticrm/core'
|
||||||
import { Issue, IssueStatus } from '@anticrm/tracker'
|
import { Issue, IssueStatus } from '@anticrm/tracker'
|
||||||
import { showPopup } from '@anticrm/ui'
|
import { showPopup } from '@anticrm/ui'
|
||||||
import StatusFilterMenuSection from './StatusFilterMenuSection.svelte'
|
|
||||||
import PriorityFilterMenuSection from './PriorityFilterMenuSection.svelte'
|
|
||||||
import ProjectFilterMenuSection from './ProjectFilterMenuSection.svelte'
|
|
||||||
import FilterMenu from '../FilterMenu.svelte'
|
|
||||||
import {
|
import {
|
||||||
defaultPriorities,
|
defaultPriorities,
|
||||||
FilterAction,
|
FilterAction,
|
||||||
@ -27,6 +23,11 @@
|
|||||||
getIssueFilterAssetsByType,
|
getIssueFilterAssetsByType,
|
||||||
IssueFilter
|
IssueFilter
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
import FilterMenu from '../FilterMenu.svelte'
|
||||||
|
import PriorityFilterMenuSection from './PriorityFilterMenuSection.svelte'
|
||||||
|
import ProjectFilterMenuSection from './ProjectFilterMenuSection.svelte'
|
||||||
|
import SprintFilterMenuSection from './SprintFilterMenuSection.svelte'
|
||||||
|
import StatusFilterMenuSection from './StatusFilterMenuSection.svelte'
|
||||||
|
|
||||||
export let targetHtml: HTMLElement
|
export let targetHtml: HTMLElement
|
||||||
export let filters: IssueFilter[] = []
|
export let filters: IssueFilter[] = []
|
||||||
@ -42,6 +43,7 @@
|
|||||||
$: groupedByStatus = getGroupedIssues('status', issues, defaultStatusIds)
|
$: groupedByStatus = getGroupedIssues('status', issues, defaultStatusIds)
|
||||||
$: groupedByPriority = getGroupedIssues('priority', issues, defaultPriorities)
|
$: groupedByPriority = getGroupedIssues('priority', issues, defaultPriorities)
|
||||||
$: groupedByProject = getGroupedIssues('project', issues)
|
$: groupedByProject = getGroupedIssues('project', issues)
|
||||||
|
$: groupedBySprint = getGroupedIssues('sprint', issues)
|
||||||
|
|
||||||
const handleStatusFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
const handleStatusFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||||
const statusGroups: { [key: string]: number } = {}
|
const statusGroups: { [key: string]: number } = {}
|
||||||
@ -103,6 +105,25 @@
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSprintFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||||
|
const sprintGroups: { [key: string]: number } = {}
|
||||||
|
|
||||||
|
for (const [project, value] of Object.entries(groupedBySprint)) {
|
||||||
|
sprintGroups[project] = value?.length ?? 0
|
||||||
|
}
|
||||||
|
showPopup(
|
||||||
|
SprintFilterMenuSection,
|
||||||
|
{
|
||||||
|
groups: sprintGroups,
|
||||||
|
selectedElements: currentFilterQuery?.sprint?.[currentFilterMode] ?? [],
|
||||||
|
index,
|
||||||
|
onUpdate,
|
||||||
|
onBack
|
||||||
|
},
|
||||||
|
targetHtml
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const actions: FilterAction[] = [
|
const actions: FilterAction[] = [
|
||||||
{
|
{
|
||||||
...getIssueFilterAssetsByType('status'),
|
...getIssueFilterAssetsByType('status'),
|
||||||
@ -115,6 +136,10 @@
|
|||||||
{
|
{
|
||||||
...getIssueFilterAssetsByType('project'),
|
...getIssueFilterAssetsByType('project'),
|
||||||
onSelect: handleProjectFilterMenuSectionOpened
|
onSelect: handleProjectFilterMenuSectionOpened
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...getIssueFilterAssetsByType('sprint'),
|
||||||
|
onSelect: handleSprintFilterMenuSectionOpened
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
@ -131,6 +131,7 @@
|
|||||||
lookup: {
|
lookup: {
|
||||||
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }],
|
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }],
|
||||||
project: tracker.class.Project,
|
project: tracker.class.Project,
|
||||||
|
sprint: tracker.class.Sprint,
|
||||||
assignee: contact.class.Employee
|
assignee: contact.class.Employee
|
||||||
},
|
},
|
||||||
sort: issuesGroupBySorting[groupBy]
|
sort: issuesGroupBySorting[groupBy]
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
<!--
|
||||||
|
// 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 { translate } from '@anticrm/platform'
|
||||||
|
import { IconNavPrev } from '@anticrm/ui'
|
||||||
|
import FilterMenuSection from '../FilterMenuSection.svelte'
|
||||||
|
import tracker from '../../plugin'
|
||||||
|
import { FilterSectionElement } from '../../utils'
|
||||||
|
import { getClient } from '@anticrm/presentation'
|
||||||
|
|
||||||
|
export let selectedElements: any[] = []
|
||||||
|
export let groups: { [key: string]: number }
|
||||||
|
export let index: number = 0
|
||||||
|
export let onUpdate: (result: { [p: string]: any }, filterIndex?: number) => void
|
||||||
|
export let onBack: (() => void) | undefined = undefined
|
||||||
|
|
||||||
|
const getFilterElements = async (groups: { [key: string]: number }, selected: any[]) => {
|
||||||
|
const elements: FilterSectionElement[] = []
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const sprints = await client.findAll(tracker.class.Sprint, {})
|
||||||
|
for (const [key, value] of Object.entries(groups)) {
|
||||||
|
const sprint = key === 'null' ? null : key
|
||||||
|
const label = sprint
|
||||||
|
? sprints.find(({ _id }) => _id === sprint)?.label
|
||||||
|
: await translate(tracker.string.NoSprint, {})
|
||||||
|
|
||||||
|
if (!label) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elements.splice(sprint ? 1 : 0, 0, {
|
||||||
|
icon: tracker.icon.Sprint,
|
||||||
|
title: label,
|
||||||
|
count: value,
|
||||||
|
isSelected: selected.includes(sprint),
|
||||||
|
onSelect: () => onUpdate({ sprint: sprint }, index)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return onBack
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
icon: IconNavPrev,
|
||||||
|
title: await translate(tracker.string.Back, {}),
|
||||||
|
onSelect: onBack
|
||||||
|
},
|
||||||
|
...elements
|
||||||
|
]
|
||||||
|
: elements
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await getFilterElements(groups, selectedElements) then actions}
|
||||||
|
<FilterMenuSection {actions} {onBack} on:close />
|
||||||
|
{/await}
|
@ -39,6 +39,7 @@ export const issuesGroupByOptions: Record<IssuesGrouping, IntlString> = {
|
|||||||
[IssuesGrouping.Assignee]: tracker.string.Assignee,
|
[IssuesGrouping.Assignee]: tracker.string.Assignee,
|
||||||
[IssuesGrouping.Priority]: tracker.string.Priority,
|
[IssuesGrouping.Priority]: tracker.string.Priority,
|
||||||
[IssuesGrouping.Project]: tracker.string.Project,
|
[IssuesGrouping.Project]: tracker.string.Project,
|
||||||
|
[IssuesGrouping.Sprint]: tracker.string.Sprint,
|
||||||
[IssuesGrouping.NoGrouping]: tracker.string.NoGrouping
|
[IssuesGrouping.NoGrouping]: tracker.string.NoGrouping
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,5 +101,6 @@ export const issuesGroupBySorting: Record<IssuesGrouping, SortingQuery<Issue>> =
|
|||||||
[IssuesGrouping.Assignee]: { '$lookup.assignee.name': SortingOrder.Ascending },
|
[IssuesGrouping.Assignee]: { '$lookup.assignee.name': SortingOrder.Ascending },
|
||||||
[IssuesGrouping.Priority]: { priority: SortingOrder.Ascending },
|
[IssuesGrouping.Priority]: { priority: SortingOrder.Ascending },
|
||||||
[IssuesGrouping.Project]: { '$lookup.project.label': SortingOrder.Ascending },
|
[IssuesGrouping.Project]: { '$lookup.project.label': SortingOrder.Ascending },
|
||||||
|
[IssuesGrouping.Sprint]: { '$lookup.sprint.label': SortingOrder.Ascending },
|
||||||
[IssuesGrouping.NoGrouping]: {}
|
[IssuesGrouping.NoGrouping]: {}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ export interface Selection {
|
|||||||
currentSpecial?: string
|
currentSpecial?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee' | 'project'>
|
export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee' | 'project' | 'sprint'>
|
||||||
export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate' | 'rank'>
|
export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate' | 'rank'>
|
||||||
|
|
||||||
export const issuesGroupKeyMap: Record<IssuesGrouping, IssuesGroupByKeys | undefined> = {
|
export const issuesGroupKeyMap: Record<IssuesGrouping, IssuesGroupByKeys | undefined> = {
|
||||||
@ -59,6 +59,7 @@ export const issuesGroupKeyMap: Record<IssuesGrouping, IssuesGroupByKeys | undef
|
|||||||
[IssuesGrouping.Priority]: 'priority',
|
[IssuesGrouping.Priority]: 'priority',
|
||||||
[IssuesGrouping.Assignee]: 'assignee',
|
[IssuesGrouping.Assignee]: 'assignee',
|
||||||
[IssuesGrouping.Project]: 'project',
|
[IssuesGrouping.Project]: 'project',
|
||||||
|
[IssuesGrouping.Sprint]: 'sprint',
|
||||||
[IssuesGrouping.NoGrouping]: undefined
|
[IssuesGrouping.NoGrouping]: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,10 +79,11 @@ export const issuesSortOrderMap: Record<IssuesOrderByKeys, SortingOrder> = {
|
|||||||
rank: SortingOrder.Ascending
|
rank: SortingOrder.Ascending
|
||||||
}
|
}
|
||||||
|
|
||||||
export const issuesGroupEditorMap: Record<'status' | 'priority' | 'project', AnyComponent | undefined> = {
|
export const issuesGroupEditorMap: Record<'status' | 'priority' | 'project' | 'sprint', AnyComponent | undefined> = {
|
||||||
status: tracker.component.StatusEditor,
|
status: tracker.component.StatusEditor,
|
||||||
priority: tracker.component.PriorityEditor,
|
priority: tracker.component.PriorityEditor,
|
||||||
project: tracker.component.ProjectEditor
|
project: tracker.component.ProjectEditor,
|
||||||
|
sprint: tracker.component.SprintEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getIssuesModificationDatePeriodTime = (period: IssuesDateModificationPeriod | null): number => {
|
export const getIssuesModificationDatePeriodTime = (period: IssuesDateModificationPeriod | null): number => {
|
||||||
@ -180,6 +182,12 @@ export const getIssueFilterAssetsByType = (type: string): { icon: Asset, label:
|
|||||||
label: tracker.string.Project
|
label: tracker.string.Project
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'sprint': {
|
||||||
|
return {
|
||||||
|
icon: tracker.icon.Sprint,
|
||||||
|
label: tracker.string.Sprint
|
||||||
|
}
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@ -447,6 +455,21 @@ export async function getKanbanStatuses (
|
|||||||
]
|
]
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
if (groupBy === IssuesGrouping.Sprint) {
|
||||||
|
const noSprint = await translate(tracker.string.NoSprint, {})
|
||||||
|
return issues.reduce<TypeState[]>((result, issue) => {
|
||||||
|
if (result.find(({ _id }) => _id === issue.sprint) !== undefined) return result
|
||||||
|
return [
|
||||||
|
...result,
|
||||||
|
{
|
||||||
|
_id: issue.sprint,
|
||||||
|
title: issue.$lookup?.sprint?.label ?? noSprint,
|
||||||
|
color: UNSET_COLOR,
|
||||||
|
icon: undefined
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -481,6 +504,7 @@ export function getDefaultViewOptionsConfig (): ViewOptionModel[] {
|
|||||||
{ id: 'assignee', label: tracker.string.Assignee },
|
{ id: 'assignee', label: tracker.string.Assignee },
|
||||||
{ id: 'priority', label: tracker.string.Priority },
|
{ id: 'priority', label: tracker.string.Priority },
|
||||||
{ id: 'project', label: tracker.string.Project },
|
{ id: 'project', label: tracker.string.Project },
|
||||||
|
{ id: 'sprint', label: tracker.string.Sprint },
|
||||||
{ id: 'noGrouping', label: tracker.string.NoGrouping }
|
{ id: 'noGrouping', label: tracker.string.NoGrouping }
|
||||||
],
|
],
|
||||||
type: 'dropdown'
|
type: 'dropdown'
|
||||||
|
@ -73,6 +73,7 @@ export enum IssuesGrouping {
|
|||||||
Assignee = 'assignee',
|
Assignee = 'assignee',
|
||||||
Priority = 'priority',
|
Priority = 'priority',
|
||||||
Project = 'project',
|
Project = 'project',
|
||||||
|
Sprint = 'sprint',
|
||||||
NoGrouping = 'noGrouping'
|
NoGrouping = 'noGrouping'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Class, ClassifierKind, Doc, Mixin, Ref } from '@anticrm/core'
|
import { Class, ClassifierKind, Doc, Mixin, Ref } from '@anticrm/core'
|
||||||
import { getClient } from '@anticrm/presentation'
|
import { getClient } from '@anticrm/presentation'
|
||||||
|
import setting from '@anticrm/setting'
|
||||||
import { Label } from '@anticrm/ui'
|
import { Label } from '@anticrm/ui'
|
||||||
import { getMixinStyle } from '../utils'
|
import { getMixinStyle } from '../utils'
|
||||||
|
|
||||||
@ -41,7 +42,12 @@
|
|||||||
|
|
||||||
mixins = hierarchy
|
mixins = hierarchy
|
||||||
.getDescendants(parentClass)
|
.getDescendants(parentClass)
|
||||||
.filter((m) => hierarchy.getClass(m).kind === ClassifierKind.MIXIN && hierarchy.hasMixin(value, m))
|
.filter(
|
||||||
|
(m) =>
|
||||||
|
hierarchy.getClass(m).kind === ClassifierKind.MIXIN &&
|
||||||
|
hierarchy.hasMixin(value, m) &&
|
||||||
|
!hierarchy.hasMixin(hierarchy.getClass(m), setting.mixin.UserMixin)
|
||||||
|
)
|
||||||
.map((m) => hierarchy.getClass(m) as Mixin<Doc>)
|
.map((m) => hierarchy.getClass(m) as Mixin<Doc>)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -111,45 +111,53 @@
|
|||||||
const newValue = await result(filter, () => {
|
const newValue = await result(filter, () => {
|
||||||
makeQuery(query, filters)
|
makeQuery(query, filters)
|
||||||
})
|
})
|
||||||
if (newQuery[filter.key.key] === undefined) {
|
|
||||||
newQuery[filter.key.key] = newValue
|
let filterKey = filter.key.key
|
||||||
|
|
||||||
|
const attr = client.getHierarchy().getAttribute(filter.key._class, filter.key.key)
|
||||||
|
if (client.getHierarchy().isMixin(attr.attributeOf)) {
|
||||||
|
filterKey = attr.attributeOf + '.' + filter.key.key
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newQuery[filterKey] === undefined) {
|
||||||
|
newQuery[filterKey] = newValue
|
||||||
} else {
|
} else {
|
||||||
let merged = false
|
let merged = false
|
||||||
for (const key in newValue) {
|
for (const key in newValue) {
|
||||||
if (newQuery[filter.key.key][key] === undefined) {
|
if (newQuery[filterKey][key] === undefined) {
|
||||||
if (key === '$in' && typeof newQuery[filter.key.key] === 'string') {
|
if (key === '$in' && typeof newQuery[filterKey] === 'string') {
|
||||||
newQuery[filter.key.key] = { $in: newValue[key].filter((p: any) => p === newQuery[filter.key.key]) }
|
newQuery[filterKey] = { $in: newValue[key].filter((p: any) => p === newQuery[filterKey]) }
|
||||||
} else {
|
} else {
|
||||||
newQuery[filter.key.key][key] = newValue[key]
|
newQuery[filterKey][key] = newValue[key]
|
||||||
}
|
}
|
||||||
merged = true
|
merged = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (key === '$in') {
|
if (key === '$in') {
|
||||||
newQuery[filter.key.key][key] = newQuery[filter.key.key][key].filter((p: any) => newValue[key].includes(p))
|
newQuery[filterKey][key] = newQuery[filterKey][key].filter((p: any) => newValue[key].includes(p))
|
||||||
merged = true
|
merged = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (key === '$nin') {
|
if (key === '$nin') {
|
||||||
newQuery[filter.key.key][key] = [...newQuery[filter.key.key][key], ...newValue[key]]
|
newQuery[filterKey][key] = [...newQuery[filterKey][key], ...newValue[key]]
|
||||||
merged = true
|
merged = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (key === '$lt') {
|
if (key === '$lt') {
|
||||||
newQuery[filter.key.key][key] =
|
newQuery[filterKey][key] =
|
||||||
newQuery[filter.key.key][key] < newValue[key] ? newQuery[filter.key.key][key] : newValue[key]
|
newQuery[filterKey][key] < newValue[key] ? newQuery[filterKey][key] : newValue[key]
|
||||||
merged = true
|
merged = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (key === '$gt') {
|
if (key === '$gt') {
|
||||||
newQuery[filter.key.key][key] =
|
newQuery[filterKey][key] =
|
||||||
newQuery[filter.key.key][key] > newValue[key] ? newQuery[filter.key.key][key] : newValue[key]
|
newQuery[filterKey][key] > newValue[key] ? newQuery[filterKey][key] : newValue[key]
|
||||||
merged = true
|
merged = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!merged) {
|
if (!merged) {
|
||||||
Object.assign(newQuery[filter.key.key], newValue)
|
Object.assign(newQuery[filterKey], newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
function getTargetClass (): Ref<Class<Doc>> | undefined {
|
function getTargetClass (): Ref<Class<Doc>> | undefined {
|
||||||
try {
|
try {
|
||||||
return (hierarchy.getAttribute(_class, filter.key.key).type as RefTo<Doc>).to
|
return (hierarchy.getAttribute(filter.key._class, filter.key.key).type as RefTo<Doc>).to
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
const mixin = hierarchy.as(clazz, view.mixin.ClassFilters)
|
const mixin = hierarchy.as(clazz, view.mixin.ClassFilters)
|
||||||
if (mixin.filters === undefined) return []
|
if (mixin.filters === undefined) return []
|
||||||
const filters = mixin.filters.map((p) => {
|
const filters = mixin.filters.map((p) => {
|
||||||
return typeof p === 'string' ? buildFilterFromKey(p) : p
|
return typeof p === 'string' ? buildFilterFromKey(_class, p) : p
|
||||||
})
|
})
|
||||||
const result: KeyFilter[] = []
|
const result: KeyFilter[] = []
|
||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
@ -44,12 +44,12 @@
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFilterFromKey (key: string): KeyFilter | undefined {
|
function buildFilterFromKey (_class: Ref<Class<Doc>>, key: string): KeyFilter | undefined {
|
||||||
const attribute = hierarchy.getAttribute(_class, key)
|
const attribute = hierarchy.getAttribute(_class, key)
|
||||||
return buildFilter(key, attribute)
|
return buildFilter(_class, key, attribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFilter (key: string, attribute: AnyAttribute): KeyFilter | undefined {
|
function buildFilter (_class: Ref<Class<Doc>>, key: string, attribute: AnyAttribute): KeyFilter | undefined {
|
||||||
const isCollection = hierarchy.isDerived(attribute.type._class, core.class.Collection)
|
const isCollection = hierarchy.isDerived(attribute.type._class, core.class.Collection)
|
||||||
const targetClass = isCollection ? (attribute.type as Collection<AttachedDoc>).of : attribute.type._class
|
const targetClass = isCollection ? (attribute.type as Collection<AttachedDoc>).of : attribute.type._class
|
||||||
const clazz = hierarchy.getClass(targetClass)
|
const clazz = hierarchy.getClass(targetClass)
|
||||||
@ -73,17 +73,45 @@
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildFilterForAttr (_class: Ref<Class<Doc>>, attribute: AnyAttribute, result: KeyFilter[]): void {
|
||||||
|
if (attribute.isCustom !== true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (attribute.label === undefined || attribute.hidden) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const value = getValue(attribute.name, attribute.type)
|
||||||
|
if (result.findIndex((p) => p.key === value) !== -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const filter = buildFilter(_class, value, attribute)
|
||||||
|
if (filter !== undefined) {
|
||||||
|
result.push(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function buildFilterFor (
|
||||||
|
_class: Ref<Class<Doc>>,
|
||||||
|
allAttributes: Map<string, AnyAttribute>,
|
||||||
|
result: KeyFilter[]
|
||||||
|
): void {
|
||||||
|
for (const [, attribute] of allAttributes) {
|
||||||
|
buildFilterForAttr(_class, attribute, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getTypes (_class: Ref<Class<Doc>>): KeyFilter[] {
|
function getTypes (_class: Ref<Class<Doc>>): KeyFilter[] {
|
||||||
const result = getFilters(_class)
|
const result = getFilters(_class)
|
||||||
const allAttributes = hierarchy.getAllAttributes(_class)
|
const allAttributes = hierarchy.getAllAttributes(_class)
|
||||||
for (const [, attribute] of allAttributes) {
|
buildFilterFor(_class, allAttributes, result)
|
||||||
if (attribute.isCustom !== true) continue
|
|
||||||
if (attribute.label === undefined || attribute.hidden) continue
|
const desc = hierarchy.getDescendants(_class)
|
||||||
const value = getValue(attribute.name, attribute.type)
|
for (const d of desc) {
|
||||||
if (result.findIndex((p) => p.key === value) !== -1) continue
|
const extra = hierarchy.getAllAttributes(d, _class)
|
||||||
const filter = buildFilter(value, attribute)
|
for (const [k, v] of extra) {
|
||||||
if (filter !== undefined) {
|
if (!allAttributes.has(k)) {
|
||||||
result.push(filter)
|
allAttributes.set(k, v)
|
||||||
|
buildFilterForAttr(d, v, result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const key = { key: filter.key.key }
|
const key = { key: filter.key.key }
|
||||||
const promise = getPresenter(client, _class, key, key)
|
const promise = getPresenter(client, filter.key._class, key, key)
|
||||||
|
|
||||||
let values = new Map<any, number>()
|
let values = new Map<any, number>()
|
||||||
let selectedValues: Set<any> = new Set<any>(filter.value.map((p) => p[0]))
|
let selectedValues: Set<any> = new Set<any>(filter.value.map((p) => p[0]))
|
||||||
@ -53,8 +53,7 @@
|
|||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
let prefix = ''
|
let prefix = ''
|
||||||
|
const attr = client.getHierarchy().getAttribute(filter.key._class, filter.key.key)
|
||||||
const attr = client.getHierarchy().getAttribute(_class, filter.key.key)
|
|
||||||
if (client.getHierarchy().isMixin(attr.attributeOf)) {
|
if (client.getHierarchy().isMixin(attr.attributeOf)) {
|
||||||
prefix = attr.attributeOf + '.'
|
prefix = attr.attributeOf + '.'
|
||||||
console.log('prefix', prefix)
|
console.log('prefix', prefix)
|
||||||
@ -66,7 +65,11 @@
|
|||||||
const res = await objectsPromise
|
const res = await objectsPromise
|
||||||
|
|
||||||
for (const object of res) {
|
for (const object of res) {
|
||||||
const realValue = getObjectValue(filter.key.key, object)
|
let asDoc = object
|
||||||
|
if (client.getHierarchy().isMixin(filter.key._class)) {
|
||||||
|
asDoc = client.getHierarchy().as(object, filter.key._class)
|
||||||
|
}
|
||||||
|
const realValue = getObjectValue(filter.key.key, asDoc)
|
||||||
const value = getValue(realValue)
|
const value = getValue(realValue)
|
||||||
values.set(value, (values.get(value) ?? 0) + 1)
|
values.set(value, (values.get(value) ?? 0) + 1)
|
||||||
realValues.set(value, (realValues.get(value) ?? new Set()).add(realValue))
|
realValues.set(value, (realValues.get(value) ?? new Set()).add(realValue))
|
||||||
|
Loading…
Reference in New Issue
Block a user