From 275b2b0800af4debc37d89f4b9380256ad78b86b Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Thu, 30 Dec 2021 16:04:32 +0700 Subject: [PATCH] Leads with Mixins (#737) Signed-off-by: Andrey Sobolev --- dev/client-resources/package.json | 3 +- dev/client-resources/src/connection.ts | 4 +- dev/docker-compose.yaml | 4 +- dev/readme.md | 16 ++ models/contact/package.json | 3 +- models/contact/src/index.ts | 3 +- models/contact/src/plugin.ts | 4 + models/core/src/component.ts | 3 +- models/core/src/core.ts | 10 +- models/core/src/index.ts | 3 +- models/core/src/tx.ts | 4 +- models/lead/src/index.ts | 51 +++-- models/lead/src/migration.ts | 4 +- models/lead/src/plugin.ts | 4 +- packages/core/src/__tests__/memdb.test.ts | 17 +- packages/core/src/client.ts | 17 +- packages/core/src/component.ts | 2 + packages/core/src/hierarchy.ts | 62 +++++- packages/core/src/memdb.ts | 12 +- packages/core/src/tx.ts | 61 ++++-- packages/model/src/dsl.ts | 16 +- packages/presentation/src/attributes.ts | 32 +++ .../src/components/AttributeBarEditor.svelte | 23 +- .../src/components/AttributeEditor.svelte | 13 +- .../src/components/AttributesBar.svelte | 3 +- .../src/components/UsersPopup.svelte | 5 +- packages/presentation/src/index.ts | 7 +- packages/query/src/index.ts | 103 ++++++--- packages/ui/src/components/Component.svelte | 2 +- .../ui/src/components/EditWithIcon.svelte | 8 + packages/ui/src/index.ts | 10 +- plugins/activity-resources/src/activity.ts | 53 +++-- .../src/components/TxView.svelte | 48 +++- .../src/components/EditContact.svelte | 205 ++++++++++++++---- .../src/components/EditPerson.svelte | 8 +- plugins/devmodel-resources/src/index.ts | 10 +- plugins/devmodel/src/index.ts | 7 +- plugins/lead-assets/lang/en.json | 2 + plugins/lead-resources/package.json | 3 +- .../src/components/CreateLead.svelte | 18 +- .../src/components/Customers.svelte | 112 ++++++++++ .../src/components/LeadPresenter.svelte | 8 +- .../src/components/Leads.svelte | 87 ++++++++ .../src/components/LeadsPopup.svelte | 38 ++++ .../src/components/LeadsPresenter.svelte | 34 +++ plugins/lead-resources/src/index.ts | 8 +- plugins/lead-resources/src/plugin.ts | 10 +- plugins/lead/src/index.ts | 19 +- .../src/components/EditApplication.svelte | 19 +- .../src/components/Navigator.svelte | 12 +- plugins/workbench-resources/src/index.ts | 3 +- plugins/workbench/src/index.ts | 1 + server/core/src/fulltext.ts | 5 +- server/core/src/storage.ts | 16 +- server/mongo/src/storage.ts | 97 ++++++++- 55 files changed, 1079 insertions(+), 253 deletions(-) create mode 100644 dev/readme.md create mode 100644 packages/presentation/src/attributes.ts create mode 100644 plugins/lead-resources/src/components/Customers.svelte create mode 100644 plugins/lead-resources/src/components/Leads.svelte create mode 100644 plugins/lead-resources/src/components/LeadsPopup.svelte create mode 100644 plugins/lead-resources/src/components/LeadsPresenter.svelte diff --git a/dev/client-resources/package.json b/dev/client-resources/package.json index e4aa3ca6dc..7e8e092b1d 100644 --- a/dev/client-resources/package.json +++ b/dev/client-resources/package.json @@ -31,6 +31,7 @@ "@anticrm/client": "~0.6.1", "@anticrm/dev-storage": "~0.6.6", "@anticrm/server-core": "~0.6.1", - "@anticrm/model-all": "~0.6.0" + "@anticrm/model-all": "~0.6.0", + "@anticrm/devmodel": "~0.6.0" } } diff --git a/dev/client-resources/src/connection.ts b/dev/client-resources/src/connection.ts index 648853521f..cd2c9e2543 100644 --- a/dev/client-resources/src/connection.ts +++ b/dev/client-resources/src/connection.ts @@ -15,9 +15,10 @@ import { Class, ClientConnection, Doc, DocumentQuery, FindOptions, FindResult, Ref, ServerStorage, Tx, TxHander, TxResult, DOMAIN_TX, MeasureMetricsContext } from '@anticrm/core' import { createInMemoryAdapter, createInMemoryTxAdapter } from '@anticrm/dev-storage' -import { protoDeserialize, protoSerialize } from '@anticrm/platform' +import { protoDeserialize, protoSerialize, setMetadata } from '@anticrm/platform' import type { DbConfiguration } from '@anticrm/server-core' import { createServerStorage, FullTextAdapter, IndexedDoc } from '@anticrm/server-core' +import devmodel from '@anticrm/devmodel' class ServerStorageWrapper implements ClientConnection { measureCtx = new MeasureMetricsContext('client', {}) @@ -80,5 +81,6 @@ export async function connect (handler: (tx: Tx) => void): Promise { } @Model(contact.class.Contact, core.class.Doc, DOMAIN_CONTACT) +@UX('Contact' as IntlString, contact.icon.Person, undefined, 'name') export class TContact extends TDoc implements Contact { @Prop(TypeString(), 'Name' as IntlString) @Index(IndexKind.FullText) @@ -147,7 +148,7 @@ export function createModel (builder: Builder): void { } ] } - }) + }, contact.app.Contacts) builder.createDoc(view.class.Viewlet, core.space.Model, { attachTo: contact.class.Person, diff --git a/models/contact/src/plugin.ts b/models/contact/src/plugin.ts index 5ad1559feb..79d4b94202 100644 --- a/models/contact/src/plugin.ts +++ b/models/contact/src/plugin.ts @@ -19,8 +19,12 @@ import contact, { contactId } from '@anticrm/contact' import type { Channel } from '@anticrm/contact' import type { AnyComponent } from '@anticrm/ui' import {} from '@anticrm/core' +import { Application } from '@anticrm/workbench' export const ids = mergeIds(contactId, contact, { + app: { + Contacts: '' as Ref + }, component: { PersonPresenter: '' as AnyComponent, ContactPresenter: '' as AnyComponent, diff --git a/models/core/src/component.ts b/models/core/src/component.ts index 8b30e41e92..3cfa5b7a3d 100644 --- a/models/core/src/component.ts +++ b/models/core/src/component.ts @@ -13,13 +13,12 @@ // limitations under the License. // -import type { Class, Doc, Mixin, Ref, Type } from '@anticrm/core' +import type { Class, Ref, Type } from '@anticrm/core' import core, { coreId } from '@anticrm/core' import { mergeIds } from '@anticrm/platform' export default mergeIds(coreId, core, { class: { - Mixin: '' as Ref>>, Type: '' as Ref>> } }) diff --git a/models/core/src/core.ts b/models/core/src/core.ts index 6451b14758..bdae243d8a 100644 --- a/models/core/src/core.ts +++ b/models/core/src/core.ts @@ -30,7 +30,8 @@ import type { Type, Collection, RefTo, - ArrOf + ArrOf, + Interface } from '@anticrm/core' import { DOMAIN_MODEL } from '@anticrm/core' import { Model, Prop, TypeRef, TypeString, TypeTimestamp } from '@anticrm/model' @@ -81,6 +82,13 @@ export class TClass extends TDoc implements Class { @Model(core.class.Mixin, core.class.Class) export class TMixin extends TClass implements Mixin {} +@Model(core.class.Interface, core.class.Class) +export class TInterface extends TDoc implements Interface { + kind!: ClassifierKind + label!: IntlString + extends?: Ref>[] +} + @Model(core.class.Attribute, core.class.Doc) export class TAttribute extends TDoc implements AnyAttribute { attributeOf!: Ref> diff --git a/models/core/src/index.ts b/models/core/src/index.ts index fe1ad0c022..26fee6bea6 100644 --- a/models/core/src/index.ts +++ b/models/core/src/index.ts @@ -15,7 +15,7 @@ import { Builder } from '@anticrm/model' import core from './component' -import { TAttribute, TArrOf, TClass, TDoc, TMixin, TObj, TType, TTypeString, TTypeBoolean, TTypeTimestamp, TTypeDate, TAttachedDoc, TCollection, TRefTo } from './core' +import { TAttribute, TArrOf, TClass, TDoc, TMixin, TObj, TType, TTypeString, TTypeBoolean, TTypeTimestamp, TTypeDate, TAttachedDoc, TCollection, TRefTo, TInterface } from './core' import { TSpace, TAccount } from './security' import { TTx, TTxCreateDoc, TTxMixin, TTxUpdateDoc, TTxCUD, TTxPutBag, TTxRemoveDoc, TTxBulkWrite, TTxCollectionCUD } from './tx' @@ -31,6 +31,7 @@ export function createModel (builder: Builder): void { TDoc, TClass, TMixin, + TInterface, TTx, TTxCUD, TTxCreateDoc, diff --git a/models/core/src/tx.ts b/models/core/src/tx.ts index c85487dba6..d5a83901fb 100644 --- a/models/core/src/tx.ts +++ b/models/core/src/tx.ts @@ -19,8 +19,8 @@ import type { Data, Doc, DocumentUpdate, - ExtendedAttributes, Mixin, + MixinUpdate, PropertyType, Ref, Space, @@ -73,7 +73,7 @@ export class TTxPutBag extends TTxCUD implements Tx @Model(core.class.TxMixin, core.class.TxCUD) export class TTxMixin extends TTxCUD implements TxMixin { mixin!: Ref> - attributes!: ExtendedAttributes + attributes!: MixinUpdate } @Model(core.class.TxUpdateDoc, core.class.TxCUD) diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index 9d46b0dade..ed55e79d90 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -15,19 +15,19 @@ // // To help typescript locate view plugin properly -import type { Contact, Employee } from '@anticrm/contact' +import type { Employee } from '@anticrm/contact' import type { Doc, FindOptions, Ref } from '@anticrm/core' -import type { Funnel, Lead } from '@anticrm/lead' -import { Builder, Collection, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model' +import type { Customer, Funnel, Lead } from '@anticrm/lead' +import { Builder, Collection, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model' import attachment from '@anticrm/model-attachment' import chunter from '@anticrm/model-chunter' -import contact from '@anticrm/model-contact' +import contact, { TPerson } from '@anticrm/model-contact' import core from '@anticrm/model-core' import task, { TSpaceWithStates, TTask } from '@anticrm/model-task' import view from '@anticrm/model-view' import workbench from '@anticrm/model-workbench' import type { IntlString } from '@anticrm/platform' -import type {} from '@anticrm/view' +import type { } from '@anticrm/view' import lead from './plugin' @Model(lead.class.Funnel, task.class.SpaceWithStates) @@ -37,12 +37,12 @@ export class TFunnel extends TSpaceWithStates implements Funnel {} @Model(lead.class.Lead, task.class.Task) @UX('Lead' as IntlString, lead.icon.Lead, undefined, 'title') export class TLead extends TTask implements Lead { + @Prop(TypeRef(contact.class.Contact), lead.string.Customer) + declare attachedTo: Ref + @Prop(TypeString(), 'Title' as IntlString) title!: string - @Prop(TypeRef(contact.class.Contact), lead.string.Customer) - customer!: Ref - @Prop(Collection(chunter.class.Comment), 'Comments' as IntlString) comments?: number @@ -53,8 +53,18 @@ export class TLead extends TTask implements Lead { declare assignee: Ref | null } +@Mixin(lead.mixin.Customer, contact.class.Contact) +@UX('Customer' as IntlString, contact.icon.Person) // <-- Use general customer icons here. +export class TCustomer extends TPerson implements Customer { + @Prop(Collection(lead.class.Lead), 'Leads' as IntlString) + leads?: number + + @Prop(TypeString(), 'Description' as IntlString) + description!: string +} + export function createModel (builder: Builder): void { - builder.createModel(TFunnel, TLead) + builder.createModel(TFunnel, TLead, TCustomer) builder.mixin(lead.class.Funnel, core.class.Class, workbench.mixin.SpaceView, { view: { @@ -71,6 +81,15 @@ export function createModel (builder: Builder): void { icon: lead.icon.LeadApplication, hidden: false, navigatorModel: { + specials: [ + { + id: 'customers', + label: lead.string.Customers, + icon: contact.icon.Person, // <-- Put contact general icon here. + component: lead.component.Customers, + position: 'bottom' + } + ], spaces: [ { label: lead.string.Funnels, @@ -103,18 +122,18 @@ export function createModel (builder: Builder): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions options: { lookup: { - customer: contact.class.Contact, + attachedTo: contact.class.Contact, state: task.class.State } } as FindOptions, // TODO: fix config: [ '', - '$lookup.customer', + '$lookup.attachedTo', '$lookup.state', { presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' }, { presenter: chunter.component.CommentsPresenter, label: 'Comments', sortingKey: 'comments' }, 'modifiedOn', - '$lookup.customer.channels' + '$lookup.attachedTo.channels' ] }) @@ -144,6 +163,10 @@ export function createModel (builder: Builder): void { presenter: lead.component.LeadPresenter }) + builder.mixin(lead.class.Lead, core.class.Class, view.mixin.AttributeEditor, { + editor: lead.component.Leads + }) + builder.createDoc( task.class.KanbanTemplateSpace, core.space.Model, @@ -159,6 +182,6 @@ export function createModel (builder: Builder): void { ) } -export { default } from './plugin' -export { leadOperation } from './migration' export { createDeps } from './creation' +export { leadOperation } from './migration' +export { default } from './plugin' diff --git a/models/lead/src/migration.ts b/models/lead/src/migration.ts index a01c695135..474acf5a1f 100644 --- a/models/lead/src/migration.ts +++ b/models/lead/src/migration.ts @@ -42,7 +42,7 @@ export const leadOperation: MigrateOperation = { await client.update>(DOMAIN_TX, { _id: tx._id }, { attributes: { ...tx.attributes, - attachedTo: tx.attributes.customer, + attachedTo: (tx.attributes as any).customer, attachedToClass: contact.class.Contact } }) @@ -57,7 +57,7 @@ export const leadOperation: MigrateOperation = { for (const lead of leads) { if (lead.attachedTo === undefined) { await ops.updateDoc(lead._class, lead.space, lead._id, { - attachedTo: lead.customer, + attachedTo: (lead as any).customer, attachedToClass: contact.class.Contact }) } diff --git a/models/lead/src/plugin.ts b/models/lead/src/plugin.ts index 8b74debc69..d01432f513 100644 --- a/models/lead/src/plugin.ts +++ b/models/lead/src/plugin.ts @@ -38,7 +38,9 @@ export default mergeIds(leadId, lead, { EditLead: '' as AnyComponent, KanbanCard: '' as AnyComponent, LeadPresenter: '' as AnyComponent, - TemplatesIcon: '' as AnyComponent + TemplatesIcon: '' as AnyComponent, + Customers: '' as AnyComponent, + Leads: '' as AnyComponent }, space: { DefaultFunnel: '' as Ref diff --git a/packages/core/src/__tests__/memdb.test.ts b/packages/core/src/__tests__/memdb.test.ts index dc1fbd4dcb..8ca1e32bd3 100644 --- a/packages/core/src/__tests__/memdb.test.ts +++ b/packages/core/src/__tests__/memdb.test.ts @@ -45,15 +45,6 @@ describe('memdb', () => { expect(result.length).toBe(txes.filter((tx) => tx._class === core.class.TxCreateDoc).length) }) - it('should query model', async () => { - const { model } = await createModel() - - const result = await model.findAll(core.class.Class, {}) - expect(result.length).toBeGreaterThan(5) - const result2 = await model.findAll('class:workbench.Application' as Ref>, { _id: undefined }) - expect(result2).toHaveLength(0) - }) - it('should create space', async () => { const { model } = await createModel() @@ -85,11 +76,17 @@ describe('memdb', () => { expect(result2.length).toBe(0) }) + it('should fail query wrong class', async () => { + const { model } = await createModel() + + await expect(model.findAll('class:workbench.Application' as Ref>, { _id: undefined })).rejects.toThrow() + }) + it('should create mixin', async () => { const { model } = await createModel() const ops = new TxOperations(model, core.account.System) - await ops.createMixin(core.class.Obj, core.class.Class, test.mixin.TestMixin, { arr: ['hello'] }) + await ops.createMixin(core.class.Obj, core.class.Class, core.space.Model, test.mixin.TestMixin, { arr: ['hello'] }) const objClass = (await model.findAll(core.class.Class, { _id: core.class.Obj }))[0] as any expect(objClass['test:mixin:TestMixin'].arr).toEqual(expect.arrayContaining(['hello'])) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index aea331ab75..55255f13ee 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -67,10 +67,16 @@ class ClientImpl implements Client { options?: FindOptions ): Promise> { const domain = this.hierarchy.getDomain(_class) - if (domain === DOMAIN_MODEL) { - return await this.model.findAll(_class, query, options) + const result = (domain === DOMAIN_MODEL) + ? await this.model.findAll(_class, query, options) + : await this.conn.findAll(_class, query, options) + + // In case of mixin we need to create mixin proxies. + const baseClass = this.hierarchy.getBaseClass(_class) + if (baseClass !== _class) { + return result.map(v => this.hierarchy.as(v, _class)) } - return await this.conn.findAll(_class, query, options) + return result } async findOne( @@ -86,8 +92,11 @@ class ClientImpl implements Client { this.hierarchy.tx(tx) await this.model.tx(tx) } + + // We need to handle it on server, before performing local live query updates. + const result = await this.conn.tx(tx) this.notify?.(tx) - return await this.conn.tx(tx) + return result } async updateFromRemote (tx: Tx): Promise { diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 39deef8015..9be0a2419d 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -14,6 +14,7 @@ // import type { Plugin, StatusCode } from '@anticrm/platform' import { plugin } from '@anticrm/platform' +import { Mixin } from '.' import type { Account, ArrOf, AnyAttribute, AttachedDoc, 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' @@ -28,6 +29,7 @@ export default plugin(coreId, { Doc: '' as Ref>, AttachedDoc: '' as Ref>, Class: '' as Ref>>, + Mixin: '' as Ref>>, Interface: '' as Ref>>, Attribute: '' as Ref>, Tx: '' as Ref>, diff --git a/packages/core/src/hierarchy.ts b/packages/core/src/hierarchy.ts index d5d2e9e56e..2e4b7990aa 100644 --- a/packages/core/src/hierarchy.ts +++ b/packages/core/src/hierarchy.ts @@ -19,6 +19,9 @@ import core from './component' import type { Tx, TxCreateDoc, TxMixin } from './tx' import { TxProcessor } from './tx' +const PROXY_TARGET_KEY = '$___proxy_target' +const PROXY_MIXIN_CLASS_KEY = '$__mixin' + /** * @public */ @@ -35,6 +38,13 @@ export class Hierarchy { const ancestorProxy = ancestor.kind === ClassifierKind.MIXIN ? this.getMixinProxyHandler(ancestor._id) : null return { get (target: any, property: string, receiver: any): any { + if (property === PROXY_TARGET_KEY) { + return target + } + // We need to override _class property, to return proper mixin class. + if (property === PROXY_MIXIN_CLASS_KEY) { + return mixin + } const value = target[mixin]?.[property] if (value === undefined) { return ancestorProxy !== null ? ancestorProxy.get?.(target, property, receiver) : target[property] @@ -58,6 +68,28 @@ export class Hierarchy { return new Proxy(doc, this.getMixinProxyHandler(mixin)) as M } + static toDoc(doc: D): D { + const targetDoc = (doc as any)[PROXY_TARGET_KEY] + if (targetDoc !== undefined) { + return targetDoc as D + } + return doc + } + + static mixinClass(doc: D): Ref>|undefined { + return (doc as any)[PROXY_MIXIN_CLASS_KEY] + } + + hasMixin(doc: D, mixin: Ref>): boolean { + const d = Hierarchy.toDoc(doc) + return typeof (d as any)[mixin] === 'object' + } + + isMixin (_class: Ref>): boolean { + const data = this.classifiers.get(_class) + return data !== undefined && this._isMixin(data) + } + getAncestors (_class: Ref): Ref[] { const result = this.ancestors.get(_class) if (result === undefined) { @@ -113,7 +145,7 @@ export class Hierarchy { } private txCreateDoc (tx: TxCreateDoc): void { - if (tx.objectClass === core.class.Class || tx.objectClass === core.class.Interface) { + if (tx.objectClass === core.class.Class || tx.objectClass === core.class.Interface || tx.objectClass === core.class.Mixin) { const _id = tx.objectId as Ref this.classifiers.set(_id, TxProcessor.createDoc2Doc(tx as TxCreateDoc)) this.addAncestors(_id) @@ -127,7 +159,7 @@ export class Hierarchy { private txMixin (tx: TxMixin): void { if (tx.objectClass === core.class.Class) { const obj = this.getClass(tx.objectId as Ref>) as any - obj[tx.mixin] = tx.attributes + TxProcessor.updateMixin4Doc(obj, tx.mixin, tx.attributes) } } @@ -144,6 +176,19 @@ export class Hierarchy { return false } + /** + * Return first non interface/mixin parent + */ + getBaseClass(_class: Ref>): Ref> { + let cl: Ref> | undefined = _class + while (cl !== undefined) { + const clz = this.getClass(cl) + if (this.isClass(clz)) return cl + cl = clz.extends + } + return core.class.Doc + } + /** * Check if passed _class implements passed interfaces `from`. * It will check for class parents and they interfaces. @@ -220,7 +265,7 @@ export class Hierarchy { private ancestorsOf (classifier: Ref): Ref[] { const attrs = this.classifiers.get(classifier) const result: Ref[] = [] - if (this.isClass(attrs)) { + if (this.isClass(attrs) || this._isMixin(attrs)) { const cls = attrs as Class if (cls.extends !== undefined) { result.push(cls.extends) @@ -237,6 +282,10 @@ export class Hierarchy { return attrs?.kind === ClassifierKind.CLASS } + private _isMixin (attrs?: Classifier): boolean { + return attrs?.kind === ClassifierKind.MIXIN + } + private isInterface (attrs?: Classifier): boolean { return attrs?.kind === ClassifierKind.INTERFACE } @@ -251,9 +300,12 @@ export class Hierarchy { attributes.set(attribute.name, attribute) } - getAllAttributes (clazz: Ref): Map { + getAllAttributes (clazz: Ref, to?: Ref): Map { const result = new Map() - const ancestors = this.getAncestors(clazz) + let ancestors = this.getAncestors(clazz) + if (to !== undefined) { + ancestors = ancestors.filter(c => this.isDerived(c, to) && c !== to) + } for (const cls of ancestors) { const attributes = this.attributes.get(cls) diff --git a/packages/core/src/memdb.ts b/packages/core/src/memdb.ts index d652df41c0..e6e186b850 100644 --- a/packages/core/src/memdb.ts +++ b/packages/core/src/memdb.ts @@ -99,17 +99,23 @@ export abstract class MemDb extends TxProcessor { options?: FindOptions ): Promise> { let result: Doc[] + const baseClass = this.hierarchy.getBaseClass(_class) if ( Object.prototype.hasOwnProperty.call(query, '_id') && (typeof query._id === 'string' || query._id?.$in !== undefined || query._id === undefined || query._id === null) ) { - result = this.getByIdQuery(query, _class) + result = this.getByIdQuery(query, baseClass) } else { - result = this.getObjectsByClass(_class) + result = this.getObjectsByClass(baseClass) } result = matchQuery(result, query) + if (baseClass !== _class) { + // We need to filter instances without mixin was set + result = result.filter(r => (r as any)[_class] !== undefined) + } + if (options?.lookup !== undefined) result = this.lookup(result as T[], options.lookup) if (options?.sort !== undefined) resultSort(result, options?.sort) @@ -206,7 +212,7 @@ export class ModelDb extends MemDb implements Storage { // TODO: process ancessor mixins protected async txMixin (tx: TxMixin): Promise { const obj = this.getObject(tx.objectId) as any - obj[tx.mixin] = tx.attributes + TxProcessor.updateMixin4Doc(obj, tx.mixin, tx.attributes) return {} } } diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts index 76046b6093..94928ca7f1 100644 --- a/packages/core/src/tx.ts +++ b/packages/core/src/tx.ts @@ -19,6 +19,7 @@ import type { DocumentQuery, FindOptions, FindResult, Storage, WithLookup, TxRes import core from './component' import { generateId } from './utils' import { _getOperator } from './operator' +import { Hierarchy } from './hierarchy' /** * @public @@ -72,20 +73,21 @@ export interface TxBulkWrite extends Tx { /** * @public */ -export type ExtendedAttributes = Omit +export type MixinUpdate = Partial> & PushOptions> & IncOptions> /** + * Define Create/Update for mixin attributes. * @public */ export interface TxMixin extends TxCUD { mixin: Ref> - attributes: ExtendedAttributes + attributes: MixinUpdate } /** * @public */ -export type ArrayAsElement = { +export type ArrayAsElement = { [P in keyof T]: T[P] extends Arr ? X : never } @@ -108,21 +110,21 @@ export interface MoveDescriptor { /** * @public */ -export type ArrayAsElementPosition = { +export type ArrayAsElementPosition = { [P in keyof T]: T[P] extends Arr ? X | Position : never } /** * @public */ -export type ArrayMoveDescriptor = { +export type ArrayMoveDescriptor = { [P in keyof T]: T[P] extends Arr ? MoveDescriptor : never } /** * @public */ -export type NumberProperties = { +export type NumberProperties = { [P in keyof T]: T[P] extends number | undefined ? T[P] : never } @@ -134,7 +136,7 @@ export type OmitNever = Omit> /** * @public */ -export interface PushOptions { +export interface PushOptions { $push?: Partial>> $pull?: Partial>> $move?: Partial>> @@ -153,7 +155,7 @@ export interface PushMixinOptions { /** * @public */ -export interface IncOptions { +export interface IncOptions { $inc?: Partial>> } @@ -231,7 +233,8 @@ export abstract class TxProcessor implements WithTx { } as T } - static updateDoc2Doc(doc: T, tx: TxUpdateDoc): T { + static updateDoc2Doc(rawDoc: T, tx: TxUpdateDoc): T { + const doc = Hierarchy.toDoc(rawDoc) const ops = tx.operations as any for (const key in ops) { if (key.startsWith('$')) { @@ -243,7 +246,23 @@ export abstract class TxProcessor implements WithTx { } doc.modifiedBy = tx.modifiedBy doc.modifiedOn = tx.modifiedOn - return doc + return rawDoc + } + + static updateMixin4Doc(rawDoc: D, mixinClass: Ref>, operations: MixinUpdate): D { + const ops = operations as any + const doc = Hierarchy.toDoc(rawDoc) + const mixin = (doc as any)[mixinClass] ?? {} + for (const key in ops) { + if (key.startsWith('$')) { + const operator = _getOperator(key) + operator(mixin, ops[key]) + } else { + mixin[key] = ops[key] + } + } + (doc as any)[mixinClass] = mixin + return rawDoc } protected abstract txCreateDoc (tx: TxCreateDoc): Promise @@ -405,10 +424,22 @@ export class TxOperations implements Storage { createMixin( objectId: Ref, objectClass: Ref>, + objectSpace: Ref, mixin: Ref>, - attributes: ExtendedAttributes + attributes: MixinUpdate ): Promise { - const tx = this.txFactory.createTxMixin(objectId, objectClass, mixin, attributes) + const tx = this.txFactory.createTxMixin(objectId, objectClass, objectSpace, mixin, attributes) + return this.storage.tx(tx) + } + + updateMixin( + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + mixin: Ref>, + attributes: MixinUpdate + ): Promise { + const tx = this.txFactory.createTxMixin(objectId, objectClass, objectSpace, mixin, attributes) return this.storage.tx(tx) } } @@ -515,7 +546,7 @@ export class TxFactory { } } - createTxMixin(objectId: Ref, objectClass: Ref>, mixin: Ref>, attributes: ExtendedAttributes): TxMixin { + createTxMixin(objectId: Ref, objectClass: Ref>, objectSpace: Ref, mixin: Ref>, attributes: MixinUpdate): TxMixin { return { _id: generateId(), _class: core.class.TxMixin, @@ -524,9 +555,9 @@ export class TxFactory { modifiedOn: Date.now(), objectId, objectClass, - objectSpace: core.space.Model, + objectSpace, mixin, - attributes + attributes: attributes } } diff --git a/packages/model/src/dsl.ts b/packages/model/src/dsl.ts index d7e101463d..278bc25060 100644 --- a/packages/model/src/dsl.ts +++ b/packages/model/src/dsl.ts @@ -14,10 +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 + Account, ArrOf as TypeArrOf, AttachedDoc, Attribute, Class, Classifier, ClassifierKind, Collection as TypeCollection, Data, Doc, Domain, generateId, IndexKind, Interface, Mixin as IMixin, MixinUpdate, Obj, PropertyType, Ref, RefTo, Space, Tx, TxCreateDoc, TxFactory, TxProcessor, Type } from '@anticrm/core' import type { Asset, IntlString } from '@anticrm/platform' import toposort from 'toposort' @@ -225,8 +222,13 @@ const txFactory = new TxFactory(core.account.System) function _generateTx (tx: ClassTxes): Tx[] { const objectId = tx._id + const _cl = { + [ClassifierKind.CLASS]: core.class.Class, + [ClassifierKind.INTERFACE]: core.class.Interface, + [ClassifierKind.MIXIN]: core.class.Mixin + } const createTx = txFactory.createTxCreateDoc( - core.class.Class, + _cl[tx.kind], core.space.Model, { ...(tx.domain !== undefined ? { domain: tx.domain } : {}), @@ -307,9 +309,9 @@ export class Builder { objectId: Ref, objectClass: Ref>, mixin: Ref>, - attributes: ExtendedAttributes + attributes: MixinUpdate ): void { - this.txes.push(txFactory.createTxMixin(objectId, objectClass, mixin, attributes)) + this.txes.push(txFactory.createTxMixin(objectId, objectClass, core.space.Model, mixin, attributes)) } getTxes (): Tx[] { diff --git a/packages/presentation/src/attributes.ts b/packages/presentation/src/attributes.ts new file mode 100644 index 0000000000..caf3475669 --- /dev/null +++ b/packages/presentation/src/attributes.ts @@ -0,0 +1,32 @@ +import core, { AnyAttribute, AttachedDoc, Class, Client, Doc, Ref, TxOperations } from '@anticrm/core' + +/** + * @public + */ +export interface KeyedAttribute { + key: string + attr: AnyAttribute +} + +export async function updateAttribute (client: Client & TxOperations, object: Doc, _class: Ref>, attribute: KeyedAttribute, value: any): Promise { + const doc = object + const attributeKey = attribute.key + const attr = attribute.attr + if (client.getHierarchy().isMixin(attr.attributeOf)) { + await client.updateMixin(doc._id, _class, doc.space, attr.attributeOf, { [attributeKey]: value }) + } else if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) { + const adoc = object as AttachedDoc + await client.updateCollection(_class, object.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection, { [attributeKey]: value }) + } else { + await client.updateDoc(_class, doc.space, doc._id, { [attributeKey]: value }) + } +} + +export function getAttribute (client: Client, object: any, key: KeyedAttribute): any { + // Check if attr is mixin and return it's value + if (client.getHierarchy().isMixin(key.attr.attributeOf)) { + return (object[key.attr.attributeOf] ?? {})[key.key] + } else { + return object[key.key] + } +} diff --git a/packages/presentation/src/components/AttributeBarEditor.svelte b/packages/presentation/src/components/AttributeBarEditor.svelte index 88ab0dd124..5c35cc6562 100644 --- a/packages/presentation/src/components/AttributeBarEditor.svelte +++ b/packages/presentation/src/components/AttributeBarEditor.svelte @@ -15,16 +15,16 @@ --> @@ -69,17 +66,17 @@ {#if showHeader}