mirror of
https://github.com/hcengineering/platform.git
synced 2025-01-01 16:11:23 +03:00
parent
9a96035b00
commit
4a5532af10
packages/presentation/src/components/message
plugins
bitrix-resources/src/components
bitrix
setting-resources/src/components
view-resources/src/components
@ -110,7 +110,8 @@
|
||||
{:else if node.nodeName === 'S'}
|
||||
<s><svelte:self nodes={node.childNodes} /></s>
|
||||
{:else}
|
||||
Unknown {node.nodeName}
|
||||
unknown: {node.nodeName}
|
||||
<svelte:self nodes={node.childNodes} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
@ -25,7 +25,7 @@
|
||||
|
||||
import { bitrixQueue } from '../queue'
|
||||
import CreateMapping from './CreateMapping.svelte'
|
||||
import EntiryMapping from './EntityMapping.svelte'
|
||||
import EntityMapping from './EntityMapping.svelte'
|
||||
|
||||
export let integration: Integration
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
{#each mappings as mapping}
|
||||
<EntiryMapping {mapping} {bitrixClient} {statusList} />
|
||||
<EntityMapping {mapping} {bitrixClient} {statusList} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import setting from '@hcengineering/setting-resources/src/plugin'
|
||||
@ -125,7 +125,10 @@
|
||||
<div class="ml-2">
|
||||
{#if value !== null && value !== undefined && 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'}
|
||||
{field.items?.find((it) => it.ID === value)?.VALUE}
|
||||
{:else}
|
||||
|
@ -3,17 +3,21 @@
|
||||
BitrixClient,
|
||||
BitrixEntityMapping,
|
||||
BitrixFieldMapping,
|
||||
Fields,
|
||||
performSynchronization,
|
||||
StatusValue,
|
||||
toClassRef
|
||||
} from '@hcengineering/bitrix'
|
||||
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 { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
|
||||
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 EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
|
||||
import { NumberEditor } from '@hcengineering/view-resources'
|
||||
import bitrix from '../plugin'
|
||||
import FieldMappingPresenter from './FieldMappingPresenter.svelte'
|
||||
|
||||
export let mapping: WithLookup<BitrixEntityMapping>
|
||||
@ -40,6 +44,11 @@
|
||||
loading = true
|
||||
const uploadUrl = (window.location.origin + getMetadata(login.metadata.UploadUrl)) 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 {
|
||||
await performSynchronization({
|
||||
bitrixClient,
|
||||
@ -57,7 +66,8 @@
|
||||
monitor: (total: number) => {
|
||||
docsProcessed++
|
||||
state = `processed: ${docsProcessed}/${total ?? 1}`
|
||||
}
|
||||
},
|
||||
extraFilter: filterFields.length === 0 ? undefined : mappedFilter
|
||||
})
|
||||
} catch (err: any) {
|
||||
state = err.message
|
||||
@ -66,6 +76,49 @@
|
||||
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>
|
||||
|
||||
<Expandable label={getEmbeddedLabel(mapping.type)}>
|
||||
@ -102,6 +155,8 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="buttons-divider" />
|
||||
<Button icon={IconAdd} on:click={addFilter} />
|
||||
<div class="buttons-divider" />
|
||||
<div class="flex-row-center">
|
||||
<div class="p-1">
|
||||
{state}
|
||||
@ -131,3 +186,29 @@
|
||||
{/each}
|
||||
</div>
|
||||
</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>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hcengineering/bitrix",
|
||||
"version": "0.6.16",
|
||||
"version": "0.6.19",
|
||||
"main": "lib/index.js",
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import chunter, { Comment } from '@hcengineering/chunter'
|
||||
import contact, { combineName, EmployeeAccount } from '@hcengineering/contact'
|
||||
import contact, { combineName, Contact, EmployeeAccount } from '@hcengineering/contact'
|
||||
import core, {
|
||||
AccountRole,
|
||||
ApplyOperations,
|
||||
@ -30,6 +30,8 @@ import {
|
||||
BitrixEntityMapping,
|
||||
BitrixEntityType,
|
||||
BitrixFieldMapping,
|
||||
BitrixFiles,
|
||||
BitrixOwnerType,
|
||||
BitrixSyncDoc,
|
||||
LoginInfo
|
||||
} from './types'
|
||||
@ -147,7 +149,7 @@ export async function syncDocument (
|
||||
attachedTo: resultDoc.document._id,
|
||||
[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(
|
||||
(it) => hierarchy.as<Doc, BitrixSyncDoc>(it, bitrix.mixin.BitrixSyncDoc).bitrixId === ed.bitrixId
|
||||
)
|
||||
@ -171,13 +173,13 @@ export async function syncDocument (
|
||||
})
|
||||
if (resp.status === 200) {
|
||||
const uuid = await resp.text()
|
||||
|
||||
upd(edData, ed)
|
||||
await applyOp.addCollection(
|
||||
attachment.class.Attachment,
|
||||
resultDoc.document.space,
|
||||
resultDoc.document._id,
|
||||
resultDoc.document._class,
|
||||
'attachments',
|
||||
ed._class,
|
||||
ed.space,
|
||||
ed.attachedTo,
|
||||
ed.attachedToClass,
|
||||
ed.collection,
|
||||
{
|
||||
file: uuid,
|
||||
lastModified: edData.lastModified,
|
||||
@ -186,8 +188,8 @@ export async function syncDocument (
|
||||
type: edData.type
|
||||
},
|
||||
attachmentId,
|
||||
resultDoc.document.modifiedOn,
|
||||
resultDoc.document.modifiedBy
|
||||
ed.modifiedOn,
|
||||
ed.modifiedBy
|
||||
)
|
||||
}
|
||||
} catch (err: any) {
|
||||
@ -355,10 +357,11 @@ export function processComment (comment: string): string {
|
||||
|
||||
// 1 day
|
||||
const syncPeriod = 1000 * 60 * 60 * 24
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function performSynchronization (ops: {
|
||||
export interface SyncOptions {
|
||||
client: TxOperations
|
||||
bitrixClient: BitrixClient
|
||||
space: Ref<Space> | undefined
|
||||
@ -368,41 +371,83 @@ export async function performSynchronization (ops: {
|
||||
frontUrl: string
|
||||
loginInfo: LoginInfo
|
||||
monitor: (total: number) => void
|
||||
blobProvider?: (blobRef: any) => Promise<Blob | undefined>
|
||||
}): Promise<void> {
|
||||
blobProvider?: (blobRef: { file: string, id: string }) => Promise<Blob | undefined>
|
||||
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 ownerTypes = await ops.bitrixClient.call('crm.enum.ownertype', {})
|
||||
|
||||
const ownerTypeValues = ownerTypes.result as BitrixOwnerType[]
|
||||
|
||||
const commentFieldKeys = Object.keys(commentFields.result)
|
||||
|
||||
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>>()
|
||||
|
||||
// Fill all users and create new ones, if required.
|
||||
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 {
|
||||
if (ops.space === undefined || ops.mapping.$lookup?.fields === undefined) {
|
||||
return
|
||||
return []
|
||||
}
|
||||
let processed = 0
|
||||
|
||||
let added = 0
|
||||
|
||||
const sel = ['*', 'UF_*']
|
||||
if (ops.mapping.type === BitrixEntityType.Lead) {
|
||||
sel.push('EMAIL')
|
||||
sel.push('IM')
|
||||
}
|
||||
const sel = ['*', 'UF_*', 'EMAIL', 'IM']
|
||||
|
||||
const allTagElements = await ops.client.findAll<TagElement>(tags.class.TagElement, {})
|
||||
|
||||
while (added < ops.limit) {
|
||||
const result = await ops.bitrixClient.call(ops.mapping.type + '.list', {
|
||||
const q: Record<string, any> = {
|
||||
select: sel,
|
||||
order: { ID: ops.direction },
|
||||
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[]
|
||||
|
||||
@ -445,7 +490,7 @@ export async function performSynchronization (ops: {
|
||||
ops.space,
|
||||
fields,
|
||||
r,
|
||||
userList,
|
||||
ops.userList,
|
||||
existingDoc,
|
||||
defaultCategories,
|
||||
allTagElements,
|
||||
@ -453,7 +498,7 @@ export async function performSynchronization (ops: {
|
||||
)
|
||||
|
||||
if (ops.mapping.comments) {
|
||||
await downloadComments(res, ops, commentFieldKeys, userList)
|
||||
await downloadComments(res, ops, ops.commentFieldKeys, ops.userList, ops.ownerTypeValues)
|
||||
}
|
||||
|
||||
added++
|
||||
@ -461,12 +506,34 @@ export async function performSynchronization (ops: {
|
||||
await syncDocument(ops.client, existingDoc, res, ops.loginInfo, ops.frontUrl, () => {
|
||||
ops.monitor?.(total)
|
||||
})
|
||||
if (existingDoc !== undefined) {
|
||||
res.document._id = existingDoc._id as Ref<BitrixSyncDoc>
|
||||
}
|
||||
resultDocs.push(res.document)
|
||||
for (const d of res.extraDocs) {
|
||||
// update tags if required
|
||||
if (d._class === tags.class.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) {
|
||||
break
|
||||
}
|
||||
@ -481,11 +548,53 @@ export async function performSynchronization (ops: {
|
||||
}
|
||||
|
||||
processed = result.next
|
||||
if (processed === undefined) {
|
||||
// No more elements
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
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 (
|
||||
res: ConvertResult,
|
||||
ops: {
|
||||
@ -498,15 +607,21 @@ async function downloadComments (
|
||||
frontUrl: string
|
||||
loginInfo: LoginInfo
|
||||
monitor: (total: number) => void
|
||||
blobProvider?: ((blobRef: any) => Promise<Blob | undefined>) | undefined
|
||||
blobProvider?: ((blobRef: { file: string, id: string }) => Promise<Blob | undefined>) | undefined
|
||||
},
|
||||
commentFieldKeys: string[],
|
||||
userList: Map<string, Ref<EmployeeAccount>>
|
||||
userList: Map<string, Ref<EmployeeAccount>>,
|
||||
ownerTypeValues: BitrixOwnerType[]
|
||||
): 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', {
|
||||
filter: {
|
||||
ENTITY_ID: res.document.bitrixId,
|
||||
ENTITY_TYPE: ops.mapping.type.replace('crm.', '')
|
||||
ENTITY_TYPE: entityType
|
||||
},
|
||||
select: commentFieldKeys,
|
||||
order: { ID: ops.direction }
|
||||
@ -523,14 +638,53 @@ async function downloadComments (
|
||||
collection: 'comments',
|
||||
space: res.document.space,
|
||||
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)
|
||||
}
|
||||
const communications = await ops.bitrixClient.call('crm.activity.list', {
|
||||
order: { ID: 'DESC' },
|
||||
filter: {
|
||||
OWNER_ID: res.document.bitrixId
|
||||
OWNER_ID: res.document.bitrixId,
|
||||
OWNER_TYPE: ownerType.ID
|
||||
},
|
||||
select: ['*', 'COMMUNICATIONS']
|
||||
})
|
||||
@ -538,12 +692,23 @@ async function downloadComments (
|
||||
? (communications.result as BitrixActivity[])
|
||||
: [communications.result as BitrixActivity]
|
||||
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 } = {
|
||||
_id: generateId(),
|
||||
_class: chunter.class.Comment,
|
||||
message: `e-mail:<br/>
|
||||
Subject: ${comm.SUBJECT}
|
||||
${comm.DESCRIPTION}`,
|
||||
message,
|
||||
bitrixId: comm.ID,
|
||||
type: 'email',
|
||||
attachedTo: res.document._id,
|
||||
@ -553,6 +718,7 @@ async function downloadComments (
|
||||
modifiedBy: userList.get(comm.AUTHOR_ID) ?? core.account.System,
|
||||
modifiedOn: new Date(comm.CREATED ?? new Date().toString()).getTime()
|
||||
}
|
||||
|
||||
res.extraSync.push(c)
|
||||
}
|
||||
}
|
||||
@ -569,7 +735,7 @@ async function synchronizeUsers (
|
||||
frontUrl: string
|
||||
loginInfo: LoginInfo
|
||||
monitor: (total: number) => void
|
||||
blobProvider?: ((blobRef: any) => Promise<Blob | undefined>) | undefined
|
||||
blobProvider?: ((blobRef: { file: string, id: string }) => Promise<Blob | undefined>) | undefined
|
||||
},
|
||||
allEmployee: FindResult<EmployeeAccount>
|
||||
): Promise<void> {
|
||||
|
@ -101,7 +101,17 @@ export enum BitrixEntityType {
|
||||
Binding = 'crm.timeline.bindings',
|
||||
Lead = 'crm.lead',
|
||||
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 = [
|
||||
{ label: 'Leads', id: BitrixEntityType.Lead },
|
||||
// { label: 'Comments', id: BitrixEntityType.Comment },
|
||||
{ label: 'Company', id: BitrixEntityType.Company }
|
||||
// { label: 'Activity', id: BitrixEntityType.Activity }
|
||||
{ label: 'Company', id: BitrixEntityType.Company },
|
||||
{ label: 'Contacts', id: BitrixEntityType.Contact }
|
||||
]
|
||||
|
||||
/**
|
||||
@ -242,7 +251,36 @@ export interface BitrixFieldMapping extends AttachedDoc {
|
||||
export interface BitrixActivity {
|
||||
ID: string
|
||||
SUBJECT: string
|
||||
COMMUNICATIONS?: {
|
||||
ENTITY_SETTINGS?: {
|
||||
LAST_NAME: string
|
||||
NAME: string
|
||||
LEAD_TITLE: string
|
||||
}
|
||||
}[]
|
||||
DESCRIPTION: string
|
||||
AUTHOR_ID: string
|
||||
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
|
||||
}
|
||||
>
|
||||
|
@ -57,7 +57,7 @@ export interface ConvertResult {
|
||||
mixins: Record<Ref<Mixin<Doc>>, Data<Doc>> // Mixins of document we will achive
|
||||
extraDocs: Doc[] // Extra documents we will achive, etc.
|
||||
extraSync: (AttachedDoc & BitrixSyncDoc)[] // Extra documents we will achive, etc.
|
||||
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,
|
||||
defaultCategories: TagCategory[],
|
||||
allTagElements: TagElement[],
|
||||
blobProvider?: (blobRef: any) => Promise<Blob | undefined>
|
||||
blobProvider?: (blobRef: { file: string, id: string }) => Promise<Blob | undefined>
|
||||
): Promise<ConvertResult> {
|
||||
const hierarchy = client.getHierarchy()
|
||||
const bitrixId = `${rawDocument.ID as string}`
|
||||
@ -93,7 +93,11 @@ export async function convert (
|
||||
|
||||
const newExtraSyncDocs: (AttachedDoc & BitrixSyncDoc)[] = []
|
||||
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 extractValue = (field?: string, alternatives?: string[]): any | undefined => {
|
||||
@ -120,10 +124,12 @@ export async function convert (
|
||||
return lval.map((it) => it.VALUE)
|
||||
}
|
||||
} 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 }))
|
||||
} 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)) {
|
||||
return lval.join(', ')
|
||||
}
|
||||
@ -365,6 +371,11 @@ export async function convert (
|
||||
return new File([response], fname, { type: response.type })
|
||||
}
|
||||
}
|
||||
},
|
||||
(file, attach) => {
|
||||
attach.attachedTo = document._id
|
||||
attach.size = file.size
|
||||
attach.type = file.type
|
||||
}
|
||||
])
|
||||
}
|
||||
|
@ -49,6 +49,23 @@
|
||||
let classes: Ref<Class<Doc>>[] = []
|
||||
clQuery.query(core.class.Class, {}, (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>
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { Class, ClassifierKind, Doc, Mixin, Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import setting from '@hcengineering/setting'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { Icon, Label, tooltip } from '@hcengineering/ui'
|
||||
import { getMixinStyle } from '../utils'
|
||||
|
||||
export let value: Doc
|
||||
@ -43,10 +43,8 @@
|
||||
mixins = hierarchy
|
||||
.getDescendants(parentClass)
|
||||
.filter(
|
||||
(m) =>
|
||||
hierarchy.getClass(m).kind === ClassifierKind.MIXIN &&
|
||||
hierarchy.hasMixin(value, m) &&
|
||||
!hierarchy.hasMixin(hierarchy.getClass(m), setting.mixin.UserMixin)
|
||||
(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>)
|
||||
}
|
||||
@ -55,8 +53,19 @@
|
||||
{#if mixins.length > 0}
|
||||
<div class="mixin-container">
|
||||
{#each mixins as mixin}
|
||||
<div class="mixin-selector" style={getMixinStyle(mixin._id, true)}>
|
||||
<Label label={mixin.label} />
|
||||
{@const userMixin = hierarchy.hasMixin(mixin, setting.mixin.UserMixin)}
|
||||
<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>
|
||||
{/each}
|
||||
</div>
|
||||
@ -83,5 +92,8 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.user-selector {
|
||||
min-width: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -51,6 +51,8 @@
|
||||
|
||||
export let prefferedSorting: string = 'modifiedOn'
|
||||
|
||||
export let limit = 200
|
||||
|
||||
// If defined, will show a number of dummy items before real data will appear.
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
|
||||
@ -92,6 +94,7 @@
|
||||
sortKey: string | string[],
|
||||
sortOrder: SortingOrder,
|
||||
lookup: Lookup<Doc>,
|
||||
limit: number,
|
||||
options?: FindOptions<Doc>
|
||||
) {
|
||||
const sort = Array.isArray(sortKey)
|
||||
@ -114,13 +117,13 @@
|
||||
dispatch('content', objects)
|
||||
loading = loading === 1 ? 0 : -1
|
||||
},
|
||||
{ sort, limit: 200, ...options, lookup }
|
||||
{ sort, limit, ...options, lookup }
|
||||
)
|
||||
if (update && ++loading > 0) {
|
||||
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> => {
|
||||
selection = row
|
||||
@ -366,7 +369,15 @@
|
||||
<div class="content" class:padding={showNotification || enableChecking}>
|
||||
<Label label={view.string.Total} />: {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}
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user