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 { 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<StateType, Item[]> = new Map<StateType, Item[]>()
let isDragging = false
async function updateDone (query: DocumentUpdate<Item>): Promise<void> {
@ -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<void> => {
selection = object.pos
if (!checkedSet.has(object.it._id)) {
const showMenu = async (evt: MouseEvent, object: Item): Promise<void> => {
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 })
}
</script>
@ -263,7 +289,7 @@
<ScrollBox>
<div class="kanban-content">
{#each states as state, si (state._id)}
{@const stateObjects = getStateObjects(objects, state, dragCard)}
{@const stateObjects = objectByState.get(state._id) ?? []}
<div
class="panel-container step-lr75"

View File

@ -16,9 +16,9 @@
import { Doc, Ref } from '@hcengineering/core'
import { createEventDispatcher } from 'svelte'
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 dragCard: Item | undefined
export let objects: Item[]
@ -26,10 +26,10 @@
export let checkedSet: Set<Ref<Doc>>
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 @@
</script>
{#each stateObjects as object, i}
{@const dragged = isDragging && object.it._id === dragCard?._id}
{@const dragged = isDragging && object._id === dragCard?._id}
<div
bind:this={stateRefs[i]}
transition:slideD|local={{ isDragging }}
@ -61,9 +61,9 @@
>
<div
class="card-container"
class:selection={selection !== undefined ? objects[selection]?._id === object.it._id : false}
class:checked={checkedSet.has(object.it._id)}
on:mouseover={() => 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
}}
>
<slot name="card" object={toAny(object.it)} {dragged} />
<slot name="card" object={toAny(object)} {dragged} />
</div>
</div>
{/each}

View File

@ -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
*/

View File

@ -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]