mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 11:31:57 +03:00
Board: Add labels & members & date to Kanban Card (#1462)
Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
parent
b3fb2a7034
commit
43c0413cd0
@ -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; }
|
||||
|
@ -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,21 +111,23 @@
|
||||
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);
|
||||
|
||||
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;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.dateTimeButtonNoLabel {
|
||||
padding: 0;
|
||||
|
@ -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 class="ml-1">
|
||||
<CardLabels bind:value={object} isInline={true} />
|
||||
</div>
|
||||
<div class="flex-row-center">
|
||||
<div class="mr-2">
|
||||
<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>
|
||||
|
@ -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)}
|
||||
|
@ -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}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
{#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>
|
||||
|
@ -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>
|
||||
<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}
|
||||
|
Loading…
Reference in New Issue
Block a user