From 298a277729530c8453417004ab89bda7119c6d7a Mon Sep 17 00:00:00 2001 From: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com> Date: Thu, 19 May 2022 12:38:12 +0600 Subject: [PATCH] Table filter (#1790) Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com> --- models/recruit/src/index.ts | 9 + models/view/src/index.ts | 35 ++++ models/view/src/plugin.ts | 3 + packages/core/src/memdb.ts | 5 +- packages/ui/lang/en.json | 2 + packages/ui/lang/ru.json | 2 + packages/ui/src/components/TimeSince.svelte | 8 +- packages/ui/src/plugin.ts | 2 + plugins/view-assets/lang/en.json | 10 +- plugins/view-assets/lang/ru.json | 10 +- .../src/components/TableBrowser.svelte | 6 +- .../src/components/filter/FilterBar.svelte | 163 ++++++++++++++++++ .../components/filter/FilterSection.svelte | 96 +++++++++++ .../components/filter/FilterTypePopup.svelte | 162 +++++++++++++++++ .../src/components/filter/ObjectFilter.svelte | 147 ++++++++++++++++ .../components/filter/TimestampFilter.svelte | 84 +++++++++ .../src/components/filter/ValueFilter.svelte | 136 +++++++++++++++ plugins/view-resources/src/index.ts | 6 + plugins/view-resources/src/plugin.ts | 10 +- plugins/view-resources/src/utils.ts | 2 +- plugins/view/src/index.ts | 47 +++++ 21 files changed, 938 insertions(+), 7 deletions(-) create mode 100644 plugins/view-resources/src/components/filter/FilterBar.svelte create mode 100644 plugins/view-resources/src/components/filter/FilterSection.svelte create mode 100644 plugins/view-resources/src/components/filter/FilterTypePopup.svelte create mode 100644 plugins/view-resources/src/components/filter/ObjectFilter.svelte create mode 100644 plugins/view-resources/src/components/filter/TimestampFilter.svelte create mode 100644 plugins/view-resources/src/components/filter/ValueFilter.svelte diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 8574079be7..8327641694 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -471,6 +471,15 @@ export function createModel (builder: Builder): void { builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.IgnoreActions, { actions: [view.action.Delete] }) + + builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ClassFilters, { + filters: ['title', 'source', 'city', 'modifiedOn'] + }) + + builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ClassFilters, { + filters: ['attachedTo', 'assignee', 'modifiedOn'] + }) + createReviewModel(builder) // createAction(builder, { ...viewTemplates.open, target: recruit.class.Vacancy, context: { mode: ['browser', 'context'] } }) diff --git a/models/view/src/index.ts b/models/view/src/index.ts index f8885bfb7a..92672491d6 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -23,12 +23,15 @@ import type { Action, ActionCategory, AttributeEditor, + AttributeFilter, AttributePresenter, BuildModelKey, + ClassFilters, CollectionEditor, HTMLPresenter, IgnoreActions, KeyBinding, + KeyFilter, LinkPresenter, ObjectEditor, ObjectEditorHeader, @@ -76,6 +79,16 @@ export function classPresenter ( } } +@Mixin(view.mixin.ClassFilters, core.class.Class) +export class TClassFilters extends TClass implements ClassFilters { + filters!: (string | KeyFilter)[] +} + +@Mixin(view.mixin.AttributeFilter, core.class.Class) +export class TAttributeFilter extends TClass implements AttributeFilter { + component!: AnyComponent +} + @Mixin(view.mixin.AttributeEditor, core.class.Class) export class TAttributeEditor extends TClass implements AttributeEditor { editor!: AnyComponent @@ -224,6 +237,8 @@ export const actionTemplates = template({ export function createModel (builder: Builder): void { builder.createModel( + TClassFilters, + TAttributeFilter, TAttributeEditor, TAttributePresenter, TCollectionEditor, @@ -474,6 +489,26 @@ export function createModel (builder: Builder): void { pattern: '(www.)?github.com/', component: view.component.GithubPresenter }) + + builder.mixin(core.class.TypeString, core.class.Class, view.mixin.AttributeFilter, { + component: view.component.ValueFilter + }) + + builder.mixin(core.class.TypeNumber, core.class.Class, view.mixin.AttributeFilter, { + component: view.component.ValueFilter + }) + + builder.mixin(core.class.TypeDate, core.class.Class, view.mixin.AttributeFilter, { + component: view.component.ValueFilter + }) + + builder.mixin(core.class.RefTo, core.class.Class, view.mixin.AttributeFilter, { + component: view.component.ObjectFilter + }) + + builder.mixin(core.class.TypeTimestamp, core.class.Class, view.mixin.AttributeFilter, { + component: view.component.TimestampFilter + }) } export default view diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index 0babddf2ed..898064a149 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -60,6 +60,9 @@ export default mergeIds(viewId, view, { Open: '' as ViewAction }, component: { + ObjectFilter: '' as AnyComponent, + ValueFilter: '' as AnyComponent, + TimestampFilter: '' as AnyComponent, StringEditor: '' as AnyComponent, StringPresenter: '' as AnyComponent, IntlStringPresenter: '' as AnyComponent, diff --git a/packages/core/src/memdb.ts b/packages/core/src/memdb.ts index 540b4ee48e..93427e84ed 100644 --- a/packages/core/src/memdb.ts +++ b/packages/core/src/memdb.ts @@ -151,7 +151,10 @@ export abstract class MemDb extends TxProcessor { result = result.filter((r) => (r as any)[_class] !== undefined) } - if (options?.lookup !== undefined) result = await this.lookup(result as T[], options.lookup) + if (options?.lookup !== undefined) { + result = await this.lookup(result as T[], options.lookup) + result = matchQuery(result, query, _class, this.hierarchy) + } if (options?.sort !== undefined) resultSort(result, options?.sort, _class, this.hierarchy) const total = result.length diff --git a/packages/ui/lang/en.json b/packages/ui/lang/en.json index 8434e967ba..7b54583343 100644 --- a/packages/ui/lang/en.json +++ b/packages/ui/lang/en.json @@ -7,6 +7,8 @@ "Minutes": "{minutes, plural, =0 {less than a minute ago} =1 {a minute ago} other {# minutes ago}}", "Hours": "{hours, plural, =0 {less than an hour ago} =1 {an hour ago} other {# hours ago}}", "Days": "{days, plural, =0 {today} =1 {yesterday} other {# days ago}}", + "Months": "{months, plural, =0 {this month} =1 {a month aago} other {# months ago}}", + "Years": "{years, plural, =0 {this year} =1 {a year ago} other {# years ago}}", "ShowMore": "Show more", "ShowLess": "Show less", "Search": "Search", diff --git a/packages/ui/lang/ru.json b/packages/ui/lang/ru.json index 97b57f565c..6ef1b9d737 100644 --- a/packages/ui/lang/ru.json +++ b/packages/ui/lang/ru.json @@ -7,6 +7,8 @@ "Minutes": "{minutes, plural, =0 {меньше минуты назад} =1 {минуту назад} other {# минут назад}}", "Hours": "{hours, plural, =0 {меньше часа назад} =1 {час назад} other {# часов назад}}", "Days": "{days, plural, =0 {сегода} =1 {вчера} other {# дней назад}}", + "Months": "{months, plural, =0 {в этом месяце} =1 {месяц назад} =2 {2 месяца назад} =3 {3 месяца назад} =4 {4 месяца назад} other {# месяцев назад}}", + "Years": "{years, plural, =0 {в этом году} =1 {год назад} =2 {2 года назад} =3 {3 года назад} =4 {4 года назад} other {# лет назад}}", "ShowMore": "Показать больше", "ShowLess": "Показать меньше", "Search": "Поиск", diff --git a/packages/ui/src/components/TimeSince.svelte b/packages/ui/src/components/TimeSince.svelte index e1b02288b3..86053533b8 100644 --- a/packages/ui/src/components/TimeSince.svelte +++ b/packages/ui/src/components/TimeSince.svelte @@ -25,6 +25,8 @@ const MINUTE = SECOND * 60 const HOUR = MINUTE * 60 const DAY = HOUR * 24 + const MONTH = DAY * 30 + const YEAR = MONTH * 12 let time: string = '' @@ -35,8 +37,12 @@ time = await translate(ui.string.Minutes, { minutes: Math.floor(passed / MINUTE) }) } else if (passed < DAY) { time = await translate(ui.string.Hours, { hours: Math.floor(passed / HOUR) }) - } else { + } else if (passed < MONTH) { time = await translate(ui.string.Days, { days: Math.floor(passed / DAY) }) + } else if (passed < YEAR) { + time = await translate(ui.string.Months, { months: Math.floor(passed / MONTH) }) + } else { + time = await translate(ui.string.Years, { years: Math.floor(passed / YEAR) }) } } diff --git a/packages/ui/src/plugin.ts b/packages/ui/src/plugin.ts index dc90e4edb8..0d793db9d9 100644 --- a/packages/ui/src/plugin.ts +++ b/packages/ui/src/plugin.ts @@ -32,6 +32,8 @@ export default plugin(uiId, { Minutes: '' as IntlString, Hours: '' as IntlString, Days: '' as IntlString, + Months: '' as IntlString, + Years: '' as IntlString, ShowMore: '' as IntlString, ShowLess: '' as IntlString, Search: '' as IntlString, diff --git a/plugins/view-assets/lang/en.json b/plugins/view-assets/lang/en.json index f269b58693..3cded693be 100644 --- a/plugins/view-assets/lang/en.json +++ b/plugins/view-assets/lang/en.json @@ -37,6 +37,14 @@ "WithTime": "WithTime", "CreatingAttribute": "Creating an attribute", "CreatingAttributeConfirm": "Warning: It will not be possible for now to change or delete created attribute.", - "CustomizeView": "Customize view" + "CustomizeView": "Customize view", + "Filter": "Filter", + "ClearFilters": "Clear filters", + "FilterIs": "is", + "FilterIsNot": "is not", + "FilterIsEither": "is either of", + "FilterStatesCount": "{value, plural, =1 {1 state} other {# states}}", + "Before": "Before", + "After": "After" } } diff --git a/plugins/view-assets/lang/ru.json b/plugins/view-assets/lang/ru.json index a6a09f25e6..9bfa86a8d3 100644 --- a/plugins/view-assets/lang/ru.json +++ b/plugins/view-assets/lang/ru.json @@ -35,6 +35,14 @@ "ShowPreview": "Предпросмотр документа", "ShowActions": "Показать действия", "RestoreDefaults": "По умолчанию", - "CustomizeView": "Настроить отображение" + "CustomizeView": "Настроить отображение", + "Filter": "Фильтр", + "ClearFilters": "Очистить", + "FilterIs": "равен", + "FilterIsNot": "не равен", + "FilterIsEither": "один из", + "FilterStatesCount": "{value, plural, =1 {1 состоянию} other {# состояний}}", + "Before": "До", + "After": "После" } } diff --git a/plugins/view-resources/src/components/TableBrowser.svelte b/plugins/view-resources/src/components/TableBrowser.svelte index ccbb5d9b68..9c4ce2bbec 100644 --- a/plugins/view-resources/src/components/TableBrowser.svelte +++ b/plugins/view-resources/src/components/TableBrowser.svelte @@ -20,6 +20,7 @@ import { ActionContext } from '..' import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '../selection' import { LoadingProps } from '../utils' + import FilterBar from './filter/FilterBar.svelte' import Table from './Table.svelte' export let _class: Ref> @@ -32,6 +33,8 @@ // If defined, will show a number of dummy items before real data will appear. export let loadingProps: LoadingProps | undefined = undefined + let resultQuery = query + let table: Table const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => { if (dir === 'vertical') { @@ -51,13 +54,14 @@ }} /> + (resultQuery = e.detail)} /> + + +{#if visible} +
+ {#each filters as filter, i} + { + makeQuery(query, filters) + }} + on:remove={() => { + remove(i) + }} + /> + {/each} +
+
+ {#if filters.length} +
+
+ {/if} +
+{/if} diff --git a/plugins/view-resources/src/components/filter/FilterSection.svelte b/plugins/view-resources/src/components/filter/FilterSection.svelte new file mode 100644 index 0000000000..bf8248bc2b --- /dev/null +++ b/plugins/view-resources/src/components/filter/FilterSection.svelte @@ -0,0 +1,96 @@ + + + +
+
+
+
+
+
+
+
+
+
+ + diff --git a/plugins/view-resources/src/components/filter/FilterTypePopup.svelte b/plugins/view-resources/src/components/filter/FilterTypePopup.svelte new file mode 100644 index 0000000000..c669fab5c7 --- /dev/null +++ b/plugins/view-resources/src/components/filter/FilterTypePopup.svelte @@ -0,0 +1,162 @@ + + + +
+
+
+
+ {#each getTypes(_class) as type, i} + + + {/each} +
+
+
+
+ + diff --git a/plugins/view-resources/src/components/filter/ObjectFilter.svelte b/plugins/view-resources/src/components/filter/ObjectFilter.svelte new file mode 100644 index 0000000000..223b2d4163 --- /dev/null +++ b/plugins/view-resources/src/components/filter/ObjectFilter.svelte @@ -0,0 +1,147 @@ + + + +
+
+ {}} + on:change + /> +
+
+
+ {#await promise then attribute} + {#each Array.from(values) as value} + + {/each} + {/await} +
+
+
diff --git a/plugins/view-resources/src/components/filter/TimestampFilter.svelte b/plugins/view-resources/src/components/filter/TimestampFilter.svelte new file mode 100644 index 0000000000..3ce8d89d2e --- /dev/null +++ b/plugins/view-resources/src/components/filter/TimestampFilter.svelte @@ -0,0 +1,84 @@ + + + +
+
+
+ {#each values as value, i} + + {/each} +
+
+
diff --git a/plugins/view-resources/src/components/filter/ValueFilter.svelte b/plugins/view-resources/src/components/filter/ValueFilter.svelte new file mode 100644 index 0000000000..3b3327cfe6 --- /dev/null +++ b/plugins/view-resources/src/components/filter/ValueFilter.svelte @@ -0,0 +1,136 @@ + + + +
+
+ {}} + on:change + /> +
+
+
+ {#await promise then attribute} + {#each Array.from(values) as value} + + {/each} + {/await} +
+
+
diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 74b4eb88df..44d125e612 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -46,6 +46,9 @@ import RefEditor from './components/typeEditors/RefEditor.svelte' import DocAttributeBar from './components/DocAttributeBar.svelte' import ViewletSetting from './components/ViewletSetting.svelte' import TableBrowser from './components/TableBrowser.svelte' +import ValueFilter from './components/filter/ValueFilter.svelte' +import ObjectFilter from './components/filter/ObjectFilter.svelte' +import TimestampFilter from './components/filter/TimestampFilter.svelte' export { getActions, invokeAction } from './actions' export { default as ActionContext } from './components/ActionContext.svelte' @@ -72,6 +75,9 @@ export { export default async (): Promise => ({ actionImpl: actionImpl, component: { + ObjectFilter, + ValueFilter, + TimestampFilter, TableBrowser, ViewletSetting, CreateAttribute, diff --git a/plugins/view-resources/src/plugin.ts b/plugins/view-resources/src/plugin.ts index 1991b6b20f..66abde5fa2 100644 --- a/plugins/view-resources/src/plugin.ts +++ b/plugins/view-resources/src/plugin.ts @@ -40,6 +40,14 @@ export default mergeIds(viewId, view, { ActionPlaceholder: '' as IntlString, CreatingAttribute: '' as IntlString, RestoreDefaults: '' as IntlString, - CreatingAttributeConfirm: '' as IntlString + CreatingAttributeConfirm: '' as IntlString, + Filter: '' as IntlString, + ClearFilters: '' as IntlString, + FilterIs: '' as IntlString, + FilterIsNot: '' as IntlString, + FilterIsEither: '' as IntlString, + FilterStatesCount: '' as IntlString, + Before: '' as IntlString, + After: '' as IntlString } }) diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index 03f5a2b861..485c757a2e 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -123,7 +123,7 @@ async function getAttributePresenter ( } } -async function getPresenter ( +export async function getPresenter ( client: Client, _class: Ref>, key: BuildModelKey, diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index d29dc1485d..f1ed7d3b80 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -24,8 +24,10 @@ import type { Lookup, Mixin, Obj, + ObjQueryType, Ref, Space, + Type, UXObject } from '@anticrm/core' import type { Asset, IntlString, Plugin, Resource, Status } from '@anticrm/platform' @@ -34,6 +36,49 @@ import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui' import { PopupPosAlignment } from '@anticrm/ui/src/types' import type { Preference } from '@anticrm/preference' +/** + * @public + */ +export interface KeyFilter { + key: string + component: AnyComponent + label: IntlString + icon: Asset | undefined +} + +/** + * @public + */ +export interface FilterMode { + label: IntlString + isAvailable: (values: any[]) => boolean + result: (values: any[]) => ObjQueryType +} + +/** + * @public + */ +export interface Filter { + key: KeyFilter + mode: FilterMode + modes: FilterMode[] + value: any[] +} + +/** + * @public + */ +export interface ClassFilters extends Class { + filters: (KeyFilter | string)[] +} + +/** + * @public + */ +export interface AttributeFilter extends Class> { + component: AnyComponent +} + /** * @public */ @@ -303,6 +348,8 @@ export interface ViewletPreference extends Preference { */ const view = plugin(viewId, { mixin: { + ClassFilters: '' as Ref>, + AttributeFilter: '' as Ref>, AttributeEditor: '' as Ref>, CollectionEditor: '' as Ref>, AttributePresenter: '' as Ref>,