platform/plugins/task-resources/src/components/kanban/KanbanView.svelte
Andrey Sobolev f0a4edee49
TSK-1309, TSK-1310, TSK-571 ()
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
2023-04-23 09:50:41 +06:00

324 lines
9.5 KiB
Svelte

<!--
// 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 contact from '@hcengineering/contact'
import {
CategoryType,
Class,
Doc,
DocumentQuery,
DocumentUpdate,
FindOptions,
generateId,
Ref
} from '@hcengineering/core'
import { Item, Kanban as KanbanUI } from '@hcengineering/kanban'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient, statusStore } from '@hcengineering/presentation'
import { Kanban, SpaceWithStates, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task'
import { getEventPositionElement, Label, showPopup } from '@hcengineering/ui'
import {
AttributeModel,
CategoryOption,
Viewlet,
ViewOptionModel,
ViewOptions,
ViewQueryOption
} from '@hcengineering/view'
import {
ActionContext,
focusStore,
getCategories,
getCategorySpaces,
getGroupByValues,
getPresenter,
groupBy,
ListSelectionProvider,
Menu,
noCategory,
SelectDirection,
selectionStore,
setGroupByValues
} from '@hcengineering/view-resources'
import view from '@hcengineering/view-resources/src/plugin'
import { onMount } from 'svelte'
import task from '../../plugin'
import KanbanDragDone from './KanbanDragDone.svelte'
export let _class: Ref<Class<Task>>
export let space: Ref<SpaceWithStates> | undefined = undefined
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let query: DocumentQuery<Task> = {}
export let viewOptionsConfig: ViewOptionModel[] | undefined
export let viewOptions: ViewOptions
export let viewlet: Viewlet
export let options: FindOptions<Task> | undefined
$: currentSpace = space
$: groupByKey = (viewOptions.groupBy[0] ?? noCategory) as TaskGrouping
$: orderBy = viewOptions.orderBy
$: sort = { [orderBy[0]]: orderBy[1] }
$: dontUpdateRank = orderBy[0] !== TaskOrdering.Manual
const spaceQuery = createQuery()
let currentProject: SpaceWithStates | undefined
$: spaceQuery.query(task.class.SpaceWithStates, { _id: currentSpace }, (res) => {
currentProject = res.shift()
})
let resultQuery: DocumentQuery<any> = { ...query }
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => (resultQuery = { ...p, ...query }))
const client = getClient()
const hierarchy = client.getHierarchy()
async function getResultQuery (
query: DocumentQuery<Task>,
viewOptions: ViewOptionModel[] | undefined,
viewOptionsStore: ViewOptions
): Promise<DocumentQuery<Task>> {
if (viewOptions === undefined) return query
let result = hierarchy.clone(query)
for (const viewOption of viewOptions) {
if (viewOption.actionTarget !== 'query') continue
const queryOption = viewOption as ViewQueryOption
const f = await getResource(queryOption.action)
result = f(viewOptionsStore[queryOption.key] ?? queryOption.defaultValue, query)
}
return result
}
let kanbanUI: KanbanUI
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
kanbanUI?.select(offset, of, dir)
})
onMount(() => {
;(document.activeElement as HTMLElement)?.blur()
})
const showMenu = async (ev: MouseEvent, items: Doc[]): Promise<void> => {
ev.preventDefault()
showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(ev), () => {
// selection = undefined
})
}
const issuesQuery = createQuery()
let tasks: Task[] = []
$: groupByDocs = groupBy(tasks, groupByKey, categories)
$: issuesQuery.query<Task>(
_class,
resultQuery,
(result) => {
tasks = result
},
{
...options,
lookup: {
...options?.lookup,
assignee: contact.class.Employee,
space: task.class.SpaceWithStates,
state: task.class.State,
doneState: task.class.DoneState
},
sort: {
...options?.sort,
...sort
}
}
)
$: listProvider.update(tasks)
let categories: CategoryType[] = []
const queryId = generateId()
$: updateCategories(_class, tasks, groupByKey, viewOptions, viewOptionsConfig)
function update () {
updateCategories(_class, tasks, groupByKey, viewOptions, viewOptionsConfig)
}
async function updateCategories (
_class: Ref<Class<Doc>>,
docs: Doc[],
groupByKey: string,
viewOptions: ViewOptions,
viewOptionsModel: ViewOptionModel[] | undefined
) {
categories = await getCategories(client, _class, docs, groupByKey, $statusStore, viewlet.descriptor)
for (const viewOption of viewOptionsModel ?? []) {
if (viewOption.actionTarget !== 'category') continue
const categoryFunc = viewOption as CategoryOption
if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
const categoryAction = await getResource(categoryFunc.action)
const spaces = getCategorySpaces(categories)
if (space !== undefined) {
spaces.push(space)
}
const res = await categoryAction(
_class,
spaces.length > 0 ? { space: { $in: Array.from(spaces.values()) } } : {},
groupByKey,
update,
queryId,
$statusStore,
viewlet.descriptor
)
if (res !== undefined) {
categories = res
break
}
}
}
}
function getHeader (_class: Ref<Class<Doc>>, groupByKey: string): void {
if (groupByKey === noCategory) {
headerComponent = undefined
} else {
getPresenter(client, _class, { key: groupByKey }, { key: groupByKey }).then((p) => (headerComponent = p))
}
}
let headerComponent: AttributeModel | undefined
$: getHeader(_class, groupByKey)
const getUpdateProps = (doc: Doc, category: CategoryType): DocumentUpdate<Item> | undefined => {
const groupValue =
typeof category === 'object' ? category.values.find((it) => it.space === doc.space)?._id : category
if (groupValue === undefined) {
return undefined
}
if ((doc as any)[groupByKey] === groupValue) {
return
}
return {
[groupByKey]: groupValue,
space: doc.space
}
}
$: clazz = client.getHierarchy().getClass(_class)
$: presenterMixin = client.getHierarchy().as(clazz, task.mixin.KanbanCard)
$: cardPresenter = getResource(presenterMixin.card)
let kanban: Kanban
const kanbanQuery = createQuery()
$: kanbanQuery.query(task.class.Kanban, { attachedTo: space }, (result) => {
kanban = result[0]
})
const getDoneUpdate = (e: any) => ({ doneState: e.detail._id } as DocumentUpdate<Doc>)
</script>
{#await cardPresenter then presenter}
<ActionContext
context={{
mode: 'browser'
}}
/>
<KanbanUI
bind:this={kanbanUI}
{categories}
{dontUpdateRank}
objects={tasks}
getGroupByValues={(groupByDocs, category) =>
groupByKey === noCategory ? tasks : getGroupByValues(groupByDocs, category)}
{setGroupByValues}
{getUpdateProps}
{groupByDocs}
on:obj-focus={(evt) => {
listProvider.updateFocus(evt.detail)
}}
selection={listProvider.current($focusStore)}
checked={$selectionStore ?? []}
on:check={(evt) => {
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
}}
on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)}
>
<svelte:fragment slot="header" let:state let:count>
<!-- {@const status = $statusStore.get(state._id)} -->
<div class="header flex-col">
<div class="flex-row-center">
{#if groupByKey === noCategory}
<span class="text-base fs-bold overflow-label content-accent-color pointer-events-none">
<Label label={view.string.NoGrouping} />
</span>
{:else if headerComponent}
<svelte:component this={headerComponent.presenter} value={state} {space} kind={'list-header'} />
{/if}
<span class="ml-1">
{count}
</span>
</div>
</div>
</svelte:fragment>
<svelte:fragment slot="card" let:object let:dragged>
<svelte:component this={presenter} {object} {dragged} {groupByKey} />
</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(getDoneUpdate(e))
}}
/>
</svelte:fragment>
</KanbanUI>
{/await}
<style lang="scss">
.names {
font-size: 0.8125rem;
}
.header {
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--divider-color);
.label {
color: var(--caption-color);
.counter {
color: rgba(var(--caption-color), 0.8);
}
}
}
.tracker-card {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
// padding: 0.5rem 1rem;
min-height: 6.5rem;
}
.states-bar {
flex-shrink: 10;
width: fit-content;
margin: 0.625rem 1rem 0;
}
</style>