Fix Activity icons (#788)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-01-11 16:09:52 +07:00 committed by GitHub
parent decf27ef0d
commit a7fcf166fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 174 additions and 107 deletions

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 770 B

After

Width:  |  Height:  |  Size: 770 B

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

View File

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

View File

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

View File

@ -108,6 +108,8 @@ export interface AttributeModel {
// Extra properties for component
props?: Record<string, any>
sortingKey: string
// Extra icon if applicable
icon?: Asset
}
/**