List empty groups (#2600)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-02-08 12:42:26 +06:00 committed by GitHub
parent 994a356ea5
commit 8e2ddd6760
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 431 additions and 242 deletions

View File

@ -521,6 +521,14 @@ export function createModel (builder: Builder): void {
actionTartget: 'query', actionTartget: 'query',
action: tracker.function.SubIssueQuery, action: tracker.function.SubIssueQuery,
label: tracker.string.SubIssues label: tracker.string.SubIssues
},
{
key: 'shouldShowAll',
type: 'toggle',
defaultValue: false,
actionTartget: 'category',
action: view.function.ShowEmptyGroups,
label: view.string.ShowEmptyGroups
} }
] ]
} }
@ -596,6 +604,7 @@ export function createModel (builder: Builder): void {
['dueDate', SortingOrder.Descending], ['dueDate', SortingOrder.Descending],
['rank', SortingOrder.Ascending] ['rank', SortingOrder.Ascending]
], ],
groupDepth: 1,
other: [] other: []
} }
@ -696,7 +705,10 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, { builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: tracker.class.Issue, attachTo: tracker.class.Issue,
descriptor: tracker.viewlet.Kanban, descriptor: tracker.viewlet.Kanban,
viewOptions: issuesOptions, viewOptions: {
...issuesOptions,
groupDepth: 1
},
config: [] config: []
}) })
@ -876,6 +888,22 @@ export function createModel (builder: Builder): void {
fields: ['assignee'] fields: ['assignee']
}) })
builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.AllValuesFunc, {
func: tracker.function.GetAllStatuses
})
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AllValuesFunc, {
func: tracker.function.GetAllPriority
})
builder.mixin(tracker.class.Project, core.class.Class, view.mixin.AllValuesFunc, {
func: tracker.function.GetAllProjects
})
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.AllValuesFunc, {
func: tracker.function.GetAllSprints
})
builder.createDoc( builder.createDoc(
workbench.class.Application, workbench.class.Application,
core.space.Model, core.space.Model,
@ -933,13 +961,13 @@ export function createModel (builder: Builder): void {
{ {
id: activeId, id: activeId,
label: tracker.string.Active, label: tracker.string.Active,
// icon: tracker.icon.TrackerApplication, icon: tracker.icon.CategoryStarted,
component: tracker.component.Active component: tracker.component.Active
}, },
{ {
id: backlogId, id: backlogId,
label: tracker.string.Backlog, label: tracker.string.Backlog,
// icon: tracker.icon.TrackerApplication, icon: tracker.icon.CategoryBacklog,
component: tracker.component.Backlog component: tracker.component.Backlog
}, },
{ {

View File

@ -62,7 +62,8 @@ import type {
ViewletDescriptor, ViewletDescriptor,
ViewletPreference, ViewletPreference,
ViewOptionsModel, ViewOptionsModel,
ViewOptions ViewOptions,
AllValuesFunc
} from '@hcengineering/view' } from '@hcengineering/view'
import view from './plugin' import view from './plugin'
@ -215,6 +216,11 @@ export class TSortFuncs extends TClass implements ClassSortFuncs {
func!: SortFunc func!: SortFunc
} }
@Mixin(view.mixin.AllValuesFunc, core.class.Class)
export class TAllValuesFunc extends TClass implements AllValuesFunc {
func!: Resource<(space: Ref<Space> | undefined) => Promise<any[]>>
}
@Model(view.class.ViewletPreference, preference.class.Preference) @Model(view.class.ViewletPreference, preference.class.Preference)
export class TViewletPreference extends TPreference implements ViewletPreference { export class TViewletPreference extends TPreference implements ViewletPreference {
attachedTo!: Ref<Viewlet> attachedTo!: Ref<Viewlet>
@ -339,7 +345,8 @@ export function createModel (builder: Builder): void {
TLinkPresenter, TLinkPresenter,
TArrayEditor, TArrayEditor,
TInlineAttributEditor, TInlineAttributEditor,
TFilteredView TFilteredView,
TAllValuesFunc
) )
classPresenter( classPresenter(

View File

@ -13,10 +13,9 @@
// limitations under the License. // limitations under the License.
// //
import { ObjQueryType } from '@hcengineering/core' import { IntlString, mergeIds } from '@hcengineering/platform'
import { IntlString, mergeIds, Resource } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui' import type { AnyComponent } from '@hcengineering/ui'
import { Filter, ViewAction, viewId } from '@hcengineering/view' import { FilterFunction, ViewAction, ViewCategoryAction, viewId } from '@hcengineering/view'
import view from '@hcengineering/view-resources/src/plugin' import view from '@hcengineering/view-resources/src/plugin'
export default mergeIds(viewId, view, { export default mergeIds(viewId, view, {
@ -93,13 +92,14 @@ export default mergeIds(viewId, view, {
MarkdownFormatting: '' as IntlString MarkdownFormatting: '' as IntlString
}, },
function: { function: {
FilterObjectInResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, FilterObjectInResult: '' as FilterFunction,
FilterObjectNinResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, FilterObjectNinResult: '' as FilterFunction,
FilterValueInResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, FilterValueInResult: '' as FilterFunction,
FilterValueNinResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, FilterValueNinResult: '' as FilterFunction,
FilterBeforeResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, FilterBeforeResult: '' as FilterFunction,
FilterAfterResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, FilterAfterResult: '' as FilterFunction,
FilterNestedMatchResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, FilterNestedMatchResult: '' as FilterFunction,
FilterNestedDontMatchResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>> FilterNestedDontMatchResult: '' as FilterFunction,
ShowEmptyGroups: '' as ViewCategoryAction
} }
}) })

View File

@ -13,13 +13,13 @@
// limitations under the License. // limitations under the License.
// //
import { Client, Doc, ObjQueryType, Ref, Space } from '@hcengineering/core' import { Client, Doc, Ref, Space } from '@hcengineering/core'
import type { IntlString, Resource, StatusCode } from '@hcengineering/platform' import type { IntlString, Resource, StatusCode } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform' import { mergeIds } from '@hcengineering/platform'
import recruit, { recruitId } from '@hcengineering/recruit' import recruit, { recruitId } from '@hcengineering/recruit'
import { TagCategory } from '@hcengineering/tags' import { TagCategory } from '@hcengineering/tags'
import { AnyComponent } from '@hcengineering/ui' import { AnyComponent } from '@hcengineering/ui'
import { Filter, FilterMode } from '@hcengineering/view' import { FilterFunction, FilterMode } from '@hcengineering/view'
export default mergeIds(recruitId, recruit, { export default mergeIds(recruitId, recruit, {
status: { status: {
@ -137,9 +137,9 @@ export default mergeIds(recruitId, recruit, {
}, },
function: { function: {
ApplicationTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>, ApplicationTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>,
HasActiveApplicant: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, HasActiveApplicant: '' as FilterFunction,
HasNoActiveApplicant: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, HasNoActiveApplicant: '' as FilterFunction,
NoneApplications: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>> NoneApplications: '' as FilterFunction
}, },
filter: { filter: {
HasActive: '' as Ref<FilterMode>, HasActive: '' as Ref<FilterMode>,

View File

@ -11,11 +11,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { ObjQueryType } from '@hcengineering/core'
import { IntlString, mergeIds, Resource } from '@hcengineering/platform' import { IntlString, mergeIds, Resource } from '@hcengineering/platform'
import tags, { tagsId } from '@hcengineering/tags' import tags, { tagsId } from '@hcengineering/tags'
import { AnyComponent } from '@hcengineering/ui' import { AnyComponent } from '@hcengineering/ui'
import { Filter } from '@hcengineering/view' import { FilterFunction } from '@hcengineering/view'
export default mergeIds(tagsId, tags, { export default mergeIds(tagsId, tags, {
component: { component: {
@ -53,8 +52,8 @@ export default mergeIds(tagsId, tags, {
Initial: '' as IntlString Initial: '' as IntlString
}, },
function: { function: {
FilterTagsInResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, FilterTagsInResult: '' as FilterFunction,
FilterTagsNinResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, FilterTagsNinResult: '' as FilterFunction,
CreateTagElement: '' as Resource<(props?: Record<string, any>) => Promise<void>> CreateTagElement: '' as Resource<(props?: Record<string, any>) => Promise<void>>
} }
}) })

View File

@ -118,7 +118,6 @@
"Grouping": "Grouping", "Grouping": "Grouping",
"Ordering": "Ordering", "Ordering": "Ordering",
"CompletedIssues": "Completed issues", "CompletedIssues": "Completed issues",
"ShowEmptyGroups": "Show empty groups",
"NoGrouping": "No grouping", "NoGrouping": "No grouping",
"NoAssignee": "No assignee", "NoAssignee": "No assignee",
"LastUpdated": "Last updated", "LastUpdated": "Last updated",

View File

@ -118,7 +118,6 @@
"Grouping": "Группировка", "Grouping": "Группировка",
"Ordering": "Сортировка", "Ordering": "Сортировка",
"CompletedIssues": "Завершённые задачи", "CompletedIssues": "Завершённые задачи",
"ShowEmptyGroups": "Показывать пустые группы",
"NoGrouping": "Нет группировки", "NoGrouping": "Нет группировки",
"NoAssignee": "Нет исполнителя", "NoAssignee": "Нет исполнителя",
"LastUpdated": "Последнее обновление", "LastUpdated": "Последнее обновление",

View File

@ -13,25 +13,26 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact from '@hcengineering/contact' import contact, { Employee } from '@hcengineering/contact'
import { Class, Doc, DocumentQuery, Lookup, Ref, SortingOrder, WithLookup } from '@hcengineering/core' import { Class, Doc, DocumentQuery, IdMap, Lookup, Ref, toIdMap, WithLookup } from '@hcengineering/core'
import { Kanban, TypeState } from '@hcengineering/kanban' import { Kanban, TypeState } from '@hcengineering/kanban'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags' import tags from '@hcengineering/tags'
import { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team } from '@hcengineering/tracker' import { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Project, Sprint, Team } from '@hcengineering/tracker'
import { import {
Button, Button,
Component, Component,
getEventPositionElement, getEventPositionElement,
Icon,
IconAdd, IconAdd,
Loading, Loading,
showPanel, showPanel,
showPopup, showPopup,
tooltip tooltip
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view' import { CategoryOption, ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view'
import { import {
focusStore, focusStore,
ListSelectionProvider, ListSelectionProvider,
@ -41,9 +42,10 @@
} from '@hcengineering/view-resources' } from '@hcengineering/view-resources'
import ActionContext from '@hcengineering/view-resources/src/components/ActionContext.svelte' import ActionContext from '@hcengineering/view-resources/src/components/ActionContext.svelte'
import Menu from '@hcengineering/view-resources/src/components/Menu.svelte' import Menu from '@hcengineering/view-resources/src/components/Menu.svelte'
import { getCategories } from '@hcengineering/view-resources/src/utils'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import tracker from '../../plugin' import tracker from '../../plugin'
import { getIssueStatusStates, getKanbanStatuses, getPriorityStates, issuesGroupBySorting } from '../../utils' import { issuesGroupBySorting, mapKanbanCategories } from '../../utils'
import CreateIssue from '../CreateIssue.svelte' import CreateIssue from '../CreateIssue.svelte'
import ProjectEditor from '../projects/ProjectEditor.svelte' import ProjectEditor from '../projects/ProjectEditor.svelte'
import AssigneePresenter from './AssigneePresenter.svelte' import AssigneePresenter from './AssigneePresenter.svelte'
@ -68,7 +70,6 @@
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual $: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
const spaceQuery = createQuery() const spaceQuery = createQuery()
const statusesQuery = createQuery()
let currentTeam: Team | undefined let currentTeam: Team | undefined
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => { $: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
@ -97,20 +98,6 @@
return result return result
} }
let issueStatuses: WithLookup<IssueStatus>[] | undefined
$: issueStatusStates = getIssueStatusStates(issueStatuses)
$: statusesQuery.query(
tracker.class.IssueStatus,
{ attachedTo: currentSpace },
(is) => {
issueStatuses = is
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
)
function toIssue (object: any): WithLookup<Issue> { function toIssue (object: any): WithLookup<Issue> {
return object as WithLookup<Issue> return object as WithLookup<Issue>
} }
@ -138,9 +125,9 @@
}) })
} }
const issuesQuery = createQuery() const issuesQuery = createQuery()
let issueStates: TypeState[] = [] let issues: Issue[] = []
const lookupIssue: Lookup<Issue> = { const lookupIssue: Lookup<Issue> = {
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }], status: tracker.class.IssueStatus,
project: tracker.class.Project, project: tracker.class.Project,
sprint: tracker.class.Sprint, sprint: tracker.class.Sprint,
assignee: contact.class.Employee assignee: contact.class.Employee
@ -148,8 +135,8 @@
$: issuesQuery.query( $: issuesQuery.query(
tracker.class.Issue, tracker.class.Issue,
resultQuery, resultQuery,
async (result) => { (result) => {
issueStates = await getKanbanStatuses(groupBy, result) issues = result
}, },
{ {
lookup: lookupIssue, lookup: lookupIssue,
@ -157,27 +144,107 @@
} }
) )
let priorityStates: TypeState[] = [] const assigneeQuery = createQuery()
getPriorityStates().then((states) => { let assignee: Employee[] = []
priorityStates = states $: assigneeQuery.query(contact.class.Employee, {}, (result) => {
assignee = result
}) })
function getIssueStates (
groupBy: IssuesGrouping, const statusesQuery = createQuery()
states: TypeState[], let statuses: WithLookup<IssueStatus>[] = []
statusStates: TypeState[], let statusesMap: IdMap<IssueStatus> = new Map()
priorityStates: TypeState[] $: statusesQuery.query(
tracker.class.IssueStatus,
{
space: currentSpace
},
(result) => {
statuses = result
statusesMap = toIdMap(result)
},
{
lookup: { category: tracker.class.IssueStatusCategory }
}
)
const projectsQuery = createQuery()
let projects: Project[] = []
$: projectsQuery.query(
tracker.class.Project,
{
space: currentSpace
},
(result) => {
projects = result
}
)
const sprintsQuery = createQuery()
let sprints: Sprint[] = []
$: sprintsQuery.query(
tracker.class.Sprint,
{
space: currentSpace
},
(result) => {
sprints = result
}
)
let states: TypeState[]
$: updateCategories(
tracker.class.Issue,
issues,
groupBy,
viewOptions,
viewOptionsConfig,
statuses,
projects,
sprints,
assignee
)
async function updateCategories (
_class: Ref<Class<Doc>>,
docs: Doc[],
groupByKey: string,
viewOptions: ViewOptions,
viewOptionsModel: ViewOptionModel[] | undefined,
statuses: WithLookup<IssueStatus>[],
projects: Project[],
sprints: Sprint[],
assignee: Employee[]
) { ) {
if (states.length > 0) return states let categories = await getCategories(client, _class, docs, groupByKey)
if (groupBy === IssuesGrouping.Status) return statusStates for (const viewOption of viewOptionsModel ?? []) {
if (groupBy === IssuesGrouping.Priority) return priorityStates if (viewOption.actionTartget !== 'category') continue
return [] const categoryFunc = viewOption as CategoryOption
if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
const f = await getResource(categoryFunc.action)
const res = await f(_class, space, groupByKey)
if (res !== undefined) {
for (const category of categories) {
if (!res.includes(category)) {
res.push(category)
}
}
categories = res
break
}
}
}
const indexes = new Map(categories.map((p, i) => [p, i]))
const res = await mapKanbanCategories(groupByKey, categories, statuses, projects, sprints, assignee)
res.sort((a, b) => {
const aIndex = indexes.get(a._id ?? undefined) ?? -1
const bIndex = indexes.get(b._id ?? undefined) ?? -1
return aIndex - bIndex
})
states = res
} }
$: states = getIssueStates(groupBy, issueStates, issueStatusStates, priorityStates)
const fullFilled: { [key: string]: boolean } = {} const fullFilled: { [key: string]: boolean } = {}
const getState = (state: any): WithLookup<IssueStatus> | undefined => {
return issueStatuses?.filter((is) => is._id === state._id)[0]
}
</script> </script>
{#if !states?.length} {#if !states?.length}
@ -211,11 +278,17 @@
on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)} on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)}
> >
<svelte:fragment slot="header" let:state let:count> <svelte:fragment slot="header" let:state let:count>
{@const stateWLU = getState(state)} {@const status = statusesMap.get(state._id)}
<div class="header flex-col"> <div class="header flex-col">
<div class="flex-between label font-medium w-full h-full"> <div class="flex-between label font-medium w-full h-full">
<div class="flex-row-center gap-2"> <div class="flex-row-center gap-2">
{#if stateWLU !== undefined}<IssueStatusIcon value={stateWLU} size={'small'} />{/if} {#if state.icon}
{#if groupBy === 'status' && status}
<IssueStatusIcon value={status} size="small" />
{:else}
<Icon icon={state.icon} size="small" />
{/if}
{/if}
<span class="lines-limit-2 ml-2">{state.title}</span> <span class="lines-limit-2 ml-2">{state.title}</span>
<span class="counter ml-2 text-md">{count}</span> <span class="counter ml-2 text-md">{count}</span>
</div> </div>

View File

@ -106,7 +106,17 @@ import IssueTemplates from './components/templates/IssueTemplates.svelte'
import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte' import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte'
import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte' import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte'
import MoveAndDeleteSprintPopup from './components/sprints/MoveAndDeleteSprintPopup.svelte' import MoveAndDeleteSprintPopup from './components/sprints/MoveAndDeleteSprintPopup.svelte'
import { moveIssuesToAnotherSprint, issueStatusSort, issuePrioritySort, sprintSort, subIssueQuery } from './utils' import {
moveIssuesToAnotherSprint,
issueStatusSort,
issuePrioritySort,
sprintSort,
subIssueQuery,
getAllStatuses,
getAllPriority,
getAllProjects,
getAllSprints
} from './utils'
import { deleteObject } from '@hcengineering/view-resources/src/utils' import { deleteObject } from '@hcengineering/view-resources/src/utils'
import CreateTeam from './components/teams/CreateTeam.svelte' import CreateTeam from './components/teams/CreateTeam.svelte'
@ -384,7 +394,11 @@ export default async (): Promise<Resources> => ({
IssueStatusSort: issueStatusSort, IssueStatusSort: issueStatusSort,
IssuePrioritySort: issuePrioritySort, IssuePrioritySort: issuePrioritySort,
SprintSort: sprintSort, SprintSort: sprintSort,
SubIssueQuery: subIssueQuery SubIssueQuery: subIssueQuery,
GetAllStatuses: getAllStatuses,
GetAllPriority: getAllPriority,
GetAllProjects: getAllProjects,
GetAllSprints: getAllSprints
}, },
actionImpl: { actionImpl: {
EditWorkflowStatuses: editWorkflowStatuses, EditWorkflowStatuses: editWorkflowStatuses,

View File

@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import { Client, Doc, DocumentQuery, Ref } from '@hcengineering/core' import { Client, Doc, Ref, Space } from '@hcengineering/core'
import type { IntlString, Metadata, Resource } from '@hcengineering/platform' import type { IntlString, Metadata, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform' import { mergeIds } from '@hcengineering/platform'
import { IssueDraft } from '@hcengineering/tracker' import { IssueDraft, IssuePriority, IssueStatus, Project, Sprint } from '@hcengineering/tracker'
import { AnyComponent } from '@hcengineering/ui' import { AnyComponent } from '@hcengineering/ui'
import { SortFunc, Viewlet } from '@hcengineering/view' import { SortFunc, Viewlet, ViewQueryAction } from '@hcengineering/view'
import tracker, { trackerId } from '../../tracker/lib' import tracker, { trackerId } from '../../tracker/lib'
export default mergeIds(trackerId, tracker, { export default mergeIds(trackerId, tracker, {
@ -143,7 +143,6 @@ export default mergeIds(trackerId, tracker, {
Grouping: '' as IntlString, Grouping: '' as IntlString,
Ordering: '' as IntlString, Ordering: '' as IntlString,
CompletedIssues: '' as IntlString, CompletedIssues: '' as IntlString,
ShowEmptyGroups: '' as IntlString,
NoGrouping: '' as IntlString, NoGrouping: '' as IntlString,
NoAssignee: '' as IntlString, NoAssignee: '' as IntlString,
LastUpdated: '' as IntlString, LastUpdated: '' as IntlString,
@ -377,6 +376,10 @@ export default mergeIds(trackerId, tracker, {
IssueStatusSort: '' as SortFunc, IssueStatusSort: '' as SortFunc,
IssuePrioritySort: '' as SortFunc, IssuePrioritySort: '' as SortFunc,
SprintSort: '' as SortFunc, SprintSort: '' as SortFunc,
SubIssueQuery: '' as Resource<(value: any, query: DocumentQuery<Doc>) => DocumentQuery<Doc>> 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>>>>
} }
}) })

View File

@ -13,13 +13,14 @@
// limitations under the License. // limitations under the License.
// //
import { Employee } from '@hcengineering/contact' import { Employee, formatName } from '@hcengineering/contact'
import core, { import core, {
AttachedData, AttachedData,
Doc, Doc,
DocumentQuery, DocumentQuery,
Ref, Ref,
SortingOrder, SortingOrder,
Space,
toIdMap, toIdMap,
TxCollectionCUD, TxCollectionCUD,
TxOperations, TxOperations,
@ -37,6 +38,7 @@ import {
IssuesOrdering, IssuesOrdering,
IssueStatus, IssueStatus,
IssueTemplateData, IssueTemplateData,
Project,
ProjectStatus, ProjectStatus,
Sprint, Sprint,
SprintStatus, SprintStatus,
@ -51,7 +53,6 @@ import {
isWeekend, isWeekend,
MILLISECONDS_IN_WEEK MILLISECONDS_IN_WEEK
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { ViewOptionModel } from '@hcengineering/view'
import { ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources' import { ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import tracker from './plugin' import tracker from './plugin'
import { defaultPriorities, defaultProjectStatuses, defaultSprintStatuses, issuePriorities } from './types' import { defaultPriorities, defaultProjectStatuses, defaultSprintStatuses, issuePriorities } from './types'
@ -360,154 +361,108 @@ export async function sprintSort (value: Array<Ref<Sprint>>): Promise<Array<Ref<
}) })
} }
export async function getKanbanStatuses ( export async function mapKanbanCategories (
groupBy: IssuesGrouping, groupBy: string,
issues: Array<WithLookup<Issue>> categories: any[],
statuses: Array<WithLookup<IssueStatus>>,
projects: Project[],
sprints: Sprint[],
assignee: Employee[]
): Promise<TypeState[]> { ): Promise<TypeState[]> {
if (groupBy === IssuesGrouping.NoGrouping) { if (groupBy === IssuesGrouping.NoGrouping) {
return [{ _id: undefined, color: UNSET_COLOR, title: await translate(tracker.string.NoGrouping, {}) }] return [{ _id: undefined, color: UNSET_COLOR, title: await translate(tracker.string.NoGrouping, {}) }]
} }
if (groupBy === IssuesGrouping.Priority) { if (groupBy === IssuesGrouping.Priority) {
const states = issues.reduce<TypeState[]>((result, issue) => { const res: TypeState[] = []
const { priority } = issue for (const priority of categories) {
if (result.find(({ _id }) => _id === priority) !== undefined) return result const title = await translate((issuePriorities as any)[priority].label, {})
return [ res.push({
...result, _id: priority,
{ title,
_id: priority, color: UNSET_COLOR,
title: issuePriorities[priority].label, icon: (issuePriorities as any)[priority].icon
color: UNSET_COLOR,
icon: issuePriorities[priority].icon
}
]
}, [])
await Promise.all(
states.map(async (state) => {
state.title = await translate(state.title as IntlString, {})
}) })
) }
return states return res
} }
if (groupBy === IssuesGrouping.Status) { if (groupBy === IssuesGrouping.Status) {
return issues.reduce<TypeState[]>((result, issue) => { return statuses
const status = issue.$lookup?.status .filter((p) => categories.includes(p._id))
if (status === undefined || result.find(({ _id }) => _id === status._id) !== undefined) return result .map((status) => {
const category = '$lookup' in status ? status.$lookup?.category : undefined const category = '$lookup' in status ? status.$lookup?.category : undefined
return [ return {
...result,
{
_id: status._id, _id: status._id,
title: status.name, title: status.name,
icon: category?.icon, icon: category?.icon,
color: status.color ?? category?.color ?? UNSET_COLOR color: status.color ?? category?.color ?? UNSET_COLOR
} }
] })
}, [])
} }
if (groupBy === IssuesGrouping.Assignee) { if (groupBy === IssuesGrouping.Assignee) {
const noAssignee = await translate(tracker.string.NoAssignee, {}) const noAssignee = await translate(tracker.string.NoAssignee, {})
return issues.reduce<TypeState[]>((result, issue) => { const res: TypeState[] = assignee
if (result.find(({ _id }) => _id === issue.assignee) !== undefined) return result .filter((p) => categories.includes(p._id))
return [ .map((employee) => {
...result, return {
{ _id: employee._id,
_id: issue.assignee, title: formatName(employee.name),
title: issue.$lookup?.assignee?.name ?? noAssignee,
color: UNSET_COLOR, color: UNSET_COLOR,
icon: undefined icon: undefined
} }
] })
}, []) if (categories.includes(undefined)) {
res.push({
_id: null,
title: noAssignee,
color: UNSET_COLOR,
icon: undefined
})
}
return res
} }
if (groupBy === IssuesGrouping.Project) { if (groupBy === IssuesGrouping.Project) {
const noProject = await translate(tracker.string.NoProject, {}) const noProject = await translate(tracker.string.NoProject, {})
return issues.reduce<TypeState[]>((result, issue) => { const res: TypeState[] = projects
if (result.find(({ _id }) => _id === issue.project) !== undefined) return result .filter((p) => categories.includes(p._id))
return [ .map((project) => ({
...result, _id: project._id,
{ title: project.label,
_id: issue.project, color: UNSET_COLOR,
title: issue.$lookup?.project?.label ?? noProject, icon: undefined
color: UNSET_COLOR, }))
icon: undefined if (categories.includes(undefined)) {
} res.push({
] _id: null,
}, []) title: noProject,
color: UNSET_COLOR,
icon: undefined
})
}
return res
} }
if (groupBy === IssuesGrouping.Sprint) { if (groupBy === IssuesGrouping.Sprint) {
const noSprint = await translate(tracker.string.NoSprint, {}) const noSprint = await translate(tracker.string.NoSprint, {})
return issues.reduce<TypeState[]>((result, issue) => { const res: TypeState[] = sprints
if (result.find(({ _id }) => _id === issue.sprint) !== undefined) return result .filter((p) => categories.includes(p._id))
return [ .map((sprint) => ({
...result, _id: sprint._id,
{ title: sprint.label,
_id: issue.sprint, color: UNSET_COLOR,
title: issue.$lookup?.sprint?.label ?? noSprint, icon: undefined
color: UNSET_COLOR, }))
icon: undefined if (categories.includes(undefined)) {
} res.push({
] _id: null,
}, []) title: noSprint,
color: UNSET_COLOR,
icon: undefined
})
}
return res
} }
return [] return []
} }
export function getIssueStatusStates (issueStatuses: Array<WithLookup<IssueStatus>> = []): TypeState[] {
return issueStatuses.map((status) => ({
_id: status._id,
title: status.name,
color: status.color ?? status.$lookup?.category?.color ?? UNSET_COLOR,
icon: status.$lookup?.category?.icon ?? undefined
}))
}
export async function getPriorityStates (): Promise<TypeState[]> {
return await Promise.all(
defaultPriorities.map(async (priority) => ({
_id: priority,
title: await translate(issuePriorities[priority].label, {}),
color: UNSET_COLOR,
icon: issuePriorities[priority].icon
}))
)
}
export function getDefaultViewOptionsTemplatesConfig (): ViewOptionModel[] {
const groupByCategory: ViewOptionModel = {
key: 'groupBy',
label: tracker.string.Grouping,
defaultValue: 'project',
values: [
{ id: 'assignee', label: tracker.string.Assignee },
{ id: 'priority', label: tracker.string.Priority },
{ id: 'project', label: tracker.string.Project },
{ id: 'sprint', label: tracker.string.Sprint },
{ id: '#no_category', label: tracker.string.NoGrouping }
],
type: 'dropdown'
}
const orderByCategory: ViewOptionModel = {
key: 'orderBy',
label: tracker.string.Ordering,
defaultValue: 'priority',
values: [
{ id: 'modifiedOn', label: tracker.string.LastUpdated },
{ id: 'priority', label: tracker.string.Priority },
{ id: 'dueDate', label: tracker.string.DueDate }
],
type: 'dropdown'
}
const showEmptyGroups: ViewOptionModel = {
key: 'shouldShowEmptyGroups',
label: tracker.string.ShowEmptyGroups,
defaultValue: false,
type: 'toggle'
}
const result: ViewOptionModel[] = [groupByCategory, orderByCategory]
result.push(showEmptyGroups)
return result
}
/** /**
* @public * @public
*/ */
@ -591,6 +546,43 @@ export function subIssueQuery (value: boolean, query: DocumentQuery<Issue>): Doc
return value ? query : { ...query, attachedTo: tracker.ids.NoParent } 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()
})
})
}
export async function getAllPriority (space: Ref<Space> | undefined): Promise<IssuePriority[] | 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 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 function subIssueListProvider (subIssues: Issue[], target: Ref<Issue>): void { export function subIssueListProvider (subIssues: Issue[], target: Ref<Issue>): void {
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => { const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (dir === 'vertical') { if (dir === 'vertical') {

View File

@ -387,17 +387,6 @@ export interface Scrum extends Doc {
attachments?: number attachments?: number
} }
/**
* @public
*/
export interface ViewOptions {
groupBy: IssuesGrouping
orderBy: IssuesOrdering
completedIssuesPeriod: IssuesDateModificationPeriod
shouldShowEmptyGroups: boolean
shouldShowSubIssues: boolean
}
/** /**
* @public * @public
*/ */

View File

@ -64,6 +64,7 @@
"Then": "Then", "Then": "Then",
"ShowPreviewOnClick": "Please click to show document index preview...", "ShowPreviewOnClick": "Please click to show document index preview...",
"Showed": "Showed", "Showed": "Showed",
"Total": "Total" "Total": "Total",
"ShowEmptyGroups": "Show empty groups"
} }
} }

View File

@ -61,6 +61,7 @@
"Then": "Затем", "Then": "Затем",
"ShowPreviewOnClick": "Пожалуйста нажмите чтобы увидеть предпросмотр...", "ShowPreviewOnClick": "Пожалуйста нажмите чтобы увидеть предпросмотр...",
"Showed": "Показано", "Showed": "Показано",
"Total": "Всего" "Total": "Всего",
"ShowEmptyGroups": "Показывать пустые группы"
} }
} }

View File

@ -33,7 +33,6 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const changeStatus = async (newStatus: any) => { const changeStatus = async (newStatus: any) => {
console.log('CHANGE VALUE', newStatus)
if (newStatus === '#null') { if (newStatus === '#null') {
newStatus = null newStatus = null
return return
@ -120,7 +119,6 @@
<SelectPopup <SelectPopup
value={valuesToShow} value={valuesToShow}
on:close={(evt) => { on:close={(evt) => {
console.log(evt)
changeStatus(evt.detail) changeStatus(evt.detail)
}} }}
placeholder={placeholder ?? view.string.Filter} placeholder={placeholder ?? view.string.Filter}
@ -138,7 +136,6 @@
allowDeselect={true} allowDeselect={true}
selected={current} selected={current}
on:close={(evt) => { on:close={(evt) => {
console.log(evt)
changeStatus(evt.detail === null ? null : evt.detail?._id) changeStatus(evt.detail === null ? null : evt.detail?._id)
}} }}
placeholder={placeholder ?? view.string.Filter} placeholder={placeholder ?? view.string.Filter}

View File

@ -34,16 +34,11 @@
} }
}) })
$: groups =
viewOptions.groupBy[viewOptions.groupBy.length - 1] === noCategory
? viewOptions.groupBy
: [...viewOptions.groupBy, noCategory]
function selectGrouping (value: string, i: number) { function selectGrouping (value: string, i: number) {
viewOptions.groupBy[i] = value viewOptions.groupBy[i] = value
if (value === noCategory) { if (value === noCategory) {
viewOptions.groupBy.length = i viewOptions.groupBy.length = i + 1
} else { } else if (config.groupDepth === undefined || config.groupDepth > viewOptions.groupBy.length) {
viewOptions.groupBy.length = i + 1 viewOptions.groupBy.length = i + 1
viewOptions.groupBy[i + 1] = noCategory viewOptions.groupBy[i + 1] = noCategory
} }
@ -62,7 +57,7 @@
<div class="antiCard"> <div class="antiCard">
<div class="antiCard-group grid"> <div class="antiCard-group grid">
{#each groups as group, i} {#each viewOptions.groupBy as group, i}
<span class="label"><Label label={i === 0 ? view.string.Grouping : view.string.Then} /></span> <span class="label"><Label label={i === 0 ? view.string.Grouping : view.string.Then} /></span>
<div class="value grouping"> <div class="value grouping">
<DropdownLabelsIntl <DropdownLabelsIntl
@ -93,12 +88,12 @@
}} }}
/> />
</div> </div>
{#each config.other as model} {#each config.other.filter((p) => !p.hidden?.(viewOptions)) as model}
<span class="label"><Label label={model.label} /></span> <span class="label"><Label label={model.label} /></span>
<div class="value"> <div class="value">
{#if isToggleType(model)} {#if isToggleType(model)}
<MiniToggle <MiniToggle
on={viewOptions[model.key]} on={viewOptions[model.key] ?? model.defaultValue}
on:change={() => { on:change={() => {
viewOptions[model.key] = !viewOptions[model.key] viewOptions[model.key] = !viewOptions[model.key]
dispatch('update', { key: model.key, value: viewOptions[model.key] }) dispatch('update', { key: model.key, value: viewOptions[model.key] })
@ -109,7 +104,7 @@
<DropdownLabelsIntl <DropdownLabelsIntl
label={model.label} label={model.label}
{items} {items}
selected={viewOptions[model.key]} selected={viewOptions[model.key] ?? model.defaultValue}
width="10rem" width="10rem"
justify="left" justify="left"
on:selected={(e) => { on:selected={(e) => {

View File

@ -347,8 +347,6 @@
}, },
getEventPositionElement(e), getEventPositionElement(e),
(val) => { (val) => {
console.log('val')
console.log(val)
if (val !== undefined) { if (val !== undefined) {
const value = classes.get(_class)?.find((it) => it.value === val) const value = classes.get(_class)?.find((it) => it.value === val)
if (value) { if (value) {

View File

@ -58,7 +58,6 @@
const attr = hieararchy.getAttribute(filter.key._class, filter.key.key) const attr = hieararchy.getAttribute(filter.key._class, filter.key.key)
if (hieararchy.isMixin(attr.attributeOf)) { if (hieararchy.isMixin(attr.attributeOf)) {
prefix = attr.attributeOf + '.' prefix = attr.attributeOf + '.'
console.log('prefix', prefix)
} }
objectsPromise = client.findAll( objectsPromise = client.findAll(
_class, _class,

View File

@ -14,11 +14,11 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Class, Doc, Lookup, Ref, Space } from '@hcengineering/core' import { Class, Doc, Lookup, Ref, Space } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { getResource, IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { AnyComponent } from '@hcengineering/ui' import { AnyComponent } from '@hcengineering/ui'
import { AttributeModel, BuildModelKey, ViewOptions } from '@hcengineering/view' import { AttributeModel, BuildModelKey, CategoryOption, ViewOptionModel, ViewOptions } from '@hcengineering/view'
import { buildModel, getCategories, getPresenter, groupBy, getAdditionalHeader } from '../../utils' import { buildModel, getAdditionalHeader, getCategories, getPresenter, groupBy } from '../../utils'
import { noCategory } from '../../viewOptions' import { noCategory } from '../../viewOptions'
import ListCategory from './ListCategory.svelte' import ListCategory from './ListCategory.svelte'
@ -42,13 +42,41 @@
export let initIndex = 0 export let initIndex = 0
export let newObjectProps: Record<string, any> export let newObjectProps: Record<string, any>
export let docByIndex: Map<number, Doc> export let docByIndex: Map<number, Doc>
export let viewOptionsConfig: ViewOptionModel[] | undefined
$: groupByKey = viewOptions.groupBy[level] ?? noCategory $: groupByKey = viewOptions.groupBy[level] ?? noCategory
$: groupedDocs = groupBy(docs, groupByKey) $: groupedDocs = groupBy(docs, groupByKey)
let categories: any[] = [] let categories: any[] = []
$: getCategories(client, _class, docs, groupByKey).then((p) => { $: updateCategories(_class, docs, groupByKey, viewOptions, viewOptionsConfig)
categories = p
}) async function updateCategories (
_class: Ref<Class<Doc>>,
docs: Doc[],
groupByKey: string,
viewOptions: ViewOptions,
viewOptionsModel: ViewOptionModel[] | undefined
) {
categories = await getCategories(client, _class, docs, groupByKey)
if (level === 0) {
for (const viewOption of viewOptionsModel ?? []) {
if (viewOption.actionTartget !== 'category') continue
const categoryFunc = viewOption as CategoryOption
if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
const f = await getResource(categoryFunc.action)
const res = await f(_class, space, groupByKey)
if (res !== undefined) {
for (const category of categories) {
if (!res.includes(category)) {
res.push(category)
}
}
categories = res
return
}
}
}
}
}
const client = getClient() const client = getClient()

View File

@ -23,7 +23,7 @@
showPopup, showPopup,
Spinner Spinner
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { AttributeModel, BuildModelKey, ViewOptions } from '@hcengineering/view' import { AttributeModel, BuildModelKey, ViewOptionModel, ViewOptions } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { FocusSelection, focusStore } from '../../selection' import { FocusSelection, focusStore } from '../../selection'
import Menu from '../Menu.svelte' import Menu from '../Menu.svelte'
@ -57,6 +57,7 @@
export let viewOptions: ViewOptions export let viewOptions: ViewOptions
export let newObjectProps: Record<string, any> export let newObjectProps: Record<string, any>
export let docByIndex: Map<number, Doc> export let docByIndex: Map<number, Doc>
export let viewOptionsConfig: ViewOptionModel[] | undefined
$: lastLevel = level + 1 >= viewOptions.groupBy.length $: lastLevel = level + 1 >= viewOptions.groupBy.length
@ -155,6 +156,7 @@
level={level + 1} level={level + 1}
{initIndex} {initIndex}
{docByIndex} {docByIndex}
{viewOptionsConfig}
on:check on:check
on:uncheckAll on:uncheckAll
on:row-focus on:row-focus

View File

@ -78,6 +78,7 @@ import {
} from './filter' } from './filter'
import { IndexedDocumentPreview } from '@hcengineering/presentation' import { IndexedDocumentPreview } from '@hcengineering/presentation'
import { showEmptyGroups } from './viewOptions'
function PositionElementAlignment (e?: Event): PopupAlignment | undefined { function PositionElementAlignment (e?: Event): PopupAlignment | undefined {
return getEventPopupPositionElement(e) return getEventPopupPositionElement(e)
@ -192,6 +193,7 @@ export default async (): Promise<Resources> => ({
FilterBeforeResult: beforeResult, FilterBeforeResult: beforeResult,
FilterAfterResult: afterResult, FilterAfterResult: afterResult,
FilterNestedMatchResult: nestedMatchResult, FilterNestedMatchResult: nestedMatchResult,
FilterNestedDontMatchResult: nestedDontMatchResult FilterNestedDontMatchResult: nestedDontMatchResult,
ShowEmptyGroups: showEmptyGroups
} }
}) })

View File

@ -64,6 +64,7 @@ export default mergeIds(viewId, view, {
Then: '' as IntlString, Then: '' as IntlString,
ShowPreviewOnClick: '' as IntlString, ShowPreviewOnClick: '' as IntlString,
Showed: '' as IntlString, Showed: '' as IntlString,
ShowEmptyGroups: '' as IntlString,
Total: '' as IntlString Total: '' as IntlString
} }
}) })

View File

@ -1,4 +1,6 @@
import { SortingOrder } from '@hcengineering/core' import { Class, Doc, Ref, SortingOrder, Space } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { getCurrentLocation, locationToUrl } from '@hcengineering/ui' import { getCurrentLocation, locationToUrl } from '@hcengineering/ui'
import { import {
DropdownViewOption, DropdownViewOption,
@ -8,6 +10,7 @@ import {
ViewOptions, ViewOptions,
ViewOptionsModel ViewOptionsModel
} from '@hcengineering/view' } from '@hcengineering/view'
import view from './plugin'
export const noCategory = '#no_category' export const noCategory = '#no_category'
@ -82,3 +85,28 @@ export function migrateViewOpttions (): void {
localStorage.setItem(key, JSON.stringify(res)) localStorage.setItem(key, JSON.stringify(res))
} }
} }
export async function showEmptyGroups (
_class: Ref<Class<Doc>>,
space: Ref<Space> | undefined,
key: string
): Promise<any[] | undefined> {
const client = getClient()
const hierarchy = client.getHierarchy()
const attr = hierarchy.getAttribute(_class, key)
if (attr === undefined) return
const { attrClass } = getAttributePresenterClass(hierarchy, attr)
const attributeClass = hierarchy.getClass(attrClass)
const mixin = hierarchy.as(attributeClass, view.mixin.AllValuesFunc)
if (mixin.func !== undefined) {
const f = await getResource(mixin.func)
const res = await f(space)
if (res !== undefined) {
const sortFunc = hierarchy.as(attributeClass, view.mixin.SortFuncs)
if (sortFunc?.func === undefined) return res
const f = await getResource(sortFunc.func)
return await f(res)
}
}
}

View File

@ -59,9 +59,14 @@ export interface KeyFilter {
export interface FilterMode extends Doc { export interface FilterMode extends Doc {
label: IntlString label: IntlString
disableValueSelector?: boolean disableValueSelector?: boolean
result: Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>> result: FilterFunction
} }
/**
* @public
*/
export type FilterFunction = Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>
/** /**
* @public * @public
*/ */
@ -238,6 +243,13 @@ export interface ClassSortFuncs extends Class<Doc> {
func: SortFunc func: SortFunc
} }
/**
* @public
*/
export interface AllValuesFunc extends Class<Doc> {
func: Resource<(space: Ref<Space> | undefined) => Promise<any[]>>
}
/** /**
* @public * @public
*/ */
@ -468,16 +480,36 @@ export interface ViewOption {
defaultValue: any defaultValue: any
label: IntlString label: IntlString
hidden?: (viewOptions: ViewOptions) => boolean hidden?: (viewOptions: ViewOptions) => boolean
actionTartget?: 'query' actionTartget?: 'query' | 'category'
action?: Resource<(value: any, ...params: any) => any> action?: Resource<(value: any, ...params: any) => any>
} }
/**
* @public
*/
export type ViewCategoryAction = Resource<
(_class: Ref<Class<Doc>>, space: Ref<Space> | undefined, key: string) => Promise<any[] | undefined>
>
/**
* @public
*/
export interface CategoryOption extends ViewOption {
actionTartget: 'category'
action: ViewCategoryAction
}
/**
* @public
*/
export type ViewQueryAction = Resource<(value: any, query: DocumentQuery<Doc>) => DocumentQuery<Doc>>
/** /**
* @public * @public
*/ */
export interface ViewQueryOption extends ViewOption { export interface ViewQueryOption extends ViewOption {
actionTartget: 'query' actionTartget: 'query'
action: Resource<(value: any, query: DocumentQuery<Doc>) => DocumentQuery<Doc>> action: ViewQueryAction
} }
/** /**
@ -514,6 +546,7 @@ export interface ViewOptionsModel {
groupBy: string[] groupBy: string[]
orderBy: OrderOption[] orderBy: OrderOption[]
other: ViewOptionModel[] other: ViewOptionModel[]
groupDepth?: number
} }
/** /**
@ -542,7 +575,8 @@ const view = plugin(viewId, {
IgnoreActions: '' as Ref<Mixin<IgnoreActions>>, IgnoreActions: '' as Ref<Mixin<IgnoreActions>>,
PreviewPresenter: '' as Ref<Mixin<PreviewPresenter>>, PreviewPresenter: '' as Ref<Mixin<PreviewPresenter>>,
ListHeaderExtra: '' as Ref<Mixin<ListHeaderExtra>>, ListHeaderExtra: '' as Ref<Mixin<ListHeaderExtra>>,
SortFuncs: '' as Ref<Mixin<ClassSortFuncs>> SortFuncs: '' as Ref<Mixin<ClassSortFuncs>>,
AllValuesFunc: '' as Ref<Mixin<AllValuesFunc>>
}, },
class: { class: {
ViewletPreference: '' as Ref<Class<ViewletPreference>>, ViewletPreference: '' as Ref<Class<ViewletPreference>>,

View File

@ -133,7 +133,6 @@
on:select={(result) => { on:select={(result) => {
if (result.detail !== undefined) { if (result.detail !== undefined) {
viewlet = viewlets.find((vl) => vl._id === result.detail.id) viewlet = viewlets.find((vl) => vl._id === result.detail.id)
console.log('set viewlet by space headed')
if (viewlet) setActiveViewletId(viewlet._id) if (viewlet) setActiveViewletId(viewlet._id)
} }
}} }}

View File

@ -165,8 +165,9 @@ test.describe('tracker layout tests', () => {
} else { } else {
orderedIssueNames = issuesProps.map((props) => props.name).reverse() orderedIssueNames = issuesProps.map((props) => props.name).reverse()
} }
await setViewOrder(page, order)
await page.click(ViewletSelectors.Board) await page.click(ViewletSelectors.Board)
await setViewGroup(page, 'No grouping')
await setViewOrder(page, order)
await expect(locator).toContainText(orderedIssueNames) await expect(locator).toContainText(orderedIssueNames)
}) })
} }