Objects sort (#672)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2021-12-20 15:06:31 +06:00 committed by GitHub
parent 9a8d9f8804
commit 81000b50f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 97 additions and 50 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -136,6 +136,7 @@ export interface Class<T extends Obj> extends Classifier {
implements?: Ref<Interface<Doc>>[]
domain?: Domain
shortLabel?: IntlString
sortingKey?: string
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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