diff --git a/models/task/src/index.ts b/models/task/src/index.ts index a2a8515b3f..fa25f1a24f 100644 --- a/models/task/src/index.ts +++ b/models/task/src/index.ts @@ -54,7 +54,7 @@ export const DOMAIN_TASK = 'task' as Domain export const DOMAIN_STATE = 'state' as Domain export const DOMAIN_KANBAN = 'kanban' as Domain @Model(task.class.State, core.class.Doc, DOMAIN_STATE, [task.interface.DocWithRank]) -@UX('State' as IntlString, undefined, undefined, 'rank') +@UX('State' as IntlString, task.icon.TaskState, undefined, 'rank') export class TState extends TDoc implements State { @Prop(TypeString(), 'Title' as IntlString) title!: string @@ -65,7 +65,7 @@ export class TState extends TDoc implements State { } @Model(task.class.DoneState, core.class.Doc, DOMAIN_STATE, [task.interface.DocWithRank]) -@UX('Done' as IntlString, undefined, undefined, 'title') +@UX('Done' as IntlString, task.icon.TaskState, undefined, 'title') export class TDoneState extends TDoc implements DoneState { @Prop(TypeString(), 'Title' as IntlString) title!: string diff --git a/plugins/activity-assets/assets/icons.svg b/plugins/activity-assets/assets/icons.svg index 22075d5191..647f5be9a2 100644 --- a/plugins/activity-assets/assets/icons.svg +++ b/plugins/activity-assets/assets/icons.svg @@ -1,5 +1,5 @@ <svg xmlns="http://www.w3.org/2000/svg" style="display: none;"> - <symbol id="activity" viewBox="0 0 20 20" width='20' height='20' fill="none"> + <symbol id="activity" viewBox="0 0 20 20" width='16' height='16' fill="none"> <path d="M6.03772 12.3181L8.532 9.07628L11.3772 11.3112L13.818 8.16095" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" fill='none'/> <ellipse cx="16.6632" cy="3.50027" rx="1.60183" ry="1.60183" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12.4372 2.6001H6.38078C3.87125 2.6001 2.31519 4.37737 2.31519 6.8869V13.6222C2.31519 16.1318 3.84074 17.9014 6.38078 17.9014H13.5509C16.0604 17.9014 17.6165 16.1318 17.6165 13.6222V7.75647" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> diff --git a/plugins/activity-resources/src/components/TxView.svelte b/plugins/activity-resources/src/components/TxView.svelte index f1b1220590..cca472e3a9 100644 --- a/plugins/activity-resources/src/components/TxView.svelte +++ b/plugins/activity-resources/src/components/TxView.svelte @@ -18,34 +18,26 @@ import type { TxViewlet } from '@anticrm/activity' import activity from '@anticrm/activity' import contact, { EmployeeAccount, formatName } from '@anticrm/contact' - import core, { Class, Doc, Ref, TxCUD, TxUpdateDoc } from '@anticrm/core' - import { getResource, IntlString } from '@anticrm/platform' + import { Doc, Ref } from '@anticrm/core' + import { Asset, getResource } from '@anticrm/platform' import { getClient } from '@anticrm/presentation' import { - AnyComponent, - AnySvelteComponent, Component, Icon, IconEdit, IconMoreH, Label, - Menu, - showPopup, - TimeSince, - ShowMore + Menu, ShowMore, showPopup, + TimeSince } from '@anticrm/ui' import type { AttributeModel } from '@anticrm/view' - import { buildModel, getActions, getObjectPresenter } from '@anticrm/view-resources' - import { activityKey, ActivityKey, DisplayTx } from '../activity' + import { getActions } from '@anticrm/view-resources' + import { ActivityKey, DisplayTx } from '../activity' + import { getValue, TxDisplayViewlet, updateViewlet } from './utils' export let tx: DisplayTx export let viewlets: Map<ActivityKey, TxViewlet> - type TxDisplayViewlet = - | (Pick<TxViewlet, 'icon' | 'label' | 'display' | 'editable' | 'hideOnRemove'> & { - component?: AnyComponent | AnySvelteComponent - }) - | undefined let ptx: DisplayTx | undefined @@ -53,6 +45,7 @@ let props: any let employee: EmployeeAccount | undefined let model: AttributeModel[] = [] + let modelIcon: Asset | undefined = undefined let edit = false @@ -66,43 +59,12 @@ const client = getClient() - async function createPseudoViewlet (dtx: DisplayTx, label: string): Promise<TxDisplayViewlet> { - const doc = dtx.doc - if (doc === undefined) { - return - } - const docClass: Class<Doc> = client.getModel().getObject(doc._class) - - const presenter = await getObjectPresenter(client, doc._class, { key: 'doc-presenter' }) - if (presenter !== undefined) { - return { - display: 'inline', - icon: docClass.icon ?? activity.icon.Activity, - label: (`${label} ` + docClass.label) as IntlString, - component: presenter.presenter - } - } - } - - async function updateViewlet (dtx: DisplayTx): Promise<{ viewlet: TxDisplayViewlet; id: Ref<TxCUD<Doc>> }> { - const key = activityKey(dtx.tx.objectClass, dtx.tx._class) - let viewlet: TxDisplayViewlet = viewlets.get(key) - - props = { tx: dtx.tx, value: dtx.doc, edit } - - if (viewlet === undefined && dtx.tx._class === core.class.TxCreateDoc) { - // Check if we have a class presenter we could have a pseudo viewlet based on class presenter. - viewlet = await createPseudoViewlet(dtx, 'created') - } - if (viewlet === undefined && dtx.tx._class === core.class.TxRemoveDoc) { - viewlet = await createPseudoViewlet(dtx, 'deleted') - } - return { viewlet, id: dtx.tx._id } - } - - $: updateViewlet(tx).then((result) => { + $: updateViewlet(client, viewlets, tx).then((result) => { if (result.id === tx.tx._id) { viewlet = result.viewlet + model = result.model + modelIcon = result.modelIcon + props = { ...result.props, edit } } }) @@ -112,47 +74,7 @@ employee = account }) - $: if (tx.updateTx !== undefined) { - const _class = tx.updateTx.objectClass - const ops = { - client, - _class, - keys: Object.keys(tx.updateTx.operations).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)) - }) - } 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: 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 - return await client.findOne(m._class, { _id: val as Ref<Doc> }) - } - return val - } const showMenu = async (ev: MouseEvent): Promise<void> => { const actions = await getActions(client, tx.doc as Doc) showPopup( @@ -192,7 +114,11 @@ {#if viewlet} <Icon icon={viewlet.icon} size="small" /> {:else} - <Icon icon={activity.icon.Activity} size="small" /> + {#if viewlet === undefined && model.length > 0} + <Icon icon={modelIcon !== undefined ? modelIcon : IconEdit} size="small" /> + {:else} + <Icon icon={activity.icon.Activity} size="small" /> + {/if} {/if} </div> @@ -226,7 +152,7 @@ {/if} {#if viewlet === undefined && model.length > 0 && tx.updateTx} {#each model as m} - {#await getValue(m, tx.updateTx.operations) then value} + {#await getValue(client, m, tx.updateTx.operations) then value} {#if value === null} <span>unset <Label label={m.label} /></span> {:else} @@ -237,7 +163,7 @@ {/each} {:else if viewlet === undefined && model.length > 0 && tx.mixinTx} {#each model as m} - {#await getValue(m, tx.mixinTx.attributes) then value} + {#await getValue(client, m, tx.mixinTx.attributes) then value} {#if value === null} <span>unset <Label label={m.label} /></span> {:else} @@ -247,13 +173,11 @@ {/await} {/each} {:else if viewlet && viewlet.display === 'inline' && viewlet.component} - <div> - {#if typeof viewlet.component === 'string'} - <Component is={viewlet.component} {props} on:close={onCancelEdit} /> - {:else} - <svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} /> - {/if} - </div> + {#if typeof viewlet.component === 'string'} + <Component is={viewlet.component} {props} on:close={onCancelEdit} /> + {:else} + <svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} /> + {/if} {/if} </div> <div class="time"><TimeSince value={tx.tx.modifiedOn} /></div> diff --git a/plugins/activity-resources/src/components/utils.ts b/plugins/activity-resources/src/components/utils.ts new file mode 100644 index 0000000000..250d2154f3 --- /dev/null +++ b/plugins/activity-resources/src/components/utils.ts @@ -0,0 +1,135 @@ +import type { TxViewlet } from '@anticrm/activity' +import activity from '@anticrm/activity' +import core, { Class, Client, Doc, Ref, TxCUD, TxOperations } from '@anticrm/core' +import { Asset, IntlString } from '@anticrm/platform' +import { AnyComponent, AnySvelteComponent } from '@anticrm/ui' +import { AttributeModel } from '@anticrm/view' +import { buildModel, getObjectPresenter } from '@anticrm/view-resources' +import { ActivityKey, activityKey, DisplayTx } from '../activity' + +export type TxDisplayViewlet = + | (Pick<TxViewlet, 'icon' | 'label' | 'display' | 'editable' | 'hideOnRemove'> & { + component?: AnyComponent | AnySvelteComponent + }) + | undefined + +async function createPseudoViewlet ( + client: Client & TxOperations, + dtx: DisplayTx, + label: string +): Promise<TxDisplayViewlet> { + const doc = dtx.doc + if (doc === undefined) { + return + } + const docClass: Class<Doc> = client.getModel().getObject(doc._class) + + const presenter = await getObjectPresenter(client, doc._class, { key: 'doc-presenter' }) + if (presenter !== undefined) { + return { + display: 'inline', + icon: docClass.icon ?? activity.icon.Activity, + label: (`${label} ` + docClass.label) as IntlString, + component: presenter.presenter + } + } +} + +export async function updateViewlet ( + client: Client & TxOperations, + viewlets: Map<ActivityKey, TxViewlet>, + dtx: DisplayTx +): Promise<{ + viewlet: TxDisplayViewlet + id: Ref<TxCUD<Doc>> + model: AttributeModel[] + props: any + modelIcon: Asset | undefined + }> { + const key = activityKey(dtx.tx.objectClass, dtx.tx._class) + let viewlet: TxDisplayViewlet = viewlets.get(key) + + const props = { tx: dtx.tx, value: dtx.doc, dtx } + let model: AttributeModel[] = [] + let modelIcon: Asset | undefined + + if (viewlet === undefined) { + ;({ viewlet, model } = await checkInlineViewlets(dtx, viewlet, client, model)) + if (model !== undefined) { + // Check for State attribute + for (const a of model) { + if (a.icon !== undefined) { + modelIcon = a.icon + break + } + } + } + } + return { viewlet, id: dtx.tx._id, model, props, modelIcon } +} + +async function checkInlineViewlets ( + dtx: DisplayTx, + viewlet: TxDisplayViewlet, + client: Client & TxOperations, + model: AttributeModel[] +): Promise<{ viewlet: TxDisplayViewlet, model: AttributeModel[] }> { + if (dtx.tx._class === core.class.TxCreateDoc) { + // Check if we have a class presenter we could have a pseudo viewlet based on class presenter. + viewlet = await createPseudoViewlet(client, dtx, 'created') + } + if (dtx.tx._class === core.class.TxRemoveDoc) { + viewlet = await createPseudoViewlet(client, dtx, 'deleted') + } + if (dtx.tx._class === core.class.TxUpdateDoc) { + model = await createUpdateModel(dtx, client, model) + } + return { viewlet, model } +} + +async function createUpdateModel ( + dtx: DisplayTx, + client: Client & TxOperations, + model: AttributeModel[] +): Promise<AttributeModel[]> { + if (dtx.updateTx !== undefined) { + const _class = dtx.updateTx.objectClass + const ops = { + client, + _class, + keys: Object.keys(dtx.updateTx.operations).filter((id) => !id.startsWith('$')), + ignoreMissing: true + } + const hiddenAttrs = getHiddenAttrs(client, _class) + model = (await buildModel(ops)).filter((x) => !hiddenAttrs.has(x.key)) + } else if (dtx.mixinTx !== undefined) { + const _class = dtx.mixinTx.mixin + const ops = { + client, + _class, + keys: Object.keys(dtx.mixinTx.attributes).filter((id) => !id.startsWith('$')), + ignoreMissing: true + } + const hiddenAttrs = getHiddenAttrs(client, _class) + model = (await buildModel(ops)).filter((x) => !hiddenAttrs.has(x.key)) + } + return model +} + +function getHiddenAttrs (client: Client & TxOperations, _class: Ref<Class<Doc>>): Set<string> { + return new Set( + [...client.getHierarchy().getAllAttributes(_class).entries()] + .filter(([, attr]) => attr.hidden === true) + .map(([k]) => k) + ) +} + +export async function getValue (client: Client & TxOperations, m: AttributeModel, utx: any): Promise<any> { + const val = utx[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 + return await client.findOne(m._class, { _id: val as Ref<Doc> }) + } + return val +} diff --git a/plugins/task-assets/assets/icons.svg b/plugins/task-assets/assets/icons.svg index 48d2c7308f..bb89076f86 100644 --- a/plugins/task-assets/assets/icons.svg +++ b/plugins/task-assets/assets/icons.svg @@ -19,4 +19,8 @@ <path d="M13.3,8.3c-0.1,2.8-2.5,5.1-5.4,5.1C5,13.4,2.6,11,2.6,8c0-2.9,2.3-5.2,5.1-5.4c0.1-0.4,0.2-0.7,0.4-1c0,0-0.1,0-0.1,0 C4.4,1.7,1.6,4.5,1.6,8c0,3.5,2.9,6.4,6.4,6.4s6.4-2.9,6.4-6.4c0,0,0-0.1,0-0.1C14,8.1,13.7,8.2,13.3,8.3z"/> <ellipse cx="12.1" cy="3.9" rx="2.5" ry="2.5"/> </symbol> + <symbol id="task-state" viewBox="0 0 16 16"> + <path d="M13.3,8.3c-0.1,2.8-2.5,5.1-5.4,5.1C5,13.4,2.6,11,2.6,8c0-2.9,2.3-5.2,5.1-5.4c0.1-0.4,0.2-0.7,0.4-1c0,0-0.1,0-0.1,0 C4.4,1.7,1.6,4.5,1.6,8c0,3.5,2.9,6.4,6.4,6.4s6.4-2.9,6.4-6.4c0,0,0-0.1,0-0.1C14,8.1,13.7,8.2,13.3,8.3z"/> + <ellipse cx="12.1" cy="3.9" rx="2.5" ry="2.5"/> + </symbol> </svg> diff --git a/plugins/task-assets/src/index.ts b/plugins/task-assets/src/index.ts index 36e9231e03..b18b8aea69 100644 --- a/plugins/task-assets/src/index.ts +++ b/plugins/task-assets/src/index.ts @@ -22,7 +22,8 @@ loadMetadata(task.icon, { Kanban: `${icons}#kanban`, TodoCheck: `${icons}#todo-check`, TodoUnCheck: `${icons}#todo-uncheck`, - ManageStatuses: `${icons}#manage-statuses` + ManageStatuses: `${icons}#manage-statuses`, + TaskState: `${icons}#task-state` }) addStringsLoader(taskId, async (lang: string) => await import(`../lang/${lang}.json`)) diff --git a/plugins/task/src/index.ts b/plugins/task/src/index.ts index b82e99e808..4fc9991b3b 100644 --- a/plugins/task/src/index.ts +++ b/plugins/task/src/index.ts @@ -212,7 +212,8 @@ const task = plugin(taskId, { Kanban: '' as Asset, TodoCheck: '' as Asset, TodoUnCheck: '' as Asset, - ManageStatuses: '' as Asset + ManageStatuses: '' as Asset, + TaskState: '' as Asset }, global: { // Global task root, if not attached to some other object. diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index 1b4689694e..918a921506 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -79,7 +79,8 @@ async function getAttributePresenter (client: Client, _class: Ref<Class<Obj>>, k sortingKey, _class: attrClass, label: preserveKey.label ?? attribute.label, - presenter + presenter, + icon: presenterMixin.icon } } @@ -141,7 +142,6 @@ export async function buildModel (options: BuildModelOptions): Promise<Attribute return errorPresenter } }) - console.log(model) return (await Promise.all(model)).filter(a => a !== undefined) as AttributeModel[] } diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 79fe0458e3..2f7e5e1f06 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -108,6 +108,8 @@ export interface AttributeModel { // Extra properties for component props?: Record<string, any> sortingKey: string + // Extra icon if applicable + icon?: Asset } /**