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