mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-26 02:10:07 +00:00
Objects sort (#672)
Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
parent
9a8d9f8804
commit
81000b50f6
@ -15,7 +15,7 @@
|
||||
|
||||
import type { Domain, Type, Ref } from '@anticrm/core'
|
||||
import { DOMAIN_MODEL, IndexKind } from '@anticrm/core'
|
||||
import { Builder, Model, Prop, TypeString, UX, Index, Collection } from '@anticrm/model'
|
||||
import { Builder, Model, Prop, TypeString, UX, Index, Collection, ArrOf } from '@anticrm/model'
|
||||
import type { IntlString, Asset } from '@anticrm/platform'
|
||||
import chunter from '@anticrm/model-chunter'
|
||||
import core, { TAccount, TDoc, TSpace, TType } from '@anticrm/model-core'
|
||||
@ -44,14 +44,14 @@ export class TChannelProvider extends TDoc implements ChannelProvider {
|
||||
placeholder!: string
|
||||
}
|
||||
|
||||
@Model(contact.class.TypeChannels, core.class.Type)
|
||||
@Model(contact.class.TypeChannel, core.class.Type)
|
||||
export class TTypeChannels extends TType {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function TypeChannels (): Type<Channel[]> {
|
||||
return { _class: contact.class.TypeChannels, label: 'TypeChannels' as IntlString }
|
||||
export function TypeChannel (): Type<Channel> {
|
||||
return { _class: contact.class.TypeChannel, label: 'Channel' as IntlString }
|
||||
}
|
||||
|
||||
@Model(contact.class.Contact, core.class.Doc, DOMAIN_CONTACT)
|
||||
@ -62,7 +62,7 @@ export class TContact extends TDoc implements Contact {
|
||||
|
||||
avatar?: string
|
||||
|
||||
@Prop(TypeChannels(), 'Contact Info' as IntlString)
|
||||
@Prop(ArrOf(TypeChannel()), 'Contact Info' as IntlString)
|
||||
channels!: Channel[]
|
||||
|
||||
@Prop(Collection(attachment.class.Attachment), 'Attachments' as IntlString)
|
||||
@ -73,14 +73,14 @@ export class TContact extends TDoc implements Contact {
|
||||
}
|
||||
|
||||
@Model(contact.class.Person, contact.class.Contact)
|
||||
@UX('Person' as IntlString, contact.icon.Person)
|
||||
@UX('Person' as IntlString, contact.icon.Person, undefined, 'name')
|
||||
export class TPerson extends TContact implements Person {
|
||||
@Prop(TypeString(), 'City' as IntlString)
|
||||
city!: string
|
||||
}
|
||||
|
||||
@Model(contact.class.Organization, contact.class.Contact)
|
||||
@UX('Organization' as IntlString, contact.icon.Company)
|
||||
@UX('Organization' as IntlString, contact.icon.Company, undefined, 'name')
|
||||
export class TOrganization extends TContact implements Organization {}
|
||||
|
||||
@Model(contact.class.Employee, contact.class.Person)
|
||||
@ -158,7 +158,7 @@ export function createModel (builder: Builder): void {
|
||||
config: [
|
||||
'',
|
||||
'city',
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files' },
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
|
||||
'modifiedOn',
|
||||
'channels'
|
||||
]
|
||||
@ -170,7 +170,7 @@ export function createModel (builder: Builder): void {
|
||||
open: contact.component.EditContact,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {},
|
||||
config: ['', { presenter: attachment.component.AttachmentsPresenter, label: 'Files' }, 'modifiedOn', 'channels']
|
||||
config: ['', { presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' }, 'modifiedOn', 'channels']
|
||||
})
|
||||
|
||||
builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectEditor, {
|
||||
@ -185,7 +185,7 @@ export function createModel (builder: Builder): void {
|
||||
editor: contact.component.EditOrganization
|
||||
})
|
||||
|
||||
builder.mixin(contact.class.TypeChannels, core.class.Class, view.mixin.AttributePresenter, {
|
||||
builder.mixin(contact.class.TypeChannel, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: contact.component.ChannelsPresenter
|
||||
})
|
||||
|
||||
|
@ -44,6 +44,6 @@ export const ids = mergeIds(contactId, contact, {
|
||||
CreateOrganizations: '' as IntlString
|
||||
},
|
||||
class: {
|
||||
TypeChannels: '' as Ref<Class<Type<Channel[]>>>
|
||||
TypeChannel: '' as Ref<Class<Type<Channel>>>
|
||||
}
|
||||
})
|
||||
|
@ -36,7 +36,7 @@ import lead from './plugin'
|
||||
export class TFunnel extends TSpaceWithStates implements Funnel {}
|
||||
|
||||
@Model(lead.class.Lead, task.class.Task)
|
||||
@UX('Lead' as IntlString, lead.icon.Lead)
|
||||
@UX('Lead' as IntlString, lead.icon.Lead, undefined, 'title')
|
||||
export class TLead extends TTask implements Lead {
|
||||
@Prop(TypeString(), 'Title' as IntlString)
|
||||
title!: string
|
||||
@ -108,8 +108,8 @@ export function createModel (builder: Builder): void {
|
||||
'',
|
||||
'$lookup.customer',
|
||||
'$lookup.state',
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments' },
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments', sortingKey: 'comments' },
|
||||
'modifiedOn',
|
||||
'$lookup.customer.channels'
|
||||
]
|
||||
|
@ -70,7 +70,7 @@ export class TCandidate extends TPerson implements Candidate {
|
||||
}
|
||||
|
||||
@Model(recruit.class.Applicant, task.class.Task)
|
||||
@UX('Application' as IntlString, recruit.icon.Application, 'APP' as IntlString)
|
||||
@UX('Application' as IntlString, recruit.icon.Application, 'APP' as IntlString, 'number')
|
||||
export class TApplicant extends TTask implements Applicant {
|
||||
// We need to declare, to provide property with label
|
||||
@Prop(TypeRef(recruit.class.Candidate), 'Candidate' as IntlString)
|
||||
@ -160,9 +160,9 @@ export function createModel (builder: Builder): void {
|
||||
'',
|
||||
'title',
|
||||
'city',
|
||||
{ presenter: recruit.component.ApplicationsPresenter, label: 'Apps' },
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments' },
|
||||
{ presenter: recruit.component.ApplicationsPresenter, label: 'Apps', sortingKey: 'applications' },
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments', sortingKey: 'comments' },
|
||||
'modifiedOn',
|
||||
'channels'
|
||||
]
|
||||
@ -187,9 +187,8 @@ export function createModel (builder: Builder): void {
|
||||
'$lookup.assignee',
|
||||
'$lookup.state',
|
||||
'$lookup.doneState',
|
||||
// '$lookup.attachedTo.city',
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments' },
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments', sortingKey: 'comments' },
|
||||
'modifiedOn',
|
||||
'$lookup.attachedTo.channels'
|
||||
]
|
||||
|
@ -54,6 +54,7 @@ export const DOMAIN_TASK = 'task' as Domain
|
||||
export const DOMAIN_STATE = 'state' as Domain
|
||||
export const DOMAIN_KANBAN = 'kanban' as Domain
|
||||
@Model(task.class.State, core.class.Doc, DOMAIN_STATE, [core.interface.DocWithRank])
|
||||
@UX('State' as IntlString, undefined, undefined, 'title')
|
||||
export class TState extends TDoc implements State {
|
||||
@Prop(TypeString(), 'Title' as IntlString)
|
||||
title!: string
|
||||
@ -64,6 +65,7 @@ export class TState extends TDoc implements State {
|
||||
}
|
||||
|
||||
@Model(task.class.DoneState, core.class.Doc, DOMAIN_STATE, [core.interface.DocWithRank])
|
||||
@UX('Done' as IntlString, undefined, undefined, 'title')
|
||||
export class TDoneState extends TDoc implements DoneState {
|
||||
@Prop(TypeString(), 'Title' as IntlString)
|
||||
title!: string
|
||||
@ -107,7 +109,7 @@ export class TSpaceWithStates extends TSpace {}
|
||||
export class TProject extends TSpaceWithStates implements Project {}
|
||||
|
||||
@Model(task.class.Issue, task.class.Task, DOMAIN_TASK)
|
||||
@UX('Task' as IntlString, task.icon.Task, 'Task' as IntlString)
|
||||
@UX('Task' as IntlString, task.icon.Task, 'Task' as IntlString, 'number')
|
||||
export class TIssue extends TTask implements Issue {
|
||||
// We need to declare, to provide property with label
|
||||
@Prop(TypeRef(core.class.Doc), 'Parent' as IntlString)
|
||||
@ -254,8 +256,8 @@ export function createModel (builder: Builder): void {
|
||||
'',
|
||||
'name',
|
||||
'$lookup.assignee',
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments' },
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments', sortingKey: 'comments' },
|
||||
'modifiedOn'
|
||||
]
|
||||
})
|
||||
|
@ -15,17 +15,21 @@
|
||||
//
|
||||
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { Builder, Model, TypeString, TypeBoolean, Prop, Collection } from '@anticrm/model'
|
||||
import { Builder, Model, TypeString, TypeBoolean, Prop, ArrOf } from '@anticrm/model'
|
||||
import core, { TAttachedDoc, TDoc } from '@anticrm/model-core'
|
||||
import contact from '@anticrm/model-contact'
|
||||
import telegram from './plugin'
|
||||
import type { TelegramMessage, SharedTelegramMessage, SharedTelegramMessages } from '@anticrm/telegram'
|
||||
import type { Domain } from '@anticrm/core'
|
||||
import type { Domain, Type } from '@anticrm/core'
|
||||
import setting from '@anticrm/setting'
|
||||
import activity from '@anticrm/activity'
|
||||
|
||||
export const DOMAIN_TELEGRAM = 'telegram' as Domain
|
||||
|
||||
function TypeSharedMessage (): Type<SharedTelegramMessage> {
|
||||
return { _class: telegram.class.SharedMessage, label: 'Shared message' as IntlString }
|
||||
}
|
||||
|
||||
@Model(telegram.class.Message, core.class.Doc, DOMAIN_TELEGRAM)
|
||||
export class TTelegramMessage extends TDoc implements TelegramMessage {
|
||||
@Prop(TypeString(), 'Content' as IntlString)
|
||||
@ -43,7 +47,7 @@ export class TTelegramMessage extends TDoc implements TelegramMessage {
|
||||
|
||||
@Model(telegram.class.SharedMessages, core.class.AttachedDoc, DOMAIN_TELEGRAM)
|
||||
export class TSharedTelegramMessages extends TAttachedDoc implements SharedTelegramMessages {
|
||||
@Prop(Collection(telegram.class.SharedMessage), 'Messages' as IntlString)
|
||||
@Prop(ArrOf(TypeSharedMessage()), 'Messages' as IntlString)
|
||||
messages!: SharedTelegramMessage[]
|
||||
}
|
||||
|
||||
|
@ -136,6 +136,7 @@ export interface Class<T extends Obj> extends Classifier {
|
||||
implements?: Ref<Interface<Doc>>[]
|
||||
domain?: Domain
|
||||
shortLabel?: IntlString
|
||||
sortingKey?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,7 +14,7 @@
|
||||
//
|
||||
import type { Plugin, StatusCode } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import type { Account, AnyAttribute, AttachedDoc, DocWithRank, Class, Doc, Interface, Obj, PropertyType, Ref, Space, Timestamp, Type, Collection, RefTo } from './classes'
|
||||
import type { Account, ArrOf, AnyAttribute, AttachedDoc, DocWithRank, Class, Doc, Interface, Obj, PropertyType, Ref, Space, Timestamp, Type, Collection, RefTo } from './classes'
|
||||
import type { Tx, TxBulkWrite, TxCollectionCUD, TxCreateDoc, TxCUD, TxMixin, TxPutBag, TxRemoveDoc, TxUpdateDoc } from './tx'
|
||||
|
||||
/**
|
||||
@ -46,6 +46,7 @@ export default plugin(coreId, {
|
||||
TypeTimestamp: '' as Ref<Class<Type<Timestamp>>>,
|
||||
TypeDate: '' as Ref<Class<Type<Timestamp | Date>>>,
|
||||
RefTo: '' as Ref<Class<RefTo<Doc>>>,
|
||||
ArrOf: '' as Ref<Class<ArrOf<Doc>>>,
|
||||
Collection: '' as Ref<Class<Collection<AttachedDoc>>>,
|
||||
Bag: '' as Ref<Class<Type<Record<string, PropertyType>>>>
|
||||
},
|
||||
|
@ -75,18 +75,30 @@ function arrayOrValue (vv: any): any[] {
|
||||
export function resultSort<T extends Doc> (result: T[], sortOptions: SortingQuery<T>): void {
|
||||
const sortFunc = (a: any, b: any): number => {
|
||||
for (const key in sortOptions) {
|
||||
let aValue = getNestedValue(key, a)
|
||||
if (typeof aValue === 'object') {
|
||||
aValue = JSON.stringify(aValue)
|
||||
}
|
||||
let bValue = getNestedValue(key, b)
|
||||
if (typeof bValue === 'object') {
|
||||
bValue = JSON.stringify(bValue)
|
||||
}
|
||||
const result = typeof aValue === 'string' ? aValue.localeCompare(bValue) : aValue - bValue
|
||||
const aValue = getValue(key, a)
|
||||
const bValue = getValue(key, b)
|
||||
const result = getSortingResult(aValue, bValue)
|
||||
if (result !== 0) return result * (sortOptions[key] as number)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
result.sort(sortFunc)
|
||||
}
|
||||
|
||||
function getSortingResult (aValue: any, bValue: any): number {
|
||||
if (typeof aValue === 'undefined') {
|
||||
return typeof bValue === 'undefined' ? 0 : -1
|
||||
}
|
||||
if (typeof bValue === 'undefined') {
|
||||
return 1
|
||||
}
|
||||
return typeof aValue === 'string' ? aValue.localeCompare(bValue) : (aValue - bValue)
|
||||
}
|
||||
|
||||
function getValue (key: string, obj: any): any {
|
||||
let value = getNestedValue(key, obj)
|
||||
if (typeof value === 'object') {
|
||||
value = JSON.stringify(value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
//
|
||||
|
||||
import core, {
|
||||
ArrOf as TypeArrOf,
|
||||
Account,
|
||||
AttachedDoc, Collection as TypeCollection, RefTo,
|
||||
Attribute, Class, Classifier, ClassifierKind, Data, Doc, Domain, ExtendedAttributes, generateId, IndexKind, Interface, Mixin as IMixin, Obj, PropertyType, Ref, Space, Tx, TxCreateDoc, TxFactory, TxProcessor, Type
|
||||
@ -48,6 +49,7 @@ interface ClassTxes {
|
||||
txes: Array<NoIDs<Tx>>
|
||||
kind: ClassifierKind
|
||||
shortLabel?: IntlString
|
||||
sortingKey?: string
|
||||
}
|
||||
|
||||
const transactions = new Map<any, ClassTxes>()
|
||||
@ -159,13 +161,15 @@ export function Mixin<T extends Obj> (
|
||||
export function UX<T extends Obj> (
|
||||
label: IntlString,
|
||||
icon?: Asset,
|
||||
shortLabel?: IntlString
|
||||
shortLabel?: IntlString,
|
||||
sortingKey?: string
|
||||
) {
|
||||
return function classDecorator<C extends new () => T> (constructor: C): void {
|
||||
const txes = getTxes(constructor.prototype)
|
||||
txes.label = label
|
||||
txes.icon = icon
|
||||
txes.shortLabel = shortLabel
|
||||
txes.sortingKey = sortingKey
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,7 +198,8 @@ function _generateTx (tx: ClassTxes): Tx[] {
|
||||
...(tx.kind === ClassifierKind.INTERFACE ? { extends: tx.implements } : { extends: tx.extends, implements: tx.implements }),
|
||||
label: tx.label,
|
||||
icon: tx.icon,
|
||||
shortLabel: tx.shortLabel
|
||||
shortLabel: tx.shortLabel,
|
||||
sortingKey: tx.sortingKey
|
||||
},
|
||||
objectId
|
||||
)
|
||||
@ -320,6 +325,13 @@ export function Collection<T extends AttachedDoc> (clazz: Ref<Class<T>>): TypeCo
|
||||
return { _class: core.class.Collection, of: clazz, label: 'Collection' as IntlString }
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function ArrOf<T extends PropertyType> (type: Type<T>): TypeArrOf<T> {
|
||||
return { _class: core.class.ArrOf, of: type, label: 'Array' as IntlString }
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -32,7 +32,8 @@ import core, {
|
||||
AnyAttribute,
|
||||
RefTo,
|
||||
Collection,
|
||||
AttachedDoc
|
||||
AttachedDoc,
|
||||
ArrOf
|
||||
} from '@anticrm/core'
|
||||
import { LiveQuery as LQ } from '@anticrm/query'
|
||||
import { getMetadata } from '@anticrm/platform'
|
||||
@ -120,5 +121,8 @@ export function getAttributePresenterClass (attribute: AnyAttribute): Ref<Class<
|
||||
if (attrClass === core.class.Collection) {
|
||||
attrClass = (attribute.type as Collection<AttachedDoc>).of
|
||||
}
|
||||
if (attrClass === core.class.ArrOf) {
|
||||
attrClass = (attribute.type as ArrOf<AttachedDoc>).of._class
|
||||
}
|
||||
return attrClass
|
||||
}
|
||||
|
@ -76,10 +76,10 @@
|
||||
<thead>
|
||||
<tr class="tr-head">
|
||||
{#each model as attribute}
|
||||
<th class:sortable={attribute.key} class:sorted={attribute.key === sortKey} on:click={() => changeSorting(attribute.key)}>
|
||||
<th class:sortable={attribute.sortingKey} class:sorted={attribute.sortingKey === sortKey} on:click={() => changeSorting(attribute.sortingKey)}>
|
||||
<div class="flex-row-center whitespace-nowrap">
|
||||
<Label label = {attribute.label}/>
|
||||
{#if attribute.key === sortKey}
|
||||
{#if attribute.sortingKey === sortKey}
|
||||
<div class="icon">
|
||||
{#if sortOrder === SortingOrder.Ascending}
|
||||
<IconUp size={'small'} />
|
||||
|
@ -115,13 +115,13 @@
|
||||
</th>
|
||||
{/if}
|
||||
<th
|
||||
class:sortable={attribute.key}
|
||||
class:sorted={attribute.key === sortKey}
|
||||
on:click={() => changeSorting(attribute.key)}
|
||||
class:sortable={attribute.sortingKey}
|
||||
class:sorted={attribute.sortingKey === sortKey}
|
||||
on:click={() => changeSorting(attribute.sortingKey)}
|
||||
>
|
||||
<div class="flex-row-center whitespace-nowrap">
|
||||
<Label label={attribute.label} />
|
||||
{#if attribute.key === sortKey}
|
||||
{#if attribute.sortingKey === sortKey}
|
||||
<div class="icon">
|
||||
{#if sortOrder === SortingOrder.Ascending}
|
||||
<IconUp size={'small'} />
|
||||
|
@ -36,11 +36,16 @@ export async function getObjectPresenter (client: Client, _class: Ref<Class<Obj>
|
||||
}
|
||||
}
|
||||
const presenter = await getResource(presenterMixin.presenter)
|
||||
const key = typeof preserveKey === 'string' ? preserveKey : ''
|
||||
const sortingKey = clazz.sortingKey ?
|
||||
(key.length > 0 ? key + '.' + clazz.sortingKey : clazz.sortingKey)
|
||||
: key
|
||||
return {
|
||||
key: typeof preserveKey === 'string' ? preserveKey : '',
|
||||
key,
|
||||
_class,
|
||||
label: clazz.label,
|
||||
presenter
|
||||
presenter,
|
||||
sortingKey
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,9 +64,12 @@ async function getAttributePresenter (client: Client, _class: Ref<Class<Obj>>, k
|
||||
if (presenterMixin.presenter === undefined) {
|
||||
throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey))
|
||||
}
|
||||
const resultKey = typeof preserveKey === 'string' ? preserveKey : ''
|
||||
const sortingKey = attribute.type._class === core.class.ArrOf ? resultKey + '.length' : resultKey
|
||||
const presenter = await getResource(presenterMixin.presenter)
|
||||
return {
|
||||
key: typeof preserveKey === 'string' ? preserveKey : '',
|
||||
key: resultKey,
|
||||
sortingKey,
|
||||
_class: attrClass,
|
||||
label: attribute.label,
|
||||
presenter
|
||||
@ -70,9 +78,10 @@ async function getAttributePresenter (client: Client, _class: Ref<Class<Obj>>, k
|
||||
|
||||
async function getPresenter (client: Client, _class: Ref<Class<Obj>>, key: BuildModelKey, preserveKey: BuildModelKey, options?: FindOptions<Doc>): Promise<AttributeModel> {
|
||||
if (typeof key === 'object') {
|
||||
const { presenter, label } = key
|
||||
const { presenter, label, sortingKey } = key
|
||||
return {
|
||||
key: '',
|
||||
sortingKey: sortingKey ?? '',
|
||||
_class,
|
||||
label: label as IntlString,
|
||||
presenter: await getResource(presenter)
|
||||
@ -116,6 +125,7 @@ export async function buildModel (options: BuildModelOptions): Promise<Attribute
|
||||
console.error('Failed to find presenter for', key, err)
|
||||
const errorPresenter: AttributeModel = {
|
||||
key: '',
|
||||
sortingKey: '',
|
||||
presenter: ErrorPresenter,
|
||||
label: stringKey as IntlString,
|
||||
_class: core.class.TypeString,
|
||||
|
@ -84,6 +84,7 @@ export const viewId = 'view' as Plugin
|
||||
export type BuildModelKey = string | {
|
||||
presenter: AnyComponent
|
||||
label: string
|
||||
sortingKey?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,6 +97,7 @@ export interface AttributeModel {
|
||||
presenter: AnySvelteComponent
|
||||
// Extra properties for component
|
||||
props?: Record<string, any>
|
||||
sortingKey: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user