Bitrix fixes (#2674)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-02-22 09:13:36 +07:00 committed by GitHub
parent 9a96035b00
commit 4a5532af10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 400 additions and 60 deletions

View File

@ -110,7 +110,8 @@
{:else if node.nodeName === 'S'} {:else if node.nodeName === 'S'}
<s><svelte:self nodes={node.childNodes} /></s> <s><svelte:self nodes={node.childNodes} /></s>
{:else} {:else}
Unknown {node.nodeName} unknown: {node.nodeName}
<svelte:self nodes={node.childNodes} />
{/if} {/if}
{/each} {/each}
{/if} {/if}

View File

@ -25,7 +25,7 @@
import { bitrixQueue } from '../queue' import { bitrixQueue } from '../queue'
import CreateMapping from './CreateMapping.svelte' import CreateMapping from './CreateMapping.svelte'
import EntiryMapping from './EntityMapping.svelte' import EntityMapping from './EntityMapping.svelte'
export let integration: Integration export let integration: Integration
@ -83,7 +83,7 @@
</div> </div>
<div class="flex-row"> <div class="flex-row">
{#each mappings as mapping} {#each mappings as mapping}
<EntiryMapping {mapping} {bitrixClient} {statusList} /> <EntityMapping {mapping} {bitrixClient} {statusList} />
{/each} {/each}
</div> </div>
{/if} {/if}

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { BitrixClient, BitrixEntityMapping, Fields, FieldValue } from '@hcengineering/bitrix' import { BitrixClient, BitrixEntityMapping, Fields, FieldValue } from '@hcengineering/bitrix'
import core, { Enum, Ref } from '@hcengineering/core' import core, { DateRangeMode, Enum, Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform' import { getEmbeddedLabel } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting-resources/src/plugin' import setting from '@hcengineering/setting-resources/src/plugin'
@ -125,7 +125,10 @@
<div class="ml-2"> <div class="ml-2">
{#if value !== null && value !== undefined && value !== ''} {#if value !== null && value !== undefined && value !== ''}
{#if (field.type === 'datetime' || field.type === 'date') && value != null && value !== ''} {#if (field.type === 'datetime' || field.type === 'date') && value != null && value !== ''}
<DatePresenter value={new Date(value).getTime()} withTime={field.type === 'datetime'} /> <DatePresenter
value={new Date(value).getTime()}
mode={field.type === 'datetime' ? DateRangeMode.DATETIME : DateRangeMode.DATE}
/>
{:else if field.type === 'enumeration'} {:else if field.type === 'enumeration'}
{field.items?.find((it) => it.ID === value)?.VALUE} {field.items?.find((it) => it.ID === value)?.VALUE}
{:else} {:else}

View File

@ -3,17 +3,21 @@
BitrixClient, BitrixClient,
BitrixEntityMapping, BitrixEntityMapping,
BitrixFieldMapping, BitrixFieldMapping,
Fields,
performSynchronization, performSynchronization,
StatusValue,
toClassRef toClassRef
} from '@hcengineering/bitrix' } from '@hcengineering/bitrix'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import core, { Class, Doc, Ref, Space, WithLookup } from '@hcengineering/core' import core, { Class, Doc, generateId, Ref, Space, WithLookup } from '@hcengineering/core'
import login from '@hcengineering/login' import login from '@hcengineering/login'
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform' import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
import { getClient, SpaceSelect } from '@hcengineering/presentation' import { getClient, SpaceSelect } from '@hcengineering/presentation'
import { Button, Expandable, Icon, Label } from '@hcengineering/ui' import { Button, Expandable, Icon, IconAdd, IconClose, Label } from '@hcengineering/ui'
import DropdownLabels from '@hcengineering/ui/src/components/DropdownLabels.svelte' import DropdownLabels from '@hcengineering/ui/src/components/DropdownLabels.svelte'
import EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
import { NumberEditor } from '@hcengineering/view-resources' import { NumberEditor } from '@hcengineering/view-resources'
import bitrix from '../plugin'
import FieldMappingPresenter from './FieldMappingPresenter.svelte' import FieldMappingPresenter from './FieldMappingPresenter.svelte'
export let mapping: WithLookup<BitrixEntityMapping> export let mapping: WithLookup<BitrixEntityMapping>
@ -40,6 +44,11 @@
loading = true loading = true
const uploadUrl = (window.location.origin + getMetadata(login.metadata.UploadUrl)) as string const uploadUrl = (window.location.origin + getMetadata(login.metadata.UploadUrl)) as string
const token = (getMetadata(login.metadata.LoginToken) as string) ?? '' const token = (getMetadata(login.metadata.LoginToken) as string) ?? ''
const mappedFilter: Record<string, any> = {}
for (const f of filterFields) {
mappedFilter[f.field] = f.value
}
try { try {
await performSynchronization({ await performSynchronization({
bitrixClient, bitrixClient,
@ -57,7 +66,8 @@
monitor: (total: number) => { monitor: (total: number) => {
docsProcessed++ docsProcessed++
state = `processed: ${docsProcessed}/${total ?? 1}` state = `processed: ${docsProcessed}/${total ?? 1}`
} },
extraFilter: filterFields.length === 0 ? undefined : mappedFilter
}) })
} catch (err: any) { } catch (err: any) {
state = err.message state = err.message
@ -66,6 +76,49 @@
loading = false loading = false
} }
} }
const fieldsKey = bitrix.class.EntityMapping + '.fields.' + mapping._id
let filterFields: { _id: string; field: string; value: string }[] = []
const content = JSON.parse(localStorage.getItem(fieldsKey) ?? '[]')
filterFields = content.filterFields ?? []
limit = content.limit ?? 1
direction = content.direction ?? 'ASC'
$: localStorage.setItem(fieldsKey, JSON.stringify({ limit, filterFields, direction }))
function addFilter (): void {
filterFields = [...filterFields, { _id: generateId(), field: '', value: ' ' }]
}
let fields: Fields = {}
bitrixClient.call(mapping.type + '.fields', {}).then((res) => {
fields = res.result
})
let statusList: StatusValue[] = []
bitrixClient.call('crm.status.list', {}).then((res) => {
statusList = res.result
})
$: items = Object.entries(fields).map((it) => ({
id: it[0],
label: `${it[1].formLabel ?? it[1].title}${it[0].startsWith('UF_') ? ' *' : ''} - ${it[0]}`
}))
function updateFields (fields: Fields, statusList: StatusValue[]): void {
// Update fields with status valies if missing.
for (const f of Object.values(fields)) {
if (f.type === 'crm_status') {
f.items = statusList
.filter((it) => it.ENTITY_ID === f.statusType)
.map((it) => ({ ID: `${it.STATUS_ID}`, VALUE: it.NAME }))
}
}
}
$: updateFields(fields, statusList)
</script> </script>
<Expandable label={getEmbeddedLabel(mapping.type)}> <Expandable label={getEmbeddedLabel(mapping.type)}>
@ -102,6 +155,8 @@
/> />
</div> </div>
<div class="buttons-divider" /> <div class="buttons-divider" />
<Button icon={IconAdd} on:click={addFilter} />
<div class="buttons-divider" />
<div class="flex-row-center"> <div class="flex-row-center">
<div class="p-1"> <div class="p-1">
{state} {state}
@ -131,3 +186,29 @@
{/each} {/each}
</div> </div>
</Expandable> </Expandable>
<div class="flex-row-center">
{#each filterFields as field, pos}
{@const fValue = fields[field.field]}
<div class="item flex-row-center">
<DropdownLabels minW0={false} label={bitrix.string.FieldMapping} {items} bind:selected={field.field} />
{#if fValue?.type === 'crm_status' || fValue?.type === 'enumeration'}
<DropdownLabels
minW0={false}
label={bitrix.string.FieldMapping}
items={fValue.items?.map((it) => ({ id: it.ID, label: it.VALUE })) ?? []}
bind:selected={field.value}
/>
{:else}
<EditBox bind:value={field.value} />
{/if}
<Button
icon={IconClose}
on:click={() => {
filterFields.splice(pos, 1)
filterFields = filterFields
}}
/>
</div>
{/each}
</div>

View File

@ -1,6 +1,6 @@
{ {
"name": "@hcengineering/bitrix", "name": "@hcengineering/bitrix",
"version": "0.6.16", "version": "0.6.19",
"main": "lib/index.js", "main": "lib/index.js",
"author": "Anticrm Platform Contributors", "author": "Anticrm Platform Contributors",
"license": "EPL-2.0", "license": "EPL-2.0",

View File

@ -1,6 +1,6 @@
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import chunter, { Comment } from '@hcengineering/chunter' import chunter, { Comment } from '@hcengineering/chunter'
import contact, { combineName, EmployeeAccount } from '@hcengineering/contact' import contact, { combineName, Contact, EmployeeAccount } from '@hcengineering/contact'
import core, { import core, {
AccountRole, AccountRole,
ApplyOperations, ApplyOperations,
@ -30,6 +30,8 @@ import {
BitrixEntityMapping, BitrixEntityMapping,
BitrixEntityType, BitrixEntityType,
BitrixFieldMapping, BitrixFieldMapping,
BitrixFiles,
BitrixOwnerType,
BitrixSyncDoc, BitrixSyncDoc,
LoginInfo LoginInfo
} from './types' } from './types'
@ -147,7 +149,7 @@ export async function syncDocument (
attachedTo: resultDoc.document._id, attachedTo: resultDoc.document._id,
[bitrix.mixin.BitrixSyncDoc + '.bitrixId']: { $in: resultDoc.blobs.map((it) => it[0].bitrixId) } [bitrix.mixin.BitrixSyncDoc + '.bitrixId']: { $in: resultDoc.blobs.map((it) => it[0].bitrixId) }
}) })
for (const [ed, op] of resultDoc.blobs) { for (const [ed, op, upd] of resultDoc.blobs) {
const existing = existingBlobs.find( const existing = existingBlobs.find(
(it) => hierarchy.as<Doc, BitrixSyncDoc>(it, bitrix.mixin.BitrixSyncDoc).bitrixId === ed.bitrixId (it) => hierarchy.as<Doc, BitrixSyncDoc>(it, bitrix.mixin.BitrixSyncDoc).bitrixId === ed.bitrixId
) )
@ -171,13 +173,13 @@ export async function syncDocument (
}) })
if (resp.status === 200) { if (resp.status === 200) {
const uuid = await resp.text() const uuid = await resp.text()
upd(edData, ed)
await applyOp.addCollection( await applyOp.addCollection(
attachment.class.Attachment, ed._class,
resultDoc.document.space, ed.space,
resultDoc.document._id, ed.attachedTo,
resultDoc.document._class, ed.attachedToClass,
'attachments', ed.collection,
{ {
file: uuid, file: uuid,
lastModified: edData.lastModified, lastModified: edData.lastModified,
@ -186,8 +188,8 @@ export async function syncDocument (
type: edData.type type: edData.type
}, },
attachmentId, attachmentId,
resultDoc.document.modifiedOn, ed.modifiedOn,
resultDoc.document.modifiedBy ed.modifiedBy
) )
} }
} catch (err: any) { } catch (err: any) {
@ -355,10 +357,11 @@ export function processComment (comment: string): string {
// 1 day // 1 day
const syncPeriod = 1000 * 60 * 60 * 24 const syncPeriod = 1000 * 60 * 60 * 24
/** /**
* @public * @public
*/ */
export async function performSynchronization (ops: { export interface SyncOptions {
client: TxOperations client: TxOperations
bitrixClient: BitrixClient bitrixClient: BitrixClient
space: Ref<Space> | undefined space: Ref<Space> | undefined
@ -368,41 +371,83 @@ export async function performSynchronization (ops: {
frontUrl: string frontUrl: string
loginInfo: LoginInfo loginInfo: LoginInfo
monitor: (total: number) => void monitor: (total: number) => void
blobProvider?: (blobRef: any) => Promise<Blob | undefined> blobProvider?: (blobRef: { file: string, id: string }) => Promise<Blob | undefined>
}): Promise<void> { extraFilter?: Record<string, any>
}
interface SyncOptionsExtra {
ownerTypeValues: BitrixOwnerType[]
commentFieldKeys: string[]
allMappings: FindResult<BitrixEntityMapping>
allEmployee: FindResult<EmployeeAccount>
userList: Map<string, Ref<EmployeeAccount>>
}
/**
* @public
*/
export async function performSynchronization (ops: SyncOptions): Promise<BitrixSyncDoc[]> {
const commentFields = await ops.bitrixClient.call(BitrixEntityType.Comment + '.fields', {}) const commentFields = await ops.bitrixClient.call(BitrixEntityType.Comment + '.fields', {})
const ownerTypes = await ops.bitrixClient.call('crm.enum.ownertype', {})
const ownerTypeValues = ownerTypes.result as BitrixOwnerType[]
const commentFieldKeys = Object.keys(commentFields.result) const commentFieldKeys = Object.keys(commentFields.result)
const allEmployee = await ops.client.findAll(contact.class.EmployeeAccount, {}) const allEmployee = await ops.client.findAll(contact.class.EmployeeAccount, {})
const allMappings = await ops.client.findAll<BitrixEntityMapping>(
bitrix.class.EntityMapping,
{},
{
lookup: {
_id: {
fields: bitrix.class.FieldMapping
}
}
}
)
const userList = new Map<string, Ref<EmployeeAccount>>() const userList = new Map<string, Ref<EmployeeAccount>>()
// Fill all users and create new ones, if required. // Fill all users and create new ones, if required.
await synchronizeUsers(userList, ops, allEmployee) await synchronizeUsers(userList, ops, allEmployee)
return await doPerformSync({
...ops,
ownerTypeValues,
commentFieldKeys,
allMappings,
allEmployee,
userList
})
}
async function doPerformSync (ops: SyncOptions & SyncOptionsExtra): Promise<BitrixSyncDoc[]> {
const resultDocs: BitrixSyncDoc[] = []
try { try {
if (ops.space === undefined || ops.mapping.$lookup?.fields === undefined) { if (ops.space === undefined || ops.mapping.$lookup?.fields === undefined) {
return return []
} }
let processed = 0 let processed = 0
let added = 0 let added = 0
const sel = ['*', 'UF_*'] const sel = ['*', 'UF_*', 'EMAIL', 'IM']
if (ops.mapping.type === BitrixEntityType.Lead) {
sel.push('EMAIL')
sel.push('IM')
}
const allTagElements = await ops.client.findAll<TagElement>(tags.class.TagElement, {}) const allTagElements = await ops.client.findAll<TagElement>(tags.class.TagElement, {})
while (added < ops.limit) { while (added < ops.limit) {
const result = await ops.bitrixClient.call(ops.mapping.type + '.list', { const q: Record<string, any> = {
select: sel, select: sel,
order: { ID: ops.direction }, order: { ID: ops.direction },
start: processed start: processed
}) }
if (ops.extraFilter !== undefined) {
q.filter = ops.extraFilter
}
const result = await ops.bitrixClient.call(ops.mapping.type + '.list', q)
const fields = ops.mapping.$lookup?.fields as BitrixFieldMapping[] const fields = ops.mapping.$lookup?.fields as BitrixFieldMapping[]
@ -445,7 +490,7 @@ export async function performSynchronization (ops: {
ops.space, ops.space,
fields, fields,
r, r,
userList, ops.userList,
existingDoc, existingDoc,
defaultCategories, defaultCategories,
allTagElements, allTagElements,
@ -453,7 +498,7 @@ export async function performSynchronization (ops: {
) )
if (ops.mapping.comments) { if (ops.mapping.comments) {
await downloadComments(res, ops, commentFieldKeys, userList) await downloadComments(res, ops, ops.commentFieldKeys, ops.userList, ops.ownerTypeValues)
} }
added++ added++
@ -461,12 +506,34 @@ export async function performSynchronization (ops: {
await syncDocument(ops.client, existingDoc, res, ops.loginInfo, ops.frontUrl, () => { await syncDocument(ops.client, existingDoc, res, ops.loginInfo, ops.frontUrl, () => {
ops.monitor?.(total) ops.monitor?.(total)
}) })
if (existingDoc !== undefined) {
res.document._id = existingDoc._id as Ref<BitrixSyncDoc>
}
resultDocs.push(res.document)
for (const d of res.extraDocs) { for (const d of res.extraDocs) {
// update tags if required // update tags if required
if (d._class === tags.class.TagElement) { if (d._class === tags.class.TagElement) {
allTagElements.push(d as TagElement) allTagElements.push(d as TagElement)
} }
} }
if (ops.mapping.type === BitrixEntityType.Company) {
// We need to perform contact mapping if they are defined.
const contactMapping = ops.allMappings.find((it) => it.type === BitrixEntityType.Contact)
if (contactMapping !== undefined) {
await performOrganizationContactSynchronization(
{
...ops,
mapping: contactMapping,
limit: 100
},
{
res
}
)
}
}
if (added >= ops.limit) { if (added >= ops.limit) {
break break
} }
@ -481,11 +548,53 @@ export async function performSynchronization (ops: {
} }
processed = result.next processed = result.next
if (processed === undefined) {
// No more elements
break
}
} }
} catch (err: any) { } catch (err: any) {
console.error(err) console.error(err)
} }
return resultDocs
} }
async function performOrganizationContactSynchronization (
ops: SyncOptions & SyncOptionsExtra,
extra: {
res: ConvertResult
}
): Promise<void> {
const contacts = await doPerformSync({
...ops,
extraFilter: { COMPANY_ID: extra.res.document.bitrixId },
monitor: (total) => {
console.log('total', total)
}
})
const existingContacts = await ops.client.findAll(contact.class.Member, {
attachedTo: extra.res.document._id,
contact: { $in: contacts.map((it) => it._id as unknown as Ref<Contact>) }
})
for (const c of contacts) {
const ex = existingContacts.find((e) => e.contact === (c._id as unknown as Ref<Contact>))
if (ex === undefined) {
await ops.client.addCollection(
contact.class.Member,
extra.res.document.space,
extra.res.document._id,
extra.res.document._class,
'members',
{
contact: c._id as unknown as Ref<Contact>
}
)
}
}
// We need to create Member's for organization contacts.
}
async function downloadComments ( async function downloadComments (
res: ConvertResult, res: ConvertResult,
ops: { ops: {
@ -498,15 +607,21 @@ async function downloadComments (
frontUrl: string frontUrl: string
loginInfo: LoginInfo loginInfo: LoginInfo
monitor: (total: number) => void monitor: (total: number) => void
blobProvider?: ((blobRef: any) => Promise<Blob | undefined>) | undefined blobProvider?: ((blobRef: { file: string, id: string }) => Promise<Blob | undefined>) | undefined
}, },
commentFieldKeys: string[], commentFieldKeys: string[],
userList: Map<string, Ref<EmployeeAccount>> userList: Map<string, Ref<EmployeeAccount>>,
ownerTypeValues: BitrixOwnerType[]
): Promise<void> { ): Promise<void> {
const entityType = ops.mapping.type.replace('crm.', '')
const ownerType = ownerTypeValues.find((it) => it.SYMBOL_CODE.toLowerCase() === entityType)
if (ownerType === undefined) {
throw new Error(`No owner type found for ${entityType}`)
}
const commentsData = await ops.bitrixClient.call(BitrixEntityType.Comment + '.list', { const commentsData = await ops.bitrixClient.call(BitrixEntityType.Comment + '.list', {
filter: { filter: {
ENTITY_ID: res.document.bitrixId, ENTITY_ID: res.document.bitrixId,
ENTITY_TYPE: ops.mapping.type.replace('crm.', '') ENTITY_TYPE: entityType
}, },
select: commentFieldKeys, select: commentFieldKeys,
order: { ID: ops.direction } order: { ID: ops.direction }
@ -523,14 +638,53 @@ async function downloadComments (
collection: 'comments', collection: 'comments',
space: res.document.space, space: res.document.space,
modifiedBy: userList.get(it.AUTHOR_ID) ?? core.account.System, modifiedBy: userList.get(it.AUTHOR_ID) ?? core.account.System,
modifiedOn: new Date(it.CREATED ?? new Date().toString()).getTime() modifiedOn: new Date(it.CREATED ?? new Date().toString()).getTime(),
attachments: 0
}
if (Object.keys(it.FILES ?? {}).length > 0) {
for (const [, v] of Object.entries(it.FILES as BitrixFiles)) {
c.message += `</br> Attachment: <a href='${v.urlDownload}'>${v.name} by ${v.authorName}</a>`
// Direct link, we could download using fetch.
c.attachments = (c.attachments ?? 0) + 1
res.blobs.push([
{
_id: generateId(),
_class: attachment.class.Attachment,
attachedTo: c._id,
attachedToClass: c._class,
bitrixId: `attach-${v.id}`,
collection: 'attachments',
file: '',
lastModified: Date.now(),
modifiedBy: userList.get(it.AUTHOR_ID) ?? core.account.System,
modifiedOn: new Date(it.CREATED ?? new Date().toString()).getTime(),
name: v.name,
size: v.size,
space: c.space,
type: 'file'
},
async (): Promise<File | undefined> => {
const blob = await ops.blobProvider?.({ file: v.urlDownload, id: `${v.id}` })
if (blob !== undefined) {
return new File([blob], v.name)
}
},
(file: File, attach: Attachment) => {
attach.attachedTo = c._id
attach.type = file.type
attach.size = file.size
attach.name = file.name
}
])
}
} }
res.extraSync.push(c) res.extraSync.push(c)
} }
const communications = await ops.bitrixClient.call('crm.activity.list', { const communications = await ops.bitrixClient.call('crm.activity.list', {
order: { ID: 'DESC' }, order: { ID: 'DESC' },
filter: { filter: {
OWNER_ID: res.document.bitrixId OWNER_ID: res.document.bitrixId,
OWNER_TYPE: ownerType.ID
}, },
select: ['*', 'COMMUNICATIONS'] select: ['*', 'COMMUNICATIONS']
}) })
@ -538,12 +692,23 @@ async function downloadComments (
? (communications.result as BitrixActivity[]) ? (communications.result as BitrixActivity[])
: [communications.result as BitrixActivity] : [communications.result as BitrixActivity]
for (const comm of cr) { for (const comm of cr) {
const cummunications = comm.COMMUNICATIONS?.map((it) => it.ENTITY_SETTINGS?.LEAD_TITLE ?? '')
let message = `<p>
e-mail: ${cummunications?.join(',') ?? ''}<br/>\n
Subject: ${comm.SUBJECT}<br/>\n`
for (const [k, v] of Object.entries(comm.SETTINGS?.EMAIL_META ?? {}).concat(
Object.entries(comm.SETTINGS?.MESSAGE_HEADERS ?? {})
)) {
if (v.trim().length > 0) {
message += `<div>${k}: ${v}</div><br/>\n`
}
}
message += '</p>' + comm.DESCRIPTION
const c: Comment & { bitrixId: string, type: string } = { const c: Comment & { bitrixId: string, type: string } = {
_id: generateId(), _id: generateId(),
_class: chunter.class.Comment, _class: chunter.class.Comment,
message: `e-mail:<br/> message,
Subject: ${comm.SUBJECT}
${comm.DESCRIPTION}`,
bitrixId: comm.ID, bitrixId: comm.ID,
type: 'email', type: 'email',
attachedTo: res.document._id, attachedTo: res.document._id,
@ -553,6 +718,7 @@ async function downloadComments (
modifiedBy: userList.get(comm.AUTHOR_ID) ?? core.account.System, modifiedBy: userList.get(comm.AUTHOR_ID) ?? core.account.System,
modifiedOn: new Date(comm.CREATED ?? new Date().toString()).getTime() modifiedOn: new Date(comm.CREATED ?? new Date().toString()).getTime()
} }
res.extraSync.push(c) res.extraSync.push(c)
} }
} }
@ -569,7 +735,7 @@ async function synchronizeUsers (
frontUrl: string frontUrl: string
loginInfo: LoginInfo loginInfo: LoginInfo
monitor: (total: number) => void monitor: (total: number) => void
blobProvider?: ((blobRef: any) => Promise<Blob | undefined>) | undefined blobProvider?: ((blobRef: { file: string, id: string }) => Promise<Blob | undefined>) | undefined
}, },
allEmployee: FindResult<EmployeeAccount> allEmployee: FindResult<EmployeeAccount>
): Promise<void> { ): Promise<void> {

View File

@ -101,7 +101,17 @@ export enum BitrixEntityType {
Binding = 'crm.timeline.bindings', Binding = 'crm.timeline.bindings',
Lead = 'crm.lead', Lead = 'crm.lead',
Activity = 'crm.activity', Activity = 'crm.activity',
Company = 'crm.company' Company = 'crm.company',
Contact = 'crm.contact'
}
/**
* @public
*/
export interface BitrixOwnerType {
ID: string
NAME: string
SYMBOL_CODE: string
} }
/** /**
@ -109,9 +119,8 @@ export enum BitrixEntityType {
*/ */
export const mappingTypes = [ export const mappingTypes = [
{ label: 'Leads', id: BitrixEntityType.Lead }, { label: 'Leads', id: BitrixEntityType.Lead },
// { label: 'Comments', id: BitrixEntityType.Comment }, { label: 'Company', id: BitrixEntityType.Company },
{ label: 'Company', id: BitrixEntityType.Company } { label: 'Contacts', id: BitrixEntityType.Contact }
// { label: 'Activity', id: BitrixEntityType.Activity }
] ]
/** /**
@ -242,7 +251,36 @@ export interface BitrixFieldMapping extends AttachedDoc {
export interface BitrixActivity { export interface BitrixActivity {
ID: string ID: string
SUBJECT: string SUBJECT: string
COMMUNICATIONS?: {
ENTITY_SETTINGS?: {
LAST_NAME: string
NAME: string
LEAD_TITLE: string
}
}[]
DESCRIPTION: string DESCRIPTION: string
AUTHOR_ID: string AUTHOR_ID: string
CREATED: number CREATED: number
SETTINGS?: {
MESSAGE_HEADERS?: Record<string, string>
EMAIL_META?: Record<string, string>
}
} }
/**
* @public
*/
export type BitrixFiles = Record<
string,
{
authorId: string
authorName: string
date: string
id: number
image: boolean
name: string
size: number
type: string
urlDownload: string
urlShow: string
}
>

View File

@ -57,7 +57,7 @@ export interface ConvertResult {
mixins: Record<Ref<Mixin<Doc>>, Data<Doc>> // Mixins of document we will achive mixins: Record<Ref<Mixin<Doc>>, Data<Doc>> // Mixins of document we will achive
extraDocs: Doc[] // Extra documents we will achive, etc. extraDocs: Doc[] // Extra documents we will achive, etc.
extraSync: (AttachedDoc & BitrixSyncDoc)[] // Extra documents we will achive, etc. extraSync: (AttachedDoc & BitrixSyncDoc)[] // Extra documents we will achive, etc.
blobs: [Attachment & BitrixSyncDoc, () => Promise<File | undefined>][] blobs: [Attachment & BitrixSyncDoc, () => Promise<File | undefined>, (file: File, attach: Attachment) => void][]
} }
/** /**
@ -73,7 +73,7 @@ export async function convert (
existingDoc: WithLookup<Doc> | undefined, existingDoc: WithLookup<Doc> | undefined,
defaultCategories: TagCategory[], defaultCategories: TagCategory[],
allTagElements: TagElement[], allTagElements: TagElement[],
blobProvider?: (blobRef: any) => Promise<Blob | undefined> blobProvider?: (blobRef: { file: string, id: string }) => Promise<Blob | undefined>
): Promise<ConvertResult> { ): Promise<ConvertResult> {
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const bitrixId = `${rawDocument.ID as string}` const bitrixId = `${rawDocument.ID as string}`
@ -93,7 +93,11 @@ export async function convert (
const newExtraSyncDocs: (AttachedDoc & BitrixSyncDoc)[] = [] const newExtraSyncDocs: (AttachedDoc & BitrixSyncDoc)[] = []
const newExtraDocs: Doc[] = [] const newExtraDocs: Doc[] = []
const blobs: [Attachment & BitrixSyncDoc, () => Promise<File | undefined>][] = [] const blobs: [
Attachment & BitrixSyncDoc,
() => Promise<File | undefined>,
(file: File, attach: Attachment) => void
][] = []
const mixins: Record<Ref<Mixin<Doc>>, Data<Doc>> = {} const mixins: Record<Ref<Mixin<Doc>>, Data<Doc>> = {}
const extractValue = (field?: string, alternatives?: string[]): any | undefined => { const extractValue = (field?: string, alternatives?: string[]): any | undefined => {
@ -120,10 +124,12 @@ export async function convert (
return lval.map((it) => it.VALUE) return lval.map((it) => it.VALUE)
} }
} else if (bfield.type === 'file') { } else if (bfield.type === 'file') {
if (Array.isArray(lval)) { if (Array.isArray(lval) && bfield.isMultiple) {
return lval.map((it) => ({ id: it.id, file: it.downloadUrl })) return lval.map((it) => ({ id: it.id, file: it.downloadUrl }))
} else if (lval != null) {
return [{ id: lval.id, file: lval.downloadUrl }]
} }
} else if (bfield.type === 'string' || bfield.type === 'url') { } else if (bfield.type === 'string' || bfield.type === 'url' || bfield.type === 'crm_company') {
if (bfield.isMultiple && Array.isArray(lval)) { if (bfield.isMultiple && Array.isArray(lval)) {
return lval.join(', ') return lval.join(', ')
} }
@ -365,6 +371,11 @@ export async function convert (
return new File([response], fname, { type: response.type }) return new File([response], fname, { type: response.type })
} }
} }
},
(file, attach) => {
attach.attachedTo = document._id
attach.size = file.size
attach.type = file.type
} }
]) ])
} }

View File

@ -49,6 +49,23 @@
let classes: Ref<Class<Doc>>[] = [] let classes: Ref<Class<Doc>>[] = []
clQuery.query(core.class.Class, {}, (res) => { clQuery.query(core.class.Class, {}, (res) => {
classes = filterDescendants(hierarchy, ofClass, res) classes = filterDescendants(hierarchy, ofClass, res)
if (ofClass !== undefined) {
// We need to include all possible mixins as well
for (const ancestor of hierarchy.getAncestors(ofClass)) {
if (ancestor === ofClass) {
continue
}
const mixins = hierarchy.getDescendants(ancestor).filter((it) => hierarchy.isMixin(it))
for (const m of mixins) {
const mm = hierarchy.getClass(m)
if (!classes.includes(m) && mm.extends === ancestor && mm.label !== undefined) {
// Check if parent of
classes.push(m)
}
}
}
}
}) })
</script> </script>

View File

@ -17,7 +17,7 @@
import { Class, ClassifierKind, Doc, Mixin, Ref } from '@hcengineering/core' import { Class, ClassifierKind, Doc, Mixin, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
import { Label } from '@hcengineering/ui' import { Icon, Label, tooltip } from '@hcengineering/ui'
import { getMixinStyle } from '../utils' import { getMixinStyle } from '../utils'
export let value: Doc export let value: Doc
@ -43,10 +43,8 @@
mixins = hierarchy mixins = hierarchy
.getDescendants(parentClass) .getDescendants(parentClass)
.filter( .filter(
(m) => (m) => hierarchy.getClass(m).kind === ClassifierKind.MIXIN && hierarchy.hasMixin(value, m)
hierarchy.getClass(m).kind === ClassifierKind.MIXIN && // && !hierarchy.hasMixin(hierarchy.getClass(m), setting.mixin.UserMixin)
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>)
} }
@ -55,8 +53,19 @@
{#if mixins.length > 0} {#if mixins.length > 0}
<div class="mixin-container"> <div class="mixin-container">
{#each mixins as mixin} {#each mixins as mixin}
<div class="mixin-selector" style={getMixinStyle(mixin._id, true)}> {@const userMixin = hierarchy.hasMixin(mixin, setting.mixin.UserMixin)}
<Label label={mixin.label} /> <div class="mixin-selector" class:user-selector={userMixin} style={getMixinStyle(mixin._id, true)}>
{#if !userMixin}
<Label label={mixin.label} />
{:else}
<div use:tooltip={{ label: mixin.label }}>
{#if mixin.icon}
<Icon icon={mixin.icon} size={'small'} />
{:else}
{/if}
</div>
{/if}
</div> </div>
{/each} {/each}
</div> </div>
@ -83,5 +92,8 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.user-selector {
min-width: 24px;
}
} }
</style> </style>

View File

@ -51,6 +51,8 @@
export let prefferedSorting: string = 'modifiedOn' export let prefferedSorting: string = 'modifiedOn'
export let limit = 200
// If defined, will show a number of dummy items before real data will appear. // If defined, will show a number of dummy items before real data will appear.
export let loadingProps: LoadingProps | undefined = undefined export let loadingProps: LoadingProps | undefined = undefined
@ -92,6 +94,7 @@
sortKey: string | string[], sortKey: string | string[],
sortOrder: SortingOrder, sortOrder: SortingOrder,
lookup: Lookup<Doc>, lookup: Lookup<Doc>,
limit: number,
options?: FindOptions<Doc> options?: FindOptions<Doc>
) { ) {
const sort = Array.isArray(sortKey) const sort = Array.isArray(sortKey)
@ -114,13 +117,13 @@
dispatch('content', objects) dispatch('content', objects)
loading = loading === 1 ? 0 : -1 loading = loading === 1 ? 0 : -1
}, },
{ sort, limit: 200, ...options, lookup } { sort, limit, ...options, lookup }
) )
if (update && ++loading > 0) { if (update && ++loading > 0) {
objects = [] objects = []
} }
} }
$: update(_class, query, _sortKey, sortOrder, lookup, options) $: update(_class, query, _sortKey, sortOrder, lookup, limit, options)
const showMenu = async (ev: MouseEvent, object: Doc, row: number): Promise<void> => { const showMenu = async (ev: MouseEvent, object: Doc, row: number): Promise<void> => {
selection = row selection = row
@ -366,7 +369,15 @@
<div class="content" class:padding={showNotification || enableChecking}> <div class="content" class:padding={showNotification || enableChecking}>
<Label label={view.string.Total} />: {total} <Label label={view.string.Total} />: {total}
{#if objects.length > 0 && objects.length < total} {#if objects.length > 0 && objects.length < total}
<Label label={view.string.Shown} />: {objects.length} <!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="cursor-pointer ml-2"
on:click={() => {
limit = limit + 100
}}
>
<Label label={view.string.Shown} />: {objects.length}
</div>
{/if} {/if}
</div> </div>
</div> </div>