mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 19:58:09 +00:00
kanban fix (#2650)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
a79c030597
commit
0d0e5f6f7b
@ -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"
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user