Board: Add labels & members & date to Kanban Card (#1462)

Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
Anna No 2022-04-20 23:30:20 +07:00 committed by GitHub
parent b3fb2a7034
commit 43c0413cd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 200 additions and 67 deletions

View File

@ -106,6 +106,16 @@ p:last-child { margin-block-end: 0; }
line-height: 200%; line-height: 200%;
} }
.float-left-box {
box-sizing: border-box;
width: 100%;
float: left;
}
.float-left {
float: left;
}
/* Flex */ /* Flex */
.flex { display: flex; } .flex { display: flex; }
.inline-flex { display: inline-flex; } .inline-flex { display: inline-flex; }
@ -342,6 +352,7 @@ p:last-child { margin-block-end: 0; }
.pt-3 { padding-top: .75rem; } .pt-3 { padding-top: .75rem; }
.pt-4 { padding-top: 1rem; } .pt-4 { padding-top: 1rem; }
.pb-2 { padding-bottom: .5rem; } .pb-2 { padding-bottom: .5rem; }
.pb-3 { padding-bottom: .75rem; }
.pb-4 { padding-bottom: 1rem; } .pb-4 { padding-bottom: 1rem; }
.p-1 { padding: .25rem; } .p-1 { padding: .25rem; }
@ -391,6 +402,7 @@ p:last-child { margin-block-end: 0; }
.h-full { height: 100%; } .h-full { height: 100%; }
.h-2 { height: .5rem; } .h-2 { height: .5rem; }
.h-4 { height: 1rem; }
.h-6 { height: 1.5rem; } .h-6 { height: 1.5rem; }
.h-7 { height: 1.75rem; } .h-7 { height: 1.75rem; }
.h-8 { height: 2rem; } .h-8 { height: 2rem; }

View File

@ -30,6 +30,8 @@
export let labelNull: IntlString = ui.string.NoDate export let labelNull: IntlString = ui.string.NoDate
export let showIcon = true export let showIcon = true
export let shouldShowLabel: boolean = true export let shouldShowLabel: boolean = true
export let size: 'x-small' | 'small' = 'small'
export let kind: 'transparent' | 'primary' = 'primary'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -56,6 +58,10 @@
class="datetime-button" class="datetime-button"
class:editable class:editable
class:dateTimeButtonNoLabel={!shouldShowLabel} class:dateTimeButtonNoLabel={!shouldShowLabel}
class:primary={kind === 'primary'}
class:h-6={size === 'small'}
class:h-3={size === 'x-small'}
class:text-xs={size === 'x-small'}
on:click={() => { on:click={() => {
if (editable && !opened) { if (editable && !opened) {
opened = true opened = true
@ -75,7 +81,7 @@
> >
{#if showIcon} {#if showIcon}
<div class="btn-icon {icon}" class:buttonIconNoLabel={!shouldShowLabel}> <div class="btn-icon {icon}" class:buttonIconNoLabel={!shouldShowLabel}>
<Icon icon={icon === 'overdue' ? DPCalendarOver : DPCalendar} size={'full'} /> <Icon icon={icon === 'overdue' ? DPCalendarOver : DPCalendar} size="full" />
</div> </div>
{/if} {/if}
{#if value !== null && value !== undefined} {#if value !== null && value !== undefined}
@ -105,22 +111,24 @@
display: flex; display: flex;
align-items: center; align-items: center;
flex-shrink: 0; flex-shrink: 0;
padding: 0 0.5rem;
font-weight: 400; font-weight: 400;
min-width: 1.5rem;
width: auto; width: auto;
height: 1.5rem;
white-space: nowrap; white-space: nowrap;
line-height: 1.5rem;
color: var(--accent-color); color: var(--accent-color);
background-color: var(--noborder-bg-color);
border: 1px solid transparent;
border-radius: 0.25rem;
box-shadow: var(--button-shadow);
transition-property: border, background-color, color, box-shadow;
transition-duration: 0.15s;
cursor: default; cursor: default;
&.primary {
padding: 0 0.5rem;
min-width: 1.5rem;
background-color: var(--noborder-bg-color);
border: 1px solid transparent;
border-radius: 0.25rem;
box-shadow: var(--button-shadow);
transition-property: border, background-color, color, box-shadow;
transition-duration: 0.15s;
}
&.dateTimeButtonNoLabel { &.dateTimeButtonNoLabel {
padding: 0; padding: 0;
} }

View File

@ -15,13 +15,18 @@
--> -->
<script lang="ts"> <script lang="ts">
import { AttachmentDroppable, AttachmentsPresenter } from '@anticrm/attachment-resources' import { AttachmentDroppable, AttachmentsPresenter } from '@anticrm/attachment-resources'
import type { Card } from '@anticrm/board' import type { Card, CardDate } from '@anticrm/board'
import { CommentsPresenter } from '@anticrm/chunter-resources' import { CommentsPresenter } from '@anticrm/chunter-resources'
import type { WithLookup } from '@anticrm/core' import contact, { Employee } from '@anticrm/contact'
import type { Ref, WithLookup } from '@anticrm/core'
import notification from '@anticrm/notification' import notification from '@anticrm/notification'
import { ActionIcon, Component, IconMoreH, Label, showPanel, showPopup } from '@anticrm/ui' import { getClient, UserBoxList } from '@anticrm/presentation'
import { Button, Component, IconEdit, IconMoreH, Label, showPanel, showPopup } from '@anticrm/ui'
import { ContextMenu } from '@anticrm/view-resources' import { ContextMenu } from '@anticrm/view-resources'
import board from '../plugin' import board from '../plugin'
import { hasDate } from '../utils/CardUtils'
import CardLabels from './editor/CardLabels.svelte'
import DatePresenter from './presenters/DatePresenter.svelte'
export let object: WithLookup<Card> export let object: WithLookup<Card>
export let dragged: boolean export let dragged: boolean
@ -29,6 +34,8 @@
let loadingAttachment = 0 let loadingAttachment = 0
let dragoverAttachment = false let dragoverAttachment = false
const client = getClient()
function showMenu (ev?: Event): void { function showMenu (ev?: Event): void {
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement) showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
} }
@ -41,6 +48,14 @@
return !!e.dataTransfer?.items && e.dataTransfer?.items.length > 0 return !!e.dataTransfer?.items && e.dataTransfer?.items.length > 0
} }
function updateMembers (e: CustomEvent<Ref<Employee>[]>) {
client.update(object, { members: e.detail })
}
function updateDate (e: CustomEvent<CardDate>) {
client.update(object, { date: e.detail })
}
</script> </script>
<AttachmentDroppable <AttachmentDroppable
@ -50,7 +65,7 @@
objectId={object._id} objectId={object._id}
space={object.space} space={object.space}
canDrop={canDropAttachment}> canDrop={canDropAttachment}>
<div class="relative flex-col pt-2 pb-2 pr-4 pl-4"> <div class="relative flex-col pt-2 pb-1 pr-2 pl-2">
{#if dragoverAttachment} {#if dragoverAttachment}
<div style:pointer-events="none" class="abs-full-content h-full w-full flex-center fs-title"> <div style:pointer-events="none" class="abs-full-content h-full w-full flex-center fs-title">
<Label label={board.string.DropFileToUpload} /> <Label label={board.string.DropFileToUpload} />
@ -60,36 +75,48 @@
style:pointer-events="none" style:pointer-events="none"
class="abs-full-content background-theme-content-accent h-full w-full flex-center fs-title" /> class="abs-full-content background-theme-content-accent h-full w-full flex-center fs-title" />
{/if} {/if}
<div class="flex-between mb-4" style:pointer-events={dragoverAttachment ? 'none' : 'all'}> <div class="ml-1">
<div class="flex-col"> <CardLabels bind:value={object} isInline={true} />
<div class="fs-title cursor-pointer" on:click={showCard}>{object.title}</div> </div>
</div> <div class="absolute mr-1 mt-1" style:top="0" style:right="0">
<div class="flex-row-center"> <Button icon={IconEdit} kind="transparent" on:click={showMenu}/>
<div class="mr-2"> </div>
<div class="flex-between pb-2 ml-1" style:pointer-events={dragoverAttachment ? 'none' : 'all'} on:click={showCard}>
<div class="flex-row-center w-full" >
<div class="fs-title cursor-pointer">{object.title}</div>
<div class="ml-2">
<Component is={notification.component.NotificationPresenter} props={{ value: object }} /> <Component is={notification.component.NotificationPresenter} props={{ value: object }} />
</div> </div>
<ActionIcon
label={board.string.More}
action={(evt) => {
showMenu(evt)
}}
icon={IconMoreH}
size="small" />
</div> </div>
</div> </div>
<div class="flex-between" style:pointer-events={dragoverAttachment ? 'none' : 'all'}> <div class="flex-between mb-1" style:pointer-events={dragoverAttachment ? 'none' : 'all'}>
<div class="flex-row-center"> <div class="float-left-box">
{#if object.date && hasDate(object)}
<div class="float-left ml-1">
<DatePresenter value={object.date} isInline={true} size="x-small" on:update={updateDate} />
</div>
{/if}
{#if (object.attachments ?? 0) > 0} {#if (object.attachments ?? 0) > 0}
<div class="step-lr75"> <div class="float-left">
<AttachmentsPresenter value={object} /> <AttachmentsPresenter value={object} size="small" />
</div> </div>
{/if} {/if}
{#if (object.comments ?? 0) > 0} {#if (object.comments ?? 0) > 0}
<div class="step-lr75"> <div class="float-left">
<CommentsPresenter value={object} /> <CommentsPresenter value={object} />
</div> </div>
{/if} {/if}
</div> </div>
</div> </div>
{#if (object.members?.length ?? 0) > 0}
<div class="flex justify-end mt-1 mb-2" style:pointer-events={dragoverAttachment ? 'none' : 'all'}>
<UserBoxList
_class={contact.class.Employee}
items={object.members}
label={board.string.Members}
noItems={board.string.Members}
on:update={updateMembers} />
</div>
{/if}
</div> </div>
</AttachmentDroppable> </AttachmentDroppable>

View File

@ -1,6 +1,5 @@
<!-- <!--
// Copyright © 2020, 2021 Anticrm Platform Contributors. // Copyright © 2022 Hardcore Engineering Inc.
// Copyright © 2021 Hardcore Engineering Inc.
// //
// Licensed under the Eclipse Public License, Version 2.0 (the "License"); // 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 // you may not use this file except in compliance with the License. You may
@ -14,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import type { Card, CardDate, CardLabel } from '@anticrm/board' import type { Card, CardDate } from '@anticrm/board'
import contact, { Employee } from '@anticrm/contact' import contact, { Employee } from '@anticrm/contact'
import { getResource } from '@anticrm/platform' import { getResource } from '@anticrm/platform'
@ -25,16 +24,14 @@
import { getCardActions } from '../../utils/CardActionUtils' import { getCardActions } from '../../utils/CardActionUtils'
import { hasDate } from '../../utils/CardUtils' import { hasDate } from '../../utils/CardUtils'
import DatePresenter from '../presenters/DatePresenter.svelte' import DatePresenter from '../presenters/DatePresenter.svelte'
import LabelPresenter from '../presenters/LabelPresenter.svelte'
import MemberPresenter from '../presenters/MemberPresenter.svelte' import MemberPresenter from '../presenters/MemberPresenter.svelte'
import CardLabels from './CardLabels.svelte'
export let value: Card export let value: Card
const query = createQuery() const query = createQuery()
const client = getClient() const client = getClient()
let members: Employee[] let members: Employee[]
let labels: CardLabel[]
let membersHandler: () => void let membersHandler: () => void
let labelsHandler: () => void
let dateHandler: () => void let dateHandler: () => void
$: membersIds = members?.map(m => m._id) ?? [] $: membersIds = members?.map(m => m._id) ?? []
@ -63,28 +60,18 @@
members = [] members = []
} }
$: if (value.labels && value.labels.length > 0) {
query.query(board.class.CardLabel, { _id: { $in: value.labels } }, (result) => {
labels = result
})
} else {
labels = []
}
function updateDate (e: CustomEvent<CardDate>) { function updateDate (e: CustomEvent<CardDate>) {
client.update(value, { date: e.detail }) client.update(value, { date: e.detail })
} }
getCardActions(client, { getCardActions(client, {
_id: { $in: [board.cardAction.Dates, board.cardAction.Labels, board.cardAction.Members] } _id: { $in: [board.cardAction.Dates, board.cardAction.Members] }
}).then(async (result) => { }).then(async (result) => {
for (const action of result) { for (const action of result) {
if (action.handler) { if (action.handler) {
const handler = await getResource(action.handler) const handler = await getResource(action.handler)
if (action._id === board.cardAction.Dates) { if (action._id === board.cardAction.Dates) {
dateHandler = () => handler(value, client) dateHandler = () => handler(value, client)
} else if (action._id === board.cardAction.Labels) {
labelsHandler = () => handler(value, client)
} else if (action._id === board.cardAction.Members) { } else if (action._id === board.cardAction.Members) {
membersHandler = () => handler(value, client) membersHandler = () => handler(value, client)
} }
@ -107,17 +94,12 @@
</div> </div>
</div> </div>
{/if} {/if}
{#if labels && labels.length > 0} {#if value.labels && value.labels.length > 0}
<div class="flex-col mt-4 mr-6"> <div class="flex-col mt-4 mr-6">
<div class="text-md font-medium"> <div class="text-md font-medium">
<Label label={board.string.Labels} /> <Label label={board.string.Labels} />
</div> </div>
<div class="flex-row-center flex-gap-1"> <CardLabels {value} />
{#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> </div>
{/if} {/if}
{#if value.date && hasDate(value)} {#if value.date && hasDate(value)}

View File

@ -0,0 +1,92 @@
<!--
// Copyright © 2022 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 { getResource } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation'
import { Button, IconAdd } from '@anticrm/ui'
import board from '../../plugin'
import { getCardActions } from '../../utils/CardActionUtils'
import LabelPresenter from '../presenters/LabelPresenter.svelte'
export let value: Card
export let isInline: boolean = false
const client = getClient()
let labels: CardLabel[]
let labelsHandler: () => void
let isCompact: boolean = false
let isHovered: boolean = false
$: if (value.labels && value.labels.length > 0) {
client.findAll(board.class.CardLabel, { _id: { $in: value.labels } }).then((result) => {
labels = isInline ? result.filter((l) => !l.isHidden) : result
})
} else {
labels = []
}
if (!isInline) {
getCardActions(client, {
_id: board.cardAction.Labels
}).then(async (result) => {
if (result?.[0]?.handler) {
const handler = await getResource(result[0].handler)
labelsHandler = () => handler(value, client)
}
})
}
function toggleCompact () {
if (isInline) {
isCompact = !isCompact
}
}
function hoverIn () {
if (isInline) {
isHovered = true
}
}
function hoverOut () {
isHovered = false
}
</script>
{#if labels && labels.length > 0}
<div
class="flex-row-center flex-gap-1 mb-1"
class:labels-inline-container={isInline}
on:click={toggleCompact}
on:mouseover={hoverIn}
on:focus={hoverIn}
on:mouseout={hoverOut}
on:blur={hoverOut}>
{#each labels as label}
<LabelPresenter
value={label}
size={isCompact ? 'tiny' : isInline ? 'x-small' : undefined}
{isHovered}
on:click={labelsHandler} />
{/each}
{#if !isInline}
<Button icon={IconAdd} kind="no-border" size="large" on:click={labelsHandler} />
{/if}
</div>
{/if}

View File

@ -2,7 +2,8 @@
import { numberToHexColor, numberToRGB } from '@anticrm/ui' import { numberToHexColor, numberToRGB } from '@anticrm/ui'
export let value: number export let value: number
export let size: 'small' | 'medium' | 'large' = 'medium' export let size: 'tiny' | 'x-small' | 'small' | 'medium' | 'large' = 'medium'
export let isHovered: boolean = false
const hoverColor = numberToRGB(value, 0.6) const hoverColor = numberToRGB(value, 0.6)
const color = numberToHexColor(value) const color = numberToHexColor(value)
@ -14,6 +15,9 @@
class:h-8={size === 'large'} class:h-8={size === 'large'}
class:h-7={size === 'medium'} class:h-7={size === 'medium'}
class:h-6={size === 'small'} class:h-6={size === 'small'}
class:h-4={size === 'x-small'}
class:h-2={size === 'tiny'}
class:hovered={isHovered}
style="--color-presenter-color: {color}; --color-presenter-hoverColor: {hoverColor}" style="--color-presenter-color: {color}; --color-presenter-hoverColor: {hoverColor}"
on:click on:click
> >
@ -24,7 +28,7 @@
<style lang="scss"> <style lang="scss">
.color-presenter { .color-presenter {
background-color: var(--color-presenter-color); background-color: var(--color-presenter-color);
&:hover { &:hover, &.hovered {
background-color: var(--color-presenter-hoverColor); background-color: var(--color-presenter-hoverColor);
} }
} }

View File

@ -5,27 +5,31 @@
export let value: CardDate export let value: CardDate
export let isInline: boolean = false export let isInline: boolean = false
export let size: 'x-small' | 'small' = 'small'
let isChecked = value?.isChecked let isChecked = value?.isChecked
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const isOverdue = !!value?.dueDate && (new Date()).getTime() > value.dueDate
function check () { function check () {
if (isInline || isChecked === undefined || !value) return if (isChecked === undefined || !value) return
dispatch('update', { ...value, isChecked }) dispatch('update', { ...value, isChecked })
} }
</script> </script>
{#if value} {#if value}
<div class="flex-presenter flex-gap-1 h-full"> <div class="flex-presenter flex-gap-1 h-full">
<CheckBox bind:checked={isChecked} on:value={check} /> {#if value.dueDate}
<CheckBox bind:checked={isChecked} on:value={check} />
{/if}
<div class="flex-center h-full" on:click> <div class="flex-center h-full" on:click>
<div class="flex-row-center background-button-bg-color border-radius-1 w-full"> <div class="flex-row-center background-button-bg-color pr-1 pl-1 border-radius-1 w-full">
{#if value.startDate} {#if value.startDate}
<DatePresenter bind:value={value.startDate} /> <DatePresenter bind:value={value.startDate} {size} kind="transparent" />
{/if} {/if}
{#if value.startDate && value.dueDate}-{/if} {#if value.startDate && value.dueDate}-{/if}
{#if value.dueDate} {#if value.dueDate}
<DatePresenter bind:value={value.dueDate} withTime={true} showIcon={false} /> <DatePresenter bind:value={value.dueDate} withTime={true} icon={isOverdue ? 'overdue' : undefined} {size} kind="transparent" />
{/if} {/if}
</div> </div>
</div> </div>

View File

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