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

View File

@ -30,6 +30,8 @@
export let labelNull: IntlString = ui.string.NoDate
export let showIcon = true
export let shouldShowLabel: boolean = true
export let size: 'x-small' | 'small' = 'small'
export let kind: 'transparent' | 'primary' = 'primary'
const dispatch = createEventDispatcher()
@ -56,6 +58,10 @@
class="datetime-button"
class:editable
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={() => {
if (editable && !opened) {
opened = true
@ -75,7 +81,7 @@
>
{#if showIcon}
<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>
{/if}
{#if value !== null && value !== undefined}
@ -105,22 +111,24 @@
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0 0.5rem;
font-weight: 400;
min-width: 1.5rem;
width: auto;
height: 1.5rem;
white-space: nowrap;
line-height: 1.5rem;
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;
&.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 {
padding: 0;
}

View File

@ -15,13 +15,18 @@
-->
<script lang="ts">
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 type { WithLookup } from '@anticrm/core'
import contact, { Employee } from '@anticrm/contact'
import type { Ref, WithLookup } from '@anticrm/core'
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 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 dragged: boolean
@ -29,6 +34,8 @@
let loadingAttachment = 0
let dragoverAttachment = false
const client = getClient()
function showMenu (ev?: Event): void {
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
}
@ -41,6 +48,14 @@
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>
<AttachmentDroppable
@ -50,7 +65,7 @@
objectId={object._id}
space={object.space}
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}
<div style:pointer-events="none" class="abs-full-content h-full w-full flex-center fs-title">
<Label label={board.string.DropFileToUpload} />
@ -60,36 +75,48 @@
style:pointer-events="none"
class="abs-full-content background-theme-content-accent h-full w-full flex-center fs-title" />
{/if}
<div class="flex-between mb-4" style:pointer-events={dragoverAttachment ? 'none' : 'all'}>
<div class="flex-col">
<div class="fs-title cursor-pointer" on:click={showCard}>{object.title}</div>
</div>
<div class="flex-row-center">
<div class="mr-2">
<div class="ml-1">
<CardLabels bind:value={object} isInline={true} />
</div>
<div class="absolute mr-1 mt-1" style:top="0" style:right="0">
<Button icon={IconEdit} kind="transparent" on:click={showMenu}/>
</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 }} />
</div>
<ActionIcon
label={board.string.More}
action={(evt) => {
showMenu(evt)
}}
icon={IconMoreH}
size="small" />
</div>
</div>
<div class="flex-between" style:pointer-events={dragoverAttachment ? 'none' : 'all'}>
<div class="flex-row-center">
<div class="flex-between mb-1" style:pointer-events={dragoverAttachment ? 'none' : 'all'}>
<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}
<div class="step-lr75">
<AttachmentsPresenter value={object} />
<div class="float-left">
<AttachmentsPresenter value={object} size="small" />
</div>
{/if}
{#if (object.comments ?? 0) > 0}
<div class="step-lr75">
<div class="float-left">
<CommentsPresenter value={object} />
</div>
{/if}
</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>
</AttachmentDroppable>

View File

@ -1,6 +1,5 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
// 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
@ -14,7 +13,7 @@
// limitations under the License.
-->
<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 { getResource } from '@anticrm/platform'
@ -25,16 +24,14 @@
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'
import CardLabels from './CardLabels.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
$: membersIds = members?.map(m => m._id) ?? []
@ -63,28 +60,18 @@
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>) {
client.update(value, { date: e.detail })
}
getCardActions(client, {
_id: { $in: [board.cardAction.Dates, board.cardAction.Labels, board.cardAction.Members] }
_id: { $in: [board.cardAction.Dates, 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)
}
@ -107,17 +94,12 @@
</div>
</div>
{/if}
{#if labels && labels.length > 0}
{#if value.labels && value.labels.length > 0}
<div class="flex-col mt-4 mr-6">
<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>
<CardLabels {value} />
</div>
{/if}
{#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'
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 color = numberToHexColor(value)
@ -14,6 +15,9 @@
class:h-8={size === 'large'}
class:h-7={size === 'medium'}
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}"
on:click
>
@ -24,7 +28,7 @@
<style lang="scss">
.color-presenter {
background-color: var(--color-presenter-color);
&:hover {
&:hover, &.hovered {
background-color: var(--color-presenter-hoverColor);
}
}

View File

@ -5,27 +5,31 @@
export let value: CardDate
export let isInline: boolean = false
export let size: 'x-small' | 'small' = 'small'
let isChecked = value?.isChecked
const dispatch = createEventDispatcher()
const isOverdue = !!value?.dueDate && (new Date()).getTime() > value.dueDate
function check () {
if (isInline || isChecked === undefined || !value) return
if (isChecked === undefined || !value) return
dispatch('update', { ...value, isChecked })
}
</script>
{#if value}
<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-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}
<DatePresenter bind:value={value.startDate} />
<DatePresenter bind:value={value.startDate} {size} kind="transparent" />
{/if}
{#if value.startDate && value.dueDate}-{/if}
{#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}
</div>
</div>

View File

@ -3,11 +3,15 @@
import ColorPresenter from './ColorPresenter.svelte'
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>
{#if value}
<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 value={value.color} {isHovered} {size} on:click>
{#if size !== 'tiny'}
<div class="flex-center h-full w-full fs-title text-sm pr-1 pl-1">{value.title ?? ''}</div>
{/if}
</ColorPresenter>
{/if}