Updatable categories (#2606)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-02-08 21:37:50 +06:00 committed by GitHub
parent d0353ba142
commit 8108f2e59b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 150 additions and 60 deletions

View File

@ -218,7 +218,9 @@ export class TSortFuncs extends TClass implements ClassSortFuncs {
@Mixin(view.mixin.AllValuesFunc, core.class.Class)
export class TAllValuesFunc extends TClass implements AllValuesFunc {
func!: Resource<(space: Ref<Space> | undefined) => Promise<any[]>>
func!: Resource<
(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>
>
}
@Model(view.class.ViewletPreference, preference.class.Preference)

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import contact, { Employee } from '@hcengineering/contact'
import { Class, Doc, DocumentQuery, IdMap, Lookup, Ref, toIdMap, WithLookup } from '@hcengineering/core'
import { Class, Doc, DocumentQuery, generateId, IdMap, Lookup, Ref, toIdMap, WithLookup } from '@hcengineering/core'
import { Kanban, TypeState } from '@hcengineering/kanban'
import notification from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
@ -193,6 +193,8 @@
let states: TypeState[]
const queryId = generateId()
$: updateCategories(
tracker.class.Issue,
issues,
@ -205,6 +207,20 @@
assignee
)
function update () {
updateCategories(
tracker.class.Issue,
issues,
groupBy,
viewOptions,
viewOptionsConfig,
statuses,
projects,
sprints,
assignee
)
}
async function updateCategories (
_class: Ref<Class<Doc>>,
docs: Doc[],
@ -222,7 +238,7 @@
const categoryFunc = viewOption as CategoryOption
if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
const f = await getResource(categoryFunc.action)
const res = await f(_class, space, groupByKey)
const res = await f(_class, space, groupByKey, update, queryId)
if (res !== undefined) {
for (const category of categories) {
if (!res.includes(category)) {

View File

@ -21,7 +21,7 @@
import SprintPopup from './SprintPopup.svelte'
import { Sprint } from '@hcengineering/tracker'
export let sprint: Sprint
export let sprints: Sprint[]
export let moveAndDeleteSprint: (selectedSprint?: Sprint) => Promise<void>
let selectedSprint: Sprint | undefined
@ -34,14 +34,14 @@
<Card
canSave
label={tracker.string.MoveAndDeleteSprint}
labelProps={{ newSprint: selectedSprintLabel, deleteSprint: sprint.label }}
labelProps={{ newSprint: selectedSprintLabel, deleteSprint: sprints.map((p) => p.label) }}
okLabel={tracker.string.Delete}
okAction={() => moveAndDeleteSprint(selectedSprint)}
on:close
>
<SprintPopup
_class={tracker.class.Sprint}
ignoreSprints={[sprint]}
ignoreSprints={sprints}
allowDeselect
closeAfterSelect={false}
shadows={false}

View File

@ -201,7 +201,7 @@ async function editTeam (team: Team | undefined): Promise<void> {
}
}
async function moveAndDeleteSprint (client: TxOperations, oldSprint: Sprint, newSprint?: Sprint): Promise<void> {
async function moveAndDeleteSprints (client: TxOperations, oldSprints: Sprint[], newSprint?: Sprint): Promise<void> {
const noSprintLabel = await translate(tracker.string.NoSprint, {})
showPopup(
@ -209,37 +209,39 @@ async function moveAndDeleteSprint (client: TxOperations, oldSprint: Sprint, new
{
label: tracker.string.MoveAndDeleteSprint,
message: tracker.string.MoveAndDeleteSprintConfirm,
labelProps: { newSprint: newSprint?.label ?? noSprintLabel, deleteSprint: oldSprint.label }
labelProps: { newSprint: newSprint?.label ?? noSprintLabel, deleteSprint: oldSprints.map((p) => p.label) }
},
undefined,
(result?: boolean) => {
if (result === true) {
void moveIssuesToAnotherSprint(client, oldSprint, newSprint).then((succes) => {
if (succes) {
void deleteObject(client, oldSprint)
}
})
for (const oldSprint of oldSprints) {
void moveIssuesToAnotherSprint(client, oldSprint, newSprint).then((succes) => {
if (succes) {
void deleteObject(client, oldSprint)
}
})
}
}
}
)
}
async function deleteSprint (sprint: Sprint): Promise<void> {
async function deleteSprint (sprints: Sprint[]): Promise<void> {
const client = getClient()
// Check if available to move issues to another sprint
const firstSearchedSprint = await client.findOne(tracker.class.Sprint, { _id: { $nin: [sprint._id] } })
const firstSearchedSprint = await client.findOne(tracker.class.Sprint, { _id: { $nin: sprints.map((p) => p._id) } })
if (firstSearchedSprint !== undefined) {
showPopup(
MoveAndDeleteSprintPopup,
{
sprint,
sprints,
moveAndDeleteSprint: async (selectedSprint?: Sprint) =>
await moveAndDeleteSprint(client, sprint, selectedSprint)
await moveAndDeleteSprints(client, sprints, selectedSprint)
},
'top'
)
} else {
await moveAndDeleteSprint(client, sprint)
await moveAndDeleteSprints(client, sprints)
}
}

View File

@ -15,7 +15,7 @@
import { Client, Doc, Ref, Space } from '@hcengineering/core'
import type { IntlString, Metadata, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import { IssueDraft, IssuePriority, IssueStatus, Project, Sprint } from '@hcengineering/tracker'
import { IssueDraft } from '@hcengineering/tracker'
import { AnyComponent } from '@hcengineering/ui'
import { SortFunc, Viewlet, ViewQueryAction } from '@hcengineering/view'
import tracker, { trackerId } from '../../tracker/lib'
@ -380,9 +380,17 @@ export default mergeIds(trackerId, tracker, {
IssuePrioritySort: '' as SortFunc,
SprintSort: '' as SortFunc,
SubIssueQuery: '' as ViewQueryAction,
GetAllStatuses: '' as Resource<(space: Ref<Space> | undefined) => Promise<Array<Ref<IssueStatus>>>>,
GetAllPriority: '' as Resource<(space: Ref<Space> | undefined) => Promise<IssuePriority[]>>,
GetAllProjects: '' as Resource<(space: Ref<Space> | undefined) => Promise<Array<Ref<Project>>>>,
GetAllSprints: '' as Resource<(space: Ref<Space> | undefined) => Promise<Array<Ref<Sprint>>>>
GetAllStatuses: '' as Resource<
(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>
>,
GetAllPriority: '' as Resource<
(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>
>,
GetAllProjects: '' as Resource<
(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>
>,
GetAllSprints: '' as Resource<
(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>
>
}
})

View File

@ -16,6 +16,7 @@
import { Employee, formatName } from '@hcengineering/contact'
import core, {
AttachedData,
Class,
Doc,
DocumentQuery,
Ref,
@ -53,7 +54,7 @@ import {
isWeekend,
MILLISECONDS_IN_WEEK
} from '@hcengineering/ui'
import { ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import { CategoryQuery, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import tracker from './plugin'
import { defaultPriorities, defaultProjectStatuses, defaultSprintStatuses, issuePriorities } from './types'
@ -499,7 +500,7 @@ export async function moveIssuesToAnotherSprint (
// Update Issues by new Sprint
const awaitedUpdates = []
for (const issue of movedIssues) {
awaitedUpdates.push(client.update(issue, { sprint: newSprint?._id ?? undefined }))
awaitedUpdates.push(client.update(issue, { sprint: newSprint?._id ?? null }))
}
await Promise.all(awaitedUpdates)
@ -546,41 +547,65 @@ export function subIssueQuery (value: boolean, query: DocumentQuery<Issue>): Doc
return value ? query : { ...query, attachedTo: tracker.ids.NoParent }
}
export async function getAllStatuses (space: Ref<Space> | undefined): Promise<Array<Ref<IssueStatus>> | undefined> {
if (space === undefined) return
return await new Promise((resolve) => {
const query = createQuery(true)
query.query(tracker.class.IssueStatus, { space }, (res) => {
resolve(res.map((p) => p._id))
query.unsubscribe()
})
async function getAllSomething (
_class: Ref<Class<Doc>>,
space: Ref<Space> | undefined,
onUpdate: () => void,
queryId: Ref<Doc>
): Promise<any[] | undefined> {
const promise = new Promise<Array<Ref<Doc>>>((resolve, reject) => {
let refresh: boolean = false
const lq = CategoryQuery.getLiveQuery(queryId)
refresh = lq.query(
_class,
{
space
},
(res) => {
const result = res.map((p) => p._id)
CategoryQuery.results.set(queryId, result)
resolve(result)
onUpdate()
}
)
if (!refresh) {
resolve(CategoryQuery.results.get(queryId) ?? [])
}
})
return await promise
}
export async function getAllPriority (space: Ref<Space> | undefined): Promise<IssuePriority[] | undefined> {
export async function getAllStatuses (
space: Ref<Space> | undefined,
onUpdate: () => void,
queryId: Ref<Doc>
): Promise<any[] | undefined> {
return await getAllSomething(tracker.class.IssueStatus, space, onUpdate, queryId)
}
export async function getAllPriority (
space: Ref<Space> | undefined,
onUpdate: () => void,
queryId: Ref<Doc>
): Promise<any[] | undefined> {
return defaultPriorities
}
export async function getAllProjects (space: Ref<Team> | undefined): Promise<Array<Ref<Project>> | undefined> {
if (space === undefined) return
return await new Promise((resolve) => {
const query = createQuery(true)
query.query(tracker.class.Project, { space }, (res) => {
resolve(res.map((p) => p._id))
query.unsubscribe()
})
})
export async function getAllProjects (
space: Ref<Team> | undefined,
onUpdate: () => void,
queryId: Ref<Doc>
): Promise<any[] | undefined> {
return await getAllSomething(tracker.class.Project, space, onUpdate, queryId)
}
export async function getAllSprints (space: Ref<Team> | undefined): Promise<Array<Ref<Sprint>> | undefined> {
if (space === undefined) return
return await new Promise((resolve) => {
const query = createQuery(true)
query.query(tracker.class.Sprint, { space }, (res) => {
resolve(res.map((p) => p._id))
query.unsubscribe()
})
})
export async function getAllSprints (
space: Ref<Team> | undefined,
onUpdate: () => void,
queryId: Ref<Doc>
): Promise<any[] | undefined> {
return await getAllSomething(tracker.class.Sprint, space, onUpdate, queryId)
}
export function subIssueListProvider (subIssues: Issue[], target: Ref<Issue>): void {

View File

@ -13,13 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Lookup, Ref, Space } from '@hcengineering/core'
import { Class, Doc, generateId, Lookup, Ref, Space } from '@hcengineering/core'
import { getResource, IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { AnyComponent } from '@hcengineering/ui'
import { AttributeModel, BuildModelKey, CategoryOption, ViewOptionModel, ViewOptions } from '@hcengineering/view'
import { onDestroy } from 'svelte'
import { buildModel, getAdditionalHeader, getCategories, getPresenter, groupBy } from '../../utils'
import { noCategory } from '../../viewOptions'
import { CategoryQuery, noCategory } from '../../viewOptions'
import ListCategory from './ListCategory.svelte'
export let elementByIndex: Map<number, HTMLDivElement>
@ -49,6 +50,15 @@
let categories: any[] = []
$: updateCategories(_class, docs, groupByKey, viewOptions, viewOptionsConfig)
const queryId = generateId()
onDestroy(() => {
CategoryQuery.remove(queryId)
})
function update () {
updateCategories(_class, docs, groupByKey, viewOptions, viewOptionsConfig)
}
async function updateCategories (
_class: Ref<Class<Doc>>,
docs: Doc[],
@ -63,7 +73,7 @@
const categoryFunc = viewOption as CategoryOption
if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
const f = await getResource(categoryFunc.action)
const res = await f(_class, space, groupByKey)
const res = await f(_class, space, groupByKey, update, queryId)
if (res !== undefined) {
for (const category of categories) {
if (!res.includes(category)) {

View File

@ -1,6 +1,6 @@
import { Class, Doc, Ref, SortingOrder, Space } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { createQuery, getAttributePresenterClass, getClient, LiveQuery } from '@hcengineering/presentation'
import { getCurrentLocation, locationToUrl } from '@hcengineering/ui'
import {
DropdownViewOption,
@ -89,7 +89,9 @@ export function migrateViewOpttions (): void {
export async function showEmptyGroups (
_class: Ref<Class<Doc>>,
space: Ref<Space> | undefined,
key: string
key: string,
onUpdate: () => void,
queryId: Ref<Doc>
): Promise<any[] | undefined> {
const client = getClient()
const hierarchy = client.getHierarchy()
@ -101,7 +103,7 @@ export async function showEmptyGroups (
const mixin = hierarchy.as(attributeClass, view.mixin.AllValuesFunc)
if (mixin.func !== undefined) {
const f = await getResource(mixin.func)
const res = await f(space)
const res = await f(space, onUpdate, queryId)
if (res !== undefined) {
const sortFunc = hierarchy.as(attributeClass, view.mixin.SortFuncs)
if (sortFunc?.func === undefined) return res
@ -111,3 +113,22 @@ export async function showEmptyGroups (
}
}
}
export const CategoryQuery = {
queries: new Map<string, LiveQuery>(),
results: new Map<string, any[]>(),
getLiveQuery (index: string): LiveQuery {
const current = CategoryQuery.queries.get(index)
if (current !== undefined) return current
const query = createQuery(true)
this.queries.set(index, query)
return query
},
remove (index: string): void {
const lq = this.queries.get(index)
lq?.unsubscribe()
this.queries.delete(index)
this.results.delete(index)
}
}

View File

@ -247,7 +247,7 @@ export interface ClassSortFuncs extends Class<Doc> {
* @public
*/
export interface AllValuesFunc extends Class<Doc> {
func: Resource<(space: Ref<Space> | undefined) => Promise<any[]>>
func: Resource<(space: Ref<Space> | undefined, onUpdate: () => void, queryId: Ref<Doc>) => Promise<any[] | undefined>>
}
/**
@ -488,7 +488,13 @@ export interface ViewOption {
* @public
*/
export type ViewCategoryAction = Resource<
(_class: Ref<Class<Doc>>, space: Ref<Space> | undefined, key: string) => Promise<any[] | undefined>
(
_class: Ref<Class<Doc>>,
space: Ref<Space> | undefined,
key: string,
onUpdate: () => void,
queryId: Ref<Doc>
) => Promise<any[] | undefined>
>
/**