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',
action: tracker.function.SubIssueQuery,
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],
['rank', SortingOrder.Ascending]
],
groupDepth: 1,
other: []
}
@ -696,7 +705,10 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: tracker.class.Issue,
descriptor: tracker.viewlet.Kanban,
viewOptions: issuesOptions,
viewOptions: {
...issuesOptions,
groupDepth: 1
},
config: []
})
@ -876,6 +888,22 @@ export function createModel (builder: Builder): void {
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(
workbench.class.Application,
core.space.Model,
@ -933,13 +961,13 @@ export function createModel (builder: Builder): void {
{
id: activeId,
label: tracker.string.Active,
// icon: tracker.icon.TrackerApplication,
icon: tracker.icon.CategoryStarted,
component: tracker.component.Active
},
{
id: backlogId,
label: tracker.string.Backlog,
// icon: tracker.icon.TrackerApplication,
icon: tracker.icon.CategoryBacklog,
component: tracker.component.Backlog
},
{

View File

@ -62,7 +62,8 @@ import type {
ViewletDescriptor,
ViewletPreference,
ViewOptionsModel,
ViewOptions
ViewOptions,
AllValuesFunc
} from '@hcengineering/view'
import view from './plugin'
@ -215,6 +216,11 @@ export class TSortFuncs extends TClass implements ClassSortFuncs {
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)
export class TViewletPreference extends TPreference implements ViewletPreference {
attachedTo!: Ref<Viewlet>
@ -339,7 +345,8 @@ export function createModel (builder: Builder): void {
TLinkPresenter,
TArrayEditor,
TInlineAttributEditor,
TFilteredView
TFilteredView,
TAllValuesFunc
)
classPresenter(

View File

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

View File

@ -13,13 +13,13 @@
// 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 { mergeIds } from '@hcengineering/platform'
import recruit, { recruitId } from '@hcengineering/recruit'
import { TagCategory } from '@hcengineering/tags'
import { AnyComponent } from '@hcengineering/ui'
import { Filter, FilterMode } from '@hcengineering/view'
import { FilterFunction, FilterMode } from '@hcengineering/view'
export default mergeIds(recruitId, recruit, {
status: {
@ -137,9 +137,9 @@ export default mergeIds(recruitId, recruit, {
},
function: {
ApplicationTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>,
HasActiveApplicant: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>,
HasNoActiveApplicant: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>,
NoneApplications: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>
HasActiveApplicant: '' as FilterFunction,
HasNoActiveApplicant: '' as FilterFunction,
NoneApplications: '' as FilterFunction
},
filter: {
HasActive: '' as Ref<FilterMode>,

View File

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

View File

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

View File

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

View File

@ -13,25 +13,26 @@
// limitations under the License.
-->
<script lang="ts">
import contact from '@hcengineering/contact'
import { Class, Doc, DocumentQuery, Lookup, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import contact, { Employee } from '@hcengineering/contact'
import { Class, Doc, DocumentQuery, IdMap, Lookup, Ref, toIdMap, WithLookup } from '@hcengineering/core'
import { Kanban, TypeState } from '@hcengineering/kanban'
import notification from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
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 {
Button,
Component,
getEventPositionElement,
Icon,
IconAdd,
Loading,
showPanel,
showPopup,
tooltip
} from '@hcengineering/ui'
import { ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view'
import { CategoryOption, ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view'
import {
focusStore,
ListSelectionProvider,
@ -41,9 +42,10 @@
} from '@hcengineering/view-resources'
import ActionContext from '@hcengineering/view-resources/src/components/ActionContext.svelte'
import Menu from '@hcengineering/view-resources/src/components/Menu.svelte'
import { getCategories } from '@hcengineering/view-resources/src/utils'
import { onMount } from 'svelte'
import tracker from '../../plugin'
import { getIssueStatusStates, getKanbanStatuses, getPriorityStates, issuesGroupBySorting } from '../../utils'
import { issuesGroupBySorting, mapKanbanCategories } from '../../utils'
import CreateIssue from '../CreateIssue.svelte'
import ProjectEditor from '../projects/ProjectEditor.svelte'
import AssigneePresenter from './AssigneePresenter.svelte'
@ -68,7 +70,6 @@
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
const spaceQuery = createQuery()
const statusesQuery = createQuery()
let currentTeam: Team | undefined
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
@ -97,20 +98,6 @@
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> {
return object as WithLookup<Issue>
}
@ -138,9 +125,9 @@
})
}
const issuesQuery = createQuery()
let issueStates: TypeState[] = []
let issues: Issue[] = []
const lookupIssue: Lookup<Issue> = {
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }],
status: tracker.class.IssueStatus,
project: tracker.class.Project,
sprint: tracker.class.Sprint,
assignee: contact.class.Employee
@ -148,8 +135,8 @@
$: issuesQuery.query(
tracker.class.Issue,
resultQuery,
async (result) => {
issueStates = await getKanbanStatuses(groupBy, result)
(result) => {
issues = result
},
{
lookup: lookupIssue,
@ -157,27 +144,107 @@
}
)
let priorityStates: TypeState[] = []
getPriorityStates().then((states) => {
priorityStates = states
const assigneeQuery = createQuery()
let assignee: Employee[] = []
$: assigneeQuery.query(contact.class.Employee, {}, (result) => {
assignee = result
})
function getIssueStates (
groupBy: IssuesGrouping,
states: TypeState[],
statusStates: TypeState[],
priorityStates: TypeState[]
) {
if (states.length > 0) return states
if (groupBy === IssuesGrouping.Status) return statusStates
if (groupBy === IssuesGrouping.Priority) return priorityStates
return []
const statusesQuery = createQuery()
let statuses: WithLookup<IssueStatus>[] = []
let statusesMap: IdMap<IssueStatus> = new Map()
$: 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[]
) {
let categories = await getCategories(client, _class, docs, groupByKey)
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
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 getState = (state: any): WithLookup<IssueStatus> | undefined => {
return issueStatuses?.filter((is) => is._id === state._id)[0]
}
</script>
{#if !states?.length}
@ -211,11 +278,17 @@
on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)}
>
<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="flex-between label font-medium w-full h-full">
<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="counter ml-2 text-md">{count}</span>
</div>

View File

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

View File

@ -12,12 +12,12 @@
// See the License for the specific language governing permissions and
// 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 { mergeIds } from '@hcengineering/platform'
import { IssueDraft } from '@hcengineering/tracker'
import { IssueDraft, IssuePriority, IssueStatus, Project, Sprint } from '@hcengineering/tracker'
import { AnyComponent } from '@hcengineering/ui'
import { SortFunc, Viewlet } from '@hcengineering/view'
import { SortFunc, Viewlet, ViewQueryAction } from '@hcengineering/view'
import tracker, { trackerId } from '../../tracker/lib'
export default mergeIds(trackerId, tracker, {
@ -143,7 +143,6 @@ export default mergeIds(trackerId, tracker, {
Grouping: '' as IntlString,
Ordering: '' as IntlString,
CompletedIssues: '' as IntlString,
ShowEmptyGroups: '' as IntlString,
NoGrouping: '' as IntlString,
NoAssignee: '' as IntlString,
LastUpdated: '' as IntlString,
@ -377,6 +376,10 @@ export default mergeIds(trackerId, tracker, {
IssueStatusSort: '' as SortFunc,
IssuePrioritySort: '' 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.
//
import { Employee } from '@hcengineering/contact'
import { Employee, formatName } from '@hcengineering/contact'
import core, {
AttachedData,
Doc,
DocumentQuery,
Ref,
SortingOrder,
Space,
toIdMap,
TxCollectionCUD,
TxOperations,
@ -37,6 +38,7 @@ import {
IssuesOrdering,
IssueStatus,
IssueTemplateData,
Project,
ProjectStatus,
Sprint,
SprintStatus,
@ -51,7 +53,6 @@ import {
isWeekend,
MILLISECONDS_IN_WEEK
} from '@hcengineering/ui'
import { ViewOptionModel } from '@hcengineering/view'
import { ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import tracker from './plugin'
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 (
groupBy: IssuesGrouping,
issues: Array<WithLookup<Issue>>
export async function mapKanbanCategories (
groupBy: string,
categories: any[],
statuses: Array<WithLookup<IssueStatus>>,
projects: Project[],
sprints: Sprint[],
assignee: Employee[]
): Promise<TypeState[]> {
if (groupBy === IssuesGrouping.NoGrouping) {
return [{ _id: undefined, color: UNSET_COLOR, title: await translate(tracker.string.NoGrouping, {}) }]
}
if (groupBy === IssuesGrouping.Priority) {
const states = issues.reduce<TypeState[]>((result, issue) => {
const { priority } = issue
if (result.find(({ _id }) => _id === priority) !== undefined) return result
return [
...result,
{
const res: TypeState[] = []
for (const priority of categories) {
const title = await translate((issuePriorities as any)[priority].label, {})
res.push({
_id: priority,
title: issuePriorities[priority].label,
title,
color: UNSET_COLOR,
icon: issuePriorities[priority].icon
}
]
}, [])
await Promise.all(
states.map(async (state) => {
state.title = await translate(state.title as IntlString, {})
icon: (issuePriorities as any)[priority].icon
})
)
return states
}
return res
}
if (groupBy === IssuesGrouping.Status) {
return issues.reduce<TypeState[]>((result, issue) => {
const status = issue.$lookup?.status
if (status === undefined || result.find(({ _id }) => _id === status._id) !== undefined) return result
return statuses
.filter((p) => categories.includes(p._id))
.map((status) => {
const category = '$lookup' in status ? status.$lookup?.category : undefined
return [
...result,
{
return {
_id: status._id,
title: status.name,
icon: category?.icon,
color: status.color ?? category?.color ?? UNSET_COLOR
}
]
}, [])
})
}
if (groupBy === IssuesGrouping.Assignee) {
const noAssignee = await translate(tracker.string.NoAssignee, {})
return issues.reduce<TypeState[]>((result, issue) => {
if (result.find(({ _id }) => _id === issue.assignee) !== undefined) return result
return [
...result,
{
_id: issue.assignee,
title: issue.$lookup?.assignee?.name ?? noAssignee,
const res: TypeState[] = assignee
.filter((p) => categories.includes(p._id))
.map((employee) => {
return {
_id: employee._id,
title: formatName(employee.name),
color: UNSET_COLOR,
icon: undefined
}
]
}, [])
})
if (categories.includes(undefined)) {
res.push({
_id: null,
title: noAssignee,
color: UNSET_COLOR,
icon: undefined
})
}
return res
}
if (groupBy === IssuesGrouping.Project) {
const noProject = await translate(tracker.string.NoProject, {})
return issues.reduce<TypeState[]>((result, issue) => {
if (result.find(({ _id }) => _id === issue.project) !== undefined) return result
return [
...result,
{
_id: issue.project,
title: issue.$lookup?.project?.label ?? noProject,
const res: TypeState[] = projects
.filter((p) => categories.includes(p._id))
.map((project) => ({
_id: project._id,
title: project.label,
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) {
const noSprint = await translate(tracker.string.NoSprint, {})
return issues.reduce<TypeState[]>((result, issue) => {
if (result.find(({ _id }) => _id === issue.sprint) !== undefined) return result
return [
...result,
{
_id: issue.sprint,
title: issue.$lookup?.sprint?.label ?? noSprint,
const res: TypeState[] = sprints
.filter((p) => categories.includes(p._id))
.map((sprint) => ({
_id: sprint._id,
title: sprint.label,
color: UNSET_COLOR,
icon: undefined
}))
if (categories.includes(undefined)) {
res.push({
_id: null,
title: noSprint,
color: UNSET_COLOR,
icon: undefined
})
}
]
}, [])
return res
}
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
*/
@ -591,6 +546,43 @@ 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()
})
})
}
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 {
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (dir === 'vertical') {

View File

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

View File

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

View File

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

View File

@ -33,7 +33,6 @@
const dispatch = createEventDispatcher()
const changeStatus = async (newStatus: any) => {
console.log('CHANGE VALUE', newStatus)
if (newStatus === '#null') {
newStatus = null
return
@ -120,7 +119,6 @@
<SelectPopup
value={valuesToShow}
on:close={(evt) => {
console.log(evt)
changeStatus(evt.detail)
}}
placeholder={placeholder ?? view.string.Filter}
@ -138,7 +136,6 @@
allowDeselect={true}
selected={current}
on:close={(evt) => {
console.log(evt)
changeStatus(evt.detail === null ? null : evt.detail?._id)
}}
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) {
viewOptions.groupBy[i] = value
if (value === noCategory) {
viewOptions.groupBy.length = i
} else {
viewOptions.groupBy.length = i + 1
} else if (config.groupDepth === undefined || config.groupDepth > viewOptions.groupBy.length) {
viewOptions.groupBy.length = i + 1
viewOptions.groupBy[i + 1] = noCategory
}
@ -62,7 +57,7 @@
<div class="antiCard">
<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>
<div class="value grouping">
<DropdownLabelsIntl
@ -93,12 +88,12 @@
}}
/>
</div>
{#each config.other as model}
{#each config.other.filter((p) => !p.hidden?.(viewOptions)) as model}
<span class="label"><Label label={model.label} /></span>
<div class="value">
{#if isToggleType(model)}
<MiniToggle
on={viewOptions[model.key]}
on={viewOptions[model.key] ?? model.defaultValue}
on:change={() => {
viewOptions[model.key] = !viewOptions[model.key]
dispatch('update', { key: model.key, value: viewOptions[model.key] })
@ -109,7 +104,7 @@
<DropdownLabelsIntl
label={model.label}
{items}
selected={viewOptions[model.key]}
selected={viewOptions[model.key] ?? model.defaultValue}
width="10rem"
justify="left"
on:selected={(e) => {

View File

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

View File

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

View File

@ -14,11 +14,11 @@
-->
<script lang="ts">
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 { AnyComponent } from '@hcengineering/ui'
import { AttributeModel, BuildModelKey, ViewOptions } from '@hcengineering/view'
import { buildModel, getCategories, getPresenter, groupBy, getAdditionalHeader } from '../../utils'
import { AttributeModel, BuildModelKey, CategoryOption, ViewOptionModel, ViewOptions } from '@hcengineering/view'
import { buildModel, getAdditionalHeader, getCategories, getPresenter, groupBy } from '../../utils'
import { noCategory } from '../../viewOptions'
import ListCategory from './ListCategory.svelte'
@ -42,13 +42,41 @@
export let initIndex = 0
export let newObjectProps: Record<string, any>
export let docByIndex: Map<number, Doc>
export let viewOptionsConfig: ViewOptionModel[] | undefined
$: groupByKey = viewOptions.groupBy[level] ?? noCategory
$: groupedDocs = groupBy(docs, groupByKey)
let categories: any[] = []
$: getCategories(client, _class, docs, groupByKey).then((p) => {
categories = p
})
$: updateCategories(_class, docs, groupByKey, viewOptions, viewOptionsConfig)
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()

View File

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

View File

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

View File

@ -64,6 +64,7 @@ export default mergeIds(viewId, view, {
Then: '' as IntlString,
ShowPreviewOnClick: '' as IntlString,
Showed: '' as IntlString,
ShowEmptyGroups: '' 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 {
DropdownViewOption,
@ -8,6 +10,7 @@ import {
ViewOptions,
ViewOptionsModel
} from '@hcengineering/view'
import view from './plugin'
export const noCategory = '#no_category'
@ -82,3 +85,28 @@ export function migrateViewOpttions (): void {
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 {
label: IntlString
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
*/
@ -238,6 +243,13 @@ export interface ClassSortFuncs extends Class<Doc> {
func: SortFunc
}
/**
* @public
*/
export interface AllValuesFunc extends Class<Doc> {
func: Resource<(space: Ref<Space> | undefined) => Promise<any[]>>
}
/**
* @public
*/
@ -468,16 +480,36 @@ export interface ViewOption {
defaultValue: any
label: IntlString
hidden?: (viewOptions: ViewOptions) => boolean
actionTartget?: 'query'
actionTartget?: 'query' | 'category'
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
*/
export interface ViewQueryOption extends ViewOption {
actionTartget: 'query'
action: Resource<(value: any, query: DocumentQuery<Doc>) => DocumentQuery<Doc>>
action: ViewQueryAction
}
/**
@ -514,6 +546,7 @@ export interface ViewOptionsModel {
groupBy: string[]
orderBy: OrderOption[]
other: ViewOptionModel[]
groupDepth?: number
}
/**
@ -542,7 +575,8 @@ const view = plugin(viewId, {
IgnoreActions: '' as Ref<Mixin<IgnoreActions>>,
PreviewPresenter: '' as Ref<Mixin<PreviewPresenter>>,
ListHeaderExtra: '' as Ref<Mixin<ListHeaderExtra>>,
SortFuncs: '' as Ref<Mixin<ClassSortFuncs>>
SortFuncs: '' as Ref<Mixin<ClassSortFuncs>>,
AllValuesFunc: '' as Ref<Mixin<AllValuesFunc>>
},
class: {
ViewletPreference: '' as Ref<Class<ViewletPreference>>,

View File

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

View File

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