mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-20 07:10:02 +00:00
UBERF-4413: Kanban with huge data sets (#4076)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
fb4cdfba5e
commit
7f03c18007
@ -13,16 +13,33 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CategoryType, Doc, DocumentUpdate, Ref } from '@hcengineering/core'
|
import {
|
||||||
|
CategoryType,
|
||||||
|
Class,
|
||||||
|
Doc,
|
||||||
|
DocumentQuery,
|
||||||
|
DocumentUpdate,
|
||||||
|
FindOptions,
|
||||||
|
RateLimitter,
|
||||||
|
Ref,
|
||||||
|
Space
|
||||||
|
} from '@hcengineering/core'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import { ScrollBox, Scroller } from '@hcengineering/ui'
|
import { ScrollBox, Scroller } from '@hcengineering/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { CardDragEvent, Item } from '../types'
|
import { CardDragEvent, DocWithRank, Item } from '../types'
|
||||||
import { calcRank } from '../utils'
|
import { calcRank } from '../utils'
|
||||||
import KanbanRow from './KanbanRow.svelte'
|
import KanbanRow from './KanbanRow.svelte'
|
||||||
|
|
||||||
export let categories: CategoryType[] = []
|
export let categories: CategoryType[] = []
|
||||||
export let objects: Item[] = []
|
|
||||||
|
export let _class: Ref<Class<DocWithRank>>
|
||||||
|
export let space: Ref<Space> | undefined = undefined
|
||||||
|
export let query: DocumentQuery<DocWithRank> = {}
|
||||||
|
export let options: FindOptions<DocWithRank> | undefined = undefined
|
||||||
|
export let objects: DocWithRank[] = []
|
||||||
|
export let groupByKey: any
|
||||||
|
|
||||||
export let groupByDocs: Record<string | number, Item[]>
|
export let groupByDocs: Record<string | number, Item[]>
|
||||||
export let getGroupByValues: (groupByDocs: Record<string | number, Item[]>, category: CategoryType) => Item[]
|
export let getGroupByValues: (groupByDocs: Record<string | number, Item[]>, category: CategoryType) => Item[]
|
||||||
export let setGroupByValues: (
|
export let setGroupByValues: (
|
||||||
@ -40,7 +57,9 @@
|
|||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
async function move (state: CategoryType) {
|
const limiter = new RateLimitter(() => ({ rate: 10 }))
|
||||||
|
|
||||||
|
async function move (state: CategoryType): Promise<void> {
|
||||||
if (dragCard === undefined) {
|
if (dragCard === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -48,13 +67,18 @@
|
|||||||
const canDrop = !dragCardAvailableCategories || dragCardAvailableCategories.includes(state)
|
const canDrop = !dragCardAvailableCategories || dragCardAvailableCategories.includes(state)
|
||||||
|
|
||||||
if (!canDrop) {
|
if (!canDrop) {
|
||||||
|
dragCard = undefined
|
||||||
|
dragCardAvailableCategories = undefined
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let updates = getUpdateProps(dragCard, state)
|
let updates = getUpdateProps(dragCard, state)
|
||||||
|
|
||||||
if (updates === undefined) {
|
if (updates === undefined) {
|
||||||
|
console.log('no update props')
|
||||||
panelDragLeave(undefined, dragCardState)
|
panelDragLeave(undefined, dragCardState)
|
||||||
|
dragCard = undefined
|
||||||
|
dragCardAvailableCategories = undefined
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,6 +125,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updates = getUpdateProps(dragCard, state)
|
const updates = getUpdateProps(dragCard, state)
|
||||||
|
console.log('UPD', updates)
|
||||||
if (updates === undefined) {
|
if (updates === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -185,7 +210,7 @@
|
|||||||
}
|
}
|
||||||
isDragging = false
|
isDragging = false
|
||||||
}
|
}
|
||||||
async function onDragStart (object: Item, state: CategoryType) {
|
async function onDragStart (object: Item, state: CategoryType): Promise<void> {
|
||||||
dragCardInitialState = state
|
dragCardInitialState = state
|
||||||
dragCardState = state
|
dragCardState = state
|
||||||
dragCardInitialRank = object.rank
|
dragCardInitialRank = object.rank
|
||||||
@ -306,7 +331,7 @@
|
|||||||
|
|
||||||
$: checkedSet = new Set<Ref<Doc>>(checked.map((it) => it._id))
|
$: checkedSet = new Set<Ref<Doc>>(checked.map((it) => it._id))
|
||||||
|
|
||||||
export function check (docs: Doc[], value: boolean) {
|
export function check (docs: Doc[], value: boolean): void {
|
||||||
dispatch('check', { docs, value })
|
dispatch('check', { docs, value })
|
||||||
}
|
}
|
||||||
const showMenu = async (evt: MouseEvent, object: Item): Promise<void> => {
|
const showMenu = async (evt: MouseEvent, object: Item): Promise<void> => {
|
||||||
@ -333,8 +358,9 @@
|
|||||||
panelDragOver(event, state)
|
panelDragOver(event, state)
|
||||||
}}
|
}}
|
||||||
on:drop={() => {
|
on:drop={() => {
|
||||||
move(state)
|
void move(state).then(() => {
|
||||||
isDragging = false
|
isDragging = false
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if $$slots.header !== undefined}
|
{#if $$slots.header !== undefined}
|
||||||
@ -354,6 +380,7 @@
|
|||||||
{selection}
|
{selection}
|
||||||
{checkedSet}
|
{checkedSet}
|
||||||
{state}
|
{state}
|
||||||
|
{limiter}
|
||||||
cardDragOver={(evt, obj) => {
|
cardDragOver={(evt, obj) => {
|
||||||
cardDragOver(evt, obj, state)
|
cardDragOver(evt, obj, state)
|
||||||
}}
|
}}
|
||||||
@ -362,6 +389,10 @@
|
|||||||
}}
|
}}
|
||||||
{onDragStart}
|
{onDragStart}
|
||||||
{showMenu}
|
{showMenu}
|
||||||
|
{_class}
|
||||||
|
{query}
|
||||||
|
{options}
|
||||||
|
{groupByKey}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="card" let:object let:dragged>
|
<svelte:fragment slot="card" let:object let:dragged>
|
||||||
<slot name="card" {object} {dragged} />
|
<slot name="card" {object} {dragged} />
|
||||||
|
@ -13,11 +13,23 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CategoryType, Doc, Ref } from '@hcengineering/core'
|
import {
|
||||||
|
CategoryType,
|
||||||
|
Class,
|
||||||
|
Doc,
|
||||||
|
DocumentQuery,
|
||||||
|
FindOptions,
|
||||||
|
IdMap,
|
||||||
|
RateLimitter,
|
||||||
|
Ref,
|
||||||
|
Space,
|
||||||
|
toIdMap
|
||||||
|
} from '@hcengineering/core'
|
||||||
import ui, { Button, IconMoreH, Lazy, mouseAttractor } from '@hcengineering/ui'
|
import ui, { Button, IconMoreH, Lazy, mouseAttractor } from '@hcengineering/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { slide } from 'svelte/transition'
|
import { slide } from 'svelte/transition'
|
||||||
import { CardDragEvent, DocWithRank, Item } from '../types'
|
import { CardDragEvent, DocWithRank, Item } from '../types'
|
||||||
|
import { createQuery } from '@hcengineering/presentation'
|
||||||
|
|
||||||
export let stateObjects: Item[]
|
export let stateObjects: Item[]
|
||||||
export let isDragging: boolean
|
export let isDragging: boolean
|
||||||
@ -27,6 +39,13 @@
|
|||||||
export let checkedSet: Set<Ref<Doc>>
|
export let checkedSet: Set<Ref<Doc>>
|
||||||
export let state: CategoryType
|
export let state: CategoryType
|
||||||
|
|
||||||
|
export let _class: Ref<Class<DocWithRank>>
|
||||||
|
export let space: Ref<Space> | undefined = undefined
|
||||||
|
export let query: DocumentQuery<DocWithRank> = {}
|
||||||
|
export let options: FindOptions<DocWithRank> | undefined = undefined
|
||||||
|
export let groupByKey: any
|
||||||
|
export let limiter: RateLimitter
|
||||||
|
|
||||||
export let cardDragOver: (evt: CardDragEvent, object: Item) => void
|
export let cardDragOver: (evt: CardDragEvent, object: Item) => void
|
||||||
export let cardDrop: (evt: CardDragEvent, object: Item) => void
|
export let cardDrop: (evt: CardDragEvent, object: Item) => void
|
||||||
export let onDragStart: (object: Item, state: CategoryType) => void
|
export let onDragStart: (object: Item, state: CategoryType) => void
|
||||||
@ -52,12 +71,28 @@
|
|||||||
|
|
||||||
let limit = 50
|
let limit = 50
|
||||||
|
|
||||||
let limitedObjects: DocWithRank[] = []
|
let limitedObjects: IdMap<DocWithRank> = new Map()
|
||||||
$: limitedObjects = stateObjects.slice(0, limit)
|
|
||||||
|
const docQuery = createQuery()
|
||||||
|
|
||||||
|
$: groupQuery = { ...query, [groupByKey]: typeof state === 'object' ? { $in: state.values } : state }
|
||||||
|
|
||||||
|
$: void limiter.add(async () => {
|
||||||
|
docQuery.query(
|
||||||
|
_class,
|
||||||
|
groupQuery,
|
||||||
|
(res) => {
|
||||||
|
limitedObjects = toIdMap(res)
|
||||||
|
},
|
||||||
|
{ ...options, limit }
|
||||||
|
)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each limitedObjects as object, i (object._id)}
|
{#each stateObjects as objectRef, i (objectRef._id)}
|
||||||
{@const dragged = isDragging && object._id === dragCard?._id}
|
{@const dragged = isDragging && objectRef._id === dragCard?._id}
|
||||||
|
{@const object = limitedObjects.get(objectRef._id) ?? (objectRef._id === dragCard?._id ? dragCard : undefined)}
|
||||||
|
{#if object !== undefined}
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
bind:this={stateRefs[i]}
|
bind:this={stateRefs[i]}
|
||||||
@ -97,11 +132,12 @@
|
|||||||
</Lazy>
|
</Lazy>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#if stateObjects.length > limitedObjects.length}
|
{#if stateObjects.length > limitedObjects.size + (isDragging ? 1 : 0)}
|
||||||
<div class="p-1 flex-no-shrink clear-mins">
|
<div class="p-1 flex-no-shrink clear-mins">
|
||||||
<div class="card-container flex-between p-4">
|
<div class="card-container flex-between p-4">
|
||||||
<span class="caption-color">{limitedObjects.length}</span> / {stateObjects.length}
|
<span class="caption-color">{limitedObjects.size}</span> / {stateObjects.length}
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
icon={IconMoreH}
|
icon={IconMoreH}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import board, { Card } from '@hcengineering/board'
|
import board, { Card } from '@hcengineering/board'
|
||||||
import core, {
|
import {
|
||||||
CategoryType,
|
CategoryType,
|
||||||
Class,
|
Class,
|
||||||
Doc,
|
Doc,
|
||||||
@ -23,23 +23,25 @@
|
|||||||
DocumentUpdate,
|
DocumentUpdate,
|
||||||
FindOptions,
|
FindOptions,
|
||||||
Ref,
|
Ref,
|
||||||
SortingOrder,
|
|
||||||
Status,
|
Status,
|
||||||
WithLookup
|
WithLookup
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { Kanban as KanbanUI } from '@hcengineering/kanban'
|
import { Kanban as KanbanUI } from '@hcengineering/kanban'
|
||||||
import { ActionContext, createQuery } from '@hcengineering/presentation'
|
import { ActionContext, createQuery } from '@hcengineering/presentation'
|
||||||
import type { DocWithRank, Project } from '@hcengineering/task'
|
import type { DocWithRank, Project } from '@hcengineering/task'
|
||||||
import task from '@hcengineering/task'
|
import task, { getStates } from '@hcengineering/task'
|
||||||
import { getEventPositionElement, showPopup } from '@hcengineering/ui'
|
import { getEventPositionElement, showPopup } from '@hcengineering/ui'
|
||||||
|
import { typeStore } from '@hcengineering/task-resources'
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ListSelectionProvider,
|
ListSelectionProvider,
|
||||||
SelectDirection,
|
SelectDirection,
|
||||||
focusStore,
|
focusStore,
|
||||||
|
getCategoryQueryNoLookup,
|
||||||
getGroupByValues,
|
getGroupByValues,
|
||||||
groupBy,
|
groupBy,
|
||||||
setGroupByValues
|
setGroupByValues,
|
||||||
|
statusStore
|
||||||
} from '@hcengineering/view-resources'
|
} from '@hcengineering/view-resources'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import KanbanCard from './KanbanCard.svelte'
|
import KanbanCard from './KanbanCard.svelte'
|
||||||
@ -59,21 +61,8 @@
|
|||||||
_space = result[0]
|
_space = result[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
const statesQuery = createQuery()
|
$: states = getStates(_space, $typeStore, $statusStore.byId)
|
||||||
$: if (_space !== undefined) {
|
|
||||||
statesQuery.query(
|
|
||||||
core.class.Status,
|
|
||||||
{ space: _space.type },
|
|
||||||
(result) => {
|
|
||||||
states = result
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sort: {
|
|
||||||
rank: SortingOrder.Ascending
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
function castObject (object: any): WithLookup<Card> {
|
function castObject (object: any): WithLookup<Card> {
|
||||||
return object as WithLookup<Card>
|
return object as WithLookup<Card>
|
||||||
}
|
}
|
||||||
@ -97,6 +86,8 @@
|
|||||||
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let resultQuery: DocumentQuery<DocWithRank>
|
||||||
|
|
||||||
$: resultQuery = { ...query, isArchived: { $nin: [true] }, space }
|
$: resultQuery = { ...query, isArchived: { $nin: [true] }, space }
|
||||||
|
|
||||||
const cardQuery = createQuery()
|
const cardQuery = createQuery()
|
||||||
@ -104,7 +95,7 @@
|
|||||||
|
|
||||||
$: cardQuery.query<DocWithRank>(
|
$: cardQuery.query<DocWithRank>(
|
||||||
_class,
|
_class,
|
||||||
resultQuery,
|
getCategoryQueryNoLookup(resultQuery),
|
||||||
(result) => {
|
(result) => {
|
||||||
cards = result
|
cards = result
|
||||||
},
|
},
|
||||||
@ -122,7 +113,7 @@
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
state: groupValue,
|
status: groupValue,
|
||||||
space: doc.space
|
space: doc.space
|
||||||
} as any
|
} as any
|
||||||
}
|
}
|
||||||
@ -133,6 +124,7 @@
|
|||||||
mode: 'browser'
|
mode: 'browser'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{states.length}
|
||||||
<KanbanUI
|
<KanbanUI
|
||||||
bind:this={kanbanUI}
|
bind:this={kanbanUI}
|
||||||
objects={cards}
|
objects={cards}
|
||||||
@ -144,6 +136,10 @@
|
|||||||
}}
|
}}
|
||||||
{groupByDocs}
|
{groupByDocs}
|
||||||
{getUpdateProps}
|
{getUpdateProps}
|
||||||
|
{_class}
|
||||||
|
query={resultQuery}
|
||||||
|
{options}
|
||||||
|
groupByKey={'status'}
|
||||||
checked={$selection ?? []}
|
checked={$selection ?? []}
|
||||||
on:check={(evt) => {
|
on:check={(evt) => {
|
||||||
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
||||||
|
@ -22,13 +22,15 @@
|
|||||||
DocumentUpdate,
|
DocumentUpdate,
|
||||||
FindOptions,
|
FindOptions,
|
||||||
generateId,
|
generateId,
|
||||||
|
Lookup,
|
||||||
mergeQueries,
|
mergeQueries,
|
||||||
Ref
|
Ref
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { Item, Kanban as KanbanUI } from '@hcengineering/kanban'
|
import { DocWithRank, Item, Kanban as KanbanUI } from '@hcengineering/kanban'
|
||||||
import { getResource } from '@hcengineering/platform'
|
import { getResource } from '@hcengineering/platform'
|
||||||
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import { Project, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task'
|
import tags from '@hcengineering/tags'
|
||||||
|
import { Project, Task, TaskOrdering } from '@hcengineering/task'
|
||||||
import {
|
import {
|
||||||
ColorDefinition,
|
ColorDefinition,
|
||||||
defaultBackground,
|
defaultBackground,
|
||||||
@ -37,19 +39,12 @@
|
|||||||
showPopup,
|
showPopup,
|
||||||
themeStore
|
themeStore
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import {
|
import view, { AttributeModel, BuildModelKey, Viewlet, ViewOptionModel, ViewOptions } from '@hcengineering/view'
|
||||||
AttributeModel,
|
|
||||||
BuildModelKey,
|
|
||||||
CategoryOption,
|
|
||||||
Viewlet,
|
|
||||||
ViewOptionModel,
|
|
||||||
ViewOptions,
|
|
||||||
ViewQueryOption
|
|
||||||
} from '@hcengineering/view'
|
|
||||||
import {
|
import {
|
||||||
focusStore,
|
focusStore,
|
||||||
getCategories,
|
getCategoryQueryNoLookup,
|
||||||
getCategorySpaces,
|
getCategoryQueryNoLookupOptions,
|
||||||
|
getCategoryQueryProjection,
|
||||||
getGroupByValues,
|
getGroupByValues,
|
||||||
getPresenter,
|
getPresenter,
|
||||||
groupBy,
|
groupBy,
|
||||||
@ -59,9 +54,9 @@
|
|||||||
SelectDirection,
|
SelectDirection,
|
||||||
setGroupByValues
|
setGroupByValues
|
||||||
} from '@hcengineering/view-resources'
|
} from '@hcengineering/view-resources'
|
||||||
import view from '@hcengineering/view-resources/src/plugin'
|
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import task from '../../plugin'
|
import task from '../../plugin'
|
||||||
|
import { getTaskKanbanResultQuery, updateTaskKanbanCategories } from '../../utils'
|
||||||
import KanbanDragDone from './KanbanDragDone.svelte'
|
import KanbanDragDone from './KanbanDragDone.svelte'
|
||||||
|
|
||||||
export let _class: Ref<Class<Task>>
|
export let _class: Ref<Class<Task>>
|
||||||
@ -72,42 +67,36 @@
|
|||||||
export let viewOptions: ViewOptions
|
export let viewOptions: ViewOptions
|
||||||
export let viewlet: Viewlet
|
export let viewlet: Viewlet
|
||||||
export let config: (string | BuildModelKey)[]
|
export let config: (string | BuildModelKey)[]
|
||||||
|
export let options: FindOptions<DocWithRank> | undefined = undefined
|
||||||
|
|
||||||
export let options: FindOptions<Task> | undefined
|
$: groupByKey = viewOptions.groupBy[0] ?? noCategory
|
||||||
|
|
||||||
$: groupByKey = (viewOptions.groupBy[0] ?? noCategory) as TaskGrouping
|
|
||||||
$: orderBy = viewOptions.orderBy
|
$: orderBy = viewOptions.orderBy
|
||||||
$: sort = { [orderBy[0]]: orderBy[1] }
|
|
||||||
|
|
||||||
$: dontUpdateRank = orderBy[0] !== TaskOrdering.Manual
|
|
||||||
|
|
||||||
let accentColors = new Map<string, ColorDefinition>()
|
let accentColors = new Map<string, ColorDefinition>()
|
||||||
const setAccentColor = (n: number, ev: CustomEvent<ColorDefinition>) => {
|
const setAccentColor = (n: number, ev: CustomEvent<ColorDefinition>): void => {
|
||||||
accentColors.set(`${n}${$themeStore.dark}${groupByKey}`, ev.detail)
|
accentColors.set(`${n}${$themeStore.dark}${groupByKey}`, ev.detail)
|
||||||
accentColors = accentColors
|
accentColors = accentColors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: dontUpdateRank = orderBy[0] !== TaskOrdering.Manual
|
||||||
|
|
||||||
let resultQuery: DocumentQuery<any> = { ...query }
|
let resultQuery: DocumentQuery<any> = { ...query }
|
||||||
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => (resultQuery = mergeQueries(p, query)))
|
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
|
||||||
|
|
||||||
async function getResultQuery (
|
$: void getTaskKanbanResultQuery(client.getHierarchy(), query, viewOptionsConfig, viewOptions).then((p) => {
|
||||||
query: DocumentQuery<Task>,
|
resultQuery = mergeQueries(p, query)
|
||||||
viewOptions: ViewOptionModel[] | undefined,
|
})
|
||||||
viewOptionsStore: ViewOptions
|
|
||||||
): Promise<DocumentQuery<Task>> {
|
$: queryNoLookup = getCategoryQueryNoLookup(resultQuery)
|
||||||
if (viewOptions === undefined) return query
|
const lookup: Lookup<Task> = {
|
||||||
let result = hierarchy.clone(query)
|
...(options?.lookup ?? {}),
|
||||||
for (const viewOption of viewOptions) {
|
space: task.class.Project,
|
||||||
if (viewOption.actionTarget !== 'query') continue
|
status: core.class.Status,
|
||||||
const queryOption = viewOption as ViewQueryOption
|
_id: {
|
||||||
const f = await getResource(queryOption.action)
|
labels: tags.class.TagReference
|
||||||
result = f(viewOptionsStore[queryOption.key] ?? queryOption.defaultValue, query)
|
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
$: resultOptions = { ...options, lookup, ...(orderBy !== undefined ? { sort: { [orderBy[0]]: orderBy[1] } } : {}) }
|
||||||
|
|
||||||
let kanbanUI: KanbanUI
|
let kanbanUI: KanbanUI
|
||||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||||
@ -123,79 +112,96 @@
|
|||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(ev))
|
showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(ev))
|
||||||
}
|
}
|
||||||
const issuesQuery = createQuery()
|
// Category information only
|
||||||
let tasks: Task[] = []
|
let tasks: DocWithRank[] = []
|
||||||
|
|
||||||
$: groupByDocs = groupBy(tasks, groupByKey, categories)
|
$: groupByDocs = groupBy(tasks, groupByKey, categories)
|
||||||
|
|
||||||
$: issuesQuery.query<Task>(
|
let fastDocs: DocWithRank[] = []
|
||||||
|
let slowDocs: DocWithRank[] = []
|
||||||
|
|
||||||
|
const docsQuery = createQuery()
|
||||||
|
const docsQuerySlow = createQuery()
|
||||||
|
|
||||||
|
let fastQueryIds = new Set<Ref<DocWithRank>>()
|
||||||
|
|
||||||
|
let categoryQueryOptions: Partial<FindOptions<DocWithRank>>
|
||||||
|
$: categoryQueryOptions = {
|
||||||
|
...getCategoryQueryNoLookupOptions(resultOptions),
|
||||||
|
projection: {
|
||||||
|
...resultOptions.projection,
|
||||||
|
_id: 1,
|
||||||
|
_class: 1,
|
||||||
|
rank: 1,
|
||||||
|
...getCategoryQueryProjection(client.getHierarchy(), _class, queryNoLookup, viewOptions.groupBy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: docsQuery.query(
|
||||||
_class,
|
_class,
|
||||||
resultQuery,
|
queryNoLookup,
|
||||||
(result) => {
|
(res) => {
|
||||||
tasks = result
|
fastDocs = res
|
||||||
|
fastQueryIds = new Set(res.map((it) => it._id))
|
||||||
},
|
},
|
||||||
{
|
{ ...categoryQueryOptions, limit: 1000 }
|
||||||
...options,
|
|
||||||
lookup: {
|
|
||||||
...options?.lookup,
|
|
||||||
space: task.class.Project,
|
|
||||||
status: core.class.Status
|
|
||||||
},
|
|
||||||
sort: {
|
|
||||||
...options?.sort,
|
|
||||||
...sort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
$: docsQuerySlow.query(
|
||||||
|
_class,
|
||||||
|
queryNoLookup,
|
||||||
|
(res) => {
|
||||||
|
slowDocs = res
|
||||||
|
},
|
||||||
|
categoryQueryOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
$: tasks = [...fastDocs, ...slowDocs.filter((it) => !fastQueryIds.has(it._id))]
|
||||||
|
|
||||||
$: listProvider.update(tasks)
|
$: listProvider.update(tasks)
|
||||||
|
|
||||||
let categories: CategoryType[] = []
|
let categories: CategoryType[] = []
|
||||||
|
|
||||||
const queryId = generateId()
|
const queryId = generateId()
|
||||||
|
|
||||||
$: updateCategories(_class, space, tasks, groupByKey, viewOptions, viewOptionsConfig)
|
function update (): void {
|
||||||
|
void updateTaskKanbanCategories(
|
||||||
function update () {
|
client,
|
||||||
updateCategories(_class, space, tasks, groupByKey, viewOptions, viewOptionsConfig)
|
viewlet,
|
||||||
}
|
_class,
|
||||||
|
space,
|
||||||
async function updateCategories (
|
tasks,
|
||||||
_class: Ref<Class<Doc>>,
|
groupByKey,
|
||||||
space: Ref<Project> | undefined,
|
viewOptions,
|
||||||
docs: Doc[],
|
viewOptionsConfig,
|
||||||
groupByKey: string,
|
update,
|
||||||
viewOptions: ViewOptions,
|
queryId
|
||||||
viewOptionsModel: ViewOptionModel[] | undefined
|
).then((res) => {
|
||||||
) {
|
|
||||||
categories = await getCategories(client, _class, space, docs, groupByKey, 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)
|
|
||||||
|
|
||||||
let spaces = getCategorySpaces(categories)
|
|
||||||
if (spaces.length === 0) {
|
|
||||||
const set = new Set(docs.map((p) => p.space))
|
|
||||||
spaces = Array.from(set)
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = spaces.length > 0 ? { space: { $in: Array.from(spaces.values()) } } : space ? { space } : {}
|
|
||||||
|
|
||||||
const res = await categoryAction(_class, query, space, groupByKey, update, queryId, viewlet.descriptor)
|
|
||||||
if (res !== undefined) {
|
|
||||||
categories = res
|
categories = res
|
||||||
break
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: void updateTaskKanbanCategories(
|
||||||
|
client,
|
||||||
|
viewlet,
|
||||||
|
_class,
|
||||||
|
space,
|
||||||
|
tasks,
|
||||||
|
groupByKey,
|
||||||
|
viewOptions,
|
||||||
|
viewOptionsConfig,
|
||||||
|
update,
|
||||||
|
queryId
|
||||||
|
).then((res) => {
|
||||||
|
categories = res
|
||||||
|
})
|
||||||
|
|
||||||
function getHeader (_class: Ref<Class<Doc>>, groupByKey: string): void {
|
function getHeader (_class: Ref<Class<Doc>>, groupByKey: string): void {
|
||||||
if (groupByKey === noCategory) {
|
if (groupByKey === noCategory) {
|
||||||
headerComponent = undefined
|
headerComponent = undefined
|
||||||
} else {
|
} else {
|
||||||
getPresenter(client, _class, { key: groupByKey }, { key: groupByKey }).then((p) => (headerComponent = p))
|
void getPresenter(client, _class, { key: groupByKey }, { key: groupByKey }).then((p) => {
|
||||||
|
headerComponent = p
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,9 +214,6 @@
|
|||||||
if (groupValue === undefined) {
|
if (groupValue === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
if ((doc as any)[groupByKey] === groupValue && viewOptions.orderBy[0] !== 'rank') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
[groupByKey]: groupValue,
|
[groupByKey]: groupValue,
|
||||||
space: doc.space
|
space: doc.space
|
||||||
@ -234,12 +237,16 @@
|
|||||||
bind:this={kanbanUI}
|
bind:this={kanbanUI}
|
||||||
{categories}
|
{categories}
|
||||||
{dontUpdateRank}
|
{dontUpdateRank}
|
||||||
|
{_class}
|
||||||
|
query={resultQuery}
|
||||||
|
options={resultOptions}
|
||||||
objects={tasks}
|
objects={tasks}
|
||||||
getGroupByValues={(groupByDocs, category) =>
|
getGroupByValues={(groupByDocs, category) =>
|
||||||
groupByKey === noCategory ? tasks : getGroupByValues(groupByDocs, category)}
|
groupByKey === noCategory ? tasks : getGroupByValues(groupByDocs, category)}
|
||||||
{setGroupByValues}
|
{setGroupByValues}
|
||||||
{getUpdateProps}
|
{getUpdateProps}
|
||||||
{groupByDocs}
|
{groupByDocs}
|
||||||
|
{groupByKey}
|
||||||
on:obj-focus={(evt) => {
|
on:obj-focus={(evt) => {
|
||||||
listProvider.updateFocus(evt.detail)
|
listProvider.updateFocus(evt.detail)
|
||||||
}}
|
}}
|
||||||
@ -253,8 +260,8 @@
|
|||||||
<svelte:fragment slot="header" let:state let:count let:index>
|
<svelte:fragment slot="header" let:state let:count let:index>
|
||||||
{@const color = accentColors.get(`${index}${$themeStore.dark}${groupByKey}`)}
|
{@const color = accentColors.get(`${index}${$themeStore.dark}${groupByKey}`)}
|
||||||
{@const headerBGColor = color?.background ?? defaultBackground($themeStore.dark)}
|
{@const headerBGColor = color?.background ?? defaultBackground($themeStore.dark)}
|
||||||
|
<div style:background={headerBGColor} class="header flex-between">
|
||||||
<div style:background={headerBGColor} class="header flex-row-center">
|
<div class="flex-row-center gap-1">
|
||||||
<span
|
<span
|
||||||
class="clear-mins fs-bold overflow-label pointer-events-none"
|
class="clear-mins fs-bold overflow-label pointer-events-none"
|
||||||
style:color={color?.title ?? 'var(--theme-caption-color)'}
|
style:color={color?.title ?? 'var(--theme-caption-color)'}
|
||||||
@ -268,6 +275,7 @@
|
|||||||
{space}
|
{space}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
kind={'list-header'}
|
kind={'list-header'}
|
||||||
|
display={'kanban'}
|
||||||
colorInherit={!$themeStore.dark}
|
colorInherit={!$themeStore.dark}
|
||||||
accent
|
accent
|
||||||
on:accent-color={(ev) => {
|
on:accent-color={(ev) => {
|
||||||
@ -280,6 +288,7 @@
|
|||||||
{count}
|
{count}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="card" let:object let:dragged>
|
<svelte:fragment slot="card" let:object let:dragged>
|
||||||
<svelte:component this={presenter} {object} {dragged} {groupByKey} {config} />
|
<svelte:component this={presenter} {object} {dragged} {groupByKey} {config} />
|
||||||
|
@ -58,6 +58,7 @@ import Todos from './components/todos/Todos.svelte'
|
|||||||
|
|
||||||
export { default as AssigneePresenter } from './components/AssigneePresenter.svelte'
|
export { default as AssigneePresenter } from './components/AssigneePresenter.svelte'
|
||||||
export { StateRefPresenter, StatePresenter, TypeStatesPopup }
|
export { StateRefPresenter, StatePresenter, TypeStatesPopup }
|
||||||
|
export * from './utils'
|
||||||
|
|
||||||
async function editStatuses (object: Project, ev: Event): Promise<void> {
|
async function editStatuses (object: Project, ev: Event): Promise<void> {
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
84
plugins/task-resources/src/utils.ts
Normal file
84
plugins/task-resources/src/utils.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
type CategoryType,
|
||||||
|
type Class,
|
||||||
|
type Doc,
|
||||||
|
type DocumentQuery,
|
||||||
|
type Hierarchy,
|
||||||
|
type Ref,
|
||||||
|
type Space,
|
||||||
|
type TxOperations
|
||||||
|
} from '@hcengineering/core'
|
||||||
|
import { getResource } from '@hcengineering/platform'
|
||||||
|
import { type Task } from '@hcengineering/task'
|
||||||
|
import {
|
||||||
|
type CategoryOption,
|
||||||
|
type ViewOptionModel,
|
||||||
|
type ViewOptions,
|
||||||
|
type ViewQueryOption,
|
||||||
|
type Viewlet
|
||||||
|
} from '@hcengineering/view'
|
||||||
|
import { getCategories, getCategorySpaces } from '@hcengineering/view-resources'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export async function updateTaskKanbanCategories (
|
||||||
|
client: TxOperations,
|
||||||
|
viewlet: Viewlet,
|
||||||
|
_class: Ref<Class<Doc>>,
|
||||||
|
space: Ref<Space> | undefined,
|
||||||
|
docs: Doc[],
|
||||||
|
groupByKey: string,
|
||||||
|
viewOptions: ViewOptions,
|
||||||
|
viewOptionsModel: ViewOptionModel[] | undefined,
|
||||||
|
update: () => void,
|
||||||
|
queryId: Ref<Doc>
|
||||||
|
): Promise<CategoryType[]> {
|
||||||
|
let categories = await getCategories(client, _class, space, docs, groupByKey, viewlet.descriptor)
|
||||||
|
for (const viewOption of viewOptionsModel ?? []) {
|
||||||
|
if (viewOption.actionTarget !== 'category') continue
|
||||||
|
const categoryFunc = viewOption as CategoryOption
|
||||||
|
if ((viewOptions[viewOption.key] as boolean) ?? 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()) } } : {},
|
||||||
|
space,
|
||||||
|
groupByKey,
|
||||||
|
update,
|
||||||
|
queryId,
|
||||||
|
viewlet.descriptor
|
||||||
|
)
|
||||||
|
if (res !== undefined) {
|
||||||
|
categories = res
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return categories
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export async function getTaskKanbanResultQuery (
|
||||||
|
hierarchy: Hierarchy,
|
||||||
|
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
|
||||||
|
}
|
@ -21,16 +21,19 @@
|
|||||||
Doc,
|
Doc,
|
||||||
DocumentQuery,
|
DocumentQuery,
|
||||||
DocumentUpdate,
|
DocumentUpdate,
|
||||||
|
FindOptions,
|
||||||
generateId,
|
generateId,
|
||||||
Lookup,
|
Lookup,
|
||||||
|
mergeQueries,
|
||||||
Ref,
|
Ref,
|
||||||
WithLookup
|
WithLookup
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { Item, Kanban } from '@hcengineering/kanban'
|
import { Item, Kanban as KanbanUI } from '@hcengineering/kanban'
|
||||||
import notification from '@hcengineering/notification'
|
import notification from '@hcengineering/notification'
|
||||||
import { getResource } from '@hcengineering/platform'
|
|
||||||
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import tags from '@hcengineering/tags'
|
import tags from '@hcengineering/tags'
|
||||||
|
import { DocWithRank, getStates } from '@hcengineering/task'
|
||||||
|
import { getTaskKanbanResultQuery, typeStore, updateTaskKanbanCategories } from '@hcengineering/task-resources'
|
||||||
import { Issue, IssuesGrouping, IssuesOrdering, Project } from '@hcengineering/tracker'
|
import { Issue, IssuesGrouping, IssuesOrdering, Project } from '@hcengineering/tracker'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -44,20 +47,13 @@
|
|||||||
showPopup,
|
showPopup,
|
||||||
themeStore
|
themeStore
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import {
|
import view, { AttributeModel, BuildModelKey, Viewlet, ViewOptionModel, ViewOptions } from '@hcengineering/view'
|
||||||
AttributeModel,
|
|
||||||
BuildModelKey,
|
|
||||||
CategoryOption,
|
|
||||||
Viewlet,
|
|
||||||
ViewOptionModel,
|
|
||||||
ViewOptions,
|
|
||||||
ViewQueryOption
|
|
||||||
} from '@hcengineering/view'
|
|
||||||
import {
|
import {
|
||||||
enabledConfig,
|
enabledConfig,
|
||||||
focusStore,
|
focusStore,
|
||||||
getCategories,
|
getCategoryQueryNoLookup,
|
||||||
getCategorySpaces,
|
getCategoryQueryNoLookupOptions,
|
||||||
|
getCategoryQueryProjection,
|
||||||
getGroupByValues,
|
getGroupByValues,
|
||||||
getPresenter,
|
getPresenter,
|
||||||
groupBy,
|
groupBy,
|
||||||
@ -69,7 +65,6 @@
|
|||||||
setGroupByValues,
|
setGroupByValues,
|
||||||
statusStore
|
statusStore
|
||||||
} from '@hcengineering/view-resources'
|
} from '@hcengineering/view-resources'
|
||||||
import view from '@hcengineering/view-resources/src/plugin'
|
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
import { activeProjects } from '../../utils'
|
import { activeProjects } from '../../utils'
|
||||||
@ -83,9 +78,8 @@
|
|||||||
import PriorityEditor from './PriorityEditor.svelte'
|
import PriorityEditor from './PriorityEditor.svelte'
|
||||||
import StatusEditor from './StatusEditor.svelte'
|
import StatusEditor from './StatusEditor.svelte'
|
||||||
import EstimationEditor from './timereport/EstimationEditor.svelte'
|
import EstimationEditor from './timereport/EstimationEditor.svelte'
|
||||||
import { getStates } from '@hcengineering/task'
|
|
||||||
import { typeStore } from '@hcengineering/task-resources'
|
|
||||||
|
|
||||||
|
const _class = tracker.class.Issue
|
||||||
export let space: Ref<Project> | undefined = undefined
|
export let space: Ref<Project> | undefined = undefined
|
||||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||||
export let query: DocumentQuery<Issue> = {}
|
export let query: DocumentQuery<Issue> = {}
|
||||||
@ -93,11 +87,10 @@
|
|||||||
export let viewOptions: ViewOptions
|
export let viewOptions: ViewOptions
|
||||||
export let viewlet: Viewlet
|
export let viewlet: Viewlet
|
||||||
export let config: (string | BuildModelKey)[]
|
export let config: (string | BuildModelKey)[]
|
||||||
|
export let options: FindOptions<DocWithRank> | undefined = undefined
|
||||||
|
|
||||||
$: currentSpace = space || tracker.project.DefaultProject
|
|
||||||
$: groupByKey = (viewOptions.groupBy[0] ?? noCategory) as IssuesGrouping
|
$: groupByKey = (viewOptions.groupBy[0] ?? noCategory) as IssuesGrouping
|
||||||
$: orderBy = viewOptions.orderBy
|
$: orderBy = viewOptions.orderBy
|
||||||
$: sort = { [orderBy[0]]: orderBy[1] }
|
|
||||||
|
|
||||||
let accentColors = new Map<string, ColorDefinition>()
|
let accentColors = new Map<string, ColorDefinition>()
|
||||||
const setAccentColor = (n: number, ev: CustomEvent<ColorDefinition>) => {
|
const setAccentColor = (n: number, ev: CustomEvent<ColorDefinition>) => {
|
||||||
@ -107,36 +100,25 @@
|
|||||||
|
|
||||||
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
|
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
|
||||||
|
|
||||||
|
$: currentSpace = space ?? tracker.project.DefaultProject
|
||||||
let currentProject: Project | undefined
|
let currentProject: Project | undefined
|
||||||
$: currentProject = $activeProjects.get(currentSpace)
|
$: currentProject = $activeProjects.get(currentSpace)
|
||||||
|
|
||||||
let resultQuery: DocumentQuery<any> = query
|
let resultQuery: DocumentQuery<any> = { ...query }
|
||||||
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => (resultQuery = p))
|
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
|
||||||
|
|
||||||
async function getResultQuery (
|
$: void getTaskKanbanResultQuery(client.getHierarchy(), query, viewOptionsConfig, viewOptions).then((p) => {
|
||||||
query: DocumentQuery<Issue>,
|
resultQuery = mergeQueries(p, query)
|
||||||
viewOptions: ViewOptionModel[] | undefined,
|
})
|
||||||
viewOptionsStore: ViewOptions
|
|
||||||
): Promise<DocumentQuery<Issue>> {
|
$: queryNoLookup = getCategoryQueryNoLookup(resultQuery)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
function toIssue (object: any): WithLookup<Issue> {
|
function toIssue (object: any): WithLookup<Issue> {
|
||||||
return object as WithLookup<Issue>
|
return object as WithLookup<Issue>
|
||||||
}
|
}
|
||||||
|
|
||||||
const lookup: Lookup<Issue> = {
|
const lookup: Lookup<Issue> = {
|
||||||
|
...(options?.lookup ?? {}),
|
||||||
space: tracker.class.Project,
|
space: tracker.class.Project,
|
||||||
status: tracker.class.IssueStatus,
|
status: tracker.class.IssueStatus,
|
||||||
component: tracker.class.Component,
|
component: tracker.class.Component,
|
||||||
@ -147,7 +129,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let kanbanUI: Kanban
|
$: resultOptions = { ...options, lookup, ...(orderBy !== undefined ? { sort: { [orderBy[0]]: orderBy[1] } } : {}) }
|
||||||
|
|
||||||
|
let kanbanUI: KanbanUI
|
||||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||||
kanbanUI?.select(offset, of, dir)
|
kanbanUI?.select(offset, of, dir)
|
||||||
})
|
})
|
||||||
@ -161,82 +145,103 @@
|
|||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(ev))
|
showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(ev))
|
||||||
}
|
}
|
||||||
const issuesQuery = createQuery()
|
// Category information only
|
||||||
let issues: Issue[] = []
|
let tasks: DocWithRank[] = []
|
||||||
|
|
||||||
$: groupByDocs = groupBy(issues, groupByKey, categories)
|
$: groupByDocs = groupBy(tasks, groupByKey, categories)
|
||||||
|
|
||||||
$: issuesQuery.query(
|
let fastDocs: DocWithRank[] = []
|
||||||
tracker.class.Issue,
|
let slowDocs: DocWithRank[] = []
|
||||||
resultQuery,
|
|
||||||
(result) => {
|
const docsQuery = createQuery()
|
||||||
issues = result
|
const docsQuerySlow = createQuery()
|
||||||
},
|
|
||||||
{
|
let fastQueryIds = new Set<Ref<DocWithRank>>()
|
||||||
lookup,
|
|
||||||
sort
|
let categoryQueryOptions: Partial<FindOptions<DocWithRank>>
|
||||||
|
$: categoryQueryOptions = {
|
||||||
|
...getCategoryQueryNoLookupOptions(resultOptions),
|
||||||
|
projection: {
|
||||||
|
...resultOptions.projection,
|
||||||
|
_id: 1,
|
||||||
|
_class: 1,
|
||||||
|
rank: 1,
|
||||||
|
...getCategoryQueryProjection(client.getHierarchy(), _class, queryNoLookup, viewOptions.groupBy)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: docsQuery.query(
|
||||||
|
_class,
|
||||||
|
queryNoLookup,
|
||||||
|
(res) => {
|
||||||
|
fastDocs = res
|
||||||
|
fastQueryIds = new Set(res.map((it) => it._id))
|
||||||
|
},
|
||||||
|
{ ...categoryQueryOptions, limit: 1000 }
|
||||||
|
)
|
||||||
|
$: docsQuerySlow.query(
|
||||||
|
_class,
|
||||||
|
queryNoLookup,
|
||||||
|
(res) => {
|
||||||
|
slowDocs = res
|
||||||
|
},
|
||||||
|
categoryQueryOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
$: listProvider.update(issues)
|
$: tasks = [...fastDocs, ...slowDocs.filter((it) => !fastQueryIds.has(it._id))]
|
||||||
|
|
||||||
|
$: listProvider.update(tasks)
|
||||||
|
|
||||||
let categories: CategoryType[] = []
|
let categories: CategoryType[] = []
|
||||||
|
|
||||||
const queryId = generateId()
|
const queryId = generateId()
|
||||||
|
|
||||||
$: updateCategories(tracker.class.Issue, issues, groupByKey, viewOptions, viewOptionsConfig)
|
function update (): void {
|
||||||
|
void updateTaskKanbanCategories(
|
||||||
function update () {
|
client,
|
||||||
updateCategories(tracker.class.Issue, issues, groupByKey, viewOptions, viewOptionsConfig)
|
viewlet,
|
||||||
}
|
|
||||||
|
|
||||||
async function updateCategories (
|
|
||||||
_class: Ref<Class<Doc>>,
|
|
||||||
docs: Doc[],
|
|
||||||
groupByKey: string,
|
|
||||||
viewOptions: ViewOptions,
|
|
||||||
viewOptionsModel: ViewOptionModel[] | undefined
|
|
||||||
) {
|
|
||||||
categories = await getCategories(client, _class, space, docs, groupByKey, 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,
|
_class,
|
||||||
spaces.length > 0 ? { space: { $in: Array.from(spaces.values()) } } : {},
|
|
||||||
space,
|
space,
|
||||||
|
tasks,
|
||||||
groupByKey,
|
groupByKey,
|
||||||
|
viewOptions,
|
||||||
|
viewOptionsConfig,
|
||||||
update,
|
update,
|
||||||
queryId,
|
queryId
|
||||||
viewlet.descriptor
|
).then((res) => {
|
||||||
)
|
|
||||||
if (res !== undefined) {
|
|
||||||
categories = res
|
categories = res
|
||||||
break
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: void updateTaskKanbanCategories(
|
||||||
|
client,
|
||||||
|
viewlet,
|
||||||
|
_class,
|
||||||
|
space,
|
||||||
|
tasks,
|
||||||
|
groupByKey,
|
||||||
|
viewOptions,
|
||||||
|
viewOptionsConfig,
|
||||||
|
update,
|
||||||
|
queryId
|
||||||
|
).then((res) => {
|
||||||
|
categories = res
|
||||||
|
})
|
||||||
|
|
||||||
const fullFilled: Record<string, boolean> = {}
|
const fullFilled: Record<string, boolean> = {}
|
||||||
|
|
||||||
function getHeader (_class: Ref<Class<Doc>>, groupByKey: string): void {
|
function getHeader (_class: Ref<Class<Doc>>, groupByKey: string): void {
|
||||||
if (groupByKey === noCategory) {
|
if (groupByKey === noCategory) {
|
||||||
headerComponent = undefined
|
headerComponent = undefined
|
||||||
} else {
|
} else {
|
||||||
getPresenter(client, _class, { key: groupByKey }, { key: groupByKey }).then((p) => (headerComponent = p))
|
void getPresenter(client, _class, { key: groupByKey }, { key: groupByKey }).then((p) => {
|
||||||
|
headerComponent = p
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let headerComponent: AttributeModel | undefined
|
let headerComponent: AttributeModel | undefined
|
||||||
$: getHeader(tracker.class.Issue, groupByKey)
|
$: getHeader(_class, groupByKey)
|
||||||
|
|
||||||
const getUpdateProps = (doc: Doc, category: CategoryType): DocumentUpdate<Item> | undefined => {
|
const getUpdateProps = (doc: Doc, category: CategoryType): DocumentUpdate<Item> | undefined => {
|
||||||
const groupValue =
|
const groupValue =
|
||||||
@ -244,9 +249,6 @@
|
|||||||
if (groupValue === undefined) {
|
if (groupValue === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
if ((doc as any)[groupByKey] === groupValue && viewOptions.orderBy[0] !== 'rank') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
[groupByKey]: groupValue,
|
[groupByKey]: groupValue,
|
||||||
space: doc.space
|
space: doc.space
|
||||||
@ -273,7 +275,7 @@
|
|||||||
|
|
||||||
if ([IssuesGrouping.Component, IssuesGrouping.Milestone].includes(groupByKey)) {
|
if ([IssuesGrouping.Component, IssuesGrouping.Milestone].includes(groupByKey)) {
|
||||||
const availableCategories = []
|
const availableCategories = []
|
||||||
const clazz = hierarchy.getAttribute(tracker.class.Issue, groupByKey)
|
const clazz = client.getHierarchy().getAttribute(tracker.class.Issue, groupByKey)
|
||||||
|
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
if (!category || (issue as any)[groupByKey] === category) {
|
if (!category || (issue as any)[groupByKey] === category) {
|
||||||
@ -312,16 +314,20 @@
|
|||||||
/>
|
/>
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<Kanban
|
<KanbanUI
|
||||||
bind:this={kanbanUI}
|
bind:this={kanbanUI}
|
||||||
{categories}
|
{categories}
|
||||||
{dontUpdateRank}
|
{dontUpdateRank}
|
||||||
objects={issues}
|
{_class}
|
||||||
|
query={resultQuery}
|
||||||
|
options={resultOptions}
|
||||||
|
objects={tasks}
|
||||||
getGroupByValues={(groupByDocs, category) =>
|
getGroupByValues={(groupByDocs, category) =>
|
||||||
groupByKey === noCategory ? issues : getGroupByValues(groupByDocs, category)}
|
groupByKey === noCategory ? tasks : getGroupByValues(groupByDocs, category)}
|
||||||
{setGroupByValues}
|
{setGroupByValues}
|
||||||
{getUpdateProps}
|
{getUpdateProps}
|
||||||
{groupByDocs}
|
{groupByDocs}
|
||||||
|
{groupByKey}
|
||||||
on:obj-focus={(evt) => {
|
on:obj-focus={(evt) => {
|
||||||
listProvider.updateFocus(evt.detail)
|
listProvider.updateFocus(evt.detail)
|
||||||
}}
|
}}
|
||||||
@ -360,7 +366,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<span class="counter">
|
<span class="counter ml-1">
|
||||||
{count}
|
{count}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -386,7 +392,7 @@
|
|||||||
<div
|
<div
|
||||||
class="tracker-card"
|
class="tracker-card"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
openDoc(hierarchy, issue)
|
void openDoc(client.getHierarchy(), issue)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="card-header flex-between">
|
<div class="card-header flex-between">
|
||||||
@ -482,7 +488,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Kanban>
|
</KanbanUI>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -69,7 +69,6 @@ export default mergeIds(viewId, view, {
|
|||||||
DontMatchCriteria: '' as IntlString,
|
DontMatchCriteria: '' as IntlString,
|
||||||
MarkupEditor: '' as IntlString,
|
MarkupEditor: '' as IntlString,
|
||||||
Select: '' as IntlString,
|
Select: '' as IntlString,
|
||||||
NoGrouping: '' as IntlString,
|
|
||||||
Grouping: '' as IntlString,
|
Grouping: '' as IntlString,
|
||||||
Ordering: '' as IntlString,
|
Ordering: '' as IntlString,
|
||||||
Manual: '' as IntlString,
|
Manual: '' as IntlString,
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import core, {
|
import core, {
|
||||||
AccountRole,
|
AccountRole,
|
||||||
|
type FindOptions,
|
||||||
Hierarchy,
|
Hierarchy,
|
||||||
getCurrentAccount,
|
getCurrentAccount,
|
||||||
getObjectValue,
|
getObjectValue,
|
||||||
@ -26,6 +27,7 @@ import core, {
|
|||||||
type Client,
|
type Client,
|
||||||
type Collection,
|
type Collection,
|
||||||
type Doc,
|
type Doc,
|
||||||
|
type DocumentQuery,
|
||||||
type DocumentUpdate,
|
type DocumentUpdate,
|
||||||
type Lookup,
|
type Lookup,
|
||||||
type Obj,
|
type Obj,
|
||||||
@ -923,3 +925,57 @@ export async function getSpacePresenter (
|
|||||||
return await getResource(value.presenter)
|
return await getResource(value.presenter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function getCategoryQueryProjection (
|
||||||
|
hierarchy: Hierarchy,
|
||||||
|
_class: Ref<Class<Doc>>,
|
||||||
|
query: DocumentQuery<Doc>,
|
||||||
|
fields: string[]
|
||||||
|
): Record<string, number> {
|
||||||
|
const res: Record<string, number> = {}
|
||||||
|
for (const f of fields) {
|
||||||
|
/*
|
||||||
|
Mongo projection doesn't support properties fields which
|
||||||
|
start from $. Such field here is $search. The least we could do
|
||||||
|
is to filter all properties which start from $.
|
||||||
|
*/
|
||||||
|
if (!f.startsWith('$')) {
|
||||||
|
res[f] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const f of Object.keys(query)) {
|
||||||
|
if (!f.startsWith('$')) {
|
||||||
|
res[f] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hierarchy.isDerived(_class, core.class.AttachedDoc)) {
|
||||||
|
res.attachedTo = 1
|
||||||
|
res.attachedToClass = 1
|
||||||
|
res.collection = 1
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function getCategoryQueryNoLookup<T extends Doc = Doc> (query: DocumentQuery<T>): DocumentQuery<T> {
|
||||||
|
const newQuery: DocumentQuery<T> = {}
|
||||||
|
for (const [k, v] of Object.entries(query)) {
|
||||||
|
if (!k.startsWith('$lookup.')) {
|
||||||
|
;(newQuery as any)[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function getCategoryQueryNoLookupOptions<T extends Doc> (options: FindOptions<T>): FindOptions<T> {
|
||||||
|
const { lookup, ...resultOptions } = options
|
||||||
|
return resultOptions
|
||||||
|
}
|
||||||
|
@ -862,7 +862,8 @@ const view = plugin(viewId, {
|
|||||||
Or: '' as IntlString,
|
Or: '' as IntlString,
|
||||||
Subscribed: '' as IntlString,
|
Subscribed: '' as IntlString,
|
||||||
HyperlinkPlaceholder: '' as IntlString,
|
HyperlinkPlaceholder: '' as IntlString,
|
||||||
CopyToClipboard: '' as IntlString
|
CopyToClipboard: '' as IntlString,
|
||||||
|
NoGrouping: '' as IntlString
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
Table: '' as Asset,
|
Table: '' as Asset,
|
||||||
|
Loading…
Reference in New Issue
Block a user