From 0d0e5f6f7b52c3d98171876296c698c15463a53a Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Fri, 17 Feb 2023 08:54:00 +0600 Subject: [PATCH] kanban fix (#2650) Signed-off-by: Denis Bykhov --- packages/kanban/src/components/Kanban.svelte | 140 +++++++++++------- .../kanban/src/components/KanbanRow.svelte | 24 +-- packages/kanban/src/types.ts | 10 +- .../src/components/ViewOptions.svelte | 3 +- 4 files changed, 98 insertions(+), 79 deletions(-) diff --git a/packages/kanban/src/components/Kanban.svelte b/packages/kanban/src/components/Kanban.svelte index 19322112c8..09a35e8688 100644 --- a/packages/kanban/src/components/Kanban.svelte +++ b/packages/kanban/src/components/Kanban.svelte @@ -17,7 +17,7 @@ import { createQuery, getClient } from '@hcengineering/presentation' import { getPlatformColor, ScrollBox, Scroller } from '@hcengineering/ui' import { createEventDispatcher } from 'svelte' - import { CardDragEvent, ExtItem, Item, StateType, TypeState } from '../types' + import { CardDragEvent, Item, StateType, TypeState } from '../types' import { calcRank } from '../utils' import KanbanRow from './KanbanRow.svelte' @@ -40,6 +40,7 @@ query, (result) => { objects = result + fillStateObjects(result, fieldName) dispatch('content', objects) }, { @@ -48,18 +49,20 @@ } ) - function getStateObjects ( - objects: Item[], - state: TypeState, - dragItem?: Item // required for svelte to properly recalculate state. - ): ExtItem[] { - const stateCards = objects.filter((it) => (it as any)[fieldName] === state._id) - return stateCards.map((it, idx, arr) => ({ - it, - prev: arr[idx - 1], - next: arr[idx + 1], - pos: objects.findIndex((pi) => pi._id === it._id) - })) + function fieldNameChange (fieldName: string) { + fillStateObjects(objects, fieldName) + } + + $: fieldNameChange(fieldName) + + function fillStateObjects (objects: Item[], fieldName: string): void { + objectByState.clear() + for (const object of objects) { + const arr = objectByState.get((object as any)[fieldName]) ?? [] + arr.push(object) + objectByState.set((object as any)[fieldName], arr) + } + objectByState = objectByState } async function move (state: StateType) { @@ -94,6 +97,8 @@ let dragCardInitialRank: string | undefined let dragCardInitialState: StateType + let objectByState: Map = new Map() + let isDragging = false async function updateDone (query: DocumentUpdate): Promise { @@ -103,47 +108,71 @@ } await client.update(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) - } - } function panelDragOver (event: Event, state: TypeState): void { event.preventDefault() const card = dragCard as any if (card !== undefined && card[fieldName] !== state._id) { + const oldArr = objectByState.get(card[fieldName]) ?? [] + const index = oldArr.findIndex((p) => p._id === card._id) + if (index !== -1) { + oldArr.splice(index, 1) + objectByState.set(card[fieldName], oldArr) + } card[fieldName] = state._id - const objs = getStateObjects(objects, state) - if (!dontUpdateRank) { - card.rank = calcRank(objs[objs.length - 1]?.it, undefined) + const arr = objectByState.get(card[fieldName]) ?? [] + arr.push(card) + objectByState.set(card[fieldName], arr) + objectByState = objectByState + } + } + + function dragswap (ev: MouseEvent, i: number, s: number): boolean { + if (s === -1) return false + if (i < s) { + return ev.offsetY < (ev.target as HTMLElement).offsetHeight / 2 + } else if (i > s) { + return ev.offsetY > (ev.target as HTMLElement).offsetHeight / 2 + } + return false + } + + function cardDragOver (evt: CardDragEvent, object: Item): void { + if (dragCard !== undefined && !dontUpdateRank) { + if (object._id !== dragCard._id) { + let arr = objectByState.get((object as any)[fieldName]) ?? [] + const dragCardIndex = arr.findIndex((p) => p._id === dragCard?._id) + const targetIndex = arr.findIndex((p) => p._id === object._id) + if ( + dragswap(evt, targetIndex, dragCardIndex) && + arr[targetIndex] !== undefined && + arr[dragCardIndex] !== undefined + ) { + arr.splice(dragCardIndex, 1) + arr = [...arr.slice(0, targetIndex), dragCard, ...arr.slice(targetIndex)] + objectByState.set((object as any)[fieldName], arr) + objectByState = objectByState + } } } } - function cardDragOver (evt: CardDragEvent, object: ExtItem): void { - if (dragCard !== undefined && !dontUpdateRank) { - dragCard.rank = doCalcRank(object, evt) - } - } - function cardDrop (evt: CardDragEvent, object: ExtItem): void { + function cardDrop (evt: CardDragEvent, object: Item): void { if (!dontUpdateRank && dragCard !== undefined) { - dragCard.rank = doCalcRank(object, evt) + const arr = objectByState.get((object as any)[fieldName]) ?? [] + const s = arr.findIndex((p) => p._id === dragCard?._id) + if (s !== -1) { + const newRank = calcRank(arr[s - 1], arr[s + 1]) + dragCard.rank = newRank + } } isDragging = false } - function onDragStart (object: ExtItem, state: TypeState): void { + function onDragStart (object: Item, state: TypeState): void { dragCardInitialState = state._id - dragCardInitialRank = object.it.rank - dragCard = object.it + dragCardInitialRank = object.rank + dragCard = object isDragging = true - dispatch('obj-focus', object.it) + dispatch('obj-focus', object) } // eslint-disable-next-line let dragged: boolean = false @@ -152,9 +181,6 @@ return object } - // eslint-disable-next-line no-unused-vars - let stateObjects: ExtItem[] - const stateRefs: HTMLElement[] = [] const stateRows: KanbanRow[] = [] @@ -170,9 +196,9 @@ let pos = (of !== undefined ? objects.findIndex((it) => it._id === of._id) : selection) ?? -1 if (pos === -1) { for (const st of states) { - const stateObjs = getStateObjects(objects, st) + const stateObjs = objectByState.get(st) ?? [] if (stateObjs.length > 0) { - pos = objects.findIndex((it) => it._id === stateObjs[0].it._id) + pos = objects.findIndex((it) => it._id === stateObjs[0]._id) break } } @@ -194,24 +220,24 @@ if (objState === -1) { return } - const stateObjs = getStateObjects(objects, states[objState]) - const statePos = stateObjs.findIndex((it) => it.it._id === obj._id) + const stateObjs = objectByState.get(states[objState]) ?? [] + const statePos = stateObjs.findIndex((it) => it._id === obj._id) if (statePos === undefined) { return } if (offset === -1) { if (dir === undefined || dir === 'vertical') { - const obj = (stateObjs[statePos - 1] ?? stateObjs[0]).it + const obj = stateObjs[statePos - 1] ?? stateObjs[0] scrollInto(objState, obj) dispatch('obj-focus', obj) return } else { while (objState > 0) { objState-- - const nstateObjs = getStateObjects(objects, states[objState]) + const nstateObjs = objectByState.get(states[objState]) ?? [] if (nstateObjs.length > 0) { - const obj = (nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]).it + const obj = nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1] scrollInto(objState, obj) dispatch('obj-focus', obj) break @@ -221,16 +247,16 @@ } if (offset === 1) { if (dir === undefined || dir === 'vertical') { - const obj = (stateObjs[statePos + 1] ?? stateObjs[stateObjs.length - 1]).it + const obj = stateObjs[statePos + 1] ?? stateObjs[stateObjs.length - 1] scrollInto(objState, obj) dispatch('obj-focus', obj) return } else { while (objState < states.length - 1) { objState++ - const nstateObjs = getStateObjects(objects, states[objState]) + const nstateObjs = objectByState.get(states[objState]) ?? [] if (nstateObjs.length > 0) { - const obj = (nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]).it + const obj = nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1] scrollInto(objState, obj) dispatch('obj-focus', obj) break @@ -249,13 +275,13 @@ export function check (docs: Doc[], value: boolean) { dispatch('check', { docs, value }) } - const showMenu = async (evt: MouseEvent, object: ExtItem): Promise => { - selection = object.pos - if (!checkedSet.has(object.it._id)) { + const showMenu = async (evt: MouseEvent, object: Item): Promise => { + selection = objects.findIndex((p) => p._id === object._id) + if (!checkedSet.has(object._id)) { check(objects, false) checked = [] } - dispatch('contextmenu', { evt, objects: checked.length > 0 ? checked : object.it }) + dispatch('contextmenu', { evt, objects: checked.length > 0 ? checked : object }) } @@ -263,7 +289,7 @@
{#each states as state, si (state._id)} - {@const stateObjects = getStateObjects(objects, state, dragCard)} + {@const stateObjects = objectByState.get(state._id) ?? []}
> export let state: TypeState - export let cardDragOver: (evt: CardDragEvent, object: ExtItem) => void - export let cardDrop: (evt: CardDragEvent, object: ExtItem) => void - export let onDragStart: (object: ExtItem, state: TypeState) => void - export let showMenu: (evt: MouseEvent, object: ExtItem) => void + export let cardDragOver: (evt: CardDragEvent, object: Item) => void + export let cardDrop: (evt: CardDragEvent, object: Item) => void + export let onDragStart: (object: Item, state: TypeState) => void + export let showMenu: (evt: MouseEvent, object: Item) => void const dispatch = createEventDispatcher() @@ -43,7 +43,7 @@ $: stateRefs.length = stateObjects.length export function scroll (item: Item): void { - const pos = stateObjects.findIndex((it) => it.it._id === item._id) + const pos = stateObjects.findIndex((it) => it._id === item._id) if (pos >= 0) { stateRefs[pos]?.scrollIntoView({ behavior: 'auto', block: 'nearest' }) } @@ -51,7 +51,7 @@ {#each stateObjects as object, i} - {@const dragged = isDragging && object.it._id === dragCard?._id} + {@const dragged = isDragging && object._id === dragCard?._id}
dispatch('obj-focus', object.it)} + class:selection={selection !== undefined ? objects[selection]?._id === object._id : false} + class:checked={checkedSet.has(object._id)} + on:mouseover={() => dispatch('obj-focus', object)} on:focus={() => {}} on:contextmenu={(evt) => showMenu(evt, object)} draggable={true} @@ -76,7 +76,7 @@ isDragging = false }} > - +
{/each} diff --git a/packages/kanban/src/types.ts b/packages/kanban/src/types.ts index 34a4bd8dbc..c08cd1bfbf 100644 --- a/packages/kanban/src/types.ts +++ b/packages/kanban/src/types.ts @@ -23,15 +23,7 @@ export interface TypeState { * @public */ export type Item = DocWithRank & { state: StateType, doneState: StateType | null } -/** - * @public - */ -export interface ExtItem { - prev?: Item - it: Item - next?: Item - pos: number -} + /** * @public */ diff --git a/plugins/view-resources/src/components/ViewOptions.svelte b/plugins/view-resources/src/components/ViewOptions.svelte index 2be4e82adb..0392b7dba1 100644 --- a/plugins/view-resources/src/components/ViewOptions.svelte +++ b/plugins/view-resources/src/components/ViewOptions.svelte @@ -14,7 +14,8 @@ const dispatch = createEventDispatcher() const groups = - viewOptions.groupBy[viewOptions.groupBy.length - 1] === noCategory + viewOptions.groupBy[viewOptions.groupBy.length - 1] === noCategory || + viewOptions.groupBy.length === config.groupDepth ? [...viewOptions.groupBy] : [...viewOptions.groupBy, noCategory]