Use tags for card labels (#1888)

This commit is contained in:
Alex 2022-05-27 23:43:11 +07:00 committed by GitHub
parent 4e69e4f0d1
commit a224a154e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 214 additions and 341 deletions

View File

@ -42,6 +42,8 @@
"@anticrm/task": "~0.6.0", "@anticrm/task": "~0.6.0",
"@anticrm/model-task": "~0.6.0", "@anticrm/model-task": "~0.6.0",
"@anticrm/workbench": "~0.6.1", "@anticrm/workbench": "~0.6.1",
"@anticrm/model-preference": "~0.6.0" "@anticrm/model-preference": "~0.6.0",
"@anticrm/tags": "~0.6.2",
"@anticrm/model-tags": "~0.6.0"
} }
} }

View File

@ -14,7 +14,7 @@
// //
// To help typescript locate view plugin properly // To help typescript locate view plugin properly
import type { Board, Card, CardLabel, MenuPage, CommonBoardPreference, CardCover } from '@anticrm/board' import type { Board, Card, MenuPage, CommonBoardPreference, CardCover } from '@anticrm/board'
import type { Employee } from '@anticrm/contact' import type { Employee } from '@anticrm/contact'
import { DOMAIN_MODEL, IndexKind, Markup, Ref, Type } from '@anticrm/core' import { DOMAIN_MODEL, IndexKind, Markup, Ref, Type } from '@anticrm/core'
import { import {
@ -33,7 +33,7 @@ import {
import attachment from '@anticrm/model-attachment' import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter' import chunter from '@anticrm/model-chunter'
import contact from '@anticrm/model-contact' import contact from '@anticrm/model-contact'
import core, { TAttachedDoc, TDoc, TType } from '@anticrm/model-core' import core, { TDoc, TType } from '@anticrm/model-core'
import task, { TSpaceWithStates, TTask } from '@anticrm/model-task' import task, { TSpaceWithStates, TTask } from '@anticrm/model-task'
import view, { actionTemplates, createAction } from '@anticrm/model-view' import view, { actionTemplates, createAction } from '@anticrm/model-view'
import workbench, { Application } from '@anticrm/model-workbench' import workbench, { Application } from '@anticrm/model-workbench'
@ -49,14 +49,6 @@ export class TBoard extends TSpaceWithStates implements Board {
background!: string background!: string
} }
@Model(board.class.CardLabel, core.class.AttachedDoc, DOMAIN_MODEL)
@UX(board.string.Labels)
export class TCardLabel extends TAttachedDoc implements CardLabel {
title!: string
color!: number
isHidden?: boolean
}
function TypeCardCover (): Type<CardCover> { function TypeCardCover (): Type<CardCover> {
return { _class: board.class.CardCover, label: board.string.Cover } return { _class: board.class.CardCover, label: board.string.Cover }
} }
@ -90,9 +82,6 @@ export class TCard extends TTask implements Card {
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
description!: Markup description!: Markup
@Prop(Collection(board.class.CardLabel), board.string.Labels)
labels!: Ref<CardLabel>[]
@Prop(TypeString(), board.string.Location) @Prop(TypeString(), board.string.Location)
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
location?: string location?: string
@ -121,7 +110,7 @@ export class TMenuPage extends TDoc implements MenuPage {
} }
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel(TBoard, TCard, TCardLabel, TMenuPage, TCommonBoardPreference, TCardCover) builder.createModel(TBoard, TCard, TMenuPage, TCommonBoardPreference, TCardCover)
builder.createDoc(board.class.MenuPage, core.space.Model, { builder.createDoc(board.class.MenuPage, core.space.Model, {
component: board.component.Archive, component: board.component.Archive,
@ -216,14 +205,6 @@ export function createModel (builder: Builder): void {
presenter: board.component.CardPresenter presenter: board.component.CardPresenter
}) })
builder.mixin(board.class.CardLabel, core.class.Class, view.mixin.AttributePresenter, {
presenter: board.component.CardLabelPresenter
})
builder.mixin(board.class.CardLabel, core.class.Class, view.mixin.CollectionPresenter, {
presenter: board.component.CardLabelPresenter
})
builder.mixin(board.class.Board, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(board.class.Board, core.class.Class, view.mixin.AttributePresenter, {
presenter: board.component.BoardPresenter presenter: board.component.BoardPresenter
}) })

View File

@ -13,12 +13,29 @@
// limitations under the License. // limitations under the License.
// //
import { Doc, Ref, Space, TxOperations } from '@anticrm/core' import {
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model' AttachedDoc,
Class,
Doc,
DOMAIN_TX,
generateId,
Ref,
Space,
TxCollectionCUD,
TxCreateDoc,
TxCUD,
TxOperations,
TxProcessor,
TxUpdateDoc
} from '@anticrm/core'
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import core from '@anticrm/model-core' import core from '@anticrm/model-core'
import { createKanbanTemplate, createSequence, DOMAIN_TASK } from '@anticrm/model-task' import { createKanbanTemplate, createSequence, DOMAIN_TASK } from '@anticrm/model-task'
import task, { createKanban, KanbanTemplate } from '@anticrm/task' import task, { createKanban, KanbanTemplate } from '@anticrm/task'
import { DOMAIN_TAGS } from '@anticrm/model-tags'
import tags, { TagElement, TagReference } from '@anticrm/tags'
import board from './plugin' import board from './plugin'
import { Board, Card } from '@anticrm/board'
async function createSpace (tx: TxOperations): Promise<void> { async function createSpace (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, { const current = await tx.findOne(core.class.Space, {
@ -74,12 +91,87 @@ async function createDefaults (tx: TxOperations): Promise<void> {
await createSpace(tx) await createSpace(tx)
await createSequence(tx, board.class.Card) await createSequence(tx, board.class.Card)
await createDefaultKanban(tx) await createDefaultKanban(tx)
await createOrUpdate(
tx,
tags.class.TagCategory,
tags.space.Tags,
{
icon: tags.icon.Tags,
label: 'Other',
targetClass: board.class.Card,
tags: [],
default: true
},
board.category.Other
)
} }
interface CardLabel extends AttachedDoc {
title: string
color: number
isHidden?: boolean
}
async function migrateLabels (client: MigrationClient): Promise<void> { async function migrateLabels (client: MigrationClient): Promise<void> {
const cards = await client.find(DOMAIN_TASK, { _class: board.class.Card, labels: { $exists: false, $in: [null] } }) const objectClass = 'board:class:CardLabel' as Ref<Class<Doc>>
const txes = await client.find<TxCUD<CardLabel>>(DOMAIN_TX, { objectClass }, { sort: { modifiedOn: 1 } })
const collectionTxes = await client.find<TxCollectionCUD<Board, CardLabel>>(
DOMAIN_TX,
{ 'tx.objectClass': objectClass },
{ sort: { modifiedOn: 1 } }
)
await Promise.all([...txes, ...collectionTxes].map(({ _id }) => client.delete<Doc>(DOMAIN_TX, _id)))
const removed = txes.filter(({ _class }) => _class === core.class.TxRemoveDoc).map(({ objectId }) => objectId)
const createTxes = txes.filter(
({ _class, objectId }) => _class === core.class.TxCreateDoc && !removed.includes(objectId)
) as unknown as TxCreateDoc<CardLabel>[]
const cardLabels = createTxes.map((createTx) => {
const cardLabel = TxProcessor.createDoc2Doc(createTx)
const updateTxes = collectionTxes
.map(({ tx }) => tx)
.filter(
({ _class, objectId }) => _class === core.class.TxUpdateDoc && objectId === createTx.objectId
) as unknown as TxUpdateDoc<CardLabel>[]
return updateTxes.reduce((label, updateTx) => TxProcessor.updateDoc2Doc(label, updateTx), cardLabel)
})
await Promise.all(
cardLabels.map((cardLabel) =>
client.create<TagElement>(DOMAIN_TAGS, {
_class: tags.class.TagElement,
space: tags.space.Tags,
targetClass: board.class.Card,
category: board.category.Other,
_id: cardLabel._id as unknown as Ref<TagElement>,
modifiedBy: cardLabel.modifiedBy,
modifiedOn: cardLabel.modifiedOn,
title: cardLabel.title,
color: cardLabel.color,
description: ''
})
)
)
const cards = (await client.find<Card>(DOMAIN_TASK, { _class: board.class.Card })).filter((card) =>
Array.isArray(card.labels)
)
for (const card of cards) { for (const card of cards) {
await client.update(DOMAIN_TASK, { _id: card._id }, { labels: [] }) const labelRefs = card.labels as unknown as Array<Ref<CardLabel>>
await client.update<Card>(DOMAIN_TASK, { _id: card._id }, { labels: labelRefs.length })
for (const labelRef of labelRefs) {
const cardLabel = cardLabels.find(({ _id }) => _id === labelRef)
if (cardLabel === undefined) continue
await client.create<TagReference>(DOMAIN_TAGS, {
_class: tags.class.TagReference,
attachedToClass: board.class.Card,
_id: generateId(),
attachedTo: card._id,
space: card.space,
tag: cardLabel._id as unknown as Ref<TagElement>,
title: cardLabel.title,
color: cardLabel.color,
modifiedBy: cardLabel.modifiedBy,
modifiedOn: cardLabel.modifiedOn,
collection: 'labels'
})
}
} }
} }

View File

@ -15,12 +15,4 @@
import { Builder } from '@anticrm/model' import { Builder } from '@anticrm/model'
import serverCore from '@anticrm/server-core' export function createModel (builder: Builder): void {}
import core from '@anticrm/core'
import serverBoard from '@anticrm/server-board'
export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverBoard.trigger.OnLabelDelete
})
}

View File

@ -40,6 +40,7 @@
"@anticrm/model-chunter": "~0.6.0", "@anticrm/model-chunter": "~0.6.0",
"@anticrm/workbench": "~0.6.1", "@anticrm/workbench": "~0.6.1",
"@anticrm/view": "~0.6.0", "@anticrm/view": "~0.6.0",
"@anticrm/model-presentation": "~0.6.0" "@anticrm/model-presentation": "~0.6.0",
"@anticrm/tags": "~0.6.2"
} }
} }

View File

@ -60,6 +60,7 @@ import {
WonStateTemplate WonStateTemplate
} from '@anticrm/task' } from '@anticrm/task'
import { AnyComponent } from '@anticrm/ui' import { AnyComponent } from '@anticrm/ui'
import tags from '@anticrm/tags'
import task from './plugin' import task from './plugin'
export { createKanbanTemplate, createSequence, taskOperation } from './migration' export { createKanbanTemplate, createSequence, taskOperation } from './migration'
@ -123,6 +124,9 @@ export class TTask extends TAttachedDoc implements Task {
@Prop(Collection(task.class.TodoItem), task.string.Todos) @Prop(Collection(task.class.TodoItem), task.string.Todos)
todoItems!: number todoItems!: number
@Prop(Collection(tags.class.TagReference, task.string.TaskLabels), task.string.TaskLabels)
labels!: number
} }
@Model(task.class.TodoItem, core.class.AttachedDoc, DOMAIN_TASK, [task.interface.DocWithRank]) @Model(task.class.TodoItem, core.class.AttachedDoc, DOMAIN_TASK, [task.interface.DocWithRank])
@ -175,10 +179,6 @@ export class TIssue extends TTask implements Issue {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, undefined, attachment.string.Files) @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, undefined, attachment.string.Files)
attachments!: number attachments!: number
@Prop(TypeString(), task.string.TaskLabels)
@Index(IndexKind.FullText)
labels!: string
@Prop(TypeRef(contact.class.Employee), task.string.TaskAssignee) @Prop(TypeRef(contact.class.Employee), task.string.TaskAssignee)
declare assignee: Ref<Employee> | null declare assignee: Ref<Employee> | null
} }

View File

@ -54,6 +54,7 @@
"@anticrm/workbench": "~0.6.1", "@anticrm/workbench": "~0.6.1",
"svelte": "^3.47", "svelte": "^3.47",
"@anticrm/kanban": "~0.6.0", "@anticrm/kanban": "~0.6.0",
"@anticrm/preference": "~0.6.0" "@anticrm/preference": "~0.6.0",
"@anticrm/tags": "~0.6.2"
} }
} }

View File

@ -65,7 +65,6 @@
assignee: null, assignee: null,
description: '', description: '',
members: [], members: [],
labels: [],
location: '', location: '',
startDate: null, startDate: null,
dueDate: null dueDate: null

View File

@ -12,7 +12,7 @@
const isArchived = { $nin: [true] } const isArchived = { $nin: [true] }
const query = createQuery() const query = createQuery()
let states: Ref<State>[] let states: Ref<State>[] = []
$: query.query(task.class.State, { space, isArchived }, (result) => { $: query.query(task.class.State, { space, isArchived }, (result) => {
states = result.map(({ _id }) => _id) states = result.map(({ _id }) => _id)
}) })

View File

@ -52,7 +52,6 @@
assignee: null, assignee: null,
description: '', description: '',
members: [], members: [],
labels: [],
location: '', location: '',
startDate: null, startDate: null,
dueDate: null dueDate: null

View File

@ -13,8 +13,9 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import type { Card, CardLabel } from '@anticrm/board' import type { Card } from '@anticrm/board'
import { getClient } from '@anticrm/presentation' import { createQuery, getClient } from '@anticrm/presentation'
import tags, { TagReference } from '@anticrm/tags'
import { Button, Icon, IconAdd } from '@anticrm/ui' import { Button, Icon, IconAdd } from '@anticrm/ui'
import { invokeAction } from '@anticrm/view-resources' import { invokeAction } from '@anticrm/view-resources'
@ -28,19 +29,16 @@
const client = getClient() const client = getClient()
let labels: CardLabel[]
let labelsHandler: (e: Event) => void let labelsHandler: (e: Event) => void
let isHovered: boolean = false let isHovered: boolean = false
$: isCompact = $commonBoardPreference?.cardLabelsCompactMode let labels: TagReference[] = []
const query = createQuery()
$: if (value.labels && value.labels.length > 0) { $: query.query(tags.class.TagReference, { attachedTo: value._id }, (result) => {
client.findAll(board.class.CardLabel, { _id: { $in: value.labels } }).then((result) => { labels = result
labels = isInline ? result.filter((l) => !l.isHidden) : result
}) })
} else {
labels = [] $: isCompact = $commonBoardPreference?.cardLabelsCompactMode
}
if (!isInline) { if (!isInline) {
getCardActions(client, { getCardActions(client, {

View File

@ -1,81 +1,39 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { Board, CardLabel } from '@anticrm/board'
import { Ref } from '@anticrm/core'
import { getClient } from '@anticrm/presentation' import { getClient } from '@anticrm/presentation'
import { import { Label, Button, EditBox, Icon, IconBack, IconCheck, IconClose, hexColorToNumber } from '@anticrm/ui'
Label,
Button,
EditBox,
Icon,
IconBack,
IconCheck,
IconClose,
LinkWaterColor,
hexColorToNumber
} from '@anticrm/ui'
import board from '../../plugin' import board from '../../plugin'
import { createCardLabel, getBoardAvailableColors } from '../../utils/BoardUtils' import { createCardLabel, getBoardAvailableColors } from '../../utils/BoardUtils'
import ColorPresenter from '../presenters/ColorPresenter.svelte' import ColorPresenter from '../presenters/ColorPresenter.svelte'
import { TagElement } from '@anticrm/tags'
export let object: CardLabel | undefined export let object: TagElement | undefined
export let boardRef: Ref<Board>
export let onBack: () => void export let onBack: () => void
let selected: { let { title, color } = object ?? {}
color?: number
isHidden?: boolean
} = { color: object?.color }
let title = object?.title
const hiddenColor = hexColorToNumber(LinkWaterColor)
const client = getClient() const client = getClient()
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const colorGroups: number[][] = getBoardAvailableColors().reduce( const colorGroups = (function chunk (colors: number[]): number[][] {
(result: number[][], currentValue: string) => { return colors.length ? [colors.slice(0, 5), ...chunk(colors.slice(5))] : []
const last = result[result.length - 1] })(getBoardAvailableColors().map(hexColorToNumber))
if (last.length >= 5) {
result.push([hexColorToNumber(currentValue)])
} else {
last.push(hexColorToNumber(currentValue))
}
return result
},
[[]]
)
function selectColor (color: number, isHidden?: boolean) {
selected = { color, isHidden }
}
async function save () { async function save () {
const { color, isHidden } = selected if (!title || !color) {
if (!color) {
return return
} }
if (object) {
if (object?._id) { await client.update(object, { title, color })
await client.update(object, {
color,
title: title ?? '',
isHidden: isHidden ?? false
})
} else { } else {
await createCardLabel(client, boardRef, color, title, isHidden) await createCardLabel(client, { title, color })
} }
onBack() onBack()
} }
async function remove () { async function remove () {
if (!object?._id) { if (!object) return
return await client.remove(object)
}
await client.removeDoc(object._class, object.space, object._id)
onBack() onBack()
} }
</script> </script>
@ -118,10 +76,16 @@
<div class="flex-col mt-1 mb-1 flex-gap-2"> <div class="flex-col mt-1 mb-1 flex-gap-2">
{#each colorGroups as colorGroup} {#each colorGroups as colorGroup}
<div class="flex-row-stretch flex-gap-2"> <div class="flex-row-stretch flex-gap-2">
{#each colorGroup as color} {#each colorGroup as c}
<div class="w-14"> <div class="w-14">
<ColorPresenter value={color} size="large" on:click={() => selectColor(color)}> <ColorPresenter
{#if selected.color === color} value={c}
size="large"
on:click={() => {
color = c
}}
>
{#if c === color}
<div class="flex-center flex-grow fs-title h-full"> <div class="flex-center flex-grow fs-title h-full">
<Icon icon={IconCheck} size="small" /> <Icon icon={IconCheck} size="small" />
</div> </div>
@ -131,27 +95,12 @@
{/each} {/each}
</div> </div>
{/each} {/each}
<div class="flex-row-stretch flex-gap-2">
<div class="w-14">
<ColorPresenter value={hiddenColor} size="large" on:click={() => selectColor(hiddenColor, true)}>
{#if selected.isHidden}
<div class="flex-center flex-grow fs-title h-full">
<Icon icon={IconCheck} size="small" />
</div>
{/if}
</ColorPresenter>
</div>
<div class="flex-col text-md">
<div class="fs-bold"><Label label={board.string.NoColor} /></div>
<div><Label label={board.string.NoColorInfo} /></div>
</div>
</div>
</div> </div>
</div> </div>
<div class="ap-footer"> <div class="ap-footer">
{#if object?._id} {#if object}
<Button size="small" kind="dangerous" label={board.string.Delete} on:click={remove} /> <Button size="small" kind="dangerous" label={board.string.Delete} on:click={remove} />
{/if} {/if}
<Button label={board.string.Save} size="small" kind="primary" on:click={save} disabled={!selected.color} /> <Button label={board.string.Save} size="small" kind="primary" on:click={save} disabled={!color || !title} />
</div> </div>
</div> </div>

View File

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Card, CardLabel } from '@anticrm/board' import { Card } from '@anticrm/board'
import type { Ref } from '@anticrm/core' import type { Ref } from '@anticrm/core'
import { getClient } from '@anticrm/presentation' import tags, { TagElement, TagReference } from '@anticrm/tags'
import { createQuery, getClient } from '@anticrm/presentation'
import { import {
Button, Button,
EditBox, EditBox,
@ -16,64 +17,44 @@
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import board from '../../plugin' import board from '../../plugin'
import { getBoardLabels } from '../../utils/BoardUtils' import { addCardLabel } from '../../utils/BoardUtils'
export let object: Card export let object: Card
export let search: string | undefined = undefined export let search: string = ''
export let onEdit: (label: CardLabel) => void export let onEdit: (label: TagElement) => void
export let onCreate: () => void export let onCreate: () => void
const client = getClient() const client = getClient()
let boardCardLabels: CardLabel[] = [] let hovered: Ref<TagElement> | undefined = undefined
let filteredLabels: CardLabel[] = []
let hovered: Ref<CardLabel> | undefined = undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
function applySearch () { let labels: TagElement[] = []
if (!search || search.trim().length <= 0) { const labelsQuery = createQuery()
filteredLabels = boardCardLabels $: labelsQuery.query(
return tags.class.TagElement,
{ title: { $like: '%' + search + '%' }, targetClass: board.class.Card },
(result) => {
labels = result
} }
)
const text = search!.toUpperCase() let cardLabels: TagReference[] = []
filteredLabels = boardCardLabels.filter((l) => l.title?.toUpperCase().includes(text) ?? false) let cardLabelRefs: Ref<TagElement>[] = []
} const cardLabelsQuery = createQuery()
$: cardLabelsQuery.query(tags.class.TagReference, { attachedTo: object._id }, (result) => {
async function fetchBoardLabels () { cardLabels = result
if (object.space) { cardLabelRefs = result.map(({ tag }) => tag)
boardCardLabels = await getBoardLabels(client, object.space)
applySearch()
}
}
async function fetch () {
if (!object) {
return
}
object = (await client.findOne(object._class, { _id: object._id })) ?? object
}
async function toggle (label: CardLabel) {
if (!object) {
return
}
if (object?.labels?.includes(label._id)) {
await client.update(object, {
$pull: { labels: label._id as any } // TODO: fix as any
}) })
} else {
await client.update(object, {
$push: { labels: label._id }
})
}
fetch() async function toggle (label: TagElement) {
const cardLabel = cardLabels.find(({ tag }) => tag === label._id)
if (cardLabel) {
await client.remove(cardLabel)
return
}
addCardLabel(client, object, label)
} }
$: object.space && fetchBoardLabels()
</script> </script>
<div class="antiPopup w-85 pb-2"> <div class="antiPopup w-85 pb-2">
@ -95,17 +76,12 @@
<div class="ap-space bottom-divider" /> <div class="ap-space bottom-divider" />
<div class="flex-col ml-4 mt-2 mb-1 mr-2 flex-gap-1"> <div class="flex-col ml-4 mt-2 mb-1 mr-2 flex-gap-1">
<div class="p-2 mt-1 mb-1 border-bg-accent border-radius-1"> <div class="p-2 mt-1 mb-1 border-bg-accent border-radius-1">
<EditBox <EditBox bind:value={search} maxWidth="100%" placeholder={board.string.SearchLabels} />
bind:value={search}
maxWidth="100%"
placeholder={board.string.SearchLabels}
on:change={() => applySearch()}
/>
</div> </div>
<div class="text-md font-medium"> <div class="text-md font-medium">
<Label label={board.string.Labels} /> <Label label={board.string.Labels} />
</div> </div>
{#each filteredLabels as label} {#each labels as label}
<div <div
class="flex-row-stretch" class="flex-row-stretch"
on:mouseover={() => { on:mouseover={() => {
@ -127,8 +103,8 @@
style:box-shadow={hovered === label._id ? `-0.4rem 0 ${numberToRGB(label.color, 0.6)}` : ''} style:box-shadow={hovered === label._id ? `-0.4rem 0 ${numberToRGB(label.color, 0.6)}` : ''}
on:click={() => toggle(label)} on:click={() => toggle(label)}
> >
{label.title ?? ''} {label.title}
{#if object?.labels?.includes(label._id)} {#if cardLabelRefs.includes(label._id)}
<div class="absolute flex-center h-full mr-2" style:top="0" style:right="0"> <div class="absolute flex-center h-full mr-2" style:top="0" style:right="0">
<Icon icon={IconCheck} size="small" /> <Icon icon={IconCheck} size="small" />
</div> </div>

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Card, CardLabel } from '@anticrm/board' import { Card } from '@anticrm/board'
import { TagElement } from '@anticrm/tags'
import CardLabelsEditor from './CardLabelsEditor.svelte' import CardLabelsEditor from './CardLabelsEditor.svelte'
import CardLabelsPicker from './CardLabelsPicker.svelte' import CardLabelsPicker from './CardLabelsPicker.svelte'
@ -7,22 +8,17 @@
let editMode: { let editMode: {
isEdit?: boolean isEdit?: boolean
object?: CardLabel object?: TagElement
} = {} } = {}
let search: string | undefined = undefined let search: string | undefined = undefined
function setEditMode (isEdit: boolean, object?: CardLabel) { function setEditMode (isEdit: boolean, object?: TagElement) {
editMode = { isEdit, object } editMode = { isEdit, object }
} }
</script> </script>
{#if editMode.isEdit} {#if editMode.isEdit}
<CardLabelsEditor <CardLabelsEditor on:close object={editMode.object} onBack={() => setEditMode(false, undefined)} />
on:close
boardRef={value.space}
object={editMode.object}
onBack={() => setEditMode(false, undefined)}
/>
{:else} {:else}
<CardLabelsPicker <CardLabelsPicker
bind:search bind:search

View File

@ -12,7 +12,6 @@
import RankSelect from '../selectors/RankSelect.svelte' import RankSelect from '../selectors/RankSelect.svelte'
import { generateId, AttachedData } from '@anticrm/core' import { generateId, AttachedData } from '@anticrm/core'
import task from '@anticrm/task' import task from '@anticrm/task'
import { createMissingLabels } from '../../utils/BoardUtils'
export let value: Card export let value: Card
const client = getClient() const client = getClient()
@ -39,9 +38,6 @@
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true) const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const labels =
value.space !== selected.space ? await createMissingLabels(client, value, selected.space) : value.labels
const copy: AttachedData<Card> = { const copy: AttachedData<Card> = {
state: selected.state, state: selected.state,
doneState: null, doneState: null,
@ -52,7 +48,7 @@
description: '', description: '',
members: [], members: [],
location: '', location: '',
labels: labels ?? [], labels: value.labels,
startDate: value.startDate, startDate: value.startDate,
dueDate: value.dueDate dueDate: value.dueDate
} }

View File

@ -10,7 +10,6 @@
import SpaceSelect from '../selectors/SpaceSelect.svelte' import SpaceSelect from '../selectors/SpaceSelect.svelte'
import StateSelect from '../selectors/StateSelect.svelte' import StateSelect from '../selectors/StateSelect.svelte'
import RankSelect from '../selectors/RankSelect.svelte' import RankSelect from '../selectors/RankSelect.svelte'
import { createMissingLabels } from '../../utils/BoardUtils'
export let value: Card export let value: Card
@ -28,7 +27,6 @@
const update: DocumentUpdate<Card> = {} const update: DocumentUpdate<Card> = {}
if (selected.space !== value.space) { if (selected.space !== value.space) {
update.labels = await createMissingLabels(client, value, selected.space)
update.space = selected.space update.space = selected.space
} }

View File

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { CardLabel } from '@anticrm/board' import { TagReference } from '@anticrm/tags'
import ColorPresenter from './ColorPresenter.svelte' import ColorPresenter from './ColorPresenter.svelte'
export let value: CardLabel export let value: TagReference
export let isHovered: boolean = false export let isHovered: boolean = false
export let size: 'tiny' | 'x-small' | 'small' | 'medium' | 'large' = 'medium' export let size: 'tiny' | 'x-small' | 'small' | 'medium' | 'large' = 'medium'
</script> </script>
@ -10,7 +10,7 @@
{#if value} {#if value}
<ColorPresenter value={value.color} {isHovered} {size} on:click> <ColorPresenter value={value.color} {isHovered} {size} on:click>
{#if size !== 'tiny'} {#if size !== 'tiny'}
<div class="flex-center h-full w-full fs-title text-sm pr-1 pl-1">{value.title ?? ''}</div> <div class="flex-center h-full w-full fs-title text-sm pr-1 pl-1">{value.title}</div>
{/if} {/if}
</ColorPresenter> </ColorPresenter>
{/if} {/if}

View File

@ -1,12 +1,12 @@
import { readable } from 'svelte/store' import { readable } from 'svelte/store'
import board, { Board, CardLabel, Card, CommonBoardPreference } from '@anticrm/board' import board, { Board, Card, CommonBoardPreference } from '@anticrm/board'
import core, { Ref, TxOperations, Space } from '@anticrm/core' import core, { Ref, TxOperations } from '@anticrm/core'
import type { KanbanTemplate, TodoItem } from '@anticrm/task' import type { KanbanTemplate, TodoItem } from '@anticrm/task'
import preference from '@anticrm/preference' import preference from '@anticrm/preference'
import tags, { TagElement } from '@anticrm/tags'
import { createKanban } from '@anticrm/task' import { createKanban } from '@anticrm/task'
import { createQuery, getClient } from '@anticrm/presentation' import { createQuery, getClient } from '@anticrm/presentation'
import { import {
hexColorToNumber,
FernColor, FernColor,
FlamingoColor, FlamingoColor,
MalibuColor, MalibuColor,
@ -34,14 +34,10 @@ export async function createBoard (
members: [] members: []
}) })
await Promise.all([createBoardLabels(client, boardRef), createKanban(client, boardRef, templateId)]) await Promise.all([createKanban(client, boardRef, templateId)])
return boardRef return boardRef
} }
export async function getBoardLabels (client: TxOperations, boardRef: Ref<Board>): Promise<CardLabel[]> {
return await client.findAll(board.class.CardLabel, { attachedTo: boardRef })
}
export function getBoardAvailableColors (): string[] { export function getBoardAvailableColors (): string[] {
return [ return [
FernColor, FernColor,
@ -57,71 +53,26 @@ export function getBoardAvailableColors (): string[] {
] ]
} }
export async function createBoardLabels (client: TxOperations, boardRef: Ref<Board>): Promise<void> {
await Promise.all([
createCardLabel(client, boardRef, hexColorToNumber(FernColor)),
createCardLabel(client, boardRef, hexColorToNumber(SeaBuckthornColor)),
createCardLabel(client, boardRef, hexColorToNumber(FlamingoColor)),
createCardLabel(client, boardRef, hexColorToNumber(MalibuColor)),
createCardLabel(client, boardRef, hexColorToNumber(MoodyBlueColor))
])
}
export async function createCardLabel ( export async function createCardLabel (
client: TxOperations, client: TxOperations,
boardRef: Ref<Board>, { title, color }: { title: string, color: number }
color: number,
title?: string,
isHidden?: boolean
): Promise<void> { ): Promise<void> {
await client.createDoc(board.class.CardLabel, core.space.Model, { await client.createDoc(tags.class.TagElement, tags.space.Tags, {
attachedTo: boardRef, title,
attachedToClass: board.class.Board,
collection: 'labels',
color, color,
title: title ?? '', targetClass: board.class.Card,
isHidden: isHidden ?? false description: '',
category: board.category.Other
}) })
} }
const isEqualLabel = (l1: CardLabel, l2: CardLabel): boolean => export async function addCardLabel (client: TxOperations, card: Card, label: TagElement): Promise<void> {
l1.title === l2.title && l1.color === l2.color && (l1.isHidden ?? false) === (l2.isHidden ?? false) const { title, color, _id: tag } = label
await client.addCollection(tags.class.TagReference, card.space, card._id, card._class, 'labels', {
export async function createMissingLabels ( title,
client: TxOperations, color,
object: Card, tag
targetBoard: Ref<Space>
): Promise<Array<Ref<CardLabel>> | undefined> {
const sourceBoardLabels = await getBoardLabels(client, object.space)
const targetBoardLabels = await getBoardLabels(client, targetBoard)
const missingLabels = sourceBoardLabels.filter((srcLabel) => {
if (!object.labels?.includes(srcLabel._id)) return false
return targetBoardLabels.findIndex((targetLabel) => isEqualLabel(targetLabel, srcLabel)) === -1
}) })
await Promise.all(
missingLabels.map(async (l) => await createCardLabel(client, targetBoard, l.color, l.title, l.isHidden))
)
const updatedTargetBoardLabels = await getBoardLabels(client, targetBoard)
const labelsUpdate = object.labels
?.map((srcLabelId) => {
const srcLabel = sourceBoardLabels.find((l) => l._id === srcLabelId)
if (srcLabel === undefined) return null
const targetLabel = updatedTargetBoardLabels.find((l) => isEqualLabel(l, srcLabel))
if (targetLabel === undefined) return null
return targetLabel._id
})
.filter((l) => l !== null) as Array<Ref<CardLabel>> | undefined
return labelsUpdate
} }
export function getDateIcon (item: TodoItem): 'normal' | 'warning' | 'overdue' { export function getDateIcon (item: TodoItem): 'normal' | 'warning' | 'overdue' {

View File

@ -37,7 +37,6 @@ export async function createCard (
rank: calcRank(lastOne, undefined), rank: calcRank(lastOne, undefined),
assignee: null, assignee: null,
description: '', description: '',
labels: [],
...attribues ...attribues
} }

View File

@ -32,6 +32,7 @@
"@anticrm/view": "~0.6.0", "@anticrm/view": "~0.6.0",
"@anticrm/task": "~0.6.0", "@anticrm/task": "~0.6.0",
"@anticrm/ui": "~0.6.0", "@anticrm/ui": "~0.6.0",
"@anticrm/preference": "~0.6.0" "@anticrm/preference": "~0.6.0",
"@anticrm/tags": "~0.6.2"
} }
} }

View File

@ -15,13 +15,14 @@
// //
import { Employee } from '@anticrm/contact' import { Employee } from '@anticrm/contact'
import type { AttachedDoc, Class, Doc, Markup, Ref, Type } from '@anticrm/core' import type { Class, Doc, Markup, Ref, Type } from '@anticrm/core'
import type { Asset, IntlString, Plugin } from '@anticrm/platform' import type { Asset, IntlString, Plugin } from '@anticrm/platform'
import { plugin } from '@anticrm/platform' import { plugin } from '@anticrm/platform'
import type { Preference } from '@anticrm/preference' import type { Preference } from '@anticrm/preference'
import type { DoneState, KanbanTemplateSpace, SpaceWithStates, Task } from '@anticrm/task' import type { DoneState, KanbanTemplateSpace, SpaceWithStates, Task } from '@anticrm/task'
import type { AnyComponent } from '@anticrm/ui' import type { AnyComponent } from '@anticrm/ui'
import { Action, ActionCategory } from '@anticrm/view' import { Action, ActionCategory } from '@anticrm/view'
import { TagCategory } from '@anticrm/tags'
/** /**
* @public * @public
@ -40,15 +41,6 @@ export interface BoardView extends SpaceWithStates {
boards: Ref<Board>[] boards: Ref<Board>[]
} }
/**
* @public
*/
export interface CardLabel extends AttachedDoc {
title: string
color: number
isHidden?: boolean
}
/** /**
* @public * @public
*/ */
@ -68,8 +60,6 @@ export interface Card extends Task {
members?: Ref<Employee>[] members?: Ref<Employee>[]
labels: Ref<CardLabel>[]
location?: string location?: string
cover?: CardCover | null cover?: CardCover | null
@ -108,13 +98,13 @@ const boards = plugin(boardId, {
class: { class: {
Board: '' as Ref<Class<Board>>, Board: '' as Ref<Class<Board>>,
Card: '' as Ref<Class<Card>>, Card: '' as Ref<Class<Card>>,
CardLabel: '' as Ref<Class<CardLabel>>,
MenuPage: '' as Ref<Class<MenuPage>>, MenuPage: '' as Ref<Class<MenuPage>>,
CommonBoardPreference: '' as Ref<Class<CommonBoardPreference>>, CommonBoardPreference: '' as Ref<Class<CommonBoardPreference>>,
CardCover: '' as Ref<Class<Type<CardCover>>> CardCover: '' as Ref<Class<Type<CardCover>>>
}, },
category: { category: {
Card: '' as Ref<ActionCategory> Card: '' as Ref<ActionCategory>,
Other: '' as Ref<TagCategory>
}, },
state: { state: {
Completed: '' as Ref<DoneState> Completed: '' as Ref<DoneState>

View File

@ -84,6 +84,7 @@ export interface Task extends AttachedDoc, DocWithRank {
dueDate: Timestamp | null dueDate: Timestamp | null
startDate: Timestamp | null startDate: Timestamp | null
todoItems?: number todoItems?: number
labels?: number
} }
/** /**
@ -116,7 +117,6 @@ export interface Issue extends Task {
comments?: number comments?: number
attachments?: number attachments?: number
labels?: string
} }
/** /**

View File

@ -13,48 +13,5 @@
// limitations under the License. // limitations under the License.
// //
import type { Tx, TxCreateDoc, TxRemoveDoc } from '@anticrm/core'
import type { TriggerControl } from '@anticrm/server-core'
import type { Card, CardLabel } from '@anticrm/board'
import board from '@anticrm/board'
import core, { TxProcessor } from '@anticrm/core'
/**
* @public
*/
export async function OnLabelDelete (tx: Tx, { findAll, hierarchy, txFactory }: TriggerControl): Promise<Tx[]> {
if (tx._class !== core.class.TxRemoveDoc) {
return []
}
const rmTx = tx as TxRemoveDoc<CardLabel>
if (!hierarchy.isDerived(rmTx.objectClass, board.class.CardLabel)) {
return []
}
const createTx = (
await findAll(
core.class.TxCreateDoc,
{
objectId: rmTx.objectId
},
{ limit: 1 }
)
)[0]
if (createTx === undefined) {
return []
}
const label = TxProcessor.createDoc2Doc(createTx as TxCreateDoc<CardLabel>)
const cards = await findAll<Card>(board.class.Card, { space: label.attachedTo as any, labels: label._id })
return cards.map((card) =>
txFactory.createTxUpdateDoc<Card>(card._class, card.space, card._id, { $pull: { labels: label._id as any } })
)
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({ export default async () => ({})
trigger: {
OnLabelDelete
}
})

View File

@ -13,9 +13,8 @@
// limitations under the License. // limitations under the License.
// //
import type { Resource, Plugin } from '@anticrm/platform' import type { Plugin } from '@anticrm/platform'
import { plugin } from '@anticrm/platform' import { plugin } from '@anticrm/platform'
import type { TriggerFunc } from '@anticrm/server-core'
/** /**
* @public * @public
@ -25,8 +24,4 @@ export const serverBoardId = 'server-board' as Plugin
/** /**
* @public * @public
*/ */
export default plugin(serverBoardId, { export default plugin(serverBoardId, {})
trigger: {
OnLabelDelete: '' as Resource<TriggerFunc>
}
})