Tracker: Board view (#1325)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2022-04-08 10:01:44 +07:00
committed by GitHub
parent 6c35b112ae
commit 13f0569127
43 changed files with 895 additions and 620 deletions
@@ -25,8 +25,6 @@
import TaskPresenter from './TaskPresenter.svelte'
export let object: WithLookup<Issue>
export let draggable: boolean
export let dragged: boolean
const showMenu = (ev?: Event): void => {
showPopup(ContextMenu, { object }, ev ? (ev.target as HTMLElement) : null)
@@ -36,66 +34,41 @@
$: doneTasks = todoItems.filter((it) => it.done)
</script>
<div class="card-container" {draggable} class:draggable on:dragstart on:dragend class:dragged={dragged}>
<div class="flex-between mb-2">
<div class="flex">
<TaskPresenter value={object} />
{#if todoItems.length > 0}
<Tooltip label={task.string.TodoItems} component={task.component.TodoItemsPopup} props={{ value: object }}>
<div class="ml-2">
( {doneTasks?.length}/ {todoItems.length} )
</div>
</Tooltip>
{/if}
</div>
<div class="flex-row-center">
<div class="mr-2">
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
</div>
<ActionIcon
label={task.string.More}
action={(evt) => {
showMenu(evt)
}}
icon={IconMoreH}
size={'small'}
/>
</div>
<div class="flex-between mb-2">
<div class="flex">
<TaskPresenter value={object} />
{#if todoItems.length > 0}
<Tooltip label={task.string.TodoItems} component={task.component.TodoItemsPopup} props={{ value: object }}>
<div class="ml-2">
( {doneTasks?.length}/ {todoItems.length} )
</div>
</Tooltip>
{/if}
</div>
<div class="caption-color mb-3 lines-limit-4">{object.name}</div>
<!-- <div class="text-sm lines-limit-2">{object.description}</div> -->
<div class="flex-between">
<div class="flex-row-center">
{#if (object.attachments ?? 0) > 0}
<div class="step-lr75"><AttachmentsPresenter value={object} /></div>
{/if}
{#if (object.comments ?? 0) > 0}
<div class="step-lr75"><CommentsPresenter value={object} /></div>
{/if}
<div class="flex-row-center">
<div class="mr-2">
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
</div>
<Avatar avatar={object.$lookup?.assignee?.avatar} size={'x-small'} />
<ActionIcon
label={task.string.More}
action={(evt) => {
showMenu(evt)
}}
icon={IconMoreH}
size={'small'}
/>
</div>
</div>
<style lang="scss">
.card-container {
display: flex;
flex-direction: column;
padding: .5rem 1rem;
background-color: var(--board-card-bg-color);
border: 1px solid var(--board-card-bg-color);
border-radius: .25rem;
user-select: none;
&:hover {
background-color: var(--board-card-bg-hover);
}
&.draggable {
cursor: grab;
}
&.dragged {
padding: 1rem;
background-color: var(--board-bg-color);
}
}
</style>
<div class="caption-color mb-3 lines-limit-4">{object.name}</div>
<!-- <div class="text-sm lines-limit-2">{object.description}</div> -->
<div class="flex-between">
<div class="flex-row-center">
{#if (object.attachments ?? 0) > 0}
<div class="step-lr75"><AttachmentsPresenter value={object} /></div>
{/if}
{#if (object.comments ?? 0) > 0}
<div class="step-lr75"><CommentsPresenter value={object} /></div>
{/if}
</div>
<Avatar avatar={object.$lookup?.assignee?.avatar} size={'x-small'} />
</div>
@@ -1,233 +0,0 @@
<!--
// 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 core, {
AttachedDoc,
Class,
Doc,
DocumentQuery,
DocumentUpdate,
FindOptions, Ref, Space
} from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import task, { calcRank, DocWithRank } from '@anticrm/task'
import { AnySvelteComponent, getPlatformColor, Loading, ScrollBox } from '@anticrm/ui'
import { slide } from 'svelte/transition'
import KanbanPanel from './KanbanPanel.svelte'
type StateType = any
type Item = DocWithRank & { state: StateType; doneState: StateType | null }
export let _class: Ref<Class<Item>>
export let space: Ref<Space>
// export let open: AnyComponent
export let search: string
export let options: FindOptions<Item> | undefined
// export let config: string[]
type TypeState = { _id: StateType; title: string; color: number }
export let states: TypeState[] = []
export let stateQuery: DocumentQuery<Item> = {
/// doneState: null
}
let objects: Item[] = []
const objsQ = createQuery()
$: objsQ.query(
_class,
{
space,
...stateQuery,
...(search !== '' ? { $search: search } : {})
},
(result) => {
objects = result
},
{
...options
}
)
function getStateObjects (objects: Item[], state: TypeState, dragItem?: Item): {it:Item, prev?:Item, next?: Item}[] {
const stateCards = objects.filter(it => it.state === state._id)
stateCards.sort((a, b) => a.rank.localeCompare(b.rank))
return stateCards.map((it, idx, arr) => ({ it, prev: arr[idx - 1], next: arr[idx + 1] }))
}
async function updateItem (item: Item, update: DocumentUpdate<Item>) {
if (client.getHierarchy().isDerived(_class, core.class.AttachedDoc)) {
const adoc: AttachedDoc = item as Doc as AttachedDoc
await client.updateCollection(
_class,
space,
adoc._id as Ref<Doc> as Ref<AttachedDoc>,
adoc.attachedTo,
adoc.attachedToClass,
adoc.collection,
update
)
} else {
await client.updateDoc(item._class, item.space, item._id, update)
}
}
async function move (state: StateType) {
if (dragCard === undefined) {
return
}
let updates: DocumentUpdate<Item> = {}
if (dragCardInitialState !== state) {
updates = {
...updates,
state
}
}
if (dragCardInitialRank !== dragCard.rank) {
updates = {
...updates,
rank: dragCard.rank
}
}
if (Object.keys(updates).length > 0) {
await updateItem(dragCard, updates)
}
dragCard = undefined
}
const client = getClient()
let dragCard: Item | undefined
let dragCardInitialRank: string
let dragCardInitialState: StateType
async function cardPresenter (_class: Ref<Class<Doc>>): Promise<AnySvelteComponent> {
const clazz = client.getHierarchy().getClass(_class)
const presenterMixin = client.getHierarchy().as(clazz, task.mixin.KanbanCard)
return await getResource(presenterMixin.card)
}
let isDragging = false
async function updateDone (query: DocumentUpdate<Item>): Promise<void> {
isDragging = false
if (dragCard === undefined) {
return
}
await updateItem(dragCard, query)
}
function doCalcRank (object: {prev?: Item, it: Item, next?: Item}, event: DragEvent & { currentTarget: EventTarget & HTMLDivElement}): string {
const rect = event.currentTarget.getBoundingClientRect()
if (event.clientY < rect.top + rect.height * 2 / 3) {
return calcRank(object.prev, object.it)
} else {
return calcRank(object.it, object.next)
}
}
const slideD = (node: any, args:any) => args.isDragging ? slide(node, args) : {}
</script>
{#await cardPresenter(_class)}
<Loading />
{:then presenter}
<div class="flex-col kanban-container">
<div class="scrollable">
<ScrollBox>
<div class="kanban-content">
{#each states as state}
<KanbanPanel
label={state.title}
color={getPlatformColor(state.color)}
on:dragover={(event) => {
event.preventDefault()
if (dragCard !== undefined && dragCard.state !== state._id) {
dragCard.state = state._id
const objs = getStateObjects(objects, state)
dragCard.rank = calcRank(objs[objs.length - 1]?.it, undefined)
}
}}
on:drop={() => {
move(state._id)
isDragging = false
}}
>
<!-- <KanbanCardEmpty label={'Create new application'} /> -->
{#each getStateObjects(objects, state, dragCard) as object}
<div transition:slideD|local={{ isDragging }}
class="step-tb75"
on:dragover|preventDefault={(evt) => {
if (dragCard !== undefined) {
dragCard.rank = doCalcRank(object, evt)
}
}}
on:drop|preventDefault={(evt) => {
if (dragCard !== undefined) {
dragCard.rank = doCalcRank(object, evt)
}
isDragging = false
}}
>
<svelte:component
this={presenter}
object={object.it}
dragged={isDragging && object.it._id === dragCard?._id}
draggable={true}
on:dragstart={() => {
dragCardInitialState = state._id
dragCardInitialRank = object.it.rank
dragCard = object.it
isDragging = true
}}
on:dragend={() => {
isDragging = false
}}
/>
</div>
{/each}
</KanbanPanel>
{/each}
<slot name="additionalPanel"/>
</div>
</ScrollBox>
</div>
{#if isDragging}
<slot name="doneBar" onDone={updateDone} />
{/if}
</div>
{/await}
<style lang="scss">
.kanban-container {
position: relative;
height: 100%;
background: var(--board-bg-color);
}
.kanban-content {
display: flex;
margin: 1.5rem 2rem;
height: 100%;
}
.scrollable {
height: 100%;
}
</style>
@@ -1,54 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
//
// 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 { IntlString } from '@anticrm/platform'
import { Label } from '@anticrm/ui'
import { IconAdd } from '@anticrm/ui'
export let label: IntlString
</script>
<div class="card-container">
<div class="icon">
<IconAdd size={'small'} />
</div>
<Label {label} />
</div>
<style lang="scss">
.card-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: .75rem 1.5rem;
color: var(--theme-content-dark-color);
background-color: var(--theme-button-bg-hovered);
border: 1px dotted var(--theme-bg-accent-color);
border-radius: .75rem;
cursor: pointer;
.icon {
margin-bottom: .25rem;
opacity: .4;
}
&:hover {
color: var(--theme-caption-color);
.icon { opacity: 1; }
}
}
</style>
@@ -14,10 +14,10 @@
// limitations under the License.
-->
<script lang="ts">
import { Class,Ref,SortingOrder } from '@anticrm/core'
import { createQuery,getClient } from '@anticrm/presentation'
import type { DoneState,Kanban,State } from '@anticrm/task'
import task,{ calcRank } from '@anticrm/task'
import { Class, Ref, SortingOrder } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import type { DoneState, Kanban, State } from '@anticrm/task'
import task, { calcRank } from '@anticrm/task'
import StatesEditor from '../state/StatesEditor.svelte'
export let kanban: Kanban
@@ -34,7 +34,7 @@
const hierarchy = client.getHierarchy()
const statesQ = createQuery()
$: statesQ.query(task.class.State, { space: kanban.space }, result => { states = result}, {
$: statesQ.query(task.class.State, { space: kanban.space }, result => { states = result }, {
sort: {
rank: SortingOrder.Ascending
}
@@ -1,90 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
//
// 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 { ScrollBox } from '@anticrm/ui'
export let label: string
export let color: string
// export let counter: number
// export let addAction: () => void | undefined
</script>
<div class="panel-container step-lr75">
<div class="header">
<div class="bar" style="background-color: {color}" />
<div class="flex-between label">
<div>
<span class="lines-limit-2">{label}</span>
<!-- <span>({counter})</span> -->
</div>
<!-- <div class="tool ml-3" on:click|preventDefault={addAction}>
<IconAdd size={'small'} />
</div> -->
</div>
</div>
<div class="scroll" on:dragover on:drop>
<ScrollBox vertical>
<slot />
</ScrollBox>
</div>
</div>
<style lang="scss">
.panel-container {
display: flex;
flex-direction: column;
align-items: stretch;
width: 20rem;
height: 100%;
background-color: transparent;
border: 1px solid transparent;
border-radius: .25rem;
.header {
display: flex;
flex-direction: column;
height: 4rem;
min-height: 4rem;
user-select: none;
.bar {
height: .375rem;
border-radius: .25rem;
}
.label {
padding: 0 .5rem 0 1rem;
height: 100%;
font-weight: 500;
color: var(--theme-caption-color);
// span {
// font-weight: 400;
// color: var(--theme-content-dark-color);
// }
}
// .tool {
// opacity: .4;
// cursor: pointer;
// &:hover { opacity: 1; }
// }
}
.scroll {
margin: 0 .75rem .75rem;
min-height: 0;
height: 100%;
}
}
</style>
@@ -1,67 +0,0 @@
<script lang="ts">
import { IconAdd, Button, EditBox, ActionIcon, IconClose } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte';
import tracker from '../../plugin';
const dispatch = createEventDispatcher()
let isAdding = false
let newPanelTitle = ''
async function onAdd () {
if (!newPanelTitle) return;
dispatch('add', newPanelTitle)
newPanelTitle = ''
isAdding = false
}
</script>
<div class="panel-container step-lr75">
{#if isAdding}
<EditBox
bind:value={newPanelTitle}
maxWidth={'19rem'}
placeholder={tracker.string.NewListPlaceholder}
focus={true}
/>
<div class="list-add-controls">
<Button
icon={IconAdd}
label={tracker.string.AddList}
justify={'left'}
on:click={() => { onAdd() }}
/>
<ActionIcon
icon={IconClose}
size={'large'}
action={() => { isAdding = false }}
/>
</div>
{:else}
<Button
icon={IconAdd}
label={tracker.string.NewList}
justify={'left'}
on:click={() => { isAdding = true }}
/>
{/if}
</div>
<style lang="scss">
.panel-container {
display: flex;
flex-direction: column;
align-items: stretch;
width: 20rem;
height: fit-content;
background-color: transparent;
border: .125rem solid transparent;
border-radius: .25rem;
}
.list-add-controls {
display: flex;
flex-direction: row;
align-items: center;
margin: 0.5rem 0 0 0;
}
</style>
@@ -16,24 +16,25 @@
<script lang="ts">
import { Class, FindOptions, Ref, SortingOrder } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import type { Kanban, SpaceWithStates, State } from '@anticrm/task'
import task, { DocWithRank, DoneState } from '@anticrm/task'
import KanbanUI from './Kanban.svelte'
import { Kanban as KanbanUI } from '@anticrm/kanban'
import { getResource } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import type { Kanban, SpaceWithStates, State, Task } from '@anticrm/task'
import task from '@anticrm/task'
import KanbanDragDone from './KanbanDragDone.svelte'
type Item = DocWithRank & { state: Ref<State>, doneState: Ref<DoneState> | null }
export let _class: Ref<Class<Item>>
export let _class: Ref<Class<Task>>
export let space: Ref<SpaceWithStates>
// export let open: AnyComponent
export let search: string
export let options: FindOptions<Item> | undefined
export let options: FindOptions<Task> | undefined
// export let config: string[]
let kanban: Kanban
let states: State[] = []
const client = getClient()
const kanbanQuery = createQuery()
$: kanbanQuery.query(task.class.Kanban, { attachedTo: space }, result => { kanban = result[0] })
@@ -45,14 +46,25 @@
}
})
}
$: clazz = client.getHierarchy().getClass(_class)
$: presenterMixin = client.getHierarchy().as(clazz, task.mixin.KanbanCard)
$: cardPresenter = getResource(presenterMixin.card)
/* eslint-disable no-undef */
</script>
<KanbanUI {_class} {space} {search} {options} stateQuery={{ doneState: null }} states={states}>
// eslint-disable-next-line no-undef
<svelte:fragment slot='doneBar' let:onDone={onDone}>
<KanbanDragDone {kanban} on:done={(e) => {
// eslint-disable-next-line no-undef
onDone({ doneState: e.detail._id })
}} />
</svelte:fragment>
</KanbanUI>
{#await cardPresenter then presenter}
<KanbanUI {_class} {space} {search} {options} query={{ doneState: null }} states={states}
fieldName={'state'} rankFieldName={'rank'}>
<svelte:fragment slot='card' let:object let:dragged>
<svelte:component this={presenter} {object} {dragged}/>
</svelte:fragment>
// eslint-disable-next-line no-undef
<svelte:fragment slot='doneBar' let:onDone>
<KanbanDragDone {kanban} on:done={(e) => {
// eslint-disable-next-line no-undef
onDone({ doneState: e.detail._id })
}} />
</svelte:fragment>
</KanbanUI>
{/await}