kanban fix (#2650)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-02-17 08:54:00 +06:00 committed by GitHub
parent a79c030597
commit 0d0e5f6f7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 98 additions and 79 deletions

View File

@ -17,7 +17,7 @@
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { getPlatformColor, ScrollBox, Scroller } from '@hcengineering/ui' import { getPlatformColor, ScrollBox, Scroller } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { CardDragEvent, ExtItem, Item, StateType, TypeState } from '../types' import { CardDragEvent, Item, StateType, TypeState } from '../types'
import { calcRank } from '../utils' import { calcRank } from '../utils'
import KanbanRow from './KanbanRow.svelte' import KanbanRow from './KanbanRow.svelte'
@ -40,6 +40,7 @@
query, query,
(result) => { (result) => {
objects = result objects = result
fillStateObjects(result, fieldName)
dispatch('content', objects) dispatch('content', objects)
}, },
{ {
@ -48,18 +49,20 @@
} }
) )
function getStateObjects ( function fieldNameChange (fieldName: string) {
objects: Item[], fillStateObjects(objects, fieldName)
state: TypeState, }
dragItem?: Item // required for svelte to properly recalculate state.
): ExtItem[] { $: fieldNameChange(fieldName)
const stateCards = objects.filter((it) => (it as any)[fieldName] === state._id)
return stateCards.map((it, idx, arr) => ({ function fillStateObjects (objects: Item[], fieldName: string): void {
it, objectByState.clear()
prev: arr[idx - 1], for (const object of objects) {
next: arr[idx + 1], const arr = objectByState.get((object as any)[fieldName]) ?? []
pos: objects.findIndex((pi) => pi._id === it._id) arr.push(object)
})) objectByState.set((object as any)[fieldName], arr)
}
objectByState = objectByState
} }
async function move (state: StateType) { async function move (state: StateType) {
@ -94,6 +97,8 @@
let dragCardInitialRank: string | undefined let dragCardInitialRank: string | undefined
let dragCardInitialState: StateType let dragCardInitialState: StateType
let objectByState: Map<StateType, Item[]> = new Map<StateType, Item[]>()
let isDragging = false let isDragging = false
async function updateDone (query: DocumentUpdate<Item>): Promise<void> { async function updateDone (query: DocumentUpdate<Item>): Promise<void> {
@ -103,47 +108,71 @@
} }
await client.update(dragCard, query) 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 { function panelDragOver (event: Event, state: TypeState): void {
event.preventDefault() event.preventDefault()
const card = dragCard as any const card = dragCard as any
if (card !== undefined && card[fieldName] !== state._id) { 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 card[fieldName] = state._id
const objs = getStateObjects(objects, state) const arr = objectByState.get(card[fieldName]) ?? []
if (!dontUpdateRank) { arr.push(card)
card.rank = calcRank(objs[objs.length - 1]?.it, undefined) 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 { function cardDrop (evt: CardDragEvent, object: Item): void {
if (dragCard !== undefined && !dontUpdateRank) {
dragCard.rank = doCalcRank(object, evt)
}
}
function cardDrop (evt: CardDragEvent, object: ExtItem): void {
if (!dontUpdateRank && dragCard !== undefined) { 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 isDragging = false
} }
function onDragStart (object: ExtItem, state: TypeState): void { function onDragStart (object: Item, state: TypeState): void {
dragCardInitialState = state._id dragCardInitialState = state._id
dragCardInitialRank = object.it.rank dragCardInitialRank = object.rank
dragCard = object.it dragCard = object
isDragging = true isDragging = true
dispatch('obj-focus', object.it) dispatch('obj-focus', object)
} }
// eslint-disable-next-line // eslint-disable-next-line
let dragged: boolean = false let dragged: boolean = false
@ -152,9 +181,6 @@
return object return object
} }
// eslint-disable-next-line no-unused-vars
let stateObjects: ExtItem[]
const stateRefs: HTMLElement[] = [] const stateRefs: HTMLElement[] = []
const stateRows: KanbanRow[] = [] const stateRows: KanbanRow[] = []
@ -170,9 +196,9 @@
let pos = (of !== undefined ? objects.findIndex((it) => it._id === of._id) : selection) ?? -1 let pos = (of !== undefined ? objects.findIndex((it) => it._id === of._id) : selection) ?? -1
if (pos === -1) { if (pos === -1) {
for (const st of states) { for (const st of states) {
const stateObjs = getStateObjects(objects, st) const stateObjs = objectByState.get(st) ?? []
if (stateObjs.length > 0) { if (stateObjs.length > 0) {
pos = objects.findIndex((it) => it._id === stateObjs[0].it._id) pos = objects.findIndex((it) => it._id === stateObjs[0]._id)
break break
} }
} }
@ -194,24 +220,24 @@
if (objState === -1) { if (objState === -1) {
return return
} }
const stateObjs = getStateObjects(objects, states[objState]) const stateObjs = objectByState.get(states[objState]) ?? []
const statePos = stateObjs.findIndex((it) => it.it._id === obj._id) const statePos = stateObjs.findIndex((it) => it._id === obj._id)
if (statePos === undefined) { if (statePos === undefined) {
return return
} }
if (offset === -1) { if (offset === -1) {
if (dir === undefined || dir === 'vertical') { if (dir === undefined || dir === 'vertical') {
const obj = (stateObjs[statePos - 1] ?? stateObjs[0]).it const obj = stateObjs[statePos - 1] ?? stateObjs[0]
scrollInto(objState, obj) scrollInto(objState, obj)
dispatch('obj-focus', obj) dispatch('obj-focus', obj)
return return
} else { } else {
while (objState > 0) { while (objState > 0) {
objState-- objState--
const nstateObjs = getStateObjects(objects, states[objState]) const nstateObjs = objectByState.get(states[objState]) ?? []
if (nstateObjs.length > 0) { if (nstateObjs.length > 0) {
const obj = (nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]).it const obj = nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]
scrollInto(objState, obj) scrollInto(objState, obj)
dispatch('obj-focus', obj) dispatch('obj-focus', obj)
break break
@ -221,16 +247,16 @@
} }
if (offset === 1) { if (offset === 1) {
if (dir === undefined || dir === 'vertical') { 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) scrollInto(objState, obj)
dispatch('obj-focus', obj) dispatch('obj-focus', obj)
return return
} else { } else {
while (objState < states.length - 1) { while (objState < states.length - 1) {
objState++ objState++
const nstateObjs = getStateObjects(objects, states[objState]) const nstateObjs = objectByState.get(states[objState]) ?? []
if (nstateObjs.length > 0) { if (nstateObjs.length > 0) {
const obj = (nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]).it const obj = nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]
scrollInto(objState, obj) scrollInto(objState, obj)
dispatch('obj-focus', obj) dispatch('obj-focus', obj)
break break
@ -249,13 +275,13 @@
export function check (docs: Doc[], value: boolean) { export function check (docs: Doc[], value: boolean) {
dispatch('check', { docs, value }) dispatch('check', { docs, value })
} }
const showMenu = async (evt: MouseEvent, object: ExtItem): Promise<void> => { const showMenu = async (evt: MouseEvent, object: Item): Promise<void> => {
selection = object.pos selection = objects.findIndex((p) => p._id === object._id)
if (!checkedSet.has(object.it._id)) { if (!checkedSet.has(object._id)) {
check(objects, false) check(objects, false)
checked = [] checked = []
} }
dispatch('contextmenu', { evt, objects: checked.length > 0 ? checked : object.it }) dispatch('contextmenu', { evt, objects: checked.length > 0 ? checked : object })
} }
</script> </script>
@ -263,7 +289,7 @@
<ScrollBox> <ScrollBox>
<div class="kanban-content"> <div class="kanban-content">
{#each states as state, si (state._id)} {#each states as state, si (state._id)}
{@const stateObjects = getStateObjects(objects, state, dragCard)} {@const stateObjects = objectByState.get(state._id) ?? []}
<div <div
class="panel-container step-lr75" class="panel-container step-lr75"

View File

@ -16,9 +16,9 @@
import { Doc, Ref } from '@hcengineering/core' import { Doc, Ref } from '@hcengineering/core'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { slide } from 'svelte/transition' import { slide } from 'svelte/transition'
import { CardDragEvent, ExtItem, Item, TypeState } from '../types' import { CardDragEvent, Item, TypeState } from '../types'
export let stateObjects: ExtItem[] export let stateObjects: Item[]
export let isDragging: boolean export let isDragging: boolean
export let dragCard: Item | undefined export let dragCard: Item | undefined
export let objects: Item[] export let objects: Item[]
@ -26,10 +26,10 @@
export let checkedSet: Set<Ref<Doc>> export let checkedSet: Set<Ref<Doc>>
export let state: TypeState export let state: TypeState
export let cardDragOver: (evt: CardDragEvent, object: ExtItem) => void export let cardDragOver: (evt: CardDragEvent, object: Item) => void
export let cardDrop: (evt: CardDragEvent, object: ExtItem) => void export let cardDrop: (evt: CardDragEvent, object: Item) => void
export let onDragStart: (object: ExtItem, state: TypeState) => void export let onDragStart: (object: Item, state: TypeState) => void
export let showMenu: (evt: MouseEvent, object: ExtItem) => void export let showMenu: (evt: MouseEvent, object: Item) => void
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -43,7 +43,7 @@
$: stateRefs.length = stateObjects.length $: stateRefs.length = stateObjects.length
export function scroll (item: Item): void { 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) { if (pos >= 0) {
stateRefs[pos]?.scrollIntoView({ behavior: 'auto', block: 'nearest' }) stateRefs[pos]?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
} }
@ -51,7 +51,7 @@
</script> </script>
{#each stateObjects as object, i} {#each stateObjects as object, i}
{@const dragged = isDragging && object.it._id === dragCard?._id} {@const dragged = isDragging && object._id === dragCard?._id}
<div <div
bind:this={stateRefs[i]} bind:this={stateRefs[i]}
transition:slideD|local={{ isDragging }} transition:slideD|local={{ isDragging }}
@ -61,9 +61,9 @@
> >
<div <div
class="card-container" class="card-container"
class:selection={selection !== undefined ? objects[selection]?._id === object.it._id : false} class:selection={selection !== undefined ? objects[selection]?._id === object._id : false}
class:checked={checkedSet.has(object.it._id)} class:checked={checkedSet.has(object._id)}
on:mouseover={() => dispatch('obj-focus', object.it)} on:mouseover={() => dispatch('obj-focus', object)}
on:focus={() => {}} on:focus={() => {}}
on:contextmenu={(evt) => showMenu(evt, object)} on:contextmenu={(evt) => showMenu(evt, object)}
draggable={true} draggable={true}
@ -76,7 +76,7 @@
isDragging = false isDragging = false
}} }}
> >
<slot name="card" object={toAny(object.it)} {dragged} /> <slot name="card" object={toAny(object)} {dragged} />
</div> </div>
</div> </div>
{/each} {/each}

View File

@ -23,15 +23,7 @@ export interface TypeState {
* @public * @public
*/ */
export type Item = DocWithRank & { state: StateType, doneState: StateType | null } export type Item = DocWithRank & { state: StateType, doneState: StateType | null }
/**
* @public
*/
export interface ExtItem {
prev?: Item
it: Item
next?: Item
pos: number
}
/** /**
* @public * @public
*/ */

View File

@ -14,7 +14,8 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const groups = 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]
: [...viewOptions.groupBy, noCategory] : [...viewOptions.groupBy, noCategory]