Board: Add create / edit card label popup

Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
Anna No 2022-04-18 23:04:54 +07:00
parent 6552eec72d
commit 34220fd2ad
No known key found for this signature in database
GPG Key ID: 08C11FFC23177C87
17 changed files with 385 additions and 51 deletions

View File

@ -16,7 +16,7 @@
// To help typescript locate view plugin properly
import type { Board, Card, CardAction, CardDate, CardLabel } from '@anticrm/board'
import type { Employee } from '@anticrm/contact'
import { TxOperations as Client, Doc, DOMAIN_MODEL, FindOptions, IndexKind, Ref } from '@anticrm/core'
import { TxOperations as Client, Doc, DOMAIN_MODEL, FindOptions, IndexKind, Ref, Type, Timestamp } from '@anticrm/core'
import {
Builder,
Collection,
@ -32,7 +32,7 @@ import {
import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
import contact from '@anticrm/model-contact'
import core, { TAttachedDoc, TDoc } from '@anticrm/model-core'
import core, { TAttachedDoc, TDoc, TObj } from '@anticrm/model-core'
import task, { TSpaceWithStates, TTask } from '@anticrm/model-task'
import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
@ -40,6 +40,13 @@ import { Asset, IntlString, Resource } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
import board from './plugin'
/**
* @public
*/
export function TypeCardDate (): Type<CardDate> {
return { _class: board.class.CardDate, label: board.string.Dates }
}
@Model(board.class.Board, task.class.SpaceWithStates)
@UX(board.string.Board, board.icon.Board)
export class TBoard extends TSpaceWithStates implements Board {
@ -47,11 +54,20 @@ export class TBoard extends TSpaceWithStates implements Board {
background!: string
}
@Model(board.class.CardDate, core.class.Obj, DOMAIN_MODEL)
@UX(board.string.Dates)
export class TCardDate extends TObj implements CardDate {
dueDate?: Timestamp
isChecked?: boolean
startDate?: Timestamp
}
@Model(board.class.CardLabel, core.class.AttachedDoc, DOMAIN_MODEL)
@UX(board.string.Labels)
export class TCardLabel extends TAttachedDoc implements CardLabel {
title!: string;
color!: number;
title!: string
color!: number
isHidden?: boolean
}
@Model(board.class.Card, task.class.Task)
@ -64,6 +80,7 @@ export class TCard extends TTask implements Card {
@Prop(TypeBoolean(), board.string.IsArchived)
isArchived?: boolean
@Prop(TypeCardDate(), board.string.Dates)
date?: CardDate
@Prop(TypeMarkup(), board.string.Description)
@ -105,7 +122,7 @@ export class TCardAction extends TDoc implements CardAction {
}
export function createModel (builder: Builder): void {
builder.createModel(TBoard, TCard, TCardLabel, TCardAction)
builder.createModel(TBoard, TCard, TCardLabel, TCardDate, TCardAction)
builder.mixin(board.class.Board, core.class.Class, workbench.mixin.SpaceView, {
view: {
@ -206,6 +223,14 @@ export function createModel (builder: Builder): void {
presenter: board.component.CardPresenter
})
builder.mixin(board.class.CardLabel, core.class.Class, view.mixin.AttributePresenter, {
presenter: board.component.CardLabelPresenter
})
builder.mixin(board.class.CardDate, core.class.Class, view.mixin.AttributePresenter, {
presenter: board.component.CardDatePresenter
})
builder.mixin(board.class.Board, core.class.Class, view.mixin.AttributePresenter, {
presenter: board.component.BoardPresenter
})

View File

@ -28,6 +28,9 @@ export default mergeIds(boardId, board, {
CreateCard: '' as AnyComponent,
KanbanCard: '' as AnyComponent,
CardPresenter: '' as AnyComponent,
CardLabelPresenter: '' as AnyComponent,
CardDatePresenter: '' as AnyComponent,
BoardPresenter: '' as AnyComponent,
TemplatesIcon: '' as AnyComponent,
Cards: '' as AnyComponent,
KanbanView: '' as AnyComponent

View File

@ -174,8 +174,9 @@ p:last-child { margin-block-end: 0; }
.justify-center { justify-content: center; }
.items-baseline { align-items: baseline; }
.flex-gap-3 {gap: .75rem;}
.flex-gap-1 {gap: .25rem;}
.flex-gap-3 { gap: .75rem; }
.flex-gap-2 { gap: .5rem; }
.flex-gap-1 { gap: .25rem; }
.flex-presenter, .inline-presenter {
flex-wrap: nowrap;
@ -325,6 +326,7 @@ p:last-child { margin-block-end: 0; }
.mx-3 { margin: 0 .75rem; }
.my-4 { margin: 1rem 0; }
.pl-1 { padding-left: .25rem; }
.pl-2 { padding-left: .5rem; }
.pl-4 { padding-left: 1rem; }
.pl-8 { padding-left: 2rem; }
@ -383,6 +385,8 @@ p:last-child { margin-block-end: 0; }
.h-full { height: 100%; }
.h-2 { height: .5rem; }
.h-6 { height: 1.5rem; }
.h-7 { height: 1.75rem; }
.h-8 { height: 2rem; }
.h-9 { height: 2.25rem; }
.h-16 { height: 4rem; }
@ -390,8 +394,10 @@ p:last-child { margin-block-end: 0; }
.w-full { width: 100%; }
.w-2 { width: .5rem; }
.w-9 { width: 2.25rem; }
.w-85 {width: 21.25rem; }
.w-165 {width: 41.25rem; }
.w-14 { width: 3.5rem; }
.w-16 { width: 4rem; }
.w-85 { width: 21.25rem; }
.w-165 { width: 41.25rem; }
.min-w-0 { min-width: 0; }
.min-w-4 { min-width: 1rem; }
.min-w-9 { min-width: 2.25rem; }

View File

@ -1,6 +1,6 @@
{
"string": {
"EditBoxPlaceholder": "placeholder",
"EditBoxPlaceholder": "Type text...",
"Ok": "Ok",
"Cancel": "Cancel",
"Minutes": "{minutes, plural, =0 {less than a minute ago} =1 {a minute ago} other {# minutes ago}}",

View File

@ -12,6 +12,7 @@ export const ChetwodeBlueColor = '#6F7BC5' // dark blue
export const SalmonColor = '#F28469' // salmon
export const SeaBuckthornColor = '#F2994A' // orange (warning)
export const FlamingoColor = '#EB5757' // red (error)
export const LinkWaterColor = '#C9CBCD'
const blackColors = Object.freeze([
FeijoaColor,

View File

@ -1,5 +1,6 @@
{
"string": {
"Name": "Name",
"CreateBoard": "Create board",
"CreateCard": "Create card",
"CardName": "Card name",
@ -32,6 +33,9 @@
"Labels": "Labels",
"CreateLabel": "Create a new label",
"SearchLabels": "Search labels...",
"SelectColor": "Select a color",
"NoColor": "No color.",
"NoColorInfo": "This won't show up on the front of cards.",
"Checklist": "Checklist",
"Dates": "Dates",
"Attachments": "Attachments",

View File

@ -1,5 +1,6 @@
{
"string": {
"Name": "Название",
"CreateBoard": "Создать",
"CreateCard": "Создать",
"CardName": "Название",
@ -32,6 +33,9 @@
"Labels": "Метки",
"CreateLabel": "Создать",
"SearchLabels": "Поиск...",
"SelectColor": "Выберите цвет",
"NoColor": "Без цвета.",
"NoColorInfo": "Не будет показываться на доске.",
"Checklist": "Списки",
"Dates": "Дата",
"Attachments": "Прикрепленное",

View File

@ -0,0 +1,157 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Board, CardLabel } from '@anticrm/board'
import { Ref } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import {
Label,
Button,
EditBox,
Icon,
IconBack,
IconCheck,
IconClose,
LinkWaterColor,
hexColorToNumber
} from '@anticrm/ui'
import board from '../../plugin'
import { createCardLabel, getBoardAvailableColors } from '../../utils/BoardUtils'
import ColorPresenter from '../presenters/ColorPresenter.svelte'
export let object: CardLabel | undefined
export let boardRef: Ref<Board>
export let onBack: () => void
let selected: {
color?: number
isHidden?: boolean
} = { color: object?.color }
let title = object?.title
const hiddenColor = hexColorToNumber(LinkWaterColor)
const client = getClient()
const dispatch = createEventDispatcher()
const colorGroups: number[][] = getBoardAvailableColors().reduce(
(result: number[][], currentValue: string) => {
let last = result[result.length - 1]
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 () {
const {color, isHidden} = selected
if (!color) {
return
}
if (object?._id) {
await client.update(object, {
color,
title: title ?? '',
isHidden: isHidden ?? false,
})
} else {
await createCardLabel(client, boardRef, color, title, isHidden)
}
onBack()
}
async function remove () {
if (!object?._id) {
return
}
await client.removeDoc(object._class, object.space, object._id)
onBack()
}
</script>
<div class="antiPopup w-85">
<div class="relative fs-title flex-center h-9">
<div class="absolute flex-center ml-2 h-full" style:top="0" style:left="0">
<Button icon={IconBack} kind="transparent" size="small" on:click={onBack} />
</div>
<div>
<Label label={board.string.Labels} />
</div>
<div class="absolute flex-center mr-2 h-full" style:top="0" style:right="0">
<Button
icon={IconClose}
kind="transparent"
size="small"
on:click={() => {
dispatch('close')
}} />
</div>
</div>
<div class="ap-space bottom-divider" />
<div class="flex-col ml-4 mt-4 mr-4 flex-gap-1">
<div class="text-md font-medium">
<Label label={board.string.Name} />
</div>
<div class="p-2 mt-1 mb-1 border-bg-accent border-radius-1">
<EditBox bind:value={title} maxWidth="100%" focus={true} />
</div>
</div>
<div class="flex-col ml-4 mt-4 mb-4 mr-4 flex-gap-1">
<div class="text-md font-medium">
<Label label={board.string.SelectColor} />
</div>
<div class="flex-col mt-1 mb-1 flex-gap-2">
{#each colorGroups as colorGroup}
<div class="flex-row-stretch flex-gap-2">
{#each colorGroup as color}
<div class="w-14">
<ColorPresenter value={color} size="large" on:click={() => selectColor(color)}>
{#if selected.color === color}
<div class="flex-center flex-grow fs-title h-full">
<Icon icon={IconCheck} size="small" />
</div>
{/if}
</ColorPresenter>
</div>
{/each}
</div>
{/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 class="ap-footer">
{#if object?._id}
<Button size="small" kind="dangerous" label={board.string.Delete} on:click={remove} />
{/if}
<Button label={board.string.Save} size="small" kind="primary" on:click={save} disabled={!selected.color} />
</div>
</div>

View File

@ -2,20 +2,35 @@
import { Card, CardLabel } from '@anticrm/board'
import { Ref } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Button, EditBox, Icon, IconEdit, IconCheck, Label, numberToHexColor, numberToRGB } from '@anticrm/ui'
import {
Button,
EditBox,
Icon,
IconEdit,
IconCheck,
IconClose,
Label,
numberToHexColor,
numberToRGB
} from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import board from '../../plugin'
import { getBoardLabels } from '../../utils/BoardUtils'
import { updateCard } from '../../utils/CardUtils'
export let object: Card
export let search: string | undefined = undefined
export let onEdit: (label: CardLabel) => void
export let onCreate: () => void
const client = getClient()
let search: string | undefined = undefined
let labels = object.labels ?? []
let boardCardLabels: CardLabel[] = []
let filteredLabels: CardLabel[] = []
let hovered: Ref<CardLabel> | undefined = undefined
const dispatch = createEventDispatcher()
function applySearch() {
if (!search || search.trim().length <= 0) {
@ -27,14 +42,14 @@
filteredLabels = boardCardLabels.filter((l) => l.title?.toUpperCase().includes(text) ?? false)
}
async function fetchBoardLabels() {
async function fetchBoardLabels () {
if (object.space) {
boardCardLabels = await getBoardLabels(client, object.space)
applySearch()
}
}
function toggle(label: CardLabel) {
function toggle (label: CardLabel) {
if (!object) {
return
}
@ -52,13 +67,21 @@
</script>
<div class="antiPopup antiPopup-withHeader antiPopup-withTitle w-85 pb-2">
<div class="ap-space" />
<div class="fs-title ap-header flex-row-center">
<div class="antiPopup w-85 pb-2">
<div class="relative fs-title flex-center h-9">
<Label label={board.string.Labels} />
<div class="absolute flex-center mr-2 h-full" style:top="0" style:right="0">
<Button
icon={IconClose}
kind="transparent"
size="small"
on:click={() => {
dispatch('close')
}} />
</div>
</div>
<div class="ap-space bottom-divider" />
<div class="flex-col ml-4 mt-1 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">
<EditBox
bind:value={search}
@ -87,7 +110,7 @@
<div
class="relative flex-row-center justify-center border-radius-1 fs-title w-full h-8 mr-2"
style:background-color={numberToHexColor(label.color)}
style:box-shadow={hovered === label._id ? `-0.5rem 0 ${numberToRGB(label.color, 0.6)}` : ''}
style:box-shadow={hovered === label._id ? `-0.4rem 0 ${numberToRGB(label.color, 0.6)}` : ''}
on:click={() => toggle(label)}>
{label.title ?? ''}
{#if labels.includes(label._id)}
@ -96,10 +119,10 @@
</div>
{/if}
</div>
<Button icon={IconEdit} kind="transparent" />
<Button icon={IconEdit} kind="transparent" on:click={() => onEdit(label)} />
</div>
{/each}
<div class="mt-3" />
<Button label={board.string.CreateLabel} kind="no-border" />
<Button label={board.string.CreateLabel} kind="no-border" on:click={() => onCreate()} />
</div>
</div>

View File

@ -0,0 +1,40 @@
<script lang="ts">
import { Card, CardLabel } from '@anticrm/board'
import { createEventDispatcher } from 'svelte'
import CardLabelsEditor from './CardLabelsEditor.svelte'
import CardLabelsPicker from './CardLabelsPicker.svelte'
export let object: Card
let editMode: {
isEdit?: boolean
object?: CardLabel
} = {}
let search: string | undefined = undefined
const dispatch = createEventDispatcher()
function setEditMode(isEdit: boolean, object?: CardLabel) {
editMode = { isEdit, object }
}
function close() {
dispatch('close')
}
</script>
{#if editMode.isEdit}
<CardLabelsEditor
on:close
boardRef={object.space}
object={editMode.object}
onBack={() => setEditMode(false, undefined)} />
{:else}
<CardLabelsPicker
bind:search
on:close
{object}
onCreate={() => setEditMode(true, undefined)}
onEdit={(o) => setEditMode(true, o)} />
{/if}

View File

@ -0,0 +1,33 @@
<script lang="ts">
import { numberToHexColor, numberToRGB } from '@anticrm/ui'
export let value: number
export let size: 'small' | 'medium' | 'large' = 'medium'
let isHovered = false
</script>
{#if value}
<div
class="border-radius-1 min-w-9"
class:h-8={size === 'large'}
class:h-7={size === 'medium'}
class:h-6={size === 'small'}
style:background-color={isHovered ? numberToRGB(value, 0.6) : numberToHexColor(value)}
on:click
on:mouseover={() => {
isHovered = true
}}
on:focus={() => {
isHovered = true
}}
on:mouseout={() => {
isHovered = false
}}
on:blur={() => {
isHovered = false
}}>
<slot />
</div>
{/if}

View File

@ -6,18 +6,19 @@
export let value: Card
const client = getClient()
const {date} = value
const { date } = value
let isChecked = date?.isChecked
function update(){
function update() {
if (isChecked === undefined) return
client.update(value, {date: {...date, isChecked}})
client.update(value, { date: { ...date, isChecked } })
}
</script>
{#if date}
<div class="flex-presenter flex-gap-1 h-full">
<CheckBox bind:checked={isChecked} on:value={update}/>
<CheckBox bind:checked={isChecked} on:value={update} />
<div class="flex-center h-full" on:click>
<div class="flex-row-center background-button-bg-color border-radius-1 w-full">
{#if date.startDate}

View File

@ -1,18 +1,14 @@
<script lang="ts">
import { Button, numberToHexColor } from '@anticrm/ui'
import type { CardLabel } from '@anticrm/board'
import ColorPresenter from './ColorPresenter.svelte'
export let value: CardLabel
export let size: 'large' | 'medium'
const background = numberToHexColor(value.color)
export let size: 'small' | 'medium'
</script>
{#if value}
<div style:background class="border-radius-1">
<Button {size} kind="transparent" on:click>
<div class="flex-center h-full w-full min-w-4" slot="content">{value.title ?? ''}</div>
</Button>
</div>
<ColorPresenter value={value.color} {size} on:click>
<div class="flex-center h-full w-full fs-title text-sm pr-1 pl-1">{value.title ?? ''}</div>
</ColorPresenter>
{/if}

View File

@ -25,9 +25,11 @@ import EditCard from './components/EditCard.svelte'
import KanbanCard from './components/KanbanCard.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import KanbanView from './components/KanbanView.svelte'
import CardLabelsPicker from './components/popups/CardLabelsPicker.svelte'
import CardLabelsPopup from './components/popups/CardLabelsPopup.svelte'
import MoveView from './components/popups/MoveCard.svelte'
import DateRangePicker from './components/popups/DateRangePicker.svelte'
import CardLabelPresenter from './components/presenters/LabelPresenter.svelte'
import CardDatePresenter from './components/presenters/DatePresenter.svelte'
import { addCurrentUser, canAddCurrentUser, isArchived, isUnarchived } from './utils/CardUtils'
async function showMoveCardPopup (object: Card): Promise<void> {
@ -39,7 +41,7 @@ async function showDatePickerPopup (object: Card): Promise<void> {
}
async function showCardLabelsPopup (object: Card): Promise<void> {
showPopup(CardLabelsPicker, { object })
showPopup(CardLabelsPopup, { object })
}
export default async (): Promise<Resources> => ({
@ -49,6 +51,8 @@ export default async (): Promise<Resources> => ({
EditCard,
KanbanCard,
CardPresenter,
CardDatePresenter,
CardLabelPresenter,
TemplatesIcon,
KanbanView,
BoardPresenter

View File

@ -19,6 +19,7 @@ import type { AnyComponent } from '@anticrm/ui'
export default mergeIds(boardId, board, {
string: {
Name: '' as IntlString,
BoardName: '' as IntlString,
MakePrivate: '' as IntlString,
MakePrivateDescription: '' as IntlString,
@ -53,6 +54,9 @@ export default mergeIds(boardId, board, {
Labels: '' as IntlString,
CreateLabel: '' as IntlString,
SearchLabels: '' as IntlString,
SelectColor: '' as IntlString,
NoColor: '' as IntlString,
NoColorInfo: '' as IntlString,
Checklist: '' as IntlString,
Dates: '' as IntlString,
Attachments: '' as IntlString,
@ -96,9 +100,6 @@ export default mergeIds(boardId, board, {
NullDate: '' as IntlString
},
component: {
CreateCustomer: '' as AnyComponent,
CardsPresenter: '' as AnyComponent,
BoardPresenter: '' as AnyComponent,
Boards: '' as AnyComponent,
EditCard: '' as AnyComponent,
Members: '' as AnyComponent,

View File

@ -2,7 +2,19 @@ import core, { Ref, TxOperations } from '@anticrm/core'
import board, { Board, CardLabel } from '@anticrm/board'
import type { KanbanTemplate } from '@anticrm/task'
import { createKanban } from '@anticrm/task'
import { hexColorToNumber, FernColor, FlamingoColor, MalibuColor, MoodyBlueColor, SeaBuckthornColor } from '@anticrm/ui'
import {
hexColorToNumber,
FernColor,
FlamingoColor,
MalibuColor,
MediumTurquoiseColor,
MoodyBlueColor,
SeaBuckthornColor,
FeijoaColor,
EastSideColor,
SalmonColor,
SeagullColor
} from '@anticrm/ui'
export async function createBoard (
client: TxOperations,
@ -26,22 +38,44 @@ export async function getBoardLabels (client: TxOperations, boardRef: Ref<Board>
return await client.findAll(board.class.CardLabel, { attachedTo: boardRef })
}
export function getBoardAvailableColors (): string[] {
return [
FernColor,
SeaBuckthornColor,
FlamingoColor,
MalibuColor,
MoodyBlueColor,
FeijoaColor,
EastSideColor,
MediumTurquoiseColor,
SalmonColor,
SeagullColor
]
}
export async function createBoardLabels (client: TxOperations, boardRef: Ref<Board>): Promise<void> {
await Promise.all([
createCardLabel(client, boardRef, FernColor),
createCardLabel(client, boardRef, SeaBuckthornColor),
createCardLabel(client, boardRef, FlamingoColor),
createCardLabel(client, boardRef, MalibuColor),
createCardLabel(client, boardRef, MoodyBlueColor)
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 (client: TxOperations, boardRef: Ref<Board>, color: string, title?: string): Promise<void> {
export async function createCardLabel (
client: TxOperations,
boardRef: Ref<Board>,
color: number,
title?: string,
isHidden?: boolean
): Promise<void> {
await client.createDoc(board.class.CardLabel, core.space.Model, {
attachedTo: boardRef,
attachedToClass: board.class.Board,
collection: '',
color: hexColorToNumber(color),
title: title ?? ''
collection: 'labels',
color,
title: title ?? '',
isHidden: isHidden ?? false
})
}

View File

@ -15,7 +15,7 @@
//
import { Employee } from '@anticrm/contact'
import type { AttachedDoc, Class, TxOperations as Client, Doc, Markup, Ref, Timestamp } from '@anticrm/core'
import type { AttachedDoc, Class, TxOperations as Client, Doc, Markup, Ref, Timestamp, Obj } from '@anticrm/core'
import type { Asset, IntlString, Plugin, Resource } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@anticrm/task'
@ -44,12 +44,13 @@ export interface BoardView extends SpaceWithStates {
export interface CardLabel extends AttachedDoc {
title: string
color: number
isHidden?: boolean
}
/**
* @public
*/
export interface CardDate {
export interface CardDate extends Obj {
dueDate?: Timestamp
isChecked?: boolean
startDate?: Timestamp
@ -110,6 +111,7 @@ const boards = plugin(boardId, {
Board: '' as Ref<Class<Board>>,
Card: '' as Ref<Class<Card>>,
CardAction: '' as Ref<Class<CardAction>>,
CardDate: '' as Ref<Class<CardDate>>,
CardLabel: '' as Ref<Class<CardLabel>>
},
icon: {