mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 21:50:34 +03:00
Leads with Mixins (#737)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
b05e230246
commit
275b2b0800
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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<ClientConnect
|
||||
workspace: ''
|
||||
}
|
||||
const serverStorage = await createServerStorage(conf)
|
||||
setMetadata(devmodel.metadata.DevModel, serverStorage)
|
||||
return new ServerStorageWrapper(serverStorage, handler)
|
||||
}
|
||||
|
@ -31,7 +31,9 @@ services:
|
||||
- ELASTICSEARCH_PORT_NUMBER=9200
|
||||
- BITNAMI_DEBUG=true
|
||||
- discovery.type=single-node
|
||||
- "ES_JAVA_OPTS=-Xms1024m -Xmx1024m"
|
||||
- ES_JAVA_OPTS=-Xms1024m -Xmx1024m
|
||||
- http.cors.enabled=true
|
||||
- http.cors.allow-origin=http://localhost:8082
|
||||
healthcheck:
|
||||
interval: 20s
|
||||
retries: 10
|
||||
|
16
dev/readme.md
Normal file
16
dev/readme.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Docker Compose dev image
|
||||
|
||||
## Running platform inside docker compose
|
||||
|
||||
```bash
|
||||
rush build
|
||||
rush bundle
|
||||
rush docker:build
|
||||
docker-compose up -d --force-recreate
|
||||
```
|
||||
|
||||
## Running ElasticVUE to check elastic intance
|
||||
|
||||
```bash
|
||||
docker run -p 8082:8080 -d cars10/elasticvue
|
||||
```
|
@ -34,6 +34,7 @@
|
||||
"@anticrm/core": "~0.6.11",
|
||||
"@anticrm/ui": "~0.6.0",
|
||||
"@anticrm/platform": "~0.6.5",
|
||||
"@anticrm/contact": "~0.6.2"
|
||||
"@anticrm/contact": "~0.6.2",
|
||||
"@anticrm/workbench": "~0.6.1"
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ export function TypeChannel (): Type<Channel> {
|
||||
}
|
||||
|
||||
@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,
|
||||
|
@ -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<Application>
|
||||
},
|
||||
component: {
|
||||
PersonPresenter: '' as AnyComponent,
|
||||
ContactPresenter: '' as AnyComponent,
|
||||
|
@ -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<Class<Mixin<Doc>>>,
|
||||
Type: '' as Ref<Class<Type<any>>>
|
||||
}
|
||||
})
|
||||
|
@ -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<Obj> {
|
||||
@Model(core.class.Mixin, core.class.Class)
|
||||
export class TMixin extends TClass implements Mixin<Doc> {}
|
||||
|
||||
@Model(core.class.Interface, core.class.Class)
|
||||
export class TInterface extends TDoc implements Interface<Doc> {
|
||||
kind!: ClassifierKind
|
||||
label!: IntlString
|
||||
extends?: Ref<Interface<Doc>>[]
|
||||
}
|
||||
|
||||
@Model(core.class.Attribute, core.class.Doc)
|
||||
export class TAttribute extends TDoc implements AnyAttribute {
|
||||
attributeOf!: Ref<Class<Obj>>
|
||||
|
@ -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,
|
||||
|
@ -19,8 +19,8 @@ import type {
|
||||
Data,
|
||||
Doc,
|
||||
DocumentUpdate,
|
||||
ExtendedAttributes,
|
||||
Mixin,
|
||||
MixinUpdate,
|
||||
PropertyType,
|
||||
Ref,
|
||||
Space,
|
||||
@ -73,7 +73,7 @@ export class TTxPutBag<T extends PropertyType> extends TTxCUD<Doc> implements Tx
|
||||
@Model(core.class.TxMixin, core.class.TxCUD)
|
||||
export class TTxMixin<D extends Doc, M extends D> extends TTxCUD<D> implements TxMixin<D, M> {
|
||||
mixin!: Ref<Mixin<M>>
|
||||
attributes!: ExtendedAttributes<D, M>
|
||||
attributes!: MixinUpdate<D, M>
|
||||
}
|
||||
|
||||
@Model(core.class.TxUpdateDoc, core.class.TxCUD)
|
||||
|
@ -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<Customer>
|
||||
|
||||
@Prop(TypeString(), 'Title' as IntlString)
|
||||
title!: string
|
||||
|
||||
@Prop(TypeRef(contact.class.Contact), lead.string.Customer)
|
||||
customer!: Ref<Contact>
|
||||
|
||||
@Prop(Collection(chunter.class.Comment), 'Comments' as IntlString)
|
||||
comments?: number
|
||||
|
||||
@ -53,8 +53,18 @@ export class TLead extends TTask implements Lead {
|
||||
declare assignee: Ref<Employee> | 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<Doc>, // 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'
|
||||
|
@ -42,7 +42,7 @@ export const leadOperation: MigrateOperation = {
|
||||
await client.update<TxCreateDoc<Lead>>(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
|
||||
})
|
||||
}
|
||||
|
@ -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<Space>
|
||||
|
@ -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<Class<Doc>>, { _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<Class<Doc>>, { _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<Doc, TestMixin>(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']))
|
||||
|
||||
|
@ -67,10 +67,16 @@ class ClientImpl implements Client {
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
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<T extends Doc>(
|
||||
@ -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<void> {
|
||||
|
@ -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<Class<Doc>>,
|
||||
AttachedDoc: '' as Ref<Class<AttachedDoc>>,
|
||||
Class: '' as Ref<Class<Class<Obj>>>,
|
||||
Mixin: '' as Ref<Class<Mixin<Doc>>>,
|
||||
Interface: '' as Ref<Class<Interface<Doc>>>,
|
||||
Attribute: '' as Ref<Class<AnyAttribute>>,
|
||||
Tx: '' as Ref<Class<Tx>>,
|
||||
|
@ -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<D extends Doc>(doc: D): D {
|
||||
const targetDoc = (doc as any)[PROXY_TARGET_KEY]
|
||||
if (targetDoc !== undefined) {
|
||||
return targetDoc as D
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
static mixinClass<D extends Doc, M extends D>(doc: D): Ref<Mixin<M>>|undefined {
|
||||
return (doc as any)[PROXY_MIXIN_CLASS_KEY]
|
||||
}
|
||||
|
||||
hasMixin<D extends Doc, M extends D>(doc: D, mixin: Ref<Mixin<M>>): boolean {
|
||||
const d = Hierarchy.toDoc(doc)
|
||||
return typeof (d as any)[mixin] === 'object'
|
||||
}
|
||||
|
||||
isMixin (_class: Ref<Class<Doc>>): boolean {
|
||||
const data = this.classifiers.get(_class)
|
||||
return data !== undefined && this._isMixin(data)
|
||||
}
|
||||
|
||||
getAncestors (_class: Ref<Classifier>): Ref<Classifier>[] {
|
||||
const result = this.ancestors.get(_class)
|
||||
if (result === undefined) {
|
||||
@ -113,7 +145,7 @@ export class Hierarchy {
|
||||
}
|
||||
|
||||
private txCreateDoc (tx: TxCreateDoc<Doc>): 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<Classifier>
|
||||
this.classifiers.set(_id, TxProcessor.createDoc2Doc(tx as TxCreateDoc<Classifier>))
|
||||
this.addAncestors(_id)
|
||||
@ -127,7 +159,7 @@ export class Hierarchy {
|
||||
private txMixin (tx: TxMixin<Doc, Doc>): void {
|
||||
if (tx.objectClass === core.class.Class) {
|
||||
const obj = this.getClass(tx.objectId as Ref<Class<Obj>>) 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<T extends Doc>(_class: Ref<Mixin<T>>): Ref<Class<T>> {
|
||||
let cl: Ref<Class<T>> | 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<Classifier>): Ref<Classifier>[] {
|
||||
const attrs = this.classifiers.get(classifier)
|
||||
const result: Ref<Classifier>[] = []
|
||||
if (this.isClass(attrs)) {
|
||||
if (this.isClass(attrs) || this._isMixin(attrs)) {
|
||||
const cls = attrs as Class<Doc>
|
||||
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<Classifier>): Map<string, AnyAttribute> {
|
||||
getAllAttributes (clazz: Ref<Classifier>, to?: Ref<Classifier>): Map<string, AnyAttribute> {
|
||||
const result = new Map<string, AnyAttribute>()
|
||||
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)
|
||||
|
@ -99,17 +99,23 @@ export abstract class MemDb extends TxProcessor {
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
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<Doc, Doc>): Promise<TxResult> {
|
||||
const obj = this.getObject(tx.objectId) as any
|
||||
obj[tx.mixin] = tx.attributes
|
||||
TxProcessor.updateMixin4Doc(obj, tx.mixin, tx.attributes)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
@ -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<D extends Doc, M extends D> = Omit<M, keyof D>
|
||||
export type MixinUpdate<D extends Doc, M extends D> = Partial<Omit<M, keyof D>> & PushOptions<Omit<M, keyof D>> & IncOptions<Omit<M, keyof D>>
|
||||
|
||||
/**
|
||||
* Define Create/Update for mixin attributes.
|
||||
* @public
|
||||
*/
|
||||
export interface TxMixin<D extends Doc, M extends D> extends TxCUD<D> {
|
||||
mixin: Ref<Mixin<M>>
|
||||
attributes: ExtendedAttributes<D, M>
|
||||
attributes: MixinUpdate<D, M>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ArrayAsElement<T extends Doc> = {
|
||||
export type ArrayAsElement<T> = {
|
||||
[P in keyof T]: T[P] extends Arr<infer X> ? X : never
|
||||
}
|
||||
|
||||
@ -108,21 +110,21 @@ export interface MoveDescriptor<X extends PropertyType> {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ArrayAsElementPosition<T extends Doc> = {
|
||||
export type ArrayAsElementPosition<T extends object> = {
|
||||
[P in keyof T]: T[P] extends Arr<infer X> ? X | Position<X> : never
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ArrayMoveDescriptor<T extends Doc> = {
|
||||
export type ArrayMoveDescriptor<T extends object> = {
|
||||
[P in keyof T]: T[P] extends Arr<infer X> ? MoveDescriptor<X> : never
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type NumberProperties<T extends Doc> = {
|
||||
export type NumberProperties<T extends object> = {
|
||||
[P in keyof T]: T[P] extends number | undefined ? T[P] : never
|
||||
}
|
||||
|
||||
@ -134,7 +136,7 @@ export type OmitNever<T extends object> = Omit<T, KeysByType<T, never>>
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface PushOptions<T extends Doc> {
|
||||
export interface PushOptions<T extends object> {
|
||||
$push?: Partial<OmitNever<ArrayAsElementPosition<T>>>
|
||||
$pull?: Partial<OmitNever<ArrayAsElement<T>>>
|
||||
$move?: Partial<OmitNever<ArrayMoveDescriptor<T>>>
|
||||
@ -153,7 +155,7 @@ export interface PushMixinOptions<D extends Doc> {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IncOptions<T extends Doc> {
|
||||
export interface IncOptions<T extends object> {
|
||||
$inc?: Partial<OmitNever<NumberProperties<T>>>
|
||||
}
|
||||
|
||||
@ -231,7 +233,8 @@ export abstract class TxProcessor implements WithTx {
|
||||
} as T
|
||||
}
|
||||
|
||||
static updateDoc2Doc<T extends Doc>(doc: T, tx: TxUpdateDoc<T>): T {
|
||||
static updateDoc2Doc<T extends Doc>(rawDoc: T, tx: TxUpdateDoc<T>): 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<D extends Doc, M extends D>(rawDoc: D, mixinClass: Ref<Class<M>>, operations: MixinUpdate<D, M>): 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<Doc>): Promise<TxResult>
|
||||
@ -405,10 +424,22 @@ export class TxOperations implements Storage {
|
||||
createMixin<D extends Doc, M extends D>(
|
||||
objectId: Ref<D>,
|
||||
objectClass: Ref<Class<D>>,
|
||||
objectSpace: Ref<Space>,
|
||||
mixin: Ref<Mixin<M>>,
|
||||
attributes: ExtendedAttributes<D, M>
|
||||
attributes: MixinUpdate<D, M>
|
||||
): Promise<TxResult> {
|
||||
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<D extends Doc, M extends D>(
|
||||
objectId: Ref<D>,
|
||||
objectClass: Ref<Class<D>>,
|
||||
objectSpace: Ref<Space>,
|
||||
mixin: Ref<Mixin<M>>,
|
||||
attributes: MixinUpdate<D, M>
|
||||
): Promise<TxResult> {
|
||||
const tx = this.txFactory.createTxMixin(objectId, objectClass, objectSpace, mixin, attributes)
|
||||
return this.storage.tx(tx)
|
||||
}
|
||||
}
|
||||
@ -515,7 +546,7 @@ export class TxFactory {
|
||||
}
|
||||
}
|
||||
|
||||
createTxMixin<D extends Doc, M extends D>(objectId: Ref<D>, objectClass: Ref<Class<D>>, mixin: Ref<Mixin<M>>, attributes: ExtendedAttributes<D, M>): TxMixin<D, M> {
|
||||
createTxMixin<D extends Doc, M extends D>(objectId: Ref<D>, objectClass: Ref<Class<D>>, objectSpace: Ref<Space>, mixin: Ref<Mixin<M>>, attributes: MixinUpdate<D, M>): TxMixin<D, M> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Doc>(
|
||||
core.class.Class,
|
||||
_cl[tx.kind],
|
||||
core.space.Model,
|
||||
{
|
||||
...(tx.domain !== undefined ? { domain: tx.domain } : {}),
|
||||
@ -307,9 +309,9 @@ export class Builder {
|
||||
objectId: Ref<D>,
|
||||
objectClass: Ref<Class<D>>,
|
||||
mixin: Ref<IMixin<M>>,
|
||||
attributes: ExtendedAttributes<D, M>
|
||||
attributes: MixinUpdate<D, M>
|
||||
): void {
|
||||
this.txes.push(txFactory.createTxMixin(objectId, objectClass, mixin, attributes))
|
||||
this.txes.push(txFactory.createTxMixin(objectId, objectClass, core.space.Model, mixin, attributes))
|
||||
}
|
||||
|
||||
getTxes (): Tx[] {
|
||||
|
32
packages/presentation/src/attributes.ts
Normal file
32
packages/presentation/src/attributes.ts
Normal file
@ -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<Class<Doc>>, attribute: KeyedAttribute, value: any): Promise<void> {
|
||||
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]
|
||||
}
|
||||
}
|
@ -15,16 +15,16 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import type { AttachedDoc, Doc } from '@anticrm/core'
|
||||
import core from '@anticrm/core'
|
||||
import type { Doc } from '@anticrm/core'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import type { AnySvelteComponent } from '@anticrm/ui'
|
||||
import { CircleButton, Label } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { getAttribute, KeyedAttribute, updateAttribute } from '../attributes'
|
||||
import { getAttributePresenterClass, getClient } from '../utils'
|
||||
|
||||
// export let _class: Ref<Class<Doc>>
|
||||
export let key: string
|
||||
export let key: KeyedAttribute | string
|
||||
export let object: Doc
|
||||
export let maxWidth: string | undefined = undefined
|
||||
export let focus: boolean = false
|
||||
@ -35,7 +35,8 @@
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: attribute = hierarchy.getAttribute(_class, key)
|
||||
$: attribute = typeof key === 'string' ? hierarchy.getAttribute(_class, key) : key.attr
|
||||
$: attributeKey = typeof key === 'string' ? key : key.key
|
||||
$: typeClassId = (attribute !== undefined) ? getAttributePresenterClass(attribute) : undefined
|
||||
|
||||
let editor: Promise<AnySvelteComponent> | undefined
|
||||
@ -47,12 +48,8 @@
|
||||
}
|
||||
|
||||
function onChange (value: any) {
|
||||
if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) {
|
||||
const adoc = object as AttachedDoc
|
||||
client.updateCollection(_class, object.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection, { [key]: value })
|
||||
} else {
|
||||
client.updateDoc(_class, object.space, object._id, { [key]: value }, true)
|
||||
}
|
||||
const doc = object as Doc
|
||||
updateAttribute(client, doc, _class, { key: attributeKey, attr: attribute }, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -69,17 +66,17 @@
|
||||
{#if showHeader}
|
||||
<Label label={attribute.label} />
|
||||
{/if}
|
||||
<div class="value"><svelte:component this={instance} label={attribute?.label} placeholder={attribute?.label} {maxWidth} bind:value={object[key]} {onChange} {focus}/></div>
|
||||
<div class="value"><svelte:component this={instance} label={attribute?.label} placeholder={attribute?.label} {maxWidth} value={getAttribute(client, object, { key: attributeKey, attr: attribute })} {onChange} {focus}/></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if showHeader}
|
||||
<div class="flex-col">
|
||||
<Label label={attribute.label} />
|
||||
<div class="value"><svelte:component this={instance} label={attribute?.label} placeholder={attribute?.label} {maxWidth} bind:value={object[key]} {onChange} {focus}/></div>
|
||||
<div class="value"><svelte:component this={instance} label={attribute?.label} placeholder={attribute?.label} {maxWidth} value={getAttribute(client, object, { key: attributeKey, attr: attribute })} {onChange} {focus}/></div>
|
||||
</div>
|
||||
{:else}
|
||||
<svelte:component this={instance} {maxWidth} bind:value={object[key]} {onChange} {focus}/>
|
||||
<svelte:component this={instance} {maxWidth} value={getAttribute(client, object, { key: attributeKey, attr: attribute })} {onChange} {focus}/>
|
||||
{/if}
|
||||
|
||||
{/await}
|
||||
|
@ -15,14 +15,16 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import type { Class, Doc, Ref } from '@anticrm/core'
|
||||
import type { AttachedDoc, Class, Doc, Ref } from '@anticrm/core'
|
||||
import core from '@anticrm/core'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import type { AnySvelteComponent } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { getAttribute, KeyedAttribute, updateAttribute } from '../attributes'
|
||||
import { getAttributePresenterClass, getClient } from '../utils'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let key: string
|
||||
export let key: KeyedAttribute | string
|
||||
export let object: any
|
||||
export let maxWidth: string
|
||||
export let focus: boolean = false
|
||||
@ -30,7 +32,8 @@
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: attribute = hierarchy.getAttribute(_class, key)
|
||||
$: attribute = typeof key === 'string' ? hierarchy.getAttribute(_class, key) : key.attr
|
||||
$: attributeKey = typeof key === 'string' ? key : key.key
|
||||
$: typeClassId = (attribute !== undefined) ? getAttributePresenterClass(attribute) : undefined
|
||||
|
||||
let editor: Promise<AnySvelteComponent> | undefined
|
||||
@ -43,7 +46,7 @@
|
||||
|
||||
function onChange (value: any) {
|
||||
const doc = object as Doc
|
||||
client.updateDoc(_class, doc.space, doc._id, { [key]: value })
|
||||
updateAttribute(client, doc, _class, { key: attributeKey, attr: attribute }, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -51,7 +54,7 @@
|
||||
{#await editor}
|
||||
...
|
||||
{:then instance}
|
||||
<svelte:component this={instance} label={attribute?.label} placeholder={attribute?.label} {maxWidth} bind:value={object[key]} {onChange} {focus}/>
|
||||
<svelte:component this={instance} label={attribute?.label} placeholder={attribute?.label} {maxWidth} value={getAttribute(client, object, { key: attributeKey, attr: attribute })} {onChange} {focus}/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
|
@ -16,10 +16,11 @@
|
||||
|
||||
<script lang="ts">
|
||||
import type { Doc } from '@anticrm/core'
|
||||
import { KeyedAttribute } from '../attributes'
|
||||
import AttributeBarEditor from './AttributeBarEditor.svelte'
|
||||
|
||||
export let object: Doc
|
||||
export let keys: string[]
|
||||
export let keys: (string|KeyedAttribute)[]
|
||||
</script>
|
||||
|
||||
<div class="flex-row-center small-text">
|
||||
|
@ -38,14 +38,14 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
$: query.query(_class, { name: { $like: '%' + search + '%' } }, result => { objects = result })
|
||||
$: query.query(_class, { name: { $like: '%' + search + '%' } }, result => { objects = result }, { limit: 200 })
|
||||
afterUpdate(() => { dispatch('update', Date.now()) })
|
||||
</script>
|
||||
|
||||
<div class="popup">
|
||||
<div class="title"><Label label={title} /></div>
|
||||
<div class="flex-col header">
|
||||
<EditWithIcon icon={IconSearch} bind:value={search} placeholder={'Search...'} />
|
||||
<EditWithIcon icon={IconSearch} bind:value={search} placeholder={'Search...'} focus />
|
||||
<div class="caption"><Label label={caption} /></div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
@ -72,6 +72,7 @@
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
border-radius: .75rem;
|
||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, .2);
|
||||
max-height: 70%;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
@ -1,15 +1,15 @@
|
||||
//
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
@ -18,6 +18,7 @@ import { addStringsLoader } from '@anticrm/platform'
|
||||
import { presentationId } from './plugin'
|
||||
|
||||
export * from './utils'
|
||||
export * from './attributes'
|
||||
|
||||
export { default as UserBox } from './components/UserBox.svelte'
|
||||
export { default as UserInfo } from './components/UserInfo.svelte'
|
||||
|
@ -14,36 +14,34 @@
|
||||
//
|
||||
|
||||
import core, {
|
||||
Ref,
|
||||
AttachedDoc,
|
||||
Class,
|
||||
Doc,
|
||||
Tx,
|
||||
DocumentQuery,
|
||||
TxCreateDoc,
|
||||
TxRemoveDoc,
|
||||
Client,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
FindOptions,
|
||||
TxUpdateDoc,
|
||||
_getOperator,
|
||||
TxProcessor,
|
||||
resultSort,
|
||||
SortingQuery,
|
||||
findProperty,
|
||||
FindResult,
|
||||
Hierarchy,
|
||||
Refs,
|
||||
WithLookup,
|
||||
LookupData,
|
||||
TxMixin,
|
||||
TxPutBag,
|
||||
ModelDb,
|
||||
Ref,
|
||||
Refs,
|
||||
resultSort,
|
||||
SortingQuery,
|
||||
Tx,
|
||||
TxBulkWrite,
|
||||
TxResult,
|
||||
TxCollectionCUD,
|
||||
AttachedDoc,
|
||||
findProperty
|
||||
TxCreateDoc,
|
||||
TxMixin,
|
||||
TxProcessor,
|
||||
TxPutBag,
|
||||
TxRemoveDoc,
|
||||
TxResult,
|
||||
TxUpdateDoc,
|
||||
WithLookup
|
||||
} from '@anticrm/core'
|
||||
|
||||
import clone from 'just-clone'
|
||||
import justClone from 'just-clone'
|
||||
|
||||
interface Query {
|
||||
_class: Ref<Class<Doc>>
|
||||
@ -79,7 +77,11 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
|
||||
private match (q: Query, doc: Doc): boolean {
|
||||
if (!this.getHierarchy().isDerived(doc._class, q._class)) {
|
||||
return false
|
||||
// Check if it is not a mixin and not match class
|
||||
const mixinClass = Hierarchy.mixinClass(doc)
|
||||
if (mixinClass === undefined || !this.getHierarchy().isDerived(mixinClass, q._class)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
const query = q.query
|
||||
for (const key in query) {
|
||||
@ -157,8 +159,29 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
return {}
|
||||
}
|
||||
|
||||
protected txMixin (tx: TxMixin<Doc, Doc>): Promise<TxResult> {
|
||||
throw new Error('Method not implemented.')
|
||||
protected override async txMixin (tx: TxMixin<Doc, Doc>): Promise<TxResult> {
|
||||
for (const q of this.queries) {
|
||||
if (this.client.getHierarchy().isDerived(q._class, core.class.Tx)) {
|
||||
// handle add since Txes are immutable
|
||||
await this.handleDocAdd(q, tx)
|
||||
continue
|
||||
}
|
||||
if (q.result instanceof Promise) {
|
||||
q.result = await q.result
|
||||
}
|
||||
let updatedDoc = q.result.find((p) => p._id === tx.objectId)
|
||||
if (updatedDoc !== undefined) {
|
||||
// Create or apply mixin value
|
||||
updatedDoc = TxProcessor.updateMixin4Doc(updatedDoc, tx.mixin, tx.attributes)
|
||||
await this.callback(updatedDoc, q)
|
||||
} else {
|
||||
if (this.getHierarchy().isDerived(tx.mixin, q._class)) {
|
||||
// Mixin potentially added to object we doesn't have in out results
|
||||
await this.refresh(q)
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
protected async txCollectionCUD (tx: TxCollectionCUD<Doc, AttachedDoc>): Promise<TxResult> {
|
||||
@ -222,10 +245,23 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone document with respect to mixin inner document cloning.
|
||||
*/
|
||||
private clone<T extends Doc>(results: T[]): T[] {
|
||||
const result: T[] = []
|
||||
const h = this.getHierarchy()
|
||||
for (const doc of results) {
|
||||
const m = Hierarchy.mixinClass(doc)
|
||||
result.push(m !== undefined ? h.as(Hierarchy.toDoc(doc), m) : justClone(doc))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private async refresh (q: Query): Promise<void> {
|
||||
const res = await this.client.findAll(q._class, q.query, q.options)
|
||||
q.result = res
|
||||
q.callback(clone(res))
|
||||
q.callback(this.clone(res))
|
||||
}
|
||||
|
||||
// Check if query is partially matched.
|
||||
@ -281,10 +317,10 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
|
||||
if (q.options?.limit !== undefined && q.result.length > q.options.limit) {
|
||||
if (q.result.pop()?._id !== doc._id) {
|
||||
q.callback(clone(q.result))
|
||||
q.callback(this.clone(q.result))
|
||||
}
|
||||
} else {
|
||||
q.callback(clone(q.result))
|
||||
q.callback(this.clone(q.result))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -315,7 +351,7 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
}
|
||||
if (index > -1) {
|
||||
q.result.splice(index, 1)
|
||||
q.callback(clone(q.result))
|
||||
q.callback(this.clone(q.result))
|
||||
}
|
||||
}
|
||||
|
||||
@ -327,15 +363,12 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
return await super.tx(tx)
|
||||
}
|
||||
|
||||
// why this is separate from txUpdateDoc?
|
||||
private async __updateDoc (q: Query, updatedDoc: WithLookup<Doc>, tx: TxUpdateDoc<Doc>): Promise<void> {
|
||||
TxProcessor.updateDoc2Doc(updatedDoc, tx)
|
||||
|
||||
const ops = tx.operations as any
|
||||
for (const key in ops) {
|
||||
if (key.startsWith('$')) {
|
||||
const operator = _getOperator(key)
|
||||
operator(updatedDoc, ops[key])
|
||||
} else {
|
||||
;(updatedDoc as any)[key] = ops[key]
|
||||
if (!key.startsWith('$')) {
|
||||
if (q.options !== undefined) {
|
||||
const lookup = (q.options.lookup as any)?.[key]
|
||||
if (lookup !== undefined) {
|
||||
@ -344,8 +377,6 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
updatedDoc.modifiedBy = tx.modifiedBy
|
||||
updatedDoc.modifiedOn = tx.modifiedOn
|
||||
}
|
||||
|
||||
private sort (q: Query, tx: TxUpdateDoc<Doc>): void {
|
||||
@ -380,7 +411,7 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
}
|
||||
if (q.result.pop()?._id !== updatedDoc._id) q.callback(q.result)
|
||||
} else {
|
||||
q.callback(clone(q.result))
|
||||
q.callback(this.clone(q.result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
// import Icon from './Icon.svelte'
|
||||
import Loading from './Loading.svelte'
|
||||
import ErrorBoundary from './internal/ErrorBoundary'
|
||||
import ErrorPresenter from './ErrorPresenter.svelte';
|
||||
import ErrorPresenter from './ErrorPresenter.svelte'
|
||||
|
||||
export let is: AnyComponent
|
||||
export let props = {}
|
||||
|
@ -23,8 +23,16 @@
|
||||
export let width: string | undefined = undefined
|
||||
export let value: string | undefined = undefined
|
||||
export let placeholder: string = 'placeholder'
|
||||
export let focus: boolean = false
|
||||
|
||||
let textHTML: HTMLInputElement
|
||||
|
||||
$: if (textHTML !== undefined) {
|
||||
if (focus) {
|
||||
textHTML.focus()
|
||||
focus = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-between editbox" style={width ? 'width: ' + width : ''} on:click={() => textHTML.focus()}>
|
||||
|
@ -111,8 +111,8 @@ export function showPopup (component: AnySvelteComponent | AnyComponent, props:
|
||||
popupstore.update(popups => {
|
||||
popups.push({ is: resolved, props, element, onClose })
|
||||
return popups
|
||||
})
|
||||
})
|
||||
})
|
||||
}).catch(err => console.log(err))
|
||||
} else {
|
||||
popupstore.update(popups => {
|
||||
popups.push({ is: component, props, element, onClose })
|
||||
@ -146,9 +146,9 @@ export function closeTooltip (): void {
|
||||
}
|
||||
|
||||
export const ticker = readable(Date.now(), set => {
|
||||
const interval = setInterval(() => {
|
||||
set(Date.now())
|
||||
}, 10000)
|
||||
const interval = setInterval(() => {
|
||||
set(Date.now())
|
||||
}, 10000)
|
||||
})
|
||||
|
||||
addStringsLoader(uiId, async (lang: string) => {
|
||||
|
@ -12,6 +12,7 @@ import core, {
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxMixin,
|
||||
TxProcessor,
|
||||
TxUpdateDoc
|
||||
} from '@anticrm/core'
|
||||
@ -52,10 +53,14 @@ export interface DisplayTx {
|
||||
// Type check for updateTx
|
||||
updateTx?: TxUpdateDoc<Doc>
|
||||
|
||||
// Type check for updateTx
|
||||
mixinTx?: TxMixin<Doc, Doc>
|
||||
|
||||
// Document in case it is required.
|
||||
doc?: Doc
|
||||
|
||||
updated: boolean
|
||||
mixin: boolean
|
||||
removed: boolean
|
||||
}
|
||||
|
||||
@ -113,7 +118,7 @@ class ActivityImpl implements Activity {
|
||||
? { 'tx.objectId': object._id as Ref<AttachedDoc> }
|
||||
: {
|
||||
objectId: object._id,
|
||||
_class: { $in: [core.class.TxCreateDoc, core.class.TxUpdateDoc, core.class.TxRemoveDoc] }
|
||||
_class: { $in: [core.class.TxCreateDoc, core.class.TxUpdateDoc, core.class.TxRemoveDoc, core.class.TxMixin] }
|
||||
},
|
||||
(result) => {
|
||||
this.txes1 = result
|
||||
@ -148,15 +153,14 @@ class ActivityImpl implements Activity {
|
||||
let results: DisplayTx[] = []
|
||||
|
||||
for (const tx of txCUD) {
|
||||
const { collectionCUD, updateCUD, result, tx: ntx } = this.createDisplayTx(tx, parents)
|
||||
const { collectionCUD, updateCUD, mixinCUD, result, tx: ntx } = this.createDisplayTx(tx, parents)
|
||||
// We do not need collection object updates, in main list of displayed transactions.
|
||||
if (this.isDisplayTxRequired(collectionCUD, updateCUD, ntx, object)) {
|
||||
if (this.isDisplayTxRequired(collectionCUD, updateCUD || mixinCUD, ntx, object)) {
|
||||
// Combine previous update transaction for same field and if same operation and time treshold is ok
|
||||
results = this.integrateTxWithResults(results, result)
|
||||
this.updateRemovedState(result, results)
|
||||
}
|
||||
}
|
||||
console.log('DISPLAY TX', results)
|
||||
return Array.from(results)
|
||||
}
|
||||
|
||||
@ -175,8 +179,8 @@ class ActivityImpl implements Activity {
|
||||
return a.modifiedOn - b.modifiedOn
|
||||
}
|
||||
|
||||
isDisplayTxRequired (collectionCUD: boolean, updateCUD: boolean, ntx: TxCUD<Doc>, object: Doc): boolean {
|
||||
return !(collectionCUD && updateCUD) || ntx.objectId === object._id
|
||||
isDisplayTxRequired (collectionCUD: boolean, cudOp: boolean, ntx: TxCUD<Doc>, object: Doc): boolean {
|
||||
return !(collectionCUD && cudOp) || ntx.objectId === object._id
|
||||
}
|
||||
|
||||
private readonly getUpdateTx = (tx: TxCUD<Doc>): TxUpdateDoc<Doc> | undefined => {
|
||||
@ -216,9 +220,10 @@ class ActivityImpl implements Activity {
|
||||
createDisplayTx (
|
||||
tx: TxCUD<Doc>,
|
||||
parents: Map<Ref<Doc>, DisplayTx>
|
||||
): { collectionCUD: boolean, updateCUD: boolean, result: DisplayTx, tx: TxCUD<Doc> } {
|
||||
): { collectionCUD: boolean, updateCUD: boolean, mixinCUD: boolean, result: DisplayTx, tx: TxCUD<Doc> } {
|
||||
let collectionCUD = false
|
||||
let updateCUD = false
|
||||
let mixinCUD = false
|
||||
const hierarchy = this.client.getHierarchy()
|
||||
if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
|
||||
tx = getCollectionTx(tx as TxCollectionCUD<Doc, AttachedDoc>)
|
||||
@ -234,9 +239,10 @@ class ActivityImpl implements Activity {
|
||||
|
||||
// If we have updates also apply them all.
|
||||
updateCUD = this.checkUpdateState(result, firstTx)
|
||||
mixinCUD = this.checkMixinState(result, firstTx)
|
||||
|
||||
this.checkRemoveState(hierarchy, tx, firstTx, result)
|
||||
return { collectionCUD, updateCUD, result, tx }
|
||||
return { collectionCUD, updateCUD, mixinCUD, result, tx }
|
||||
}
|
||||
|
||||
private checkRemoveState (hierarchy: Hierarchy, tx: TxCUD<Doc>, firstTx: DisplayTx, result: DisplayTx): void {
|
||||
@ -256,6 +262,17 @@ class ActivityImpl implements Activity {
|
||||
return false
|
||||
}
|
||||
|
||||
checkMixinState (result: DisplayTx, firstTx: DisplayTx): boolean {
|
||||
if (this.client.getHierarchy().isDerived(result.tx._class, core.class.TxMixin) && result.doc !== undefined) {
|
||||
const mix = result.tx as TxMixin<Doc, Doc>
|
||||
firstTx.doc = TxProcessor.updateMixin4Doc(result.doc, mix.mixin, mix.attributes)
|
||||
firstTx.mixin = true
|
||||
result.mixin = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
newDisplayTx (tx: TxCUD<Doc>): DisplayTx {
|
||||
const hierarchy = this.client.getHierarchy()
|
||||
const createTx = hierarchy.isDerived(tx._class, core.class.TxCreateDoc) ? (tx as TxCreateDoc<Doc>) : undefined
|
||||
@ -266,20 +283,24 @@ class ActivityImpl implements Activity {
|
||||
updateTx: hierarchy.isDerived(tx._class, core.class.TxUpdateDoc) ? (tx as TxUpdateDoc<Doc>) : undefined,
|
||||
updated: false,
|
||||
removed: false,
|
||||
mixin: false,
|
||||
mixinTx: hierarchy.isDerived(tx._class, core.class.TxMixin) ? (tx as TxMixin<Doc, Doc>) : undefined,
|
||||
doc: createTx !== undefined ? TxProcessor.createDoc2Doc(createTx) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
integrateTxWithResults (results: DisplayTx[], result: DisplayTx): DisplayTx[] {
|
||||
const curUpdate = result.tx as unknown as TxUpdateDoc<Doc>
|
||||
const curUpdate: any = (result.tx._class === core.class.TxUpdateDoc)
|
||||
? (result.tx as unknown as TxUpdateDoc<Doc>).operations
|
||||
: (result.tx as unknown as TxMixin<Doc, Doc>).attributes
|
||||
|
||||
const newResult = results.filter((prevTx) => {
|
||||
if (this.isSameKindTx(prevTx, result)) {
|
||||
const prevUpdate = prevTx.tx as unknown as TxUpdateDoc<Doc>
|
||||
if (this.isSameKindTx(prevTx, result, result.tx._class)) {
|
||||
const prevUpdate: any = (prevTx.tx._class === core.class.TxUpdateDoc)
|
||||
? (prevTx.tx as unknown as TxUpdateDoc<Doc>).operations
|
||||
: (prevTx.tx as unknown as TxMixin<Doc, Doc>).attributes
|
||||
if (
|
||||
result.tx.modifiedOn - prevUpdate.modifiedOn < combineThreshold &&
|
||||
isEqualOps(prevUpdate.operations, curUpdate.operations)
|
||||
) {
|
||||
result.tx.modifiedOn - prevTx.tx.modifiedOn < combineThreshold && isEqualOps(prevUpdate, curUpdate) ) {
|
||||
// we have same keys,
|
||||
// Remember previous transactions
|
||||
result.txes.push(...prevTx.txes, prevTx.tx)
|
||||
@ -293,11 +314,11 @@ class ActivityImpl implements Activity {
|
||||
return newResult
|
||||
}
|
||||
|
||||
isSameKindTx (prevTx: DisplayTx, result: DisplayTx): boolean {
|
||||
isSameKindTx (prevTx: DisplayTx, result: DisplayTx, _class: Ref<Class<Doc>>): boolean {
|
||||
return (
|
||||
prevTx.tx.objectId === result.tx.objectId && // Same document id
|
||||
prevTx.tx._class === result.tx._class && // Same transaction class
|
||||
result.tx._class === core.class.TxUpdateDoc &&
|
||||
result.tx._class === _class &&
|
||||
prevTx.tx.modifiedBy === result.tx.modifiedBy // Same user
|
||||
)
|
||||
}
|
||||
|
@ -124,14 +124,28 @@
|
||||
.filter(([, attr]) => attr.hidden === true)
|
||||
.map(([k]) => k))
|
||||
|
||||
buildModel(ops).then((m) => {
|
||||
model = m.filter((x) => !hiddenAttrs.has(x.key))
|
||||
})
|
||||
} else if (tx.mixinTx !== undefined) {
|
||||
const _class = tx.mixinTx.mixin
|
||||
const ops = {
|
||||
client,
|
||||
_class,
|
||||
keys: Object.keys(tx.mixinTx.attributes).filter((id) => !id.startsWith('$')),
|
||||
ignoreMissing: true
|
||||
}
|
||||
const hiddenAttrs = new Set([...client.getHierarchy().getAllAttributes(_class).entries()]
|
||||
.filter(([, attr]) => attr.hidden === true)
|
||||
.map(([k]) => k))
|
||||
|
||||
buildModel(ops).then((m) => {
|
||||
model = m.filter((x) => !hiddenAttrs.has(x.key))
|
||||
})
|
||||
}
|
||||
|
||||
async function getValue (m: AttributeModel, utx: TxUpdateDoc<Doc>): Promise<any> {
|
||||
const val = (utx.operations as any)[m.key]
|
||||
console.log(m._class, m.key, val, typeof val)
|
||||
async function getValue (m: AttributeModel, utx: any): Promise<any> {
|
||||
const val = (utx as any)[m.key]
|
||||
|
||||
if (client.getHierarchy().isDerived(m._class, core.class.Doc) && typeof val === 'string') {
|
||||
// We have an reference, we need to find a real object to pass for presenter
|
||||
@ -171,7 +185,6 @@
|
||||
props = { ...props, edit }
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if (viewlet !== undefined && !((viewlet?.hideOnRemove ?? false) && tx.removed)) || model.length > 0}
|
||||
<div class="flex-between msgactivity-container">
|
||||
|
||||
@ -215,14 +228,25 @@
|
||||
{/if}
|
||||
{#if viewlet === undefined && model.length > 0 && tx.updateTx}
|
||||
{#each model as m}
|
||||
{#await getValue(m, tx.updateTx) then value}
|
||||
{#if value === null}
|
||||
<span>unset <Label label={m.label} /></span>
|
||||
{:else}
|
||||
<span>changed <Label label={m.label} /> to</span>
|
||||
<div class="strong"><svelte:component this={m.presenter} {value} /></div>
|
||||
{/if}
|
||||
{/await}
|
||||
{#await getValue(m, tx.updateTx.operations) then value}
|
||||
{#if value === null}
|
||||
<span>unset <Label label={m.label} /></span>
|
||||
{:else}
|
||||
<span>changed <Label label={m.label} /> to</span>
|
||||
<div class="strong"><svelte:component this={m.presenter} {value} /></div>
|
||||
{/if}
|
||||
{/await}
|
||||
{/each}
|
||||
{:else if viewlet === undefined && model.length > 0 && tx.mixinTx}
|
||||
{#each model as m}
|
||||
{#await getValue(m, tx.mixinTx.attributes) then value}
|
||||
{#if value === null}
|
||||
<span>unset <Label label={m.label} /></span>
|
||||
{:else}
|
||||
<span>changed <Label label={m.label} /> to</span>
|
||||
<div class="strong"><svelte:component this={m.presenter} {value} /></div>
|
||||
{/if}
|
||||
{/await}
|
||||
{/each}
|
||||
{:else if viewlet && viewlet.display === 'inline' && viewlet.component}
|
||||
<div>
|
||||
|
@ -14,19 +14,26 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Class, Doc, Ref } from '@anticrm/core'
|
||||
import { Component, AnyComponent } from '@anticrm/ui'
|
||||
import { AttributesBar, getClient, createQuery, getAttributePresenterClass } from '@anticrm/presentation'
|
||||
import { Panel } from '@anticrm/panel'
|
||||
import contact from '../plugin'
|
||||
import { Contact, formatName } from '@anticrm/contact'
|
||||
import core from '@anticrm/core'
|
||||
import core, { Class, ClassifierKind, Doc, Mixin, Ref } from '@anticrm/core'
|
||||
import { Panel } from '@anticrm/panel'
|
||||
import { Asset } from '@anticrm/platform'
|
||||
import {
|
||||
AttributesBar,
|
||||
createQuery,
|
||||
getAttributePresenterClass,
|
||||
getClient,
|
||||
KeyedAttribute
|
||||
} from '@anticrm/presentation'
|
||||
import { ActionIcon, AnyComponent, Component, Label } from '@anticrm/ui'
|
||||
|
||||
import view from '@anticrm/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import contact from '../plugin'
|
||||
|
||||
export let _id: Ref<Contact>
|
||||
let object: Contact
|
||||
let objectClass: Class<Doc>
|
||||
let rightSection: AnyComponent | undefined
|
||||
let fullSize: boolean = true
|
||||
|
||||
@ -40,37 +47,59 @@
|
||||
object = result[0]
|
||||
})
|
||||
|
||||
$: if (object) objectClass = client.getHierarchy().getClass(object._class)
|
||||
|
||||
let selectedClass: Ref<Class<Doc>> | undefined
|
||||
let prevSelected = selectedClass
|
||||
|
||||
let keys: KeyedAttribute[] = []
|
||||
let collectionKeys: KeyedAttribute[] = []
|
||||
|
||||
let mixins: Mixin<Doc>[] = []
|
||||
|
||||
let selectedMixin: Mixin<Doc> | undefined
|
||||
|
||||
$: if (object && prevSelected !== object._class) {
|
||||
prevSelected = object._class
|
||||
selectedClass = objectClass._id
|
||||
selectedMixin = undefined
|
||||
const h = client.getHierarchy()
|
||||
mixins = h.getDescendants(contact.class.Contact)
|
||||
.filter((m) => h.getClass(m).kind === ClassifierKind.MIXIN && h.hasMixin(object, m)).map(m => h.getClass(m) as Mixin<Doc>)
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let keys: string[] = []
|
||||
let collectionKeys: string[] = []
|
||||
|
||||
function getFiltredKeys (ignoreKeys: string[]): string[] {
|
||||
let keys = [...hierarchy.getAllAttributes(object._class).entries()]
|
||||
.filter(([, value]) => value.hidden !== true)
|
||||
.map(([key]) => key)
|
||||
keys = keys.filter((k) => !docKeys.has(k))
|
||||
keys = keys.filter((k) => !ignoreKeys.includes(k))
|
||||
function filterKeys (keys: KeyedAttribute[], ignoreKeys: string[]): KeyedAttribute[] {
|
||||
keys = keys.filter((k) => !docKeys.has(k.key))
|
||||
keys = keys.filter((k) => !ignoreKeys.includes(k.key))
|
||||
return keys
|
||||
}
|
||||
|
||||
function getKeys (ignoreKeys: string[]): void {
|
||||
const filtredKeys = getFiltredKeys(ignoreKeys)
|
||||
function getFiltredKeys (objectClass: Ref<Class<Doc>>, ignoreKeys: string[]): KeyedAttribute[] {
|
||||
const keys = [...hierarchy.getAllAttributes(objectClass).entries()]
|
||||
.filter(([, value]) => value.hidden !== true)
|
||||
.map(([key, attr]) => ({ key, attr }))
|
||||
|
||||
return filterKeys(keys, ignoreKeys)
|
||||
}
|
||||
|
||||
function updateKeys (ignoreKeys: string[]): void {
|
||||
const filtredKeys = getFiltredKeys(selectedClass ?? object._class, ignoreKeys)
|
||||
keys = collectionsFilter(filtredKeys, false)
|
||||
collectionKeys = collectionsFilter(filtredKeys, true)
|
||||
}
|
||||
|
||||
function collectionsFilter (keys: string[], get: boolean): string[] {
|
||||
const result: string[] = []
|
||||
function collectionsFilter (keys: KeyedAttribute[], get: boolean): KeyedAttribute[] {
|
||||
const result: KeyedAttribute[] = []
|
||||
for (const key of keys) {
|
||||
if (isCollectionAttr(key) === get) result.push(key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function isCollectionAttr (key: string): boolean {
|
||||
const attribute = hierarchy.getAttribute(object._class, key)
|
||||
return hierarchy.isDerived(attribute.type._class, core.class.Collection)
|
||||
function isCollectionAttr (key: KeyedAttribute): boolean {
|
||||
return hierarchy.isDerived(key.attr.type._class, core.class.Collection)
|
||||
}
|
||||
|
||||
async function getEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent> {
|
||||
@ -80,15 +109,22 @@
|
||||
return editorMixin.editor
|
||||
}
|
||||
|
||||
async function getCollectionEditor (key: string): Promise<AnyComponent> {
|
||||
const attribute = hierarchy.getAttribute(object._class, key)
|
||||
const attrClass = getAttributePresenterClass(attribute)
|
||||
async function getEditorOrDefault (_class: Ref<Class<Doc>> | undefined, defaultClass: Ref<Class<Doc>>): Promise<AnyComponent> {
|
||||
const editor = _class !== undefined ? await getEditor(_class) : undefined
|
||||
if (editor !== undefined) {
|
||||
return editor
|
||||
}
|
||||
return getEditor(defaultClass)
|
||||
}
|
||||
|
||||
async function getCollectionEditor (key: KeyedAttribute): Promise<AnyComponent> {
|
||||
const attrClass = getAttributePresenterClass(key.attr)
|
||||
const clazz = client.getHierarchy().getClass(attrClass)
|
||||
const editorMixin = client.getHierarchy().as(clazz, view.mixin.AttributeEditor)
|
||||
return editorMixin.editor
|
||||
}
|
||||
|
||||
$: icon = object && (hierarchy.getClass(object._class).icon as Asset)
|
||||
$: icon = (objectClass?.icon ?? contact.class.Person) as Asset
|
||||
</script>
|
||||
|
||||
{#if object !== undefined}
|
||||
@ -102,23 +138,43 @@
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<div slot="subtitle">
|
||||
{#if keys}
|
||||
<AttributesBar {object} {keys} />
|
||||
{/if}
|
||||
<div slot="subtitle" class="flex flex-reverse flex-grow">
|
||||
<div class='flex'>
|
||||
{#if mixins.length > 0}
|
||||
<div class='mixin-selector' class:selected={selectedClass === objectClass._id}>
|
||||
<ActionIcon icon={objectClass.icon} size={'medium'} label={objectClass.label} action={() => {
|
||||
selectedClass = objectClass._id
|
||||
selectedMixin = undefined
|
||||
}} />
|
||||
</div>
|
||||
{#each mixins as mixin}
|
||||
<div class='mixin-selector' class:selected={selectedClass === mixin._id}>
|
||||
<ActionIcon icon={mixin.icon} size={'medium'} label={mixin.label} action={() => {
|
||||
selectedClass = mixin._id
|
||||
selectedMixin = mixin
|
||||
}} />
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
{#if keys}
|
||||
<AttributesBar {object} {keys} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#await getEditor(object._class) then is}
|
||||
<Component
|
||||
{is}
|
||||
props={{ object }}
|
||||
on:open={(ev) => {
|
||||
getKeys(ev.detail.ignoreKeys)
|
||||
}}
|
||||
on:click={(ev) => {
|
||||
fullSize = true
|
||||
rightSection = ev.detail.presenter
|
||||
}}
|
||||
/>
|
||||
{#await getEditorOrDefault(selectedClass, object._class) then is}
|
||||
<Component
|
||||
{is}
|
||||
props={{ object }}
|
||||
on:open={(ev) => {
|
||||
updateKeys(ev.detail.ignoreKeys)
|
||||
}}
|
||||
on:click={(ev) => {
|
||||
fullSize = true
|
||||
rightSection = ev.detail.presenter
|
||||
}}
|
||||
/>
|
||||
{/await}
|
||||
{#each collectionKeys as collection}
|
||||
<div class="mt-14">
|
||||
@ -127,5 +183,68 @@
|
||||
{/await}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- {#each mixins as mixin}
|
||||
<div class="mixin-container">
|
||||
<div class="header">
|
||||
<div class="icon" />
|
||||
<Label label={mixin._class.label} />
|
||||
</div>
|
||||
<div class="attributes">
|
||||
{#if mixin.keys.length > 0}
|
||||
<AttributesBar {object} keys={mixin.keys} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="collections">
|
||||
{#each mixin.collectionKeys as collection}
|
||||
<div class="mt-14">
|
||||
{#await getCollectionEditor(collection) then is}
|
||||
<Component {is} props={{ objectId: object._id, _class: object._class, space: object.space }} />
|
||||
{/await}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each} -->
|
||||
</Panel>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.mixin-container {
|
||||
margin-top: 2rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--theme-zone-bg);
|
||||
.header {
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 150%;
|
||||
align-items: center;
|
||||
.icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
/* Dark / Green 01 */
|
||||
|
||||
background: #77c07b;
|
||||
border: 2px solid #18181e;
|
||||
border-radius: 50px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
.attributes {
|
||||
margin: 1rem;
|
||||
}
|
||||
.collections {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
}
|
||||
.mixin-selector {
|
||||
opacity: 0.6;
|
||||
margin: 0.25rem;
|
||||
|
||||
&.selected {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { createEventDispatcher, onMount, afterUpdate } from 'svelte'
|
||||
import { getCurrentAccount, Ref, Space } from '@anticrm/core'
|
||||
import { CircleButton, EditBox, showPopup, IconEdit, IconAdd, Label, IconActivity } from '@anticrm/ui'
|
||||
import { getClient, createQuery, Channels, Avatar } from '@anticrm/presentation'
|
||||
@ -58,9 +58,9 @@
|
||||
integrations = new Set(res.map((p) => p.type))
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
dispatch('open', { ignoreKeys: ['comments', 'name', 'channels'] })
|
||||
})
|
||||
const sendOpen = () => dispatch('open', { ignoreKeys: ['comments', 'name', 'channels'] })
|
||||
onMount(sendOpen)
|
||||
afterUpdate(sendOpen)
|
||||
</script>
|
||||
|
||||
{#if object !== undefined}
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { IntlString, Resources } from '@anticrm/platform'
|
||||
import { getMetadata, IntlString, Resources } from '@anticrm/platform'
|
||||
import ModelView from './components/ModelView.svelte'
|
||||
import QueryView from './components/QueryView.svelte'
|
||||
import core, { Class, Client, Doc, DocumentQuery, FindOptions, Ref, FindResult, Hierarchy, ModelDb, Tx, TxResult, WithLookup, Metrics } from '@anticrm/core'
|
||||
@ -43,7 +43,7 @@ class ModelClient implements Client {
|
||||
constructor (readonly client: Client) {
|
||||
client.notify = (tx) => {
|
||||
this.notify?.(tx)
|
||||
console.info('devmodel# notify=>', tx, this.client.getModel())
|
||||
console.info('devmodel# notify=>', tx, this.client.getModel(), getMetadata(devmodel.metadata.DevModel))
|
||||
notifications.push(tx)
|
||||
if (notifications.length > 500) {
|
||||
notifications.shift()
|
||||
@ -63,7 +63,7 @@ class ModelClient implements Client {
|
||||
|
||||
async findOne <T extends Doc>(_class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T>): Promise<WithLookup<T> | undefined> {
|
||||
const result = await this.client.findOne(_class, query, options)
|
||||
console.info('devmodel# findOne=>', _class, query, options, 'result => ', result, ' =>model', this.client.getModel())
|
||||
console.info('devmodel# findOne=>', _class, query, options, 'result => ', result, ' =>model', this.client.getModel(), getMetadata(devmodel.metadata.DevModel))
|
||||
queries.push({ _class, query, options, result: result !== undefined ? [result] : [], findOne: true })
|
||||
if (queries.length > 100) {
|
||||
queries.shift()
|
||||
@ -73,7 +73,7 @@ class ModelClient implements Client {
|
||||
|
||||
async findAll<T extends Doc>(_class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T>): Promise<FindResult<T>> {
|
||||
const result = await this.client.findAll(_class, query, options)
|
||||
console.info('devmodel# findAll=>', _class, query, options, 'result => ', result, ' =>model', this.client.getModel())
|
||||
console.info('devmodel# findAll=>', _class, query, options, 'result => ', result, ' =>model', this.client.getModel(), getMetadata(devmodel.metadata.DevModel))
|
||||
queries.push({ _class, query, options, result, findOne: false })
|
||||
if (queries.length > 100) {
|
||||
queries.shift()
|
||||
@ -83,7 +83,7 @@ class ModelClient implements Client {
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
const result = await this.client.tx(tx)
|
||||
console.info('devmodel# tx=>', tx, result)
|
||||
console.info('devmodel# tx=>', tx, result, getMetadata(devmodel.metadata.DevModel))
|
||||
transactions.push({ tx, result })
|
||||
if (transactions.length > 100) {
|
||||
transactions.shift()
|
||||
|
@ -13,10 +13,10 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Asset, Plugin, Resource } from '@anticrm/platform'
|
||||
import { ClientHook } from '@anticrm/client'
|
||||
import type { Asset, Metadata, Plugin, Resource } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
import { ClientHook } from '@anticrm/client'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -35,5 +35,8 @@ export default plugin(devModelId, {
|
||||
},
|
||||
hook: {
|
||||
Hook: '' as Resource<ClientHook>
|
||||
},
|
||||
metadata: {
|
||||
DevModel: '' as Metadata<any>
|
||||
}
|
||||
})
|
||||
|
@ -12,6 +12,8 @@
|
||||
"LeadName": "Lead name",
|
||||
"More": "More...",
|
||||
"Customer": "Customer",
|
||||
"Customers": "Customers",
|
||||
"Leads": "Leads",
|
||||
"SelectCustomer": "Select customer"
|
||||
}
|
||||
}
|
@ -44,6 +44,7 @@
|
||||
"@anticrm/view-resources": "~0.6.0",
|
||||
"@anticrm/attachment-resources": "~0.6.0",
|
||||
"@anticrm/contact-resources": "~0.6.0",
|
||||
"@anticrm/chunter-resources": "~0.6.0"
|
||||
"@anticrm/chunter-resources": "~0.6.0",
|
||||
"@anticrm/workbench-resources": "~0.6.1"
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
import { generateId } from '@anticrm/core'
|
||||
import { OK, Status } from '@anticrm/platform'
|
||||
import { Card, getClient, UserBox } from '@anticrm/presentation'
|
||||
import type { Lead } from '@anticrm/lead'
|
||||
import type { Customer, Lead } from '@anticrm/lead'
|
||||
import { EditBox, Grid, Status as StatusControl } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import lead from '../plugin'
|
||||
@ -72,11 +72,19 @@
|
||||
doneState: null,
|
||||
number: (incResult as any).object.sequence,
|
||||
title: title,
|
||||
customer: customer!,
|
||||
rank: calcRank(lastOne, undefined)
|
||||
rank: calcRank(lastOne, undefined),
|
||||
assignee: null
|
||||
}
|
||||
|
||||
await client.addCollection(lead.class.Lead, _space, customer!, contact.class.Contact, 'leads', value, leadId)
|
||||
const customerInstance = await client.findOne(contact.class.Contact, { _id: customer! })
|
||||
if (customerInstance === undefined) {
|
||||
throw new Error('contact not found')
|
||||
}
|
||||
if (!client.getHierarchy().hasMixin(customerInstance, lead.mixin.Customer)) {
|
||||
await client.createMixin<Contact, Customer>(customerInstance._id, customerInstance._class, customerInstance.space, lead.mixin.Customer, {})
|
||||
}
|
||||
|
||||
await client.addCollection(lead.class.Lead, _space, customer!, lead.mixin.Customer, 'leads', value, leadId)
|
||||
dispatch('close')
|
||||
}
|
||||
</script>
|
||||
@ -103,6 +111,6 @@
|
||||
maxWidth={'16rem'}
|
||||
focus
|
||||
/>
|
||||
<UserBox _class={contact.class.Contact} title="Customer" caption="Select customer" bind:value={customer} />
|
||||
<UserBox _class={contact.class.Contact} title={lead.string.Customer} caption={lead.string.SelectCustomer} bind:value={customer} />
|
||||
</Grid>
|
||||
</Card>
|
||||
|
112
plugins/lead-resources/src/components/Customers.svelte
Normal file
112
plugins/lead-resources/src/components/Customers.svelte
Normal file
@ -0,0 +1,112 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { EditWithIcon, Icon, IconSearch, Label, ScrollBox } from '@anticrm/ui'
|
||||
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
import lead from '../plugin'
|
||||
|
||||
let search = ''
|
||||
$: resultQuery = search === '' ? { } : { $search: search }
|
||||
</script>
|
||||
|
||||
<div class="customers-header-container">
|
||||
<div class="header-container">
|
||||
<div class="flex-row-center">
|
||||
<span class="icon"><Icon icon={lead.icon.Lead} size={'small'}/></span>
|
||||
<span class="label"><Label label={lead.string.Customers}/></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditWithIcon icon={IconSearch} placeholder={'Search'} bind:value={search} />
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="panel-component">
|
||||
<ScrollBox vertical stretch noShift>
|
||||
|
||||
<Table
|
||||
_class={lead.mixin.Customer}
|
||||
config={[
|
||||
'',
|
||||
{ key: 'leads', presenter: lead.component.LeadsPresenter, label: lead.string.Leads },
|
||||
'modifiedOn',
|
||||
'channels'
|
||||
]}
|
||||
options={ {} }
|
||||
query={ resultQuery }
|
||||
enableChecking
|
||||
/>
|
||||
</ScrollBox>
|
||||
</div>
|
||||
</div>
|
||||
<style lang="scss">
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding-bottom: 1.25rem;
|
||||
|
||||
.panel-component {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 1rem;
|
||||
height: 100%;
|
||||
border-radius: 1.25rem;
|
||||
background-color: var(--theme-bg-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
.customers-header-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: min-content;
|
||||
gap: .75rem;
|
||||
align-items: center;
|
||||
padding: 0 1.75rem 0 2.5rem;
|
||||
height: 4rem;
|
||||
min-height: 4rem;
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
.icon {
|
||||
margin-right: .5rem;
|
||||
opacity: .6;
|
||||
}
|
||||
.label, .description {
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 35rem;
|
||||
}
|
||||
.label {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.description {
|
||||
font-size: .75rem;
|
||||
color: var(--theme-content-trans-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -27,6 +27,8 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sm-tool-icon" on:click={show}>
|
||||
<span class="icon"><Icon icon={lead.icon.Lead} size={'small'} /></span>{value.title}
|
||||
</div>
|
||||
{#if value}
|
||||
<div class="sm-tool-icon" on:click={show}>
|
||||
<span class="icon"><Icon icon={lead.icon.Lead} size={'small'} /></span>{value.title}
|
||||
</div>
|
||||
{/if}
|
||||
|
87
plugins/lead-resources/src/components/Leads.svelte
Normal file
87
plugins/lead-resources/src/components/Leads.svelte
Normal file
@ -0,0 +1,87 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import type { Ref } from '@anticrm/core'
|
||||
import type { Customer, Lead } from '@anticrm/lead'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import task from '@anticrm/task'
|
||||
import { CircleButton, IconAdd, Label, showPopup } from '@anticrm/ui'
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
import lead from '../plugin'
|
||||
import CreateLead from './CreateLead.svelte'
|
||||
|
||||
export let objectId: Ref<Customer>
|
||||
|
||||
let leads: Lead[] = []
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(lead.class.Lead, { attachedTo: objectId }, result => { leads = result })
|
||||
|
||||
const createLead = (ev: MouseEvent): void =>
|
||||
showPopup(CreateLead, { candidate: objectId, preserveCandidate: true }, ev.target as HTMLElement)
|
||||
</script>
|
||||
|
||||
<div class="applications-container">
|
||||
<div class="flex-row-center">
|
||||
<div class="title">Leads</div>
|
||||
<CircleButton icon={IconAdd} size={'small'} selected on:click={createLead} />
|
||||
</div>
|
||||
{#if leads.length > 0}
|
||||
<Table
|
||||
_class={lead.class.Lead}
|
||||
config={['', '$lookup.state']}
|
||||
options={
|
||||
{
|
||||
lookup: {
|
||||
state: task.class.State
|
||||
}
|
||||
}
|
||||
}
|
||||
query={ { attachedTo: objectId } }
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex-col-center mt-5 createapp-container">
|
||||
<div class="small-text content-dark-color mt-2">
|
||||
<Label label={lead.string.NoLeadsForDocument} />
|
||||
</div>
|
||||
<div class="small-text">
|
||||
<a href={'#'} on:click={createLead}><Label label={lead.string.CreateLead} /></a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.applications-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
margin-right: .75rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
}
|
||||
|
||||
.createapp-container {
|
||||
padding: 1rem;
|
||||
color: var(--theme-caption-color);
|
||||
background: var(--theme-bg-accent-color);
|
||||
border: 1px solid var(--theme-bg-accent-color);
|
||||
border-radius: .75rem;
|
||||
}
|
||||
</style>
|
38
plugins/lead-resources/src/components/LeadsPopup.svelte
Normal file
38
plugins/lead-resources/src/components/LeadsPopup.svelte
Normal file
@ -0,0 +1,38 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { FindOptions } from '@anticrm/core'
|
||||
import type { Customer, Lead } from '@anticrm/lead'
|
||||
import task from '@anticrm/task'
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
import leads from '../plugin'
|
||||
|
||||
export let value: Customer
|
||||
const options: FindOptions<Lead> = {
|
||||
lookup: {
|
||||
state: task.class.State
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table
|
||||
_class={leads.class.Lead}
|
||||
config={['', '$lookup.state']}
|
||||
options={options}
|
||||
query={ { attachedTo: value._id } }
|
||||
/>
|
34
plugins/lead-resources/src/components/LeadsPresenter.svelte
Normal file
34
plugins/lead-resources/src/components/LeadsPresenter.svelte
Normal file
@ -0,0 +1,34 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import type { Customer } from '@anticrm/lead'
|
||||
import { Icon, Tooltip } from '@anticrm/ui'
|
||||
import LeadsPopup from './LeadsPopup.svelte'
|
||||
import leads from '../plugin'
|
||||
|
||||
export let value: Customer
|
||||
|
||||
</script>
|
||||
|
||||
{#if value.leads && value.leads > 0}
|
||||
<Tooltip label={leads.string.Leads} component={LeadsPopup} props={{ value }}>
|
||||
<div class="sm-tool-icon">
|
||||
<span class="icon"><Icon icon={leads.icon.Lead} size={'small'} /></span> {value.leads}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
@ -17,10 +17,13 @@
|
||||
import { Resources } from '@anticrm/platform'
|
||||
import CreateFunnel from './components/CreateFunnel.svelte'
|
||||
import CreateLead from './components/CreateLead.svelte'
|
||||
import Customers from './components/Customers.svelte'
|
||||
import EditLead from './components/EditLead.svelte'
|
||||
import KanbanCard from './components/KanbanCard.svelte'
|
||||
import LeadPresenter from './components/LeadPresenter.svelte'
|
||||
import LeadsPresenter from './components/LeadsPresenter.svelte'
|
||||
import TemplatesIcon from './components/TemplatesIcon.svelte'
|
||||
import Leads from './components/Leads.svelte'
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
@ -29,6 +32,9 @@ export default async (): Promise<Resources> => ({
|
||||
EditLead,
|
||||
KanbanCard,
|
||||
LeadPresenter,
|
||||
TemplatesIcon
|
||||
TemplatesIcon,
|
||||
Customers,
|
||||
LeadsPresenter,
|
||||
Leads
|
||||
}
|
||||
})
|
||||
|
@ -16,6 +16,7 @@
|
||||
import { IntlString, mergeIds } from '@anticrm/platform'
|
||||
|
||||
import lead, { leadId } from '@anticrm/lead'
|
||||
import { AnyComponent } from '@anticrm/ui'
|
||||
|
||||
export default mergeIds(leadId, lead, {
|
||||
string: {
|
||||
@ -28,6 +29,13 @@ export default mergeIds(leadId, lead, {
|
||||
SelectFunnel: '' as IntlString,
|
||||
CreateLead: '' as IntlString,
|
||||
Customer: '' as IntlString,
|
||||
SelectCustomer: '' as IntlString
|
||||
SelectCustomer: '' as IntlString,
|
||||
Customers: '' as IntlString,
|
||||
Leads: '' as IntlString,
|
||||
NoLeadsForDocument: '' as IntlString
|
||||
},
|
||||
component: {
|
||||
CreateCustomer: '' as AnyComponent,
|
||||
LeadsPresenter: '' as AnyComponent
|
||||
}
|
||||
})
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import type { Contact } from '@anticrm/contact'
|
||||
import type { Class, Ref } from '@anticrm/core'
|
||||
import { Mixin } from '@anticrm/core'
|
||||
import type { Asset, Plugin } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@anticrm/task'
|
||||
@ -25,12 +26,25 @@ import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@anticrm/task'
|
||||
*/
|
||||
export interface Funnel extends SpaceWithStates {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* @Mixin
|
||||
*/
|
||||
|
||||
export interface Customer extends Contact {
|
||||
leads?: number
|
||||
|
||||
description: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Lead extends Task {
|
||||
attachedTo: Ref<Customer>
|
||||
|
||||
title: string
|
||||
customer: Ref<Contact>
|
||||
|
||||
comments?: number
|
||||
attachments?: number
|
||||
@ -46,6 +60,9 @@ const lead = plugin(leadId, {
|
||||
Lead: '' as Ref<Class<Lead>>,
|
||||
Funnel: '' as Ref<Class<Funnel>>
|
||||
},
|
||||
mixin: {
|
||||
Customer: '' as Ref<Mixin<Customer>>
|
||||
},
|
||||
icon: {
|
||||
Funnel: '' as Asset,
|
||||
Lead: '' as Asset,
|
||||
|
@ -22,22 +22,25 @@
|
||||
import ExpandRightDouble from './icons/ExpandRightDouble.svelte'
|
||||
|
||||
import recruit from '../plugin'
|
||||
import { Ref } from '@anticrm/core'
|
||||
|
||||
export let object: Applicant
|
||||
let candidate: Candidate
|
||||
let vacancy: Vacancy
|
||||
|
||||
const candidateQuery = createQuery()
|
||||
$: if (object !== undefined)
|
||||
{candidateQuery.query(recruit.class.Candidate, { _id: object.attachedTo }, (result) => {
|
||||
candidate = result[0]
|
||||
})}
|
||||
$: if (object !== undefined) {
|
||||
candidateQuery.query(recruit.mixin.Candidate, { _id: object.attachedTo as Ref<Candidate> }, (result) => {
|
||||
candidate = result[0]
|
||||
})
|
||||
}
|
||||
|
||||
const vacancyQuery = createQuery()
|
||||
$: if (object !== undefined)
|
||||
{vacancyQuery.query(recruit.class.Vacancy, { _id: object.space }, (result) => {
|
||||
vacancy = result[0]
|
||||
})}
|
||||
$: if (object !== undefined) {
|
||||
vacancyQuery.query(recruit.class.Vacancy, { _id: object.space }, (result) => {
|
||||
vacancy = result[0]
|
||||
})
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
<script lang="ts">
|
||||
import core from '@anticrm/core'
|
||||
import type { Space } from '@anticrm/core'
|
||||
import type { NavigatorModel } from '@anticrm/workbench'
|
||||
import type { NavigatorModel, SpecialNavModel } from '@anticrm/workbench'
|
||||
import { getCurrentLocation, navigate } from '@anticrm/ui'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import view from '@anticrm/view'
|
||||
@ -47,11 +47,14 @@
|
||||
loc.path.length = 3
|
||||
navigate(loc)
|
||||
}
|
||||
function getSpecials (specials: SpecialNavModel[], state: 'top' | 'bottom'): SpecialNavModel[] {
|
||||
return specials.filter(p => (p.position ?? 'top') === state)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if model}
|
||||
{#if model.specials}
|
||||
{#each model.specials as special}
|
||||
{#each getSpecials(model.specials, 'top') as special}
|
||||
<SpecialElement label={special.label} icon={special.icon} on:click={() => selectSpecial(special.id)} />
|
||||
{/each}
|
||||
{/if}
|
||||
@ -66,6 +69,11 @@
|
||||
{#each model.spaces as m (m.label)}
|
||||
<SpacesNav model={m}/>
|
||||
{/each}
|
||||
{#if model.specials}
|
||||
{#each getSpecials(model.specials, 'bottom') as special}
|
||||
<SpecialElement label={special.label} icon={special.icon} on:click={() => selectSpecial(special.id)} />
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -15,13 +15,14 @@
|
||||
|
||||
import WorkbenchApp from './components/WorkbenchApp.svelte'
|
||||
import ApplicationPresenter from './components/ApplicationPresenter.svelte'
|
||||
import { Resources } from '@anticrm/platform'
|
||||
|
||||
/*!
|
||||
* Anticrm Platform™ Workbench Plugin
|
||||
* © 2020 Anticrm Platform Contributors. All Rights Reserved.
|
||||
* Licensed under the Eclipse Public License, Version 2.0
|
||||
*/
|
||||
export default async () => ({
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
WorkbenchApp,
|
||||
ApplicationPresenter
|
||||
|
@ -57,6 +57,7 @@ export interface SpecialNavModel {
|
||||
label: IntlString
|
||||
icon: Asset
|
||||
component: AnyComponent
|
||||
position?: 'top'|'bottom' // undefined == 'top
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,8 +59,9 @@ export class FullTextIndex implements WithFind {
|
||||
return {}
|
||||
}
|
||||
|
||||
protected txMixin (ctx: MeasureContext, tx: TxMixin<Doc, Doc>): Promise<TxResult> {
|
||||
throw new Error('Method not implemented.')
|
||||
protected async txMixin (ctx: MeasureContext, tx: TxMixin<Doc, Doc>): Promise<TxResult> {
|
||||
console.log('FullTextIndex.txMixin: Method not implemented')
|
||||
return {}
|
||||
}
|
||||
|
||||
async tx (ctx: MeasureContext, tx: Tx): Promise<TxResult> {
|
||||
|
@ -145,11 +145,19 @@ class TServerStorage implements ServerStorage {
|
||||
attachedTo = (await this.findAll(ctx, _class, { _id }, { limit: 1 }))[0]
|
||||
if (attachedTo !== undefined) {
|
||||
const txFactory = new TxFactory(tx.modifiedBy)
|
||||
return [
|
||||
txFactory.createTxUpdateDoc(_class, attachedTo.space, _id, {
|
||||
const baseClass = this.hierarchy.getBaseClass(_class)
|
||||
if (baseClass !== _class) {
|
||||
// Mixin opeeration is required.
|
||||
return [txFactory.createTxMixin(_id, attachedTo._class, attachedTo.space, _class, {
|
||||
$inc: { [colTx.collection]: isCreateTx ? 1 : -1 }
|
||||
})
|
||||
]
|
||||
})]
|
||||
} else {
|
||||
return [
|
||||
txFactory.createTxUpdateDoc(_class, attachedTo.space, _id, {
|
||||
$inc: { [colTx.collection]: isCreateTx ? 1 : -1 }
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import core, {
|
||||
Class,
|
||||
Doc,
|
||||
DocumentQuery, DOMAIN_MODEL, DOMAIN_TX, FindOptions,
|
||||
FindResult, Hierarchy, isOperator, ModelDb, Ref, SortingOrder, Tx,
|
||||
FindResult, Hierarchy, isOperator, Mixin, ModelDb, Ref, SortingOrder, Tx,
|
||||
TxCreateDoc,
|
||||
TxMixin, TxProcessor, TxPutBag,
|
||||
TxRemoveDoc,
|
||||
@ -56,12 +56,18 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
}
|
||||
translated[key] = value
|
||||
}
|
||||
const classes = this.hierarchy.getDescendants(clazz)
|
||||
const baseClass = this.hierarchy.getBaseClass(clazz)
|
||||
const classes = this.hierarchy.getDescendants(baseClass)
|
||||
|
||||
// Only replace if not specified
|
||||
if (translated._class?.$in === undefined) {
|
||||
translated._class = { $in: classes }
|
||||
}
|
||||
|
||||
if (baseClass !== clazz) {
|
||||
// Add an mixin to be exists flag
|
||||
translated[clazz] = { $exists: true }
|
||||
}
|
||||
// return Object.assign({}, query, { _class: { $in: classes } })
|
||||
return translated
|
||||
}
|
||||
@ -168,8 +174,91 @@ class MongoAdapter extends MongoAdapterBase {
|
||||
return {}
|
||||
}
|
||||
|
||||
protected txMixin (tx: TxMixin<Doc, Doc>): Promise<TxResult> {
|
||||
throw new Error('Method not implemented.')
|
||||
protected async txMixin (tx: TxMixin<Doc, Doc>): Promise<TxResult> {
|
||||
const domain = this.hierarchy.getDomain(tx.objectClass)
|
||||
|
||||
if (isOperator(tx.attributes)) {
|
||||
const operator = Object.keys(tx.attributes)[0]
|
||||
if (operator === '$move') {
|
||||
const keyval = (tx.attributes as any).$move
|
||||
const arr = tx.mixin + '.' + Object.keys(keyval)[0]
|
||||
const desc = keyval[arr]
|
||||
const ops = [
|
||||
{
|
||||
updateOne: {
|
||||
filter: { _id: tx.objectId },
|
||||
update: {
|
||||
$pull: {
|
||||
[arr]: desc.$value
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
updateOne: {
|
||||
filter: { _id: tx.objectId },
|
||||
update: {
|
||||
$set: {
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn
|
||||
},
|
||||
$push: {
|
||||
[arr]: {
|
||||
$each: [desc.$value],
|
||||
$position: desc.$position
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
return await this.db.collection(domain).bulkWrite(ops as any)
|
||||
} else {
|
||||
return await this.db.collection(domain).updateOne(
|
||||
{ _id: tx.objectId },
|
||||
{
|
||||
...this.translateMixinAttrs(tx.mixin, tx.attributes),
|
||||
$set: {
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return await this.db
|
||||
.collection(domain)
|
||||
.updateOne(
|
||||
{ _id: tx.objectId },
|
||||
{
|
||||
$set: {
|
||||
...this.translateMixinAttrs(tx.mixin, tx.attributes),
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private translateMixinAttrs (mixin: Ref<Mixin<Doc>>, attributes: Record<string, any>): Record<string, any> {
|
||||
const attrs: Record<string, any> = {}
|
||||
let count = 0
|
||||
for (const [k, v] of Object.entries(attributes)) {
|
||||
if (k.startsWith('$')) {
|
||||
attrs[k] = this.translateMixinAttrs(mixin, v)
|
||||
} else {
|
||||
attrs[mixin + '.' + k] = v
|
||||
}
|
||||
count++
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
// We need at least one attribute, to be inside for first time,
|
||||
// for mongo to create embedded object, if we don't want to get object first.
|
||||
attrs[mixin + '.' + '__mixin'] = 'true'
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
protected override async txCreateDoc (tx: TxCreateDoc<Doc>): Promise<TxResult> {
|
||||
|
Loading…
Reference in New Issue
Block a user