Bitrix fixes ()

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
packages/presentation/src/components/message
plugins

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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",

View File

@ -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> {

View File

@ -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
}
>

View File

@ -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
}
])
}

View File

@ -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>

View File

@ -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>

View File

@ -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>