From 5160d6e46850ffa01290254f95f396e3a40319ee Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:28:24 +0200 Subject: [PATCH 01/12] chore: update description --- package.json | 6 + .../controllers/DatasourceParamsController.ts | 107 +++++++++++++++--- .../datagrid-web/src/utils/columns-hash.ts | 23 +--- .../filter-commons/src/condition-utils.ts | 37 ++++++ .../src/utils/fnv-1a-hash.ts | 20 ++++ 5 files changed, 157 insertions(+), 36 deletions(-) create mode 100644 packages/shared/widget-plugin-grid/src/utils/fnv-1a-hash.ts diff --git a/package.json b/package.json index aa5c2d9994..780662b349 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,13 @@ "rc-trigger": "patches/rc-trigger.patch" }, "onlyBuiltDependencies": [ + "@swc/core", "canvas" + ], + "ignoredBuiltDependencies": [ + "@parcel/watcher", + "core-js", + "es5-ext" ] }, "prettier": "@mendix/prettier-config-web-widgets" diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts index d99e7f2d06..32f85a846d 100644 --- a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts +++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts @@ -1,9 +1,9 @@ -import { compactArray, fromCompactArray, isAnd } from "@mendix/filter-commons/condition-utils"; +import { reduceArray, restoreArray } from "@mendix/filter-commons/condition-utils"; import { QueryController } from "@mendix/widget-plugin-grid/query/query-controller"; +import { fnv1aHash } from "@mendix/widget-plugin-grid/utils/fnv-1a-hash"; import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller"; import { FilterCondition } from "mendix/filters"; -import { and } from "mendix/filters/builders"; import { makeAutoObservable, reaction } from "mobx"; import { SortInstruction } from "../typings/sorting"; @@ -22,10 +22,19 @@ type DatasourceParamsControllerSpec = { customFilters: FiltersInput; }; +type FiltersMeta = { + columnFilters: string; + customFilters: string; + combined: string; +}; + +type CondArray = Array; + export class DatasourceParamsController implements ReactiveController { private columns: Columns; private query: QueryController; private customFilters: FiltersInput; + readonly widgetName: string = "dataKey"; constructor(host: ReactiveControllerHost, spec: DatasourceParamsControllerSpec) { host.addController(this); @@ -36,16 +45,41 @@ export class DatasourceParamsController implements ReactiveController { makeAutoObservable(this, { setup: false }); } - private get derivedFilter(): FilterCondition | undefined { - const { columns, customFilters } = this; + private get derivedFilter(): { + filter: FilterCondition | undefined; + meta: FiltersMeta; + hash: string | null; + } { + // return and(compactArray(this.columns.conditions), compactArray(this.customFilters.conditions)); + return this.reduceFilters(this.columns.conditions, this.customFilters.conditions); + + // console.dir(...reduceArray(columns.conditions)); - return and(compactArray(columns.conditions), compactArray(customFilters.conditions)); + // return [this.columns.conditions, this.customFilters.conditions]; } private get derivedSortOrder(): SortInstruction[] | undefined { return this.columns.sortInstructions; } + reduceFilters( + columnFilters: CondArray, + customFilters: CondArray + ): { + filter: FilterCondition | undefined; + meta: FiltersMeta; + hash: string | null; + } { + const [columnsCond, columnsMeta] = reduceArray(columnFilters); + const [customCond, customMeta] = reduceArray(customFilters); + const [filter, combinedMeta] = reduceArray([columnsCond, customCond]); + + const meta: FiltersMeta = { columnFilters: columnsMeta, customFilters: customMeta, combined: combinedMeta }; + const hash = filter ? DatasourceParamsController.filterHash(filter) : null; + + return { filter, meta, hash }; + } + setup(): () => void { const [add, disposeAll] = disposeBatch(); add( @@ -58,7 +92,15 @@ export class DatasourceParamsController implements ReactiveController { add( reaction( () => this.derivedFilter, - filter => this.query.setFilter(filter), + (next, prev) => { + if (prev && prev.hash) { + this.clearFilterMeta(prev.hash); + } + if (next.hash) { + this.saveFilterMeta(next.hash, next.meta); + } + this.query.setFilter(next.filter); + }, { fireImmediately: true } ) ); @@ -66,20 +108,55 @@ export class DatasourceParamsController implements ReactiveController { return disposeAll; } + storageKey(hash: string): string { + return `${this.widgetName}:[${hash}]`; + } + + clearFilterMeta(hash: string): void { + sessionStorage.removeItem(this.storageKey(hash)); + } + + saveFilterMeta(hash: string | null, meta: FiltersMeta): void { + if (!hash) { + return; + } + + sessionStorage.setItem(this.storageKey(hash), JSON.stringify(meta)); + } + + readFilterMeta(hash: string): FiltersMeta | null { + const item = sessionStorage.getItem(this.storageKey(hash)); + if (!item) { + return null; + } + try { + return JSON.parse(item) as FiltersMeta; + } catch (e) { + console.error(`DatasourceParamsController.readFilterMeta: Error parsing meta for hash ${hash}`, e); + return null; + } + } + + static filterHash(filter: FilterCondition): string { + return fnv1aHash(JSON.stringify(filter)).toString(); + } + static unzipFilter( - filter?: FilterCondition - ): [columns: Array, sharedFilter: Array] { + filter?: FilterCondition, + widgetName = "dataKey" + ): [columnFilters: Array, customFilters: Array] { if (!filter) { return [[], []]; } - if (!isAnd(filter)) { + const hash = this.filterHash(filter); + const metaJson = sessionStorage.getItem(`${widgetName}:[${hash}]`); + if (!metaJson) { return [[], []]; } - if (filter.args.length !== 2) { - return [[], []]; - } - - const [columns, shared] = filter.args; - return [fromCompactArray(columns), fromCompactArray(shared)]; + const meta = JSON.parse(metaJson) as FiltersMeta; + const [x, y] = restoreArray(filter, meta.combined); + const columnFilters = restoreArray(x, meta.columnFilters); + const customFilters = restoreArray(y, meta.customFilters); + return [columnFilters, customFilters]; } } diff --git a/packages/pluggableWidgets/datagrid-web/src/utils/columns-hash.ts b/packages/pluggableWidgets/datagrid-web/src/utils/columns-hash.ts index 874bed0ab0..00d715be9f 100644 --- a/packages/pluggableWidgets/datagrid-web/src/utils/columns-hash.ts +++ b/packages/pluggableWidgets/datagrid-web/src/utils/columns-hash.ts @@ -1,25 +1,6 @@ -/* eslint-disable no-bitwise */ +import { fnv1aHash } from "@mendix/widget-plugin-grid/utils/fnv-1a-hash"; import { GridColumn } from "../typings/GridColumn"; -/** - * Generates 32 bit FNV-1a hash from the given string. - * As explained here: http://isthe.com/chongo/tech/comp/fnv/ - * - * @param s {string} String to generate hash from. - * @param [h] {number} FNV-1a hash generation init value. - * @returns {number} The result integer hash. - */ -function hash(s: string, h = 0x811c9dc5): number { - const l = s.length; - - for (let i = 0; i < l; i++) { - h ^= s.charCodeAt(i); - h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24); - } - - return h >>> 0; -} - export function getHash(columns: GridColumn[], gridName: string): string { const data = JSON.stringify({ name: gridName, @@ -31,5 +12,5 @@ export function getHash(columns: GridColumn[], gridName: string): string { canDrag: col.canDrag })) }); - return hash(data).toString(); + return fnv1aHash(data).toString(); } diff --git a/packages/shared/filter-commons/src/condition-utils.ts b/packages/shared/filter-commons/src/condition-utils.ts index 976da416b5..1d6631554c 100644 --- a/packages/shared/filter-commons/src/condition-utils.ts +++ b/packages/shared/filter-commons/src/condition-utils.ts @@ -105,6 +105,43 @@ export function compactArray(input: Array): FilterC return and(metaTag, ...items); } +export function reduceArray( + input: Array +): [cond: FilterCondition | undefined, meta: string] { + const [indexes, items] = shrink(input); + const meta = JSON.stringify([input.length, indexes]); + + switch (items.length) { + case 0: + return [undefined, meta]; + case 1: + return [items[0], meta]; + default: + return [and(...items), meta]; + } +} + +export function restoreArray(cond: FilterCondition | undefined, meta: string): Array { + const [length, indexes] = JSON.parse(meta) as ArrayMeta; + const arr: Array = Array(length).fill(undefined); + + if (indexes.length === 0) { + return arr; + } + if (indexes.length === 1) { + arr[indexes[0]] = cond; + return arr; + } + if (cond && isAnd(cond)) { + cond.args.forEach((c, i) => { + arr[indexes[i]] = c; + }); + return arr; + } + + return arr; +} + export function fromCompactArray(cond: FilterCondition): Array { const tag = isAnd(cond) ? cond.args[0] : cond; diff --git a/packages/shared/widget-plugin-grid/src/utils/fnv-1a-hash.ts b/packages/shared/widget-plugin-grid/src/utils/fnv-1a-hash.ts new file mode 100644 index 0000000000..f086fa8d82 --- /dev/null +++ b/packages/shared/widget-plugin-grid/src/utils/fnv-1a-hash.ts @@ -0,0 +1,20 @@ +/* eslint-disable no-bitwise */ + +/** + * Generates 32 bit FNV-1a hash from the given string. + * As explained here: http://isthe.com/chongo/tech/comp/fnv/ + * + * @param s {string} String to generate hash from. + * @param [h] {number} FNV-1a hash generation init value. + * @returns {number} The result integer hash. + */ +export function fnv1aHash(s: string, h: number = 0x811c9dc5): number { + const l = s.length; + + for (let i = 0; i < l; i++) { + h ^= s.charCodeAt(i); + h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24); + } + + return h >>> 0; +} From 48b2f230399340b053087b006b6f9004aa6ce0d2 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:22:05 +0200 Subject: [PATCH 02/12] refactor: rename vars --- .../controllers/DatasourceParamsController.ts | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts index 32f85a846d..3e103482ae 100644 --- a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts +++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts @@ -50,12 +50,7 @@ export class DatasourceParamsController implements ReactiveController { meta: FiltersMeta; hash: string | null; } { - // return and(compactArray(this.columns.conditions), compactArray(this.customFilters.conditions)); return this.reduceFilters(this.columns.conditions, this.customFilters.conditions); - - // console.dir(...reduceArray(columns.conditions)); - - // return [this.columns.conditions, this.customFilters.conditions]; } private get derivedSortOrder(): SortInstruction[] | undefined { @@ -108,12 +103,12 @@ export class DatasourceParamsController implements ReactiveController { return disposeAll; } - storageKey(hash: string): string { - return `${this.widgetName}:[${hash}]`; + dataKey(hash: string): string { + return DatasourceParamsController.storageKey(this.widgetName, hash); } clearFilterMeta(hash: string): void { - sessionStorage.removeItem(this.storageKey(hash)); + sessionStorage.removeItem(this.dataKey(hash)); } saveFilterMeta(hash: string | null, meta: FiltersMeta): void { @@ -121,11 +116,11 @@ export class DatasourceParamsController implements ReactiveController { return; } - sessionStorage.setItem(this.storageKey(hash), JSON.stringify(meta)); + sessionStorage.setItem(this.dataKey(hash), JSON.stringify(meta)); } readFilterMeta(hash: string): FiltersMeta | null { - const item = sessionStorage.getItem(this.storageKey(hash)); + const item = sessionStorage.getItem(this.dataKey(hash)); if (!item) { return null; } @@ -141,22 +136,35 @@ export class DatasourceParamsController implements ReactiveController { return fnv1aHash(JSON.stringify(filter)).toString(); } + static storageKey(widgetName: string, hash: string): string { + return `${widgetName}:[${hash}]`; + } + + static restoreMeta(filter: FilterCondition): FiltersMeta | null { + const hash = this.filterHash(filter); + const key = this.storageKey("dataKey", hash); + const metaJson = sessionStorage.getItem(key); + if (!metaJson) { + return null; + } + return JSON.parse(metaJson) as FiltersMeta; + } + static unzipFilter( filter?: FilterCondition, - widgetName = "dataKey" + widgetName = "DatasourceParamsController" ): [columnFilters: Array, customFilters: Array] { if (!filter) { return [[], []]; } - const hash = this.filterHash(filter); - const metaJson = sessionStorage.getItem(`${widgetName}:[${hash}]`); - if (!metaJson) { + const meta = this.restoreMeta(filter); + if (!meta) { return [[], []]; } - const meta = JSON.parse(metaJson) as FiltersMeta; - const [x, y] = restoreArray(filter, meta.combined); - const columnFilters = restoreArray(x, meta.columnFilters); - const customFilters = restoreArray(y, meta.customFilters); + + const [column, custom] = restoreArray(filter, meta.combined); + const columnFilters = restoreArray(column, meta.columnFilters); + const customFilters = restoreArray(custom, meta.customFilters); return [columnFilters, customFilters]; } } From b8185b8c622bea2a23307075ed41b02ec5194145 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:23:47 +0200 Subject: [PATCH 03/12] feat: switch data grid to new filter system --- .../src/controllers/DatasourceParamsController.ts | 10 +++++----- .../datagrid-web/src/helpers/state/RootGridStore.ts | 5 ++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts index 3e103482ae..a00b29058f 100644 --- a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts +++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts @@ -140,9 +140,9 @@ export class DatasourceParamsController implements ReactiveController { return `${widgetName}:[${hash}]`; } - static restoreMeta(filter: FilterCondition): FiltersMeta | null { + static restoreMeta(filter: FilterCondition, widgetName: string): FiltersMeta | null { const hash = this.filterHash(filter); - const key = this.storageKey("dataKey", hash); + const key = this.storageKey(widgetName, hash); const metaJson = sessionStorage.getItem(key); if (!metaJson) { return null; @@ -151,13 +151,13 @@ export class DatasourceParamsController implements ReactiveController { } static unzipFilter( - filter?: FilterCondition, - widgetName = "DatasourceParamsController" + filter: FilterCondition | undefined, + widgetName: string ): [columnFilters: Array, customFilters: Array] { if (!filter) { return [[], []]; } - const meta = this.restoreMeta(filter); + const meta = this.restoreMeta(filter, widgetName); if (!meta) { return [[], []]; } diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts index cc7e718a08..16cf5c79e7 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts @@ -38,7 +38,10 @@ export class RootGridStore extends BaseControllerHost { super(); const { props } = gate; - const [columnsInitFilter, sharedInitFilter] = DatasourceParamsController.unzipFilter(props.datasource.filter); + const [columnsInitFilter, sharedInitFilter] = DatasourceParamsController.unzipFilter( + props.datasource.filter, + props.name + ); this.gate = gate; this.staticInfo = { From 0ac45b1b8cec3c6cd5897f5a8451635adcd55e1b Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:10:34 +0200 Subject: [PATCH 04/12] feat: add combined filter store --- .../controllers/DatasourceParamsController.ts | 6 +- .../src/helpers/state/RootGridStore.ts | 3 +- .../src/stores/generic/CombinedFilter.ts | 142 ++++++++++++++++++ .../src/utils/fnv-1a-hash.ts | 20 +++ 4 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 packages/shared/widget-plugin-filtering/src/stores/generic/CombinedFilter.ts create mode 100644 packages/shared/widget-plugin-filtering/src/utils/fnv-1a-hash.ts diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts index a00b29058f..0e4f6100f9 100644 --- a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts +++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts @@ -20,6 +20,7 @@ type DatasourceParamsControllerSpec = { query: QueryController; columns: Columns; customFilters: FiltersInput; + widgetName: string; }; type FiltersMeta = { @@ -34,13 +35,14 @@ export class DatasourceParamsController implements ReactiveController { private columns: Columns; private query: QueryController; private customFilters: FiltersInput; - readonly widgetName: string = "dataKey"; + readonly widgetName: string; constructor(host: ReactiveControllerHost, spec: DatasourceParamsControllerSpec) { host.addController(this); this.columns = spec.columns; this.query = spec.query; this.customFilters = spec.customFilters; + this.widgetName = spec.widgetName; makeAutoObservable(this, { setup: false }); } @@ -137,7 +139,7 @@ export class DatasourceParamsController implements ReactiveController { } static storageKey(widgetName: string, hash: string): string { - return `${widgetName}:[${hash}]`; + return `[${widgetName}:${hash}]`; } static restoreMeta(filter: FilterCondition, widgetName: string): FiltersMeta | null { diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts index 16cf5c79e7..37e16b7b65 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts @@ -66,7 +66,8 @@ export class RootGridStore extends BaseControllerHost { new DatasourceParamsController(this, { query, columns, - customFilters: customFilterHost + customFilters: customFilterHost, + widgetName: props.name }); new RefreshController(this, { diff --git a/packages/shared/widget-plugin-filtering/src/stores/generic/CombinedFilter.ts b/packages/shared/widget-plugin-filtering/src/stores/generic/CombinedFilter.ts new file mode 100644 index 0000000000..4be038c529 --- /dev/null +++ b/packages/shared/widget-plugin-filtering/src/stores/generic/CombinedFilter.ts @@ -0,0 +1,142 @@ +import { reduceArray, restoreArray } from "@mendix/filter-commons/condition-utils"; +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; +import { FilterCondition } from "mendix/filters"; +import { autorun, reaction } from "mobx"; +import { fnv1aHash } from "../../utils/fnv-1a-hash"; + +type ConditionWithMeta = { + cond: FilterCondition | undefined; + meta: string; +}; + +interface ObservableInput { + condWithMeta: ConditionWithMeta; + metaKey: string; + hydrate(value: ConditionWithMeta): void; +} + +type MetaBag = Record; + +export class CombinedFilter { + private _inputs: ObservableInput[]; + readonly stableKey: string; + readonly ownMetaKey = "CombinedFilter"; + + constructor(spec: { stableKey: string; inputs: ObservableInput[] }) { + this._inputs = spec.inputs; + this.stableKey = spec.stableKey; + } + + storageKey(hash: string): string { + return `${this.stableKey}-${hash}`; + } + + readMetaFromStorage(key: string): MetaBag | null { + const item = sessionStorage.getItem(key); + if (!item) { + return null; + } + try { + return JSON.parse(item) as MetaBag; + } catch (e) { + console.error(`CombinedFilter.readFilterMeta: Error parsing meta for key ${key}`, e); + return null; + } + } + + clearFilterMeta(hash: string): void { + sessionStorage.removeItem(this.storageKey(hash)); + } + + filterHash(filter: FilterCondition): string { + return fnv1aHash(JSON.stringify(filter)).toString(); + } + + restoreMeta(filter: FilterCondition): MetaBag | null { + const hash = this.filterHash(filter); + const key = this.storageKey(hash); + return this.readMetaFromStorage(key); + } + + hydrate(filter: FilterCondition | undefined): void { + if (!filter) { + return; + } + + const meta = this.restoreMeta(filter); + if (!meta) { + return; + } + + const conditions = restoreArray(filter, meta[this.ownMetaKey]); + if (conditions.length !== this._inputs.length) { + console.error( + `CombinedFilter.hydrate: Number of conditions (${conditions.length}) does not match number of inputs (${this._inputs.length})` + ); + return; + } + + for (let i = 0; i < this._inputs.length; i++) { + const input = this._inputs[i]; + const condWithMeta: ConditionWithMeta = { + cond: conditions[i], + meta: meta[input.metaKey] + }; + + input.hydrate(condWithMeta); + } + } + + get filter(): FilterCondition | undefined { + return this.filterWithBag.filter; + } + + get filterWithBag(): { + filter: FilterCondition | undefined; + bag: MetaBag; + hash: string | null; + } { + const bag: MetaBag = {}; + const conditions: Array = []; + + for (const { condWithMeta: data, metaKey } of this._inputs) { + bag[metaKey] = data.meta; + conditions.push(data.cond); + } + + const [filter, meta] = reduceArray(conditions); + bag[this.ownMetaKey] = meta; + + return { filter, bag, hash: filter ? this.filterHash(filter) : null }; + } + + saveFilterMeta(hash: string | null, bag: MetaBag): void { + if (!hash) { + return; + } + + sessionStorage.setItem(this.storageKey(hash), JSON.stringify(bag)); + } + + setup(): () => void { + const [add, disposeAll] = disposeBatch(); + + add(autorun(() => console.dir(this.filter))); + + add( + reaction( + () => this.filterWithBag, + (next, prev) => { + if (prev && prev.hash) { + this.clearFilterMeta(prev.hash); + } + if (next.hash) { + this.saveFilterMeta(next.hash, next.bag); + } + } + ) + ); + + return disposeAll; + } +} diff --git a/packages/shared/widget-plugin-filtering/src/utils/fnv-1a-hash.ts b/packages/shared/widget-plugin-filtering/src/utils/fnv-1a-hash.ts new file mode 100644 index 0000000000..f086fa8d82 --- /dev/null +++ b/packages/shared/widget-plugin-filtering/src/utils/fnv-1a-hash.ts @@ -0,0 +1,20 @@ +/* eslint-disable no-bitwise */ + +/** + * Generates 32 bit FNV-1a hash from the given string. + * As explained here: http://isthe.com/chongo/tech/comp/fnv/ + * + * @param s {string} String to generate hash from. + * @param [h] {number} FNV-1a hash generation init value. + * @returns {number} The result integer hash. + */ +export function fnv1aHash(s: string, h: number = 0x811c9dc5): number { + const l = s.length; + + for (let i = 0; i < l; i++) { + h ^= s.charCodeAt(i); + h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24); + } + + return h >>> 0; +} From a1f2e65a1bca9637591bd968381ad164a8cc63da Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:23:13 +0200 Subject: [PATCH 05/12] chore: add reduceMap helper --- .../src/__tests__/condition-utils.spec.ts | 90 ++++++++++++++++++- .../filter-commons/src/condition-utils.ts | 52 +++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts b/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts index 39e062ec82..5ed9712cd4 100644 --- a/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts +++ b/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts @@ -2,7 +2,7 @@ jest.mock("mendix/filters/builders"); import { AndCondition } from "mendix/filters"; import { equals, literal } from "mendix/filters/builders"; -import { compactArray, fromCompactArray, tag } from "../condition-utils"; +import { compactArray, fromCompactArray, reduceMap, tag } from "../condition-utils"; describe("condition-utils", () => { describe("compactArray", () => { @@ -47,4 +47,92 @@ describe("condition-utils", () => { expect(result).toEqual(input); }); }); + + describe("reduceMap", () => { + it("returns undefined condition and correct metadata for map with undefined values", () => { + const input = { x: undefined, y: undefined }; + const [condition, metadata] = reduceMap(input); + + expect(condition).toBeUndefined(); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual({ + length: 0, + keys: ["x", "y"] + }); + }); + + it("returns single condition and correct metadata for map with one condition", () => { + const tagCondition = tag("test"); + const input = { a: tagCondition, b: undefined, c: undefined }; + const [condition, metadata] = reduceMap(input); + + expect(condition).toBe(tagCondition); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual({ + length: 1, + keys: ["a", "b", "c"], + 0: "a" + }); + }); + + it("returns 'and' condition and correct metadata for map with multiple conditions", () => { + const tagCondition1 = tag("test1"); + const tagCondition2 = tag("test2"); + const input = { x: tagCondition1, y: undefined, z: tagCondition2, w: undefined }; + const [condition, metadata] = reduceMap(input); + + expect(condition).toMatchObject({ name: "and", type: "function" }); + expect((condition as AndCondition).args).toHaveLength(2); + expect((condition as AndCondition).args[0]).toBe(tagCondition1); + expect((condition as AndCondition).args[1]).toBe(tagCondition2); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual({ + length: 2, + keys: ["x", "y", "z", "w"], + 0: "x", + 1: "z" + }); + }); + + it("returns undefined condition for empty map", () => { + const input = {}; + const [condition, metadata] = reduceMap(input); + + expect(condition).toBeUndefined(); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual({ + length: 0, + keys: [] + }); + }); + + it("handles map with mixed condition types", () => { + const tagCondition = tag("tag-test"); + const equalsCondition = equals(literal("field"), literal("value")); + const input = { + tag: tagCondition, + equals: equalsCondition, + empty: undefined, + another: undefined + }; + const [condition, metadata] = reduceMap(input); + + expect(condition).toMatchObject({ name: "and", type: "function" }); + expect((condition as AndCondition).args).toHaveLength(2); + expect((condition as AndCondition).args[0]).toBe(tagCondition); + expect((condition as AndCondition).args[1]).toBe(equalsCondition); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual({ + length: 2, + keys: ["tag", "equals", "empty", "another"], + 0: "tag", + 1: "equals" + }); + }); + }); }); diff --git a/packages/shared/filter-commons/src/condition-utils.ts b/packages/shared/filter-commons/src/condition-utils.ts index 1d6631554c..63aee17539 100644 --- a/packages/shared/filter-commons/src/condition-utils.ts +++ b/packages/shared/filter-commons/src/condition-utils.ts @@ -142,6 +142,58 @@ export function restoreArray(cond: FilterCondition | undefined, meta: string): A return arr; } +type ReduceMapMeta = { + [index: number]: string; + length: number; + keys: string[]; +}; + +export function reduceMap(input: Record): [FilterCondition | undefined, string] { + const meta: ReduceMapMeta = { length: 0, keys: [] }; + const conditions: FilterCondition[] = []; + for (const [key, value] of Object.entries(input)) { + meta.keys.push(key); + if (value !== undefined) { + meta[conditions.length] = key; + conditions.push(value); + } + } + meta.length = conditions.length; + + const metaJson = JSON.stringify(meta); + + switch (conditions.length) { + case 0: + return [undefined, metaJson]; + case 1: + return [conditions[0], metaJson]; + default: + return [and(...conditions), metaJson]; + } +} +/* +export function restoreMap( + cond: FilterCondition | undefined, + metaJson: string +): Record { + const meta = JSON.parse(metaJson) as ReduceMapMeta; + const result: Record = {}; + const keys = Object.keys(meta).filter(key => key !== "length"); + for (const key of keys) { + result[key] = undefined; + } + + if (meta.length === 0) { + return result; + } + + if (meta.length === 1) { + const key = keys[0]; + result[key] = cond; + return result; + } +} */ + export function fromCompactArray(cond: FilterCondition): Array { const tag = isAnd(cond) ? cond.args[0] : cond; From 5ac3a4305adf457d0ba0dd90b268bc4b96fde893 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:30:39 +0200 Subject: [PATCH 06/12] feat: add restoreMap function --- .../src/__tests__/condition-utils.spec.ts | 132 +++++++++++++++++- .../filter-commons/src/condition-utils.ts | 22 ++- 2 files changed, 146 insertions(+), 8 deletions(-) diff --git a/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts b/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts index 5ed9712cd4..1995c0cba9 100644 --- a/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts +++ b/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts @@ -2,7 +2,7 @@ jest.mock("mendix/filters/builders"); import { AndCondition } from "mendix/filters"; import { equals, literal } from "mendix/filters/builders"; -import { compactArray, fromCompactArray, reduceMap, tag } from "../condition-utils"; +import { compactArray, fromCompactArray, reduceMap, restoreMap, tag } from "../condition-utils"; describe("condition-utils", () => { describe("compactArray", () => { @@ -135,4 +135,134 @@ describe("condition-utils", () => { }); }); }); + + describe("restoreMap", () => { + it("restores map with undefined values from undefined condition", () => { + const originalInput = { x: undefined, y: undefined }; + const [condition, metadata] = reduceMap(originalInput); + + const restored = restoreMap(condition, metadata); + + expect(restored).toEqual(originalInput); + }); + + it("restores map with single condition", () => { + const tagCondition = tag("test"); + const originalInput = { a: tagCondition, b: undefined, c: undefined }; + const [condition, metadata] = reduceMap(originalInput); + + const restored = restoreMap(condition, metadata); + + expect(restored).toEqual(originalInput); + }); + + it("restores map with multiple conditions", () => { + const tagCondition1 = tag("test1"); + const tagCondition2 = tag("test2"); + const originalInput = { x: tagCondition1, y: undefined, z: tagCondition2, w: undefined }; + const [condition, metadata] = reduceMap(originalInput); + + const restored = restoreMap(condition, metadata); + + expect(restored).toEqual(originalInput); + }); + + it("restores empty map", () => { + const originalInput = {}; + const [condition, metadata] = reduceMap(originalInput); + + const restored = restoreMap(condition, metadata); + + expect(restored).toEqual(originalInput); + }); + + it("restores map with mixed condition types", () => { + const tagCondition = tag("tag-test"); + const equalsCondition = equals(literal("field"), literal("value")); + const originalInput = { + tag: tagCondition, + equals: equalsCondition, + empty: undefined, + another: undefined + }; + const [condition, metadata] = reduceMap(originalInput); + + const restored = restoreMap(condition, metadata); + + expect(restored).toEqual(originalInput); + }); + + it("handles manual metadata for single condition case", () => { + const tagCondition = tag("manual-test"); + const metadata = JSON.stringify({ + length: 1, + keys: ["first", "second", "third"], + 0: "second" + }); + + const restored = restoreMap(tagCondition, metadata); + + expect(restored).toEqual({ + first: undefined, + second: tagCondition, + third: undefined + }); + }); + + it("handles manual metadata for multiple conditions case", () => { + const tagCondition1 = tag("manual1"); + const tagCondition2 = tag("manual2"); + const andCondition = { + name: "and", + type: "function", + args: [tagCondition1, tagCondition2] + } as AndCondition; + const metadata = JSON.stringify({ + length: 2, + keys: ["a", "b", "c", "d"], + 0: "a", + 1: "c" + }); + + const restored = restoreMap(andCondition, metadata); + + expect(restored).toEqual({ + a: tagCondition1, + b: undefined, + c: tagCondition2, + d: undefined + }); + }); + + it("handles edge case with undefined condition and non-zero length metadata", () => { + const metadata = JSON.stringify({ + length: 1, + keys: ["test"], + 0: "test" + }); + + const restored = restoreMap(undefined, metadata); + + expect(restored).toEqual({ + test: undefined + }); + }); + + it("handles edge case with non-and condition but multiple length metadata", () => { + const tagCondition = tag("single"); + const metadata = JSON.stringify({ + length: 2, + keys: ["a", "b"], + 0: "a", + 1: "b" + }); + + const restored = restoreMap(tagCondition, metadata); + + expect(restored).toEqual({ + a: undefined, + b: undefined + }); + }); + }); }); diff --git a/packages/shared/filter-commons/src/condition-utils.ts b/packages/shared/filter-commons/src/condition-utils.ts index 63aee17539..6eedc9d1e8 100644 --- a/packages/shared/filter-commons/src/condition-utils.ts +++ b/packages/shared/filter-commons/src/condition-utils.ts @@ -171,15 +171,15 @@ export function reduceMap(input: Record): [ return [and(...conditions), metaJson]; } } -/* + export function restoreMap( cond: FilterCondition | undefined, metaJson: string ): Record { const meta = JSON.parse(metaJson) as ReduceMapMeta; const result: Record = {}; - const keys = Object.keys(meta).filter(key => key !== "length"); - for (const key of keys) { + + for (const key of meta.keys) { result[key] = undefined; } @@ -187,12 +187,20 @@ export function restoreMap( return result; } - if (meta.length === 1) { - const key = keys[0]; - result[key] = cond; + if (meta.length === 1 && meta[0]) { + result[meta[0]] = cond; + return result; + } + + if (cond && isAnd(cond)) { + cond.args.forEach((c, i) => { + result[meta[i]] = c; + }); return result; } -} */ + + return result; +} export function fromCompactArray(cond: FilterCondition): Array { const tag = isAnd(cond) ? cond.args[0] : cond; From c55eab319882e157da3d0308f038ec5013b5a48f Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:35:20 +0200 Subject: [PATCH 07/12] test: add unit tests for reduce/restore array --- .../src/__tests__/condition-utils.spec.ts | 202 +++++++++++++++++- 1 file changed, 200 insertions(+), 2 deletions(-) diff --git a/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts b/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts index 1995c0cba9..01e63d953e 100644 --- a/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts +++ b/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts @@ -1,8 +1,16 @@ jest.mock("mendix/filters/builders"); -import { AndCondition } from "mendix/filters"; +import { AndCondition, FilterCondition } from "mendix/filters"; import { equals, literal } from "mendix/filters/builders"; -import { compactArray, fromCompactArray, reduceMap, restoreMap, tag } from "../condition-utils"; +import { + compactArray, + fromCompactArray, + reduceArray, + reduceMap, + restoreArray, + restoreMap, + tag +} from "../condition-utils"; describe("condition-utils", () => { describe("compactArray", () => { @@ -265,4 +273,194 @@ describe("condition-utils", () => { }); }); }); + + describe("reduceArray", () => { + it("returns undefined condition and correct metadata for array with undefined values", () => { + const input = [undefined, undefined, undefined]; + const [condition, metadata] = reduceArray(input); + + expect(condition).toBeUndefined(); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual([3, []]); + }); + + it("returns single condition and correct metadata for array with one condition", () => { + const tagCondition = tag("test"); + const input = [undefined, tagCondition, undefined]; + const [condition, metadata] = reduceArray(input); + + expect(condition).toBe(tagCondition); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual([3, [1]]); + }); + + it("returns 'and' condition and correct metadata for array with multiple conditions", () => { + const tagCondition1 = tag("test1"); + const tagCondition2 = tag("test2"); + const input = [tagCondition1, undefined, tagCondition2, undefined]; + const [condition, metadata] = reduceArray(input); + + expect(condition).toMatchObject({ name: "and", type: "function" }); + expect((condition as AndCondition).args).toHaveLength(2); + expect((condition as AndCondition).args[0]).toBe(tagCondition1); + expect((condition as AndCondition).args[1]).toBe(tagCondition2); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual([4, [0, 2]]); + }); + + it("returns undefined condition for empty array", () => { + const input: Array = []; + const [condition, metadata] = reduceArray(input); + + expect(condition).toBeUndefined(); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual([0, []]); + }); + + it("handles array with mixed condition types", () => { + const tagCondition = tag("tag-test"); + const equalsCondition = equals(literal("field"), literal("value")); + const input = [tagCondition, undefined, equalsCondition, undefined, undefined]; + const [condition, metadata] = reduceArray(input); + + expect(condition).toMatchObject({ name: "and", type: "function" }); + expect((condition as AndCondition).args).toHaveLength(2); + expect((condition as AndCondition).args[0]).toBe(tagCondition); + expect((condition as AndCondition).args[1]).toBe(equalsCondition); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual([5, [0, 2]]); + }); + + it("handles array with single condition at beginning", () => { + const tagCondition = tag("first"); + const input = [tagCondition, undefined, undefined]; + const [condition, metadata] = reduceArray(input); + + expect(condition).toBe(tagCondition); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual([3, [0]]); + }); + + it("handles array with single condition at end", () => { + const tagCondition = tag("last"); + const input = [undefined, undefined, tagCondition]; + const [condition, metadata] = reduceArray(input); + + expect(condition).toBe(tagCondition); + + const parsedMeta = JSON.parse(metadata); + expect(parsedMeta).toEqual([3, [2]]); + }); + }); + + describe("restoreArray", () => { + it("restores array with undefined values from undefined condition", () => { + const originalInput = [undefined, undefined, undefined]; + const [condition, metadata] = reduceArray(originalInput); + + const restored = restoreArray(condition, metadata); + + expect(restored).toEqual(originalInput); + }); + + it("restores array with single condition", () => { + const tagCondition = tag("test"); + const originalInput = [undefined, tagCondition, undefined]; + const [condition, metadata] = reduceArray(originalInput); + + const restored = restoreArray(condition, metadata); + + expect(restored).toEqual(originalInput); + }); + + it("restores array with multiple conditions", () => { + const tagCondition1 = tag("test1"); + const tagCondition2 = tag("test2"); + const originalInput = [tagCondition1, undefined, tagCondition2, undefined]; + const [condition, metadata] = reduceArray(originalInput); + + const restored = restoreArray(condition, metadata); + + expect(restored).toEqual(originalInput); + }); + + it("restores empty array", () => { + const originalInput: Array = []; + const [condition, metadata] = reduceArray(originalInput); + + const restored = restoreArray(condition, metadata); + + expect(restored).toEqual(originalInput); + }); + + it("restores array with mixed condition types", () => { + const tagCondition = tag("tag-test"); + const equalsCondition = equals(literal("field"), literal("value")); + const originalInput = [tagCondition, undefined, equalsCondition, undefined, undefined]; + const [condition, metadata] = reduceArray(originalInput); + + const restored = restoreArray(condition, metadata); + + expect(restored).toEqual(originalInput); + }); + + it("handles manual metadata for single condition case", () => { + const tagCondition = tag("manual-test"); + const metadata = JSON.stringify([4, [2]]); + + const restored = restoreArray(tagCondition, metadata); + + expect(restored).toEqual([undefined, undefined, tagCondition, undefined]); + }); + + it("handles manual metadata for multiple conditions case", () => { + const tagCondition1 = tag("manual1"); + const tagCondition2 = tag("manual2"); + const andCondition = { + name: "and", + type: "function", + args: [tagCondition1, tagCondition2] + } as AndCondition; + const metadata = JSON.stringify([5, [1, 3]]); + + const restored = restoreArray(andCondition, metadata); + + expect(restored).toEqual([undefined, tagCondition1, undefined, tagCondition2, undefined]); + }); + + it("handles edge case with undefined condition and non-empty indexes", () => { + const metadata = JSON.stringify([3, [1]]); + + const restored = restoreArray(undefined, metadata); + + expect(restored).toEqual([undefined, undefined, undefined]); + }); + + it("handles edge case with non-and condition but multiple indexes", () => { + const tagCondition = tag("single"); + const metadata = JSON.stringify([4, [0, 2]]); + + const restored = restoreArray(tagCondition, metadata); + + expect(restored).toEqual([undefined, undefined, undefined, undefined]); + }); + + it("round-trip test with complex array", () => { + const tag1 = tag("one"); + const tag2 = tag("two"); + const eq1 = equals(literal("a"), literal("b")); + const originalInput = [tag1, undefined, eq1, undefined, tag2, undefined, undefined]; + + const [condition, metadata] = reduceArray(originalInput); + const restored = restoreArray(condition, metadata); + + expect(restored).toEqual(originalInput); + }); + }); }); From 884c3d31845e4a2c0943b4830feb176463ad693a Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:37:09 +0200 Subject: [PATCH 08/12] feat: change filters host --- .../filter-commons/src/condition-utils.ts | 1 + .../src/stores/generic/CombinedFilter.ts | 6 +-- .../src/stores/generic/CustomFilterHost.ts | 53 ++++++++++++++----- .../src/typings/ObservableFilterHost.ts | 2 +- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/packages/shared/filter-commons/src/condition-utils.ts b/packages/shared/filter-commons/src/condition-utils.ts index 6eedc9d1e8..a333cb19a2 100644 --- a/packages/shared/filter-commons/src/condition-utils.ts +++ b/packages/shared/filter-commons/src/condition-utils.ts @@ -55,6 +55,7 @@ interface TagCond { readonly arg2: TagMarker; } +/** @deprecated use for unit tests only */ export function tag(name: string): TagCond { return notEqual(literal(name), literal(MARKER)) as TagCond; } diff --git a/packages/shared/widget-plugin-filtering/src/stores/generic/CombinedFilter.ts b/packages/shared/widget-plugin-filtering/src/stores/generic/CombinedFilter.ts index 4be038c529..0b93e20419 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/generic/CombinedFilter.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/generic/CombinedFilter.ts @@ -4,7 +4,7 @@ import { FilterCondition } from "mendix/filters"; import { autorun, reaction } from "mobx"; import { fnv1aHash } from "../../utils/fnv-1a-hash"; -type ConditionWithMeta = { +export type ConditionWithMeta = { cond: FilterCondition | undefined; meta: string; }; @@ -110,7 +110,7 @@ export class CombinedFilter { return { filter, bag, hash: filter ? this.filterHash(filter) : null }; } - saveFilterMeta(hash: string | null, bag: MetaBag): void { + private _saveFilterMeta(hash: string | null, bag: MetaBag): void { if (!hash) { return; } @@ -131,7 +131,7 @@ export class CombinedFilter { this.clearFilterMeta(prev.hash); } if (next.hash) { - this.saveFilterMeta(next.hash, next.bag); + this._saveFilterMeta(next.hash, next.bag); } } ) diff --git a/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts b/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts index 8e461dfbae..d4f7b16b49 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts @@ -1,16 +1,21 @@ -import { tag } from "@mendix/filter-commons/condition-utils"; +import { reduceMap, restoreMap } from "@mendix/filter-commons/condition-utils"; import { FilterData, FiltersSettingsMap, PlainJs, Serializable } from "@mendix/filter-commons/typings/settings"; import { FilterCondition } from "mendix/filters"; -import { and } from "mendix/filters/builders"; -import { autorun, makeAutoObservable } from "mobx"; +import { action, autorun, makeAutoObservable } from "mobx"; import { Filter, ObservableFilterHost } from "../../typings/ObservableFilterHost"; +import { ConditionWithMeta } from "./CombinedFilter"; export class CustomFilterHost implements ObservableFilterHost, Serializable { private filters: Map void]> = new Map(); private settingsBuffer: FiltersSettingsMap = new Map(); + private _state: Map = new Map(); + + readonly metaKey = "custom-filter-host"; constructor() { - makeAutoObservable(this); + makeAutoObservable(this, { + hydrate: action + }); } get settings(): FiltersSettingsMap { @@ -21,22 +26,26 @@ export class CustomFilterHost implements ObservableFilterHost, Serializable { this.settingsBuffer = data; } - get conditions(): Array { - return [...this.filters].map(([key, [{ condition }]]) => { - return condition ? and(tag(key), condition) : undefined; - }); - } - observe(key: string, filter: Filter): void { this.unobserve(key); - const clear = autorun(() => { + const clearSettingsSync = autorun(() => { if (this.settingsBuffer.has(key)) { filter.fromJSON(this.settingsBuffer.get(key)); } }); + const clearStateSync = autorun(() => { + this._state.set(key, filter.condition); + }); + const skipInit = this.settingsBuffer.has(key); + if (!skipInit && this._state.has(key)) { + filter.fromViewState(this._state.get(key)!); + } + const dispose = (): void => { - clear(); + clearSettingsSync(); + clearStateSync(); this.filters.delete(key); + this._state.delete(key); }; this.filters.set(key, [filter, dispose]); } @@ -58,4 +67,24 @@ export class CustomFilterHost implements ObservableFilterHost, Serializable { this.settings = new Map(data as Array<[string, FilterData]>); } + + get condWithMeta(): ConditionWithMeta { + const conditions: Record = {}; + + for (const [key, [filter]] of this.filters) { + conditions[key] = filter.condition; + } + + const [cond, meta] = reduceMap(conditions); + + return { + cond, + meta + }; + } + + hydrate({ cond, meta }: ConditionWithMeta): void { + const map = restoreMap(cond, meta); + this._state = new Map(Object.entries(map)); + } } diff --git a/packages/shared/widget-plugin-filtering/src/typings/ObservableFilterHost.ts b/packages/shared/widget-plugin-filtering/src/typings/ObservableFilterHost.ts index 0a06416b37..66a8a70c6d 100644 --- a/packages/shared/widget-plugin-filtering/src/typings/ObservableFilterHost.ts +++ b/packages/shared/widget-plugin-filtering/src/typings/ObservableFilterHost.ts @@ -4,12 +4,12 @@ import { FilterCondition } from "mendix/filters"; export interface Filter { toJSON(): FilterData; fromJSON(data: FilterData): void; + fromViewState(data: FilterCondition): void; condition: FilterCondition | undefined; setup?: () => void | void; } export interface ObservableFilterHost { - conditions: Array; get settings(): FiltersSettingsMap; set settings(settings: FiltersSettingsMap); observe(key: string, filter: Filter): void; From 8bf2c2eef3e54d8520d9baf1307372ae6c67cf9f Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Wed, 6 Aug 2025 11:35:28 +0200 Subject: [PATCH 09/12] feat: finish "clean" xpath feature --- .../src/hocs/withLinkedRefStore.tsx | 2 +- .../src/components/WidgetHeaderContext.tsx | 2 +- .../controllers/DatasourceParamsController.ts | 147 ++---------------- .../src/helpers/state/ColumnGroupStore.ts | 43 +++-- .../src/helpers/state/RootGridStore.ts | 44 +++--- .../state/column/ColumnFilterStore.tsx | 20 ++- .../widget-plugin-filtering/src/context.ts | 6 +- .../custom-filter-api/BaseStoreProvider.ts | 14 -- .../custom-filter-api/DateStoreProvider.ts | 5 +- .../custom-filter-api/EnumStoreProvider.ts | 5 +- .../custom-filter-api/NumberStoreProvider.ts | 5 +- .../custom-filter-api/StringStoreProvider.ts | 5 +- .../src/stores/generic/CombinedFilter.ts | 28 ++-- .../src/stores/generic/CustomFilterHost.ts | 9 +- .../src/typings/ConditionWithMeta.ts | 6 + 15 files changed, 110 insertions(+), 231 deletions(-) create mode 100644 packages/shared/widget-plugin-filtering/src/typings/ConditionWithMeta.ts diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedRefStore.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedRefStore.tsx index 75ab21f9d6..76cba1bc2f 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedRefStore.tsx +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedRefStore.tsx @@ -106,7 +106,7 @@ class RefStoreProvider extends BaseStoreProvider { this.dataKey = gate.props.name; this._store = new RefFilterStore({ gate, - initCond: this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey) + initCond: null }); } diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx index 651c04806a..12db0017a1 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx @@ -19,7 +19,7 @@ const FilterContext = getGlobalFilterContextObject(); function HeaderContainer(props: WidgetHeaderContextProps): ReactElement { const selectionContext = useCreateSelectionContextValue(props.selectionHelper); return ( - + {props.children} ); diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts index 0e4f6100f9..a24dae8d7f 100644 --- a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts +++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts @@ -1,172 +1,53 @@ -import { reduceArray, restoreArray } from "@mendix/filter-commons/condition-utils"; import { QueryController } from "@mendix/widget-plugin-grid/query/query-controller"; -import { fnv1aHash } from "@mendix/widget-plugin-grid/utils/fnv-1a-hash"; import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller"; import { FilterCondition } from "mendix/filters"; -import { makeAutoObservable, reaction } from "mobx"; +import { reaction } from "mobx"; import { SortInstruction } from "../typings/sorting"; -interface Columns { - conditions: Array; - sortInstructions: SortInstruction[] | undefined; +interface ObservableFilterStore { + filter: FilterCondition | undefined; } -interface FiltersInput { - conditions: Array; +interface ObservableSortStore { + sortInstructions: SortInstruction[] | undefined; } type DatasourceParamsControllerSpec = { query: QueryController; - columns: Columns; - customFilters: FiltersInput; - widgetName: string; -}; - -type FiltersMeta = { - columnFilters: string; - customFilters: string; - combined: string; + filterHost: ObservableFilterStore; + sortHost: ObservableSortStore; }; -type CondArray = Array; - export class DatasourceParamsController implements ReactiveController { - private columns: Columns; private query: QueryController; - private customFilters: FiltersInput; - readonly widgetName: string; + private filterHost: ObservableFilterStore; + private sortHost: ObservableSortStore; constructor(host: ReactiveControllerHost, spec: DatasourceParamsControllerSpec) { host.addController(this); - this.columns = spec.columns; + this.filterHost = spec.filterHost; + this.sortHost = spec.sortHost; this.query = spec.query; - this.customFilters = spec.customFilters; - this.widgetName = spec.widgetName; - - makeAutoObservable(this, { setup: false }); - } - - private get derivedFilter(): { - filter: FilterCondition | undefined; - meta: FiltersMeta; - hash: string | null; - } { - return this.reduceFilters(this.columns.conditions, this.customFilters.conditions); - } - - private get derivedSortOrder(): SortInstruction[] | undefined { - return this.columns.sortInstructions; - } - - reduceFilters( - columnFilters: CondArray, - customFilters: CondArray - ): { - filter: FilterCondition | undefined; - meta: FiltersMeta; - hash: string | null; - } { - const [columnsCond, columnsMeta] = reduceArray(columnFilters); - const [customCond, customMeta] = reduceArray(customFilters); - const [filter, combinedMeta] = reduceArray([columnsCond, customCond]); - - const meta: FiltersMeta = { columnFilters: columnsMeta, customFilters: customMeta, combined: combinedMeta }; - const hash = filter ? DatasourceParamsController.filterHash(filter) : null; - - return { filter, meta, hash }; } setup(): () => void { const [add, disposeAll] = disposeBatch(); add( reaction( - () => this.derivedSortOrder, + () => this.sortHost.sortInstructions, sortOrder => this.query.setSortOrder(sortOrder), { fireImmediately: true } ) ); add( reaction( - () => this.derivedFilter, - (next, prev) => { - if (prev && prev.hash) { - this.clearFilterMeta(prev.hash); - } - if (next.hash) { - this.saveFilterMeta(next.hash, next.meta); - } - this.query.setFilter(next.filter); - }, + () => this.filterHost.filter, + filter => this.query.setFilter(filter), { fireImmediately: true } ) ); return disposeAll; } - - dataKey(hash: string): string { - return DatasourceParamsController.storageKey(this.widgetName, hash); - } - - clearFilterMeta(hash: string): void { - sessionStorage.removeItem(this.dataKey(hash)); - } - - saveFilterMeta(hash: string | null, meta: FiltersMeta): void { - if (!hash) { - return; - } - - sessionStorage.setItem(this.dataKey(hash), JSON.stringify(meta)); - } - - readFilterMeta(hash: string): FiltersMeta | null { - const item = sessionStorage.getItem(this.dataKey(hash)); - if (!item) { - return null; - } - try { - return JSON.parse(item) as FiltersMeta; - } catch (e) { - console.error(`DatasourceParamsController.readFilterMeta: Error parsing meta for hash ${hash}`, e); - return null; - } - } - - static filterHash(filter: FilterCondition): string { - return fnv1aHash(JSON.stringify(filter)).toString(); - } - - static storageKey(widgetName: string, hash: string): string { - return `[${widgetName}:${hash}]`; - } - - static restoreMeta(filter: FilterCondition, widgetName: string): FiltersMeta | null { - const hash = this.filterHash(filter); - const key = this.storageKey(widgetName, hash); - const metaJson = sessionStorage.getItem(key); - if (!metaJson) { - return null; - } - return JSON.parse(metaJson) as FiltersMeta; - } - - static unzipFilter( - filter: FilterCondition | undefined, - widgetName: string - ): [columnFilters: Array, customFilters: Array] { - if (!filter) { - return [[], []]; - } - const meta = this.restoreMeta(filter, widgetName); - if (!meta) { - return [[], []]; - } - - const [column, custom] = restoreArray(filter, meta.combined); - const columnFilters = restoreArray(column, meta.columnFilters); - const customFilters = restoreArray(custom, meta.customFilters); - return [columnFilters, customFilters]; - } } diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts index b3660c0767..0c26e492db 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts @@ -1,6 +1,9 @@ +import { reduceArray, restoreArray } from "@mendix/filter-commons/condition-utils"; import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings"; +import { ConditionWithMeta } from "@mendix/widget-plugin-filtering/typings/ConditionWithMeta"; +import { ObservableFilterHost } from "@mendix/widget-plugin-filtering/typings/ObservableFilterHost"; import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; -import { FilterCondition } from "mendix/filters"; + import { action, computed, makeObservable, observable } from "mobx"; import { DatagridContainerProps } from "../../../typings/DatagridProps"; import { ColumnId, GridColumn } from "../../typings/GridColumn"; @@ -13,7 +16,7 @@ import { sortInstructionsToSortRules, sortRulesToSortInstructions } from "./ColumnsSortingStore"; -import { ColumnFilterStore, ObserverBag } from "./column/ColumnFilterStore"; +import { ColumnFilterStore } from "./column/ColumnFilterStore"; import { ColumnStore } from "./column/ColumnStore"; export interface IColumnGroupStore { @@ -40,24 +43,24 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore { readonly columnFilters: ColumnFilterStore[]; + readonly metaKey = "ColumnGroupStore"; + sorting: ColumnsSortingStore; isResizing = false; constructor( props: Pick, info: StaticInfo, - initFilter: Array, - observerBag: ObserverBag + filterHost: ObservableFilterHost ) { this._allColumns = []; this.columnFilters = []; props.columns.forEach((columnProps, i) => { - const initCond = initFilter.at(i) ?? null; const column = new ColumnStore(i, columnProps, this); this._allColumnsById.set(column.columnId, column); this._allColumns[i] = column; - this.columnFilters[i] = new ColumnFilterStore(columnProps, info, initCond, observerBag); + this.columnFilters[i] = new ColumnFilterStore(columnProps, info, filterHost); }); this.sorting = new ColumnsSortingStore( @@ -72,13 +75,14 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore { _allColumnsOrdered: computed, availableColumns: computed, visibleColumns: computed, - conditions: computed.struct, + condWithMeta: computed, columnSettings: computed.struct, filterSettings: computed({ keepAlive: true }), updateProps: action, setIsResizing: action, swapColumns: action, - setColumnSettings: action + setColumnSettings: action, + hydrate: action }); } @@ -142,12 +146,6 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore { return [...this.availableColumns].filter(column => !column.isHidden); } - get conditions(): Array { - return this.columnFilters.map((store, index) => { - return this._allColumns[index].isHidden ? undefined : store.condition; - }); - } - get sortInstructions(): SortInstruction[] | undefined { return sortRulesToSortInstructions(this.sorting.rules, this._allColumns); } @@ -194,4 +192,21 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore { isLastVisible(column: ColumnStore): boolean { return this.visibleColumns.at(-1) === column; } + + get condWithMeta(): ConditionWithMeta { + const conditions = this.columnFilters.map((store, index) => { + return this._allColumns[index].isHidden ? undefined : store.condition; + }); + const [cond, meta] = reduceArray(conditions); + return { cond, meta }; + } + + hydrate({ cond, meta }: ConditionWithMeta): void { + restoreArray(cond, meta).forEach((condition, index) => { + const filter = this.columnFilters[index]; + if (filter && condition) { + filter.fromViewState(condition); + } + }); + } } diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts index 37e16b7b65..805a4c751a 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts @@ -1,4 +1,5 @@ import { createContextWithStub, FilterAPI } from "@mendix/widget-plugin-filtering/context"; +import { CombinedFilter } from "@mendix/widget-plugin-filtering/stores/generic/CombinedFilter"; import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost"; import { DatasourceController } from "@mendix/widget-plugin-grid/query/DatasourceController"; import { RefreshController } from "@mendix/widget-plugin-grid/query/RefreshController"; @@ -30,44 +31,47 @@ export class RootGridStore extends BaseControllerHost { exportProgressCtrl: ProgressStore; loaderCtrl: DerivedLoaderController; paginationCtrl: PaginationController; - readonly autonomousFilterAPI: FilterAPI; + readonly filterAPI: FilterAPI; private gate: Gate; constructor({ gate, exportCtrl }: Spec) { super(); - const { props } = gate; - const [columnsInitFilter, sharedInitFilter] = DatasourceParamsController.unzipFilter( - props.datasource.filter, - props.name - ); this.gate = gate; + this.staticInfo = { name: props.name, filtersChannelName: `datagrid/${generateUUID()}` }; - const customFilterHost = new CustomFilterHost(); + + const filterHost = new CustomFilterHost(); + const query = new DatasourceController(this, { gate }); - this.autonomousFilterAPI = createContextWithStub({ - filterObserver: customFilterHost, - sharedInitFilter, + + this.filterAPI = createContextWithStub({ + filterObserver: filterHost, parentChannelName: this.staticInfo.filtersChannelName }); - const columns = (this.columnsStore = new ColumnGroupStore(props, this.staticInfo, columnsInitFilter, { - customFilterHost, - sharedInitFilter - })); - this.settingsStore = new GridPersonalizationStore(props, this.columnsStore, customFilterHost); + + this.columnsStore = new ColumnGroupStore(props, this.staticInfo, filterHost); + + const combinedFilter = new CombinedFilter(this, { + stableKey: props.name, + inputs: [filterHost, this.columnsStore] + }); + + this.settingsStore = new GridPersonalizationStore(props, this.columnsStore, filterHost); + this.paginationCtrl = new PaginationController(this, { gate, query }); + this.exportProgressCtrl = exportCtrl; new DatasourceParamsController(this, { query, - columns, - customFilters: customFilterHost, - widgetName: props.name + filterHost: combinedFilter, + sortHost: this.columnsStore }); new RefreshController(this, { @@ -77,9 +81,11 @@ export class RootGridStore extends BaseControllerHost { this.loaderCtrl = new DerivedLoaderController({ exp: exportCtrl, - cols: columns, + cols: this.columnsStore, query }); + + combinedFilter.hydrate(props.datasource.filter); } setup(): () => void { diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx b/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx index e1e3554092..e8d4e38ee4 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx @@ -26,12 +26,12 @@ export class ColumnFilterStore implements IColumnFilterStore { private _error: APIError | null; private _filterStore: FilterStore | null = null; private _context: FilterAPI; - private _observerBag: ObserverBag; + private _filterHost: ObservableFilterHost; - constructor(props: ColumnsType, info: StaticInfo, dsViewState: FilterCondition | null, observerBag: ObserverBag) { - this._observerBag = observerBag; + constructor(props: ColumnsType, info: StaticInfo, filterHost: ObservableFilterHost) { + this._filterHost = filterHost; this._widget = props.filter; - const storeResult = this.createFilterStore(props, dsViewState); + const storeResult = this.createFilterStore(props, null); if (storeResult === null) { this._error = this._filterStore = null; } else if (storeResult.hasError) { @@ -77,8 +77,7 @@ export class ColumnFilterStore implements IColumnFilterStore { type: "direct", store }), - filterObserver: this._observerBag.customFilterHost, - sharedInitFilter: this._observerBag.sharedInitFilter + filterObserver: this._filterHost }; } @@ -86,6 +85,10 @@ export class ColumnFilterStore implements IColumnFilterStore { return {this._widget}; } + fromViewState(cond: FilterCondition): void { + this._filterStore?.fromViewState(cond); + } + get condition(): FilterCondition | undefined { return this._filterStore ? this._filterStore.condition : undefined; } @@ -108,8 +111,3 @@ const isListAttributeValue = ( ): attribute is ListAttributeValue => { return !!(attribute && attribute.isList === false); }; - -export interface ObserverBag { - customFilterHost: ObservableFilterHost; - sharedInitFilter: Array; -} diff --git a/packages/shared/widget-plugin-filtering/src/context.ts b/packages/shared/widget-plugin-filtering/src/context.ts index 382df8a092..79cdb442d0 100644 --- a/packages/shared/widget-plugin-filtering/src/context.ts +++ b/packages/shared/widget-plugin-filtering/src/context.ts @@ -1,5 +1,4 @@ import { EnumFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/EnumFilterStore"; -import { FilterCondition } from "mendix/filters"; import { Context, createContext, useContext } from "react"; import { APIError, ENOCONTEXT } from "./errors"; import { Result, error, value } from "./result-meta"; @@ -11,7 +10,6 @@ export interface FilterAPI { parentChannelName: string; provider: Result; filterObserver: ObservableFilterHost; - sharedInitFilter: Array; } export type FilterStore = InputFilterInterface | EnumFilterStore; @@ -59,13 +57,11 @@ export const useFilterContextValue = useFilterAPI; export function createContextWithStub(options: { filterObserver: ObservableFilterHost; parentChannelName: string; - sharedInitFilter: Array; }): FilterAPI { return { version: 3, parentChannelName: options.parentChannelName, provider: value(PROVIDER_STUB), - filterObserver: options.filterObserver, - sharedInitFilter: options.sharedInitFilter + filterObserver: options.filterObserver }; } diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/BaseStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/BaseStoreProvider.ts index 41299b12e1..cafbd40ecb 100644 --- a/packages/shared/widget-plugin-filtering/src/custom-filter-api/BaseStoreProvider.ts +++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/BaseStoreProvider.ts @@ -1,7 +1,5 @@ -import { isAnd, isTag } from "@mendix/filter-commons/condition-utils"; import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { ISetupable } from "@mendix/widget-plugin-mobx-kit/setupable"; -import { FilterCondition } from "mendix/filters"; import { FilterAPI } from "../context"; import { Filter } from "../typings/ObservableFilterHost"; @@ -10,18 +8,6 @@ export abstract class BaseStoreProvider implements ISetupable protected abstract filterAPI: FilterAPI; abstract readonly dataKey: string; - protected findInitFilter(conditions: Array, key: string): FilterCondition | null { - for (const cond of conditions) { - if (cond && isAnd(cond)) { - const [tag, initFilter] = cond.args; - if (isTag(tag) && tag.arg1.value === key) { - return initFilter; - } - } - } - return null; - } - setup(): () => void { const [add, disposeAll] = disposeBatch(); this.filterAPI.filterObserver.observe(this.dataKey, this._store); diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/DateStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/DateStoreProvider.ts index 65cfe580d5..fedbb94dd2 100644 --- a/packages/shared/widget-plugin-filtering/src/custom-filter-api/DateStoreProvider.ts +++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/DateStoreProvider.ts @@ -13,10 +13,7 @@ export class DateStoreProvider extends BaseStoreProvider { super(); this.filterAPI = filterAPI; this.dataKey = spec.dataKey; - this._store = new DateInputFilterStore( - spec.attributes, - this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey) - ); + this._store = new DateInputFilterStore(spec.attributes, null); } get store(): Date_InputFilterInterface { diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/EnumStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/EnumStoreProvider.ts index c72ad7167b..edf5fe97c6 100644 --- a/packages/shared/widget-plugin-filtering/src/custom-filter-api/EnumStoreProvider.ts +++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/EnumStoreProvider.ts @@ -12,10 +12,7 @@ export class EnumStoreProvider extends BaseStoreProvider { super(); this.filterAPI = filterAPI; this.dataKey = spec.dataKey; - this._store = new EnumFilterStore( - spec.attributes, - this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey) - ); + this._store = new EnumFilterStore(spec.attributes, null); } get store(): EnumFilterStore { diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/NumberStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/NumberStoreProvider.ts index da7f77e639..5b3639f346 100644 --- a/packages/shared/widget-plugin-filtering/src/custom-filter-api/NumberStoreProvider.ts +++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/NumberStoreProvider.ts @@ -13,10 +13,7 @@ export class NumberStoreProvider extends BaseStoreProvider; -export class CombinedFilter { +export class CombinedFilter implements ReactiveController { private _inputs: ObservableInput[]; readonly stableKey: string; readonly ownMetaKey = "CombinedFilter"; - constructor(spec: { stableKey: string; inputs: ObservableInput[] }) { + constructor(host: ReactiveControllerHost, spec: { stableKey: string; inputs: ObservableInput[] }) { + host.addController(this); this._inputs = spec.inputs; this.stableKey = spec.stableKey; + + makeObservable(this, { + filter: computed, + filterWithBag: computed, + hydrate: action + }); } storageKey(hash: string): string { - return `${this.stableKey}-${hash}`; + return `${this.stableKey}@${hash}`; } readMetaFromStorage(key: string): MetaBag | null { @@ -110,7 +114,7 @@ export class CombinedFilter { return { filter, bag, hash: filter ? this.filterHash(filter) : null }; } - private _saveFilterMeta(hash: string | null, bag: MetaBag): void { + saveFilterMeta(hash: string | null, bag: MetaBag): void { if (!hash) { return; } @@ -121,8 +125,6 @@ export class CombinedFilter { setup(): () => void { const [add, disposeAll] = disposeBatch(); - add(autorun(() => console.dir(this.filter))); - add( reaction( () => this.filterWithBag, @@ -131,7 +133,7 @@ export class CombinedFilter { this.clearFilterMeta(prev.hash); } if (next.hash) { - this._saveFilterMeta(next.hash, next.bag); + this.saveFilterMeta(next.hash, next.bag); } } ) diff --git a/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts b/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts index d4f7b16b49..3e850e869d 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts @@ -2,15 +2,15 @@ import { reduceMap, restoreMap } from "@mendix/filter-commons/condition-utils"; import { FilterData, FiltersSettingsMap, PlainJs, Serializable } from "@mendix/filter-commons/typings/settings"; import { FilterCondition } from "mendix/filters"; import { action, autorun, makeAutoObservable } from "mobx"; +import { ConditionWithMeta } from "../../typings/ConditionWithMeta"; import { Filter, ObservableFilterHost } from "../../typings/ObservableFilterHost"; -import { ConditionWithMeta } from "./CombinedFilter"; export class CustomFilterHost implements ObservableFilterHost, Serializable { private filters: Map void]> = new Map(); private settingsBuffer: FiltersSettingsMap = new Map(); private _state: Map = new Map(); - readonly metaKey = "custom-filter-host"; + readonly metaKey = "CustomFilterHost"; constructor() { makeAutoObservable(this, { @@ -37,8 +37,9 @@ export class CustomFilterHost implements ObservableFilterHost, Serializable { this._state.set(key, filter.condition); }); const skipInit = this.settingsBuffer.has(key); - if (!skipInit && this._state.has(key)) { - filter.fromViewState(this._state.get(key)!); + const initCond = this._state.get(key); + if (!skipInit && initCond) { + filter.fromViewState(initCond); } const dispose = (): void => { diff --git a/packages/shared/widget-plugin-filtering/src/typings/ConditionWithMeta.ts b/packages/shared/widget-plugin-filtering/src/typings/ConditionWithMeta.ts new file mode 100644 index 0000000000..b4529861e9 --- /dev/null +++ b/packages/shared/widget-plugin-filtering/src/typings/ConditionWithMeta.ts @@ -0,0 +1,6 @@ +import { FilterCondition } from "mendix/filters"; + +export type ConditionWithMeta = { + cond: FilterCondition | undefined; + meta: string; +}; From 74a4cff39ac8a68bb85381c6e7587f72e2ef5715 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Wed, 6 Aug 2025 12:09:15 +0200 Subject: [PATCH 10/12] feat: convert gallery and fix tests --- .../__tests__/DatagridDateFilter.spec.tsx | 12 ++--- .../__tests__/DatagridNumberFilter.spec.tsx | 3 +- .../__tests__/DatagridTextFilter.spec.tsx | 3 +- .../src/controllers/QueryParamsController.ts | 49 +++++++------------ .../gallery-web/src/stores/GalleryStore.ts | 10 ++-- 5 files changed, 32 insertions(+), 45 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/src/components/__tests__/DatagridDateFilter.spec.tsx b/packages/pluggableWidgets/datagrid-date-filter-web/src/components/__tests__/DatagridDateFilter.spec.tsx index e50e1ec773..1a1cda173d 100644 --- a/packages/pluggableWidgets/datagrid-date-filter-web/src/components/__tests__/DatagridDateFilter.spec.tsx +++ b/packages/pluggableWidgets/datagrid-date-filter-web/src/components/__tests__/DatagridDateFilter.spec.tsx @@ -73,8 +73,7 @@ describe("Date Filter", () => { hasError: false, value: { type: "direct", store: dateFilterStore } }, - filterObserver: {} as ObservableFilterHost, - sharedInitFilter: [] + filterObserver: {} as ObservableFilterHost }; (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext( @@ -168,8 +167,7 @@ describe("Date Filter", () => { hasError: false, value: { type: "direct", store: dateFilterStore } }, - filterObserver: {} as ObservableFilterHost, - sharedInitFilter: [] + filterObserver: {} as ObservableFilterHost }; (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext( @@ -223,8 +221,7 @@ describe("Date Filter", () => { hasError: false, value: { type: "direct", store: dateFilterStore } }, - filterObserver: {} as ObservableFilterHost, - sharedInitFilter: [] + filterObserver: {} as ObservableFilterHost }; (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext( @@ -264,8 +261,7 @@ describe("Date Filter", () => { hasError: false, value: { type: "direct", store: dateFilterStore } }, - filterObserver: {} as ObservableFilterHost, - sharedInitFilter: [] + filterObserver: {} as ObservableFilterHost }; (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext( diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx b/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx index fcf8c5dd6c..39285f4101 100644 --- a/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx +++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx @@ -53,8 +53,7 @@ const setContext = (store: NumberInputFilterStore) => { hasError: false, value: { type: "direct", store } }, - filterObserver: {} as ObservableFilterHost, - sharedInitFilter: [] + filterObserver: {} as ObservableFilterHost }; (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(filterAPI); }; diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx b/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx index 620873af79..73cd710faf 100644 --- a/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx +++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx @@ -50,8 +50,7 @@ const setContext = (store: StringInputFilterStore) => { hasError: false, value: { type: "direct", store } }, - filterObserver: {} as ObservableFilterHost, - sharedInitFilter: [] + filterObserver: {} as ObservableFilterHost }; (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(filterAPI); }; diff --git a/packages/pluggableWidgets/gallery-web/src/controllers/QueryParamsController.ts b/packages/pluggableWidgets/gallery-web/src/controllers/QueryParamsController.ts index 84c22584a6..5e8f3e914a 100644 --- a/packages/pluggableWidgets/gallery-web/src/controllers/QueryParamsController.ts +++ b/packages/pluggableWidgets/gallery-web/src/controllers/QueryParamsController.ts @@ -1,53 +1,49 @@ -import { compactArray, fromCompactArray, isAnd } from "@mendix/filter-commons/condition-utils"; -import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost"; import { DatasourceController } from "@mendix/widget-plugin-grid/query/DatasourceController"; import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller"; -import { SortStoreHost } from "@mendix/widget-plugin-sorting/stores/SortStoreHost"; -import { ListValue } from "mendix"; +import { SortInstruction } from "@mendix/widget-plugin-sorting/types/store"; + import { FilterCondition } from "mendix/filters"; -import { makeAutoObservable, reaction } from "mobx"; +import { reaction } from "mobx"; + +interface ObservableFilterStore { + filter: FilterCondition | undefined; +} + +interface ObservableSortStore { + sortOrder: SortInstruction[] | undefined; +} export class QueryParamsController implements ReactiveController { private readonly _query: DatasourceController; - private readonly _filters: CustomFilterHost; - private readonly _sort: SortStoreHost; + private readonly _filtersHost: ObservableFilterStore; + private readonly _sortHost: ObservableSortStore; constructor( host: ReactiveControllerHost, query: DatasourceController, - filters: CustomFilterHost, - sort: SortStoreHost + filters: ObservableFilterStore, + sort: ObservableSortStore ) { host.addController(this); this._query = query; - this._filters = filters; - this._sort = sort; - - makeAutoObservable(this, { setup: false }); - } - - private get _derivedSortOrder(): ListValue["sortOrder"] { - return this._sort.sortOrder; - } - - private get _derivedFilter(): FilterCondition { - return compactArray(this._filters.conditions); + this._filtersHost = filters; + this._sortHost = sort; } setup(): () => void { const [add, disposeAll] = disposeBatch(); add( reaction( - () => this._derivedSortOrder, + () => this._sortHost.sortOrder, sortOrder => this._query.setSortOrder(sortOrder), { fireImmediately: true } ) ); add( reaction( - () => this._derivedFilter, + () => this._filtersHost.filter, filter => this._query.setFilter(filter), { fireImmediately: true } ) @@ -55,11 +51,4 @@ export class QueryParamsController implements ReactiveController { return disposeAll; } - - unzipFilter(filter?: FilterCondition): Array { - if (!filter || !isAnd(filter)) { - return []; - } - return fromCompactArray(filter); - } } diff --git a/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts index 209d7dd01b..59417e1e73 100644 --- a/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts +++ b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts @@ -1,4 +1,5 @@ import { createContextWithStub, FilterAPI } from "@mendix/widget-plugin-filtering/context"; +import { CombinedFilter } from "@mendix/widget-plugin-filtering/stores/generic/CombinedFilter"; import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost"; import { DatasourceController } from "@mendix/widget-plugin-grid/query/DatasourceController"; import { PaginationController } from "@mendix/widget-plugin-grid/query/PaginationController"; @@ -67,16 +68,17 @@ export class GalleryStore extends BaseControllerHost { this._filtersHost = new CustomFilterHost(); + const combinedFilter = new CombinedFilter(this, { stableKey: spec.name, inputs: [this._filtersHost] }); + this._sortHost = new SortStoreHost({ initSort: spec.gate.props.datasource.sortOrder }); - const paramCtrl = new QueryParamsController(this, this._query, this._filtersHost, this._sortHost); + new QueryParamsController(this, this._query, combinedFilter, this._sortHost); this.filterAPI = createContextWithStub({ filterObserver: this._filtersHost, - parentChannelName: this.id, - sharedInitFilter: paramCtrl.unzipFilter(spec.gate.props.datasource.filter) + parentChannelName: this.id }); this.sortAPI = { @@ -93,6 +95,8 @@ export class GalleryStore extends BaseControllerHost { if (useStorage) { this.initPersistentStorage(spec, spec.gate); } + + combinedFilter.hydrate(spec.gate.props.datasource.filter); } initPersistentStorage(props: StaticProps, gate: GalleryPropsGate): void { From 5187bf0585dee4e9ab7bba2112fb803bcef785b1 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Wed, 6 Aug 2025 12:12:23 +0200 Subject: [PATCH 11/12] chore: remove unused code --- .../src/__tests__/condition-utils.spec.ts | 53 +------------------ .../filter-commons/src/condition-utils.ts | 52 ------------------ 2 files changed, 1 insertion(+), 104 deletions(-) diff --git a/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts b/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts index 01e63d953e..70e06443ca 100644 --- a/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts +++ b/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts @@ -2,60 +2,9 @@ jest.mock("mendix/filters/builders"); import { AndCondition, FilterCondition } from "mendix/filters"; import { equals, literal } from "mendix/filters/builders"; -import { - compactArray, - fromCompactArray, - reduceArray, - reduceMap, - restoreArray, - restoreMap, - tag -} from "../condition-utils"; +import { reduceArray, reduceMap, restoreArray, restoreMap, tag } from "../condition-utils"; describe("condition-utils", () => { - describe("compactArray", () => { - it("returns 'tag' condition for zero array", () => { - const result = compactArray([]); - expect(result).toMatchObject({ - name: "!=", - type: "function", - arg1: { value: "[0,[]]", valueType: "String" } - }); - }); - - it("returns 'tag' condition for array of undefined", () => { - const result = compactArray([undefined, undefined, undefined]); - expect(result).toMatchObject({ - name: "!=", - type: "function", - arg1: { value: "[3,[]]", valueType: "String" } - }); - }); - - it("returns 'and' condition with 3 args", () => { - const result = compactArray([tag("0"), undefined, tag("2")]); - expect(result).toMatchObject({ name: "and", type: "function" }); - expect((result as AndCondition).args).toHaveLength(3); - }); - }); - - describe("fromCompactArray", () => { - it("unpack condition created with compactArray", () => { - const input = [ - equals(literal("1"), literal("1")), - undefined, - equals(literal("foo"), literal("bar")), - undefined, - undefined, - equals(literal(new Date("2024-09-17T14:00:00.000Z")), literal(new Date("2024-09-17T14:00:00.000Z"))) - ]; - const cond = compactArray(input); - expect(cond).toBeDefined(); - const result = fromCompactArray(cond!); - expect(result).toEqual(input); - }); - }); - describe("reduceMap", () => { it("returns undefined condition and correct metadata for map with undefined values", () => { const input = { x: undefined, y: undefined }; diff --git a/packages/shared/filter-commons/src/condition-utils.ts b/packages/shared/filter-commons/src/condition-utils.ts index a333cb19a2..107b10012f 100644 --- a/packages/shared/filter-commons/src/condition-utils.ts +++ b/packages/shared/filter-commons/src/condition-utils.ts @@ -73,39 +73,10 @@ export function isTag(cond: FilterCondition): cond is TagCond { type ArrayMeta = readonly [len: number, indexes: number[]]; -function arrayTag(meta: ArrayMeta): string { - return JSON.stringify(meta); -} - -function fromArrayTag(tag: string): ArrayMeta | undefined { - let len: ArrayMeta[0]; - let indexes: ArrayMeta[1]; - try { - [len, indexes] = JSON.parse(tag); - } catch { - return undefined; - } - if (typeof len !== "number" || !Array.isArray(indexes) || !indexes.every(x => typeof x === "number")) { - return undefined; - } - return [len, indexes]; -} - function shrink(array: Array): [indexes: number[], items: T[]] { return [array.flatMap((x, i) => (x === undefined ? [] : [i])), array.filter((x): x is T => x !== undefined)]; } -export function compactArray(input: Array): FilterCondition { - const [indexes, items] = shrink(input); - const metaTag = tag(arrayTag([input.length, indexes] as const)); - - if (items.length === 0) { - return metaTag; - } - - return and(metaTag, ...items); -} - export function reduceArray( input: Array ): [cond: FilterCondition | undefined, meta: string] { @@ -203,29 +174,6 @@ export function restoreMap( return result; } -export function fromCompactArray(cond: FilterCondition): Array { - const tag = isAnd(cond) ? cond.args[0] : cond; - - const arrayMeta = isTag(tag) ? fromArrayTag(tag.arg1.value) : undefined; - - if (!arrayMeta) { - return []; - } - - const [length, indexes] = arrayMeta; - const arr: Array = Array(length).fill(undefined); - - if (!isAnd(cond)) { - return arr; - } - - cond.args.slice(1).forEach((cond, i) => { - arr[indexes[i]] = cond; - }); - - return arr; -} - export function inputStateFromCond( cond: FilterCondition, fn: (func: FilterFunction | "between" | "empty" | "notEmpty") => Fn, From b1001120f84d06ecbd2a946e5b5be96c3b59a8e7 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Fri, 15 Aug 2025 10:18:29 +0200 Subject: [PATCH 12/12] chore: add changelog record --- packages/pluggableWidgets/datagrid-web/CHANGELOG.md | 4 ++++ packages/pluggableWidgets/datagrid-web/package.json | 2 +- packages/pluggableWidgets/datagrid-web/src/package.xml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index 59cfbca641..e9714e3619 100644 --- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Changed + +- We removed all metadata stored in xpath to improve integration with other services. + ## [3.0.1] - 2025-08-05 ### Fixed diff --git a/packages/pluggableWidgets/datagrid-web/package.json b/packages/pluggableWidgets/datagrid-web/package.json index 8dd955fc0d..6a48b6b060 100644 --- a/packages/pluggableWidgets/datagrid-web/package.json +++ b/packages/pluggableWidgets/datagrid-web/package.json @@ -1,7 +1,7 @@ { "name": "@mendix/datagrid-web", "widgetName": "Datagrid", - "version": "3.0.1", + "version": "3.0.2", "description": "", "copyright": "© Mendix Technology BV 2025. All rights reserved.", "license": "Apache-2.0", diff --git a/packages/pluggableWidgets/datagrid-web/src/package.xml b/packages/pluggableWidgets/datagrid-web/src/package.xml index b224969655..106bea1dc1 100644 --- a/packages/pluggableWidgets/datagrid-web/src/package.xml +++ b/packages/pluggableWidgets/datagrid-web/src/package.xml @@ -1,6 +1,6 @@ - +