Board: Add card details (members, labels, date) (#1376)

Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
Anna No
2022-04-12 14:59:36 +07:00
committed by GitHub
parent 80a1d0c7ca
commit fa2e1b3229
17 changed files with 357 additions and 161 deletions
+3 -45
View File
@@ -14,10 +14,10 @@
// limitations under the License.
//
import { boardId, Card, CardAction } from '@anticrm/board'
import { boardId } from '@anticrm/board'
import board from '@anticrm/board-resources/src/plugin'
import type { TxOperations as Client, Ref, Space } from '@anticrm/core'
import { mergeIds, Resource } from '@anticrm/platform'
import type { Ref, Space } from '@anticrm/core'
import { mergeIds } from '@anticrm/platform'
import { KanbanTemplate, Sequence } from '@anticrm/task'
import type { AnyComponent } from '@anticrm/ui'
import { ViewletDescriptor } from '@anticrm/view'
@@ -32,48 +32,6 @@ export default mergeIds(boardId, board, {
Cards: '' as AnyComponent,
KanbanView: '' as AnyComponent
},
cardAction: {
Cover: '' as Ref<CardAction>,
Join: '' as Ref<CardAction>,
Members: '' as Ref<CardAction>,
Labels: '' as Ref<CardAction>,
Checklist: '' as Ref<CardAction>,
Dates: '' as Ref<CardAction>,
Attachments: '' as Ref<CardAction>,
CustomFields: '' as Ref<CardAction>,
AddButton: '' as Ref<CardAction>,
Move: '' as Ref<CardAction>,
Copy: '' as Ref<CardAction>,
MakeTemplate: '' as Ref<CardAction>,
Watch: '' as Ref<CardAction>,
Archive: '' as Ref<CardAction>,
SendToBoard: '' as Ref<CardAction>,
Delete: '' as Ref<CardAction>
},
cardActionHandler: {
Cover: '' as Resource<(card: Card, client: Client) => void>,
Join: '' as Resource<(card: Card, client: Client) => void>,
Members: '' as Resource<(card: Card, client: Client) => void>,
Labels: '' as Resource<(card: Card, client: Client) => void>,
Checklist: '' as Resource<(card: Card, client: Client) => void>,
Dates: '' as Resource<(card: Card, client: Client) => void>,
Attachments: '' as Resource<(card: Card, client: Client) => void>,
CustomFields: '' as Resource<(card: Card, client: Client) => void>,
AddButton: '' as Resource<(card: Card, client: Client) => void>,
Move: '' as Resource<(card: Card, client: Client) => void>,
Copy: '' as Resource<(card: Card, client: Client) => void>,
MakeTemplate: '' as Resource<(card: Card, client: Client) => void>,
Watch: '' as Resource<(card: Card, client: Client) => void>,
Archive: '' as Resource<(card: Card, client: Client) => void>,
SendToBoard: '' as Resource<(card: Card, client: Client) => void>,
Delete: '' as Resource<(card: Card, client: Client) => void>
},
cardActionSupportedHandler: {
Join: '' as Resource<(card: Card, client: Client) => boolean>,
Archive: '' as Resource<(card: Card, client: Client) => boolean>,
SendToBoard: '' as Resource<(card: Card, client: Client) => boolean>,
Delete: '' as Resource<(card: Card, client: Client) => boolean>
},
space: {
DefaultBoard: '' as Ref<Space>
},
+7 -1
View File
@@ -374,6 +374,8 @@ p:last-child { margin-block-end: 0; }
.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; }
.min-h-0 { min-height: 0; }
.clear-mins {
min-width: 0;
@@ -530,6 +532,7 @@ a.no-line {
/* Backgrounds & Colors */
.background-theme-bg-color { background-color: var(--theme-bg-color); }
.background-highlight-red { background-color: var(--highlight-red); }
.background-button-bg-color { background-color: var(--button-bg-color); }
.background-button-bg-enabled { background-color: var(--theme-button-bg-enabled); }
.background-menu-divider { background-color: var(--theme-menu-divider); }
.background-card-divider { background-color: var(--theme-card-divider); }
@@ -545,7 +548,10 @@ a.no-line {
.red-color { color: var(--highlight-red); }
.border-radius-3 {border-radius: 0.75rem;}
.border-radius-4 { border-radius: 1rem; }
.border-radius-3 { border-radius: 0.75rem; }
.border-radius-2 { border-radius: 0.5rem; }
.border-radius-1 { border-radius: 0.25rem; }
.border-bg-accent {border: 1px solid var(--theme-bg-accent-color);}
.border-primary-button { border-color: var(--primary-button-border); }
.border-button-enabled { border: 1px solid var(--theme-button-border-enabled); }
+3 -1
View File
@@ -30,6 +30,7 @@
export let width: string | undefined = undefined
export let resetIconSize: boolean = false
export let focus: boolean = false
export let isCircle: boolean = false
export let input: HTMLButtonElement | undefined = undefined
@@ -47,6 +48,8 @@
bind:this={input}
class="button {kind} {size} jf-{justify}"
class:only-icon={iconOnly}
class:border-radius-1={!isCircle}
class:border-radius-4={isCircle}
disabled={disabled || loading}
style={width ? 'width: ' + width : ''}
on:click
@@ -101,7 +104,6 @@
color: var(--accent-color);
background-color: transparent;
border: 1px solid transparent;
border-radius: .25rem;
transition-property: border, background-color, color, box-shadow;
transition-duration: .15s;
@@ -18,7 +18,7 @@
import type { AnySvelteComponent } from '../types'
import Icon from './Icon.svelte'
export let icon: Asset | AnySvelteComponent
export let icon: Asset | AnySvelteComponent | undefined
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'large'
export let transparent: boolean = false
export let selected: boolean = false
@@ -27,7 +27,12 @@
<div class="flex-center icon-button icon-{size}" class:selected class:transparent class:primary on:click|stopPropagation on:mousemove>
<div class="content">
<Icon {icon} size={'full'} />
{#if $$slots.content}
<slot name="content" />
{:else if icon}
<Icon {icon} size={'full'} />
{/if}
</div>
</div>
+24 -18
View File
@@ -25,28 +25,34 @@
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let rightSection: boolean = false
export let reverseCommands = false
export let showHeader = true
const dispatch = createEventDispatcher()
</script>
<div class="antiPanel antiComponent">
<div class:panel-content={!rightSection} class:ad-section-50={rightSection} class:divide={rightSection}>
<div class="ac-header short mirror divide">
<div class="ac-header__wrap-title">
{#if icon}<div class="ac-header__icon"><Icon {icon} size={'large'} /></div>{/if}
<div class="ac-header__wrap-description">
{#if title}
<span class="ac-header__title">{title}</span>
{#if showHeader}
<div class="ac-header short mirror divide">
<div class="ac-header__wrap-title">
{#if icon}
<div class="ac-header__icon">
<Icon {icon} size={'large'} />
</div>
{/if}
{#if subtitle}<span class="ac-header__description">{subtitle}</span>{/if}
<div class="ac-header__wrap-description">
{#if title}<span class="ac-header__title">{title}</span>{/if}
{#if subtitle}<span class="ac-header__description">{subtitle}</span>{/if}
</div>
</div>
{#if rightSection}
<div class="flex">
<slot name="commands" />
</div>
{/if}
</div>
{#if rightSection}
<div class="flex">
<slot name="commands" />
</div>
{/if}
</div>
{/if}
{#if $$slots.subtitle}
<div class="ac-subtitle">
<div class="ac-subtitle-content">
@@ -54,13 +60,13 @@
</div>
</div>
{/if}
<div class='flex-col'>
<div class="flex-col">
<slot />
</div>
</div>
{#if rightSection}
<slot name='rightSection'/>
<slot name="rightSection" />
{/if}
<div class="ad-tools" class:grow-reverse={reverseCommands}>
@@ -69,15 +75,14 @@
<slot name="commands" />
</div>
{/if}
<slot name='actions'/>
<slot name="actions" />
<div class="tool">
<ActionIcon
icon={IconClose}
size={'medium'}
action={() => {
dispatch('close')
}}
/>
}} />
</div>
</div>
</div>
@@ -89,4 +94,5 @@
flex-direction: column;
align-content: stretch;
}
</style>
@@ -28,6 +28,7 @@
export let icon: 'normal' | 'warning' | 'overdue' = 'normal'
export let labelOver: IntlString | undefined = undefined // label instead of date
export let labelNull: IntlString = ui.string.NoDate
export let showIcon = true
const dispatch = createEventDispatcher()
@@ -64,9 +65,11 @@
}
}}
>
<div class="btn-icon {icon}">
<Icon icon={icon === 'overdue' ? DPCalendarOver : DPCalendar} size={'full'}/>
</div>
{#if showIcon}
<div class="btn-icon {icon}">
<Icon icon={icon === 'overdue' ? DPCalendarOver : DPCalendar} size={'full'}/>
</div>
{/if}
{#if value !== null && value !== undefined}
{#if labelOver !== undefined}
<Label label={labelOver} />
@@ -21,7 +21,6 @@
export let value: Card
export let inline: boolean = false
</script>
{#if value}
@@ -16,17 +16,20 @@
<script lang="ts">
import type { Card } from '@anticrm/board'
import { Class, Ref } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import type { State } from '@anticrm/task'
import task from '@anticrm/task'
import { StyledTextBox } from '@anticrm/text-editor'
import { Button, EditBox, Icon, IconClose, Label, Scroller } from '@anticrm/ui'
import { EditBox, Icon, Label, Panel, Scroller } from '@anticrm/ui'
import { createEventDispatcher, onMount } from 'svelte'
import board from '../plugin'
import { getCardActions } from '../utils/CardActionUtils'
import { updateCard } from '../utils/CardUtils'
import CardActions from './editor/CardActions.svelte'
import CardActivity from './editor/CardActivity.svelte'
import CardFields from './editor/CardFields.svelte'
import CardDetails from './editor/CardDetails.svelte'
export let _id: Ref<Card>
export let _class: Ref<Class<Card>>
@@ -36,6 +39,7 @@
let object: Card | undefined
let state: State | undefined
let handleMove: () => void
$: _id &&
_class &&
@@ -48,7 +52,18 @@
state = result[0]
})
function change (field: string, value: any) {
getCardActions(client, { _id: board.cardAction.Move }).then(async (result) => {
if (result[0]?.handler) {
const handler = await getResource(result[0].handler)
handleMove = () => {
if (object) {
handler(object, client)
}
}
}
})
function change(field: string, value: any) {
if (object) {
updateCard(client, object, field, value)
}
@@ -60,61 +75,66 @@
</script>
{#if object !== undefined}
<Scroller>
<div class="flex-col-stretch h-full w-165 p-6">
<!-- TODO cover -->
<div class="close-button">
<Button icon={IconClose} kind="transparent" size="large" on:click={() => dispatch('close')} />
</div>
<div class="flex-row-streach">
<div class="w-9">
<Icon icon={board.icon.Card} size="large" />
</div>
<div class="fs-title text-lg">
<EditBox bind:value={object.title} maxWidth="39rem" focus on:change={() => change('title', object?.title)} />
</div>
</div>
<div class="flex-row-streach">
<div class="w-9" />
<div>
<Label label={board.string.InList} /><span class="state-name ml-1">{state?.title}</span>
</div>
</div>
<div class="flex-row-streach">
<div class="flex-grow mr-4">
<div class="flex-row-streach">
<div class="w-9" />
<CardFields value={object} />
<Panel showHeader={false} on:close={() => dispatch('close')}>
<Scroller>
<div class="flex-col-stretch h-full w-165 p-6">
<!-- TODO cover -->
<div class="flex-row-streach">
<div class="w-9">
<Icon icon={board.icon.Card} size="large" />
</div>
<div class="flex-row-streach mt-4 mb-2">
<div class="w-9">
<Icon icon={board.icon.Card} size="large" />
</div>
<div class="fs-title">
<Label label={board.string.Description} />
</div>
<div class="fs-title text-lg">
<EditBox
bind:value={object.title}
maxWidth="39rem"
focus
on:change={() => change('title', object?.title)}
/>
</div>
<div class="flex-row-streach">
<div class="w-9" />
<div class="background-bg-accent border-bg-accent border-radius-3 p-2 w-full">
<StyledTextBox
alwaysEdit={true}
showButtons={false}
placeholder={board.string.DescriptionPlaceholder}
bind:content={object.description}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
</div>
<!-- TODO attachments-->
<!-- TODO checklists -->
<CardActivity value={object} />
</div>
<div class="flex-row-streach">
<div class="w-9" />
<div>
<Label label={board.string.InList} />
<span class="state-name ml-1" on:click={handleMove}>{state?.title}</span>
</div>
</div>
<div class="flex-row-streach">
<div class="flex-grow mr-4">
<div class="flex-row-streach">
<div class="w-9" />
<CardDetails value={object} />
</div>
<div class="flex-row-streach mt-4 mb-2">
<div class="w-9">
<Icon icon={board.icon.Card} size="large" />
</div>
<div class="fs-title">
<Label label={board.string.Description} />
</div>
</div>
<div class="flex-row-streach">
<div class="w-9" />
<div class="background-bg-accent border-bg-accent border-radius-3 p-2 w-full">
<StyledTextBox
alwaysEdit={true}
showButtons={false}
placeholder={board.string.DescriptionPlaceholder}
bind:content={object.description}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
</div>
<!-- TODO attachments-->
<!-- TODO checklists -->
<CardActivity value={object} />
</div>
<CardActions value={object} />
<CardActions value={object} />
</div>
</div>
</div>
</Scroller>
</Scroller>
</Panel>
{/if}
<style lang="scss">
@@ -26,11 +26,11 @@
export let object: WithLookup<Card>
export let dragged: boolean
function showMenu (ev?: Event): void {
function showMenu(ev?: Event): void {
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
}
function showLead () {
function showLead() {
showPanel(board.component.EditCard, object._id, object._class, 'middle')
}
</script>
@@ -0,0 +1,105 @@
<!--
// 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 { Card, CardLabel } from '@anticrm/board'
import contact, { Employee } from '@anticrm/contact'
import { getResource } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import { Button, IconAdd, Label } from '@anticrm/ui'
import board from '../../plugin'
import { getCardActions } from '../../utils/CardActionUtils'
import { hasDate } from '../../utils/CardUtils'
import DatePresenter from '../presenters/DatePresenter.svelte'
import LabelPresenter from '../presenters/LabelPresenter.svelte'
import MemberPresenter from '../presenters/MemberPresenter.svelte'
export let value: Card
const query = createQuery()
const client = getClient()
let members: Employee[]
let labels: CardLabel[]
let membersHandler: () => void
let labelsHandler: () => void
let dateHandler: () => void
$: value.members &&
value.members.length > 0 &&
query.query(contact.class.Employee, { _id: { $in: value.members } }, (result) => {
members = result
})
$: value.labels &&
value.labels.length > 0 &&
query.query(board.class.CardLabel, { _id: { $in: value.labels } }, (result) => {
labels = result
})
getCardActions(client, {
_id: { $in: [board.cardAction.Dates, board.cardAction.Labels, board.cardAction.Members] }
}).then(async (result) => {
for (const action of result) {
if (action.handler) {
const handler = await getResource(action.handler)
if (action._id === board.cardAction.Dates) {
dateHandler = () => handler(value, client)
} else if (action._id === board.cardAction.Labels) {
labelsHandler = () => handler(value, client)
} else if (action._id === board.cardAction.Members) {
membersHandler = () => handler(value, client)
}
}
}
})
</script>
{#if value}
{#if members && members.length > 0}
<div class="flex-col mt-4 mr-6">
<div class="text-md font-medium">
<Label label={board.string.Members} />
</div>
<div class="flex-row-center flex-gap-1">
{#each members as member}
<MemberPresenter value={member} size="large" />
{/each}
<Button icon={IconAdd} isCircle={true} kind="no-border" size="large" on:click={membersHandler} />
</div>
</div>
{/if}
{#if labels && labels.length > 0}
<div class="flex-col mt-4 mr6">
<div class="text-md font-medium">
<Label label={board.string.Labels} />
</div>
<div class="flex-row-center flex-gap-1">
{#each labels as label}
<LabelPresenter value={label} size="large" on:click={labelsHandler} />
{/each}
<Button icon={IconAdd} kind="no-border" size="large" on:click={labelsHandler} />
</div>
</div>
{/if}
{#if value.date && hasDate(value)}
<div class="flex-col mt-4">
<div class="text-md font-medium">
<Label label={board.string.Dates} />
</div>
<DatePresenter value={value.date} on:click={dateHandler} />
</div>
{/if}
{/if}
@@ -1,24 +0,0 @@
<!--
// 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 { Card } from '@anticrm/board'
export let value: Card
</script>
{#if value}
<div />
{/if}
@@ -0,0 +1,27 @@
<script lang="ts">
import type { CardDate } from '@anticrm/board'
import { CheckBox, DatePresenter } from '@anticrm/ui'
// TODO: implement
export let value: CardDate
</script>
{#if value}
<div class="flex-presenter flex-gap-1 h-full">
<CheckBox checked={value.isChecked} />
<div class="flex-center h-full">
<div class="flex-row-center background-button-bg-color border-radius-1 w-full">
{#if value.startDate && value.dueDate}
<DatePresenter bind:value={value.startDate} showIcon={false} />
-
<DatePresenter bind:value={value.dueDate} withTime={true} showIcon={false} />
{:else if value.startDate}
<DatePresenter bind:value={value.startDate} />
{:else if value.dueDate}
<DatePresenter bind:value={value.dueDate} withTime={true} showIcon={false} />
{/if}
</div>
</div>
</div>
{/if}
@@ -0,0 +1,18 @@
<script lang="ts">
import { Button, getPlatformColor } from '@anticrm/ui'
import type { CardLabel } from '@anticrm/board'
export let value: CardLabel
export let size: 'large' | 'medium'
const background = getPlatformColor(value.color)
</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>
{/if}
@@ -0,0 +1,25 @@
<script lang="ts">
import type { Employee } from '@anticrm/contact'
import { getFirstName, getLastName } from '@anticrm/contact'
import { ContactPresenter } from '@anticrm/contact-resources'
import { Button, showPopup } from '@anticrm/ui'
export let value: Employee
export let size: 'large' | 'medium'
const firstName = getFirstName(value.name)
const lastName = getLastName(value.name)
const nameLabel = `${firstName?.[0] ?? ''}${lastName?.[0] ?? ''}`.toUpperCase()
</script>
{#if value}
<Button
{size}
kind="no-border"
isCircle={true}
on:click={() => {
showPopup(ContactPresenter, { value }) // TODO: show proper popup
}}
>
<div slot="content" class="text-md">{nameLabel}</div>
</Button>
{/if}
@@ -1,9 +1,9 @@
import { CardAction } from '@anticrm/board'
import { Client } from '@anticrm/core'
import { Client, DocumentQuery } from '@anticrm/core'
import board from '../plugin'
export const cardActionSorter = (a1: CardAction, a2: CardAction) => a1.position - a2.position
export const getCardActions = (client: Client) => {
return client.findAll(board.class.CardAction, {})
export const getCardActions = (client: Client, query?: DocumentQuery<CardAction>) => {
return client.findAll(board.class.CardAction, query ?? {})
}
@@ -30,6 +30,10 @@ export function hasCover (card: Card): boolean {
return !!card.coverColor || !!card.coverImage
}
export function hasDate (card: Card): boolean {
return !!card.date && (!!card.date.dueDate || !!card.date.startDate)
}
export function addCurrentUser (card: Card, client: Client): void {
const employee = (getCurrentAccount() as EmployeeAccount).employee
card.members = card.members ?? []
+50 -8
View File
@@ -108,14 +108,6 @@ const boards = plugin(boardId, {
app: {
Board: '' as Ref<Doc>
},
cardActionType: {
Suggested: 'Suggested',
Editor: 'Editor',
Cover: 'Cover',
AddToCard: 'AddToCard',
Automation: 'Automation',
Action: 'Action'
},
class: {
Board: '' as Ref<Class<Board>>,
Card: '' as Ref<Class<Card>>,
@@ -128,6 +120,56 @@ const boards = plugin(boardId, {
},
space: {
BoardTemplates: '' as Ref<KanbanTemplateSpace>
},
cardActionType: {
Suggested: 'Suggested',
Editor: 'Editor',
Cover: 'Cover',
AddToCard: 'AddToCard',
Automation: 'Automation',
Action: 'Action'
},
cardAction: {
Cover: '' as Ref<CardAction>,
Join: '' as Ref<CardAction>,
Members: '' as Ref<CardAction>,
Labels: '' as Ref<CardAction>,
Checklist: '' as Ref<CardAction>,
Dates: '' as Ref<CardAction>,
Attachments: '' as Ref<CardAction>,
CustomFields: '' as Ref<CardAction>,
AddButton: '' as Ref<CardAction>,
Move: '' as Ref<CardAction>,
Copy: '' as Ref<CardAction>,
MakeTemplate: '' as Ref<CardAction>,
Watch: '' as Ref<CardAction>,
Archive: '' as Ref<CardAction>,
SendToBoard: '' as Ref<CardAction>,
Delete: '' as Ref<CardAction>
},
cardActionHandler: {
Cover: '' as Resource<(card: Card, client: Client) => void>,
Join: '' as Resource<(card: Card, client: Client) => void>,
Members: '' as Resource<(card: Card, client: Client) => void>,
Labels: '' as Resource<(card: Card, client: Client) => void>,
Checklist: '' as Resource<(card: Card, client: Client) => void>,
Dates: '' as Resource<(card: Card, client: Client) => void>,
Attachments: '' as Resource<(card: Card, client: Client) => void>,
CustomFields: '' as Resource<(card: Card, client: Client) => void>,
AddButton: '' as Resource<(card: Card, client: Client) => void>,
Move: '' as Resource<(card: Card, client: Client) => void>,
Copy: '' as Resource<(card: Card, client: Client) => void>,
MakeTemplate: '' as Resource<(card: Card, client: Client) => void>,
Watch: '' as Resource<(card: Card, client: Client) => void>,
Archive: '' as Resource<(card: Card, client: Client) => void>,
SendToBoard: '' as Resource<(card: Card, client: Client) => void>,
Delete: '' as Resource<(card: Card, client: Client) => void>
},
cardActionSupportedHandler: {
Join: '' as Resource<(card: Card, client: Client) => boolean>,
Archive: '' as Resource<(card: Card, client: Client) => boolean>,
SendToBoard: '' as Resource<(card: Card, client: Client) => boolean>,
Delete: '' as Resource<(card: Card, client: Client) => boolean>
}
})