From bbf81ada7ca43b052002c3ec381e21bb69c4102a Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Thu, 11 May 2023 13:26:17 +0600 Subject: [PATCH] TSK-1404 Improve sprint filter (#3164) Signed-off-by: Denis Bykhov --- models/tracker/src/index.ts | 19 +- models/tracker/src/plugin.ts | 3 +- models/view/src/index.ts | 4 +- .../components/sprints/SprintFilter.svelte | 168 ++++++++++++++++++ .../components/sprints/SprintPresenter.svelte | 2 +- .../sprints/SprintStatusPresenter.svelte | 41 ++--- plugins/tracker-resources/src/index.ts | 4 +- plugins/view-resources/package.json | 1 + .../components/filter/FilterTypePopup.svelte | 136 ++++++++++---- plugins/view-resources/src/filter.ts | 21 ++- plugins/view/src/index.ts | 14 +- 11 files changed, 339 insertions(+), 74 deletions(-) create mode 100644 plugins/tracker-resources/src/components/sprints/SprintFilter.svelte diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 6265d080b9..302e7fcab6 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -1374,6 +1374,22 @@ export function createModel (builder: Builder): void { builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.ClassFilters, { filters: [] }) + builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.ClassFilters, { + filters: ['status'], + strict: true + }) + + builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.AttributeFilter, { + component: tracker.component.SprintFilter + }) + + builder.mixin(tracker.class.TypeSprintStatus, core.class.Class, view.mixin.AttributePresenter, { + presenter: tracker.component.SprintStatusPresenter + }) + + builder.mixin(tracker.class.TypeSprintStatus, core.class.Class, view.mixin.AttributeFilter, { + component: view.component.ValueFilter + }) builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ClassFilters, { filters: [] @@ -1805,8 +1821,7 @@ export function createModel (builder: Builder): void { viewOptions: sprintOptions, config: [ { - key: '', - presenter: tracker.component.SprintStatusPresenter, + key: 'status', props: { width: '1rem', kind: 'list', size: 'small', justify: 'center' } }, { key: '', presenter: tracker.component.SprintPresenter, props: { shouldUseMargin: true } }, diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index e99a8c5713..3934566731 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -52,7 +52,8 @@ export default mergeIds(trackerId, tracker, { SprintSelector: '' as AnyComponent, IssueStatistics: '' as AnyComponent, TimeSpendReportPopup: '' as AnyComponent, - NotificationIssuePresenter: '' as AnyComponent + NotificationIssuePresenter: '' as AnyComponent, + SprintFilter: '' as AnyComponent }, app: { Tracker: '' as Ref diff --git a/models/view/src/index.ts b/models/view/src/index.ts index a1ad8208c3..ad3e431fea 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -42,7 +42,7 @@ import type { IgnoreActions, InlineAttributEditor, KeyBinding, - KeyFilter, + KeyFilterPreset, LinkPresenter, LinkProvider, ListHeaderExtra, @@ -126,7 +126,7 @@ export class TFilterMode extends TDoc implements FilterMode { @Mixin(view.mixin.ClassFilters, core.class.Class) export class TClassFilters extends TClass implements ClassFilters { - filters!: (string | KeyFilter)[] + filters!: (string | KeyFilterPreset)[] ignoreKeys?: string[] | undefined strict?: boolean | undefined } diff --git a/plugins/tracker-resources/src/components/sprints/SprintFilter.svelte b/plugins/tracker-resources/src/components/sprints/SprintFilter.svelte new file mode 100644 index 0000000000..8ae3226864 --- /dev/null +++ b/plugins/tracker-resources/src/components/sprints/SprintFilter.svelte @@ -0,0 +1,168 @@ + + + +
dispatch('changeContent')}> +
+ { + getValues(search) + }} + placeholder={phTraslate} + /> +
+
+
+ {#if objectsPromise} + + {:else} + + {#each getStatuses() as group} + {@const status = sprintStatusAssets[group]} + {@const items = getStatusItem(group, values)} + {#if items.length > 0} +
+ +
+
+
+ {#each items as doc} + + {/each} + {/if} + {/each} + {/if} +
+
+
diff --git a/plugins/tracker-resources/src/components/sprints/SprintPresenter.svelte b/plugins/tracker-resources/src/components/sprints/SprintPresenter.svelte index 0de237bd96..e4481831b8 100644 --- a/plugins/tracker-resources/src/components/sprints/SprintPresenter.svelte +++ b/plugins/tracker-resources/src/components/sprints/SprintPresenter.svelte @@ -20,7 +20,7 @@ export let value: WithLookup export let shouldShowAvatar: boolean = true - export let onClick: () => void | undefined + export let onClick: (() => void) | undefined = undefined export let disabled = false export let inline: boolean = false diff --git a/plugins/tracker-resources/src/components/sprints/SprintStatusPresenter.svelte b/plugins/tracker-resources/src/components/sprints/SprintStatusPresenter.svelte index 1b6b92e4a8..247e14fa20 100644 --- a/plugins/tracker-resources/src/components/sprints/SprintStatusPresenter.svelte +++ b/plugins/tracker-resources/src/components/sprints/SprintStatusPresenter.svelte @@ -13,42 +13,29 @@ // limitations under the License. --> -{#if value} - -{/if} + diff --git a/plugins/tracker-resources/src/index.ts b/plugins/tracker-resources/src/index.ts index 1b058f6834..1720d0ce7b 100644 --- a/plugins/tracker-resources/src/index.ts +++ b/plugins/tracker-resources/src/index.ts @@ -136,6 +136,7 @@ import ProjectPresenter from './components/projects/ProjectPresenter.svelte' import ProjectSpacePresenter from './components/projects/ProjectSpacePresenter.svelte' import IssueStatistics from './components/sprints/IssueStatistics.svelte' import SprintRefPresenter from './components/sprints/SprintRefPresenter.svelte' +import SprintFilter from './components/sprints/SprintFilter.svelte' export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte' @@ -445,7 +446,8 @@ export default async (): Promise => ({ TimeSpendReportPopup, SprintDatePresenter, SprintLeadPresenter, - NotificationIssuePresenter + NotificationIssuePresenter, + SprintFilter }, completion: { IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) => diff --git a/plugins/view-resources/package.json b/plugins/view-resources/package.json index bcf5734e93..3e7c926a92 100644 --- a/plugins/view-resources/package.json +++ b/plugins/view-resources/package.json @@ -10,6 +10,7 @@ "lint": "svelte-check && eslint", "lint:fix": "eslint --fix src", "format": "prettier --write --plugin-search-dir=. src && eslint --fix src", + "svelte-check": "svelte-check --output human", "build:watch": "tsc --incremental --noEmit --outDir ./dist_cache" }, "devDependencies": { diff --git a/plugins/view-resources/src/components/filter/FilterTypePopup.svelte b/plugins/view-resources/src/components/filter/FilterTypePopup.svelte index 738547c273..397b863b39 100644 --- a/plugins/view-resources/src/components/filter/FilterTypePopup.svelte +++ b/plugins/view-resources/src/components/filter/FilterTypePopup.svelte @@ -25,7 +25,7 @@ showPopup, Submenu } from '@hcengineering/ui' - import { ClassFilters, Filter, KeyFilter } from '@hcengineering/view' + import { ClassFilters, Filter, KeyFilter, KeyFilterPreset } from '@hcengineering/view' import { createEventDispatcher } from 'svelte' import { buildFilterKey, FilterQuery } from '../../filter' import view from '../../plugin' @@ -36,6 +36,7 @@ export let filter: Filter | undefined export let index: number export let onChange: (e: Filter) => void + export let nestedFrom: KeyFilter | undefined = undefined const client = getClient() const hierarchy = client.getHierarchy() @@ -43,7 +44,7 @@ function getFilters (_class: Ref>, mixin: ClassFilters): KeyFilter[] { if (mixin.filters === undefined) return [] const filters = mixin.filters.map((p) => { - return typeof p === 'string' ? buildFilterFromKey(_class, p) : p + return typeof p === 'string' ? buildFilterFromKey(_class, p) : buildFilterFromPreset(p) }) const result: KeyFilter[] = [] for (const filter of filters) { @@ -52,6 +53,19 @@ return result } + function buildFilterFromPreset (p: KeyFilterPreset): KeyFilter | undefined { + if (p.key !== '') { + const attribute = hierarchy.getAttribute(p._class, p.key) + const clazz = hierarchy.getClass(p._class) + return { + ...p, + attribute, + label: p.label ?? attribute.label, + icon: p.icon ?? attribute.icon ?? clazz.icon ?? view.icon.Setting + } + } + } + function buildFilterFromKey (_class: Ref>, key: string): KeyFilter | undefined { const attribute = hierarchy.getAttribute(_class, key) return buildFilterKey(hierarchy, _class, key, attribute) @@ -69,7 +83,7 @@ return } const value = getValue(attribute.name, attribute.type) - if (result.findIndex((p) => p.attribute.name === value) !== -1) { + if (result.findIndex((p) => p.attribute?.name === value) !== -1) { return } const filter = buildFilterKey(hierarchy, _class, value, attribute) @@ -93,7 +107,20 @@ } } - function getTypes (_class: Ref>): KeyFilter[] { + function getTypes (_class: Ref>, nestedFrom: KeyFilter | undefined): KeyFilter[] { + if (nestedFrom !== undefined) { + return getNestedTypes(nestedFrom) + } else { + return getOwnTypes(_class) + } + } + + function getNestedTypes (type: KeyFilter): KeyFilter[] { + const targetClass = (hierarchy.getAttribute(type._class, type.key).type as RefTo).to + return getOwnTypes(targetClass) + } + + function getOwnTypes (_class: Ref>): KeyFilter[] { const clazz = hierarchy.getClass(_class) const mixin = hierarchy.as(clazz, view.mixin.ClassFilters) const result = getFilters(_class, mixin) @@ -159,24 +186,48 @@ closePopup() closeTooltip() - showPopup( - type.component, - { - _class, - space, - filter: filter || { - key: type, - value: [], - index + if (nestedFrom !== undefined && type !== nestedFrom) { + const change = (e: Filter | undefined) => { + if (nestedFrom) { + setNestedFilter(nestedFrom, e) + } + } + const targetClass = (hierarchy.getAttribute(nestedFrom._class, nestedFrom.key).type as RefTo).to + showPopup( + type.component, + { + _class: targetClass, + space, + filter: filter || { + key: type, + value: [], + index + }, + onChange: change }, - onChange - }, - target - ) + target + ) + } else { + showPopup( + type.component, + { + _class, + space, + filter: filter || { + key: type, + value: [], + index + }, + onChange + }, + target + ) + } } function hasNested (type: KeyFilter): boolean { const targetClass = (hierarchy.getAttribute(type._class, type.key).type as RefTo).to + if (targetClass === undefined) return false const clazz = hierarchy.getClass(targetClass) return hierarchy.hasMixin(clazz, view.mixin.ClassFilters) } @@ -199,38 +250,51 @@ dispatch('close') } - function getNestedProps (type: KeyFilter): any { - const targetClass = (hierarchy.getAttribute(type._class, type.key).type as RefTo).to - return { - _class: targetClass, - space, - index, - target, - onChange: (e: Filter | undefined) => { - setNestedFilter(type, e) - } - } - } - const elements: HTMLElement[] = []
dispatch('changeContent')}> - {#each getTypes(_class) as type, i} - {#if filter === undefined && type.component === view.component.ObjectFilter && hasNested(type)} + {#if nestedFrom} + + + {/if} + {#each getTypes(_class, nestedFrom) as type, i} + {#if filter === undefined && hasNested(type)} keyDown(event, i)} on:mouseover={() => { elements[i]?.focus() }} - on:click={() => { - click(type) - }} icon={type.icon} label={type.label} - props={getNestedProps(type)} + props={{ + _class, + space, + index, + target, + onChange, + nestedFrom: type + }} options={{ component: view.component.FilterTypePopup }} withHover /> diff --git a/plugins/view-resources/src/filter.ts b/plugins/view-resources/src/filter.ts index 6770949628..90f0076e65 100644 --- a/plugins/view-resources/src/filter.ts +++ b/plugins/view-resources/src/filter.ts @@ -7,7 +7,8 @@ import core, { FindResult, Hierarchy, ObjQueryType, - Ref + Ref, + RefTo } from '@hcengineering/core' import { getResource } from '@hcengineering/platform' import { LiveQuery, createQuery, getClient } from '@hcengineering/presentation' @@ -198,12 +199,28 @@ export function buildFilterKey ( key: string, attribute: AnyAttribute ): KeyFilter | undefined { + const attrOf = hierarchy.getClass(attribute.attributeOf) + const isRef = hierarchy.isDerived(attribute.type._class, core.class.RefTo) + if (isRef) { + const targetClass = (attribute.type as RefTo).to + const filter = hierarchy.classHierarchyMixin(targetClass, view.mixin.AttributeFilter) + if (filter?.component !== undefined) { + return { + _class, + key, + attribute, + label: attribute.label, + icon: attribute.icon ?? filter.icon ?? attrOf.icon ?? view.icon.Setting, + component: filter.component + } + } + } + const isCollection = hierarchy.isDerived(attribute.type._class, core.class.Collection) const targetClass = isCollection ? (attribute.type as Collection).of : attribute.type._class const clazz = hierarchy.getClass(targetClass) const filter = hierarchy.as(clazz, view.mixin.AttributeFilter) - const attrOf = hierarchy.getClass(attribute.attributeOf) if (filter.component === undefined) return undefined return { _class, diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 82fe123624..361b61880d 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -49,9 +49,19 @@ import type { /** * @public */ -export interface KeyFilter { +export interface KeyFilterPreset { _class: Ref> key: string + attribute?: AnyAttribute + component: AnyComponent + label?: IntlString + icon?: Asset | AnySvelteComponent | undefined +} + +/** + * @public + */ +export interface KeyFilter extends KeyFilterPreset { attribute: AnyAttribute component: AnyComponent label: IntlString @@ -102,7 +112,7 @@ export interface FilteredView extends Preference { * @public */ export interface ClassFilters extends Class { - filters: (KeyFilter | string)[] + filters: (KeyFilterPreset | string)[] ignoreKeys?: string[] // Ignore attributes not specified in the "filters" array