;
+ }>;
+ name: string;
+}
+
+interface StoreProvider extends ISetupable {
+ store: PickerFilterStore;
+}
+
+type Component = (props: P) => React.ReactElement;
+
+export function withDropdownLinkedAttributes
(
+ component: Component
+): Component
{
+ const StoreInjector = withInjectedStore(component);
+
+ return function FilterAPIProvider(props) {
+ const api = useStoreProvider(props);
+
+ if (api.hasError) {
+ return {api.error.message};
+ }
+
+ return ;
+ };
+}
+
+function withInjectedStore
(
+ Component: Component
+): Component
{
+ return function StoreInjector(props) {
+ const provider = useSetup(() => props.provider);
+ return ;
+ };
+}
+
+interface InjectableFilterAPI {
+ filterStore: PickerFilterStore;
+ parentChannelName?: string;
+}
+
+function useStoreProvider(props: RequiredProps): Result<{ provider: StoreProvider; channel: string }, APIError> {
+ const filterAPI = useFilterAPI();
+ return useConst(() => {
+ if (filterAPI.hasError) {
+ return error(filterAPI.error);
+ }
+
+ return value({
+ provider: new DropdownStoreProvider(filterAPI.value, {
+ attributes: props.attributes.map(obj => obj.attribute),
+ dataKey: props.name
+ }),
+ channel: filterAPI.value.parentChannelName
+ });
+ });
+}
diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/typings/DatagridDropdownFilterProps.d.ts b/packages/pluggableWidgets/datagrid-dropdown-filter-web/typings/DatagridDropdownFilterProps.d.ts
index c1c4cd5e41..a824e69a21 100644
--- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/typings/DatagridDropdownFilterProps.d.ts
+++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/typings/DatagridDropdownFilterProps.d.ts
@@ -4,7 +4,11 @@
* @author Mendix Widgets Framework Team
*/
import { CSSProperties } from "react";
-import { ActionValue, DynamicValue, EditableValue } from "mendix";
+import { ActionValue, DynamicValue, EditableValue, ListValue, ListAttributeValue, ListReferenceValue, ListReferenceSetValue } from "mendix";
+
+export type BaseTypeEnum = "attr" | "ref";
+
+export type AttrChoiceEnum = "auto" | "linked";
export interface FilterOptionsType {
caption: DynamicValue;
@@ -25,9 +29,15 @@ export interface DatagridDropdownFilterContainerProps {
class: string;
style?: CSSProperties;
tabIndex?: number;
+ baseType: BaseTypeEnum;
+ attrChoice: AttrChoiceEnum;
+ attr: ListAttributeValue;
auto: boolean;
- defaultValue?: DynamicValue;
filterOptions: FilterOptionsType[];
+ ref?: ListReferenceValue | ListReferenceSetValue;
+ refOptions?: ListValue;
+ fetchOptionsLazy: boolean;
+ defaultValue?: DynamicValue;
filterable: boolean;
multiSelect: boolean;
emptyOptionCaption?: DynamicValue;
@@ -50,9 +60,15 @@ export interface DatagridDropdownFilterPreviewProps {
readOnly: boolean;
renderMode: "design" | "xray" | "structure";
translate: (text: string) => string;
+ baseType: BaseTypeEnum;
+ attrChoice: AttrChoiceEnum;
+ attr: string;
auto: boolean;
- defaultValue: string;
filterOptions: FilterOptionsPreviewType[];
+ ref: string;
+ refOptions: {} | { caption: string } | { type: string } | null;
+ fetchOptionsLazy: boolean;
+ defaultValue: string;
filterable: boolean;
multiSelect: boolean;
emptyOptionCaption: string;
diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/DropdownStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/DropdownStoreProvider.ts
new file mode 100644
index 0000000000..abeef3dd15
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/DropdownStoreProvider.ts
@@ -0,0 +1,42 @@
+import { ListAttributeValue } from "mendix";
+import { FilterAPI } from "../context";
+import { StaticSelectFilterStore } from "../stores/picker/StaticSelectFilterStore";
+import { PickerFilterStore } from "../typings/PickerFilterStore";
+import { BaseStoreProvider } from "./BaseStoreProvider";
+import { FilterSpec } from "./typings";
+
+export class DropdownStoreProvider extends BaseStoreProvider {
+ protected _store: StaticSelectFilterStore;
+ protected filterAPI: FilterAPI;
+ readonly dataKey: string;
+
+ constructor(filterAPI: FilterAPI, spec: FilterSpec) {
+ super();
+ this.filterAPI = filterAPI;
+ this.dataKey = spec.dataKey;
+
+ // Convert AttributeMetaData to ListAttributeValue
+ const attributes = spec.attributes.map(attr => {
+ const defaultFormatter = {
+ format: (value: any) => String(value),
+ parse: (value: string) => ({ valid: true, value })
+ };
+
+ return {
+ ...attr,
+ isList: false,
+ get: (obj: any) => obj[attr.id],
+ formatter: defaultFormatter
+ } as ListAttributeValue;
+ });
+
+ this._store = new StaticSelectFilterStore(
+ attributes,
+ this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey)
+ );
+ }
+
+ get store(): PickerFilterStore {
+ return this._store;
+ }
+}
diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/index.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/index.ts
new file mode 100644
index 0000000000..45c14f0e4d
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/index.ts
@@ -0,0 +1,6 @@
+export * from "./BaseStoreProvider";
+export * from "./DateStoreProvider";
+export * from "./DropdownStoreProvider";
+export * from "./NumberStoreProvider";
+export * from "./StringStoreProvider";
+export * from "./typings";
diff --git a/packages/shared/widget-plugin-filtering/src/index.ts b/packages/shared/widget-plugin-filtering/src/index.ts
new file mode 100644
index 0000000000..e01bd3faca
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/index.ts
@@ -0,0 +1 @@
+export * from "./custom-filter-api/DropdownStoreProvider";
diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/DropdownFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/picker/DropdownFilterStore.ts
new file mode 100644
index 0000000000..977d2c8876
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/stores/picker/DropdownFilterStore.ts
@@ -0,0 +1,138 @@
+import { ListAttributeValue } from "mendix";
+import { FilterCondition, LiteralExpression } from "mendix/filters";
+import { attribute, equals, literal, or } from "mendix/filters/builders";
+import { action, computed, makeObservable, observable } from "mobx";
+import { selectedFromCond } from "../../condition-utils";
+import { disposeFx } from "../../mobx-utils";
+import { OptionWithState } from "../../typings/OptionWithState";
+import { BaseSelectStore } from "./BaseSelectStore";
+import { SearchStore } from "./SearchStore";
+
+export class DropdownFilterStore extends BaseSelectStore {
+ readonly storeType = "select";
+ _attributes: ListAttributeValue[] = [];
+ _customOptions: Array<{ caption: string; value: string }> = [];
+ search: SearchStore;
+
+ constructor(attributes: ListAttributeValue[], initCond: FilterCondition | null) {
+ super();
+ this.search = new SearchStore();
+ this._attributes = attributes;
+
+ makeObservable(this, {
+ _attributes: observable.struct,
+ _customOptions: observable.struct,
+ allOptions: computed,
+ options: computed,
+ selectedOptions: computed,
+ universe: computed,
+ condition: computed,
+ setCustomOptions: action,
+ setDefaultSelected: action,
+ updateProps: action,
+ fromViewState: action
+ });
+
+ if (initCond) {
+ this.fromViewState(initCond);
+ }
+ }
+
+ get allOptions(): OptionWithState[] {
+ const selected = this.selected;
+
+ if (this._customOptions.length > 0) {
+ return this._customOptions.map(opt => ({ ...opt, selected: selected.has(opt.value) }));
+ }
+
+ const options = this._attributes.flatMap(attr =>
+ Array.from(attr.universe ?? [], value => {
+ const stringValue = `${value}`;
+ return {
+ caption: attr.formatter.format(value),
+ value: stringValue,
+ selected: selected.has(stringValue)
+ };
+ })
+ );
+
+ return options;
+ }
+
+ get options(): OptionWithState[] {
+ if (!this.search.value) {
+ return this.allOptions;
+ }
+
+ return this.allOptions.filter(opt => opt.caption.toLowerCase().includes(this.search.value.toLowerCase()));
+ }
+
+ get selectedOptions(): OptionWithState[] {
+ return [...this.selected].flatMap(value => {
+ const option = this.allOptions.find(opt => opt.value === value);
+ return option ? [option] : [];
+ });
+ }
+
+ get universe(): Set {
+ return new Set(this._attributes.flatMap(attr => Array.from(attr.universe ?? [], value => `${value}`)));
+ }
+
+ get condition(): FilterCondition | undefined {
+ const selected = this.selected;
+ if (selected.size === 0) {
+ return undefined;
+ }
+
+ const conditions = this._attributes.flatMap(attr => {
+ const values = [...selected]
+ .map(value => attr.formatter.parse(value))
+ .filter(result => result.valid)
+ .map(result => (result as { valid: true; value: any }).value);
+
+ return values.length > 0 ? [or(...values.map(value => equals(attribute(attr.id), literal(value))))] : [];
+ });
+
+ return conditions.length > 1 ? or(...conditions) : conditions[0];
+ }
+
+ setup(): () => void {
+ const [disposers, dispose] = disposeFx();
+ disposers.push(this.search.setup());
+ return dispose;
+ }
+
+ setCustomOptions(options: Array<{ caption: string; value: string }>): void {
+ this._customOptions = options;
+ }
+
+ setDefaultSelected(defaultSelected?: Iterable): void {
+ if (!this.blockSetDefaults && defaultSelected) {
+ this.defaultSelected = defaultSelected;
+ this.setSelected(defaultSelected);
+ this.blockSetDefaults = true;
+ }
+ }
+
+ updateProps(attributes: ListAttributeValue[]): void {
+ this._attributes = attributes;
+ }
+
+ isValidValue(value: string): boolean {
+ return this.universe.has(value);
+ }
+
+ fromViewState(cond: FilterCondition): void {
+ const val = (exp: LiteralExpression): string | undefined =>
+ exp.valueType === "string" ? exp.value : exp.valueType === "boolean" ? String(exp.value) : undefined;
+
+ const selected = selectedFromCond(cond, val);
+
+ if (selected.length < 1) {
+ return;
+ }
+
+ this.setSelected(selected);
+ this.blockSetDefaults = true;
+ }
+}
From 0f4a5503c8179456d66409df90cf715373dd271a Mon Sep 17 00:00:00 2001
From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com>
Date: Fri, 18 Apr 2025 10:07:35 +0200
Subject: [PATCH 02/29] chore: split code into packages
---
.../shared/filter-commons/.prettierrc.cjs | 1 +
.../shared/filter-commons/eslint.config.mjs | 3 +
packages/shared/filter-commons/package.json | 46 ++++
.../filter-commons/src/condition-utils.ts | 224 ++++++++++++++++
.../src/typings/FilterFunctions.ts | 9 +
.../src/typings/IJSActionsControlled.ts | 11 +
.../filter-commons/src/typings/mendix.ts | 3 +
.../filter-commons/src/typings/settings.ts | 9 +
packages/shared/filter-commons/tsconfig.json | 9 +
.../.prettierrc.cjs | 1 +
.../eslint.config.mjs | 3 +
.../jest.config.cjs | 26 ++
.../package.json | 40 +++
.../src/controllers/PickerBaseController.ts | 62 +++++
.../src/controllers/PickerChangeHelper.ts | 35 +++
.../src/controllers/PickerJSActionsHelper.ts | 41 +++
.../src/controllers/RefBaseController.ts | 40 +++
.../src/controllers/RefSelectController.ts | 18 ++
.../src/controllers/SelectControllerMixin.ts | 102 ++++++++
.../src/controllers/StaticBaseController.ts | 65 +++++
.../src/controllers/StaticSelectController.ts | 10 +
.../controllers/TagPickerControllerMixin.ts | 172 ++++++++++++
.../src/stores/BaseSelectStore.ts | 71 +++++
.../src/stores/OptionsSerializer.ts | 42 +++
.../src/stores/RefFilterStore.ts | 245 ++++++++++++++++++
.../src/stores/SearchStore.ts | 46 ++++
.../src/stores/StaticSelectFilterStore.ts | 179 +++++++++++++
.../src/typings/IJSActionsControlled.ts | 11 +
.../src/typings/OptionWithState.ts | 5 +
.../src/typings/PickerFilterStore.ts | 4 +
.../src/typings/type-utils.ts | 1 +
.../tsconfig.json | 9 +
32 files changed, 1543 insertions(+)
create mode 100644 packages/shared/filter-commons/.prettierrc.cjs
create mode 100644 packages/shared/filter-commons/eslint.config.mjs
create mode 100644 packages/shared/filter-commons/package.json
create mode 100644 packages/shared/filter-commons/src/condition-utils.ts
create mode 100644 packages/shared/filter-commons/src/typings/FilterFunctions.ts
create mode 100644 packages/shared/filter-commons/src/typings/IJSActionsControlled.ts
create mode 100644 packages/shared/filter-commons/src/typings/mendix.ts
create mode 100644 packages/shared/filter-commons/src/typings/settings.ts
create mode 100644 packages/shared/filter-commons/tsconfig.json
create mode 100644 packages/shared/widget-plugin-dropdown-filter/.prettierrc.cjs
create mode 100644 packages/shared/widget-plugin-dropdown-filter/eslint.config.mjs
create mode 100644 packages/shared/widget-plugin-dropdown-filter/jest.config.cjs
create mode 100644 packages/shared/widget-plugin-dropdown-filter/package.json
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerBaseController.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerJSActionsHelper.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/controllers/RefBaseController.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/controllers/RefSelectController.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/controllers/SelectControllerMixin.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticBaseController.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticSelectController.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/controllers/TagPickerControllerMixin.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/stores/BaseSelectStore.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/stores/OptionsSerializer.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/stores/RefFilterStore.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/stores/SearchStore.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/stores/StaticSelectFilterStore.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/typings/IJSActionsControlled.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/typings/OptionWithState.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/typings/PickerFilterStore.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/src/typings/type-utils.ts
create mode 100644 packages/shared/widget-plugin-dropdown-filter/tsconfig.json
diff --git a/packages/shared/filter-commons/.prettierrc.cjs b/packages/shared/filter-commons/.prettierrc.cjs
new file mode 100644
index 0000000000..0892704ab0
--- /dev/null
+++ b/packages/shared/filter-commons/.prettierrc.cjs
@@ -0,0 +1 @@
+module.exports = require("@mendix/prettier-config-web-widgets");
diff --git a/packages/shared/filter-commons/eslint.config.mjs b/packages/shared/filter-commons/eslint.config.mjs
new file mode 100644
index 0000000000..ed68ae9e78
--- /dev/null
+++ b/packages/shared/filter-commons/eslint.config.mjs
@@ -0,0 +1,3 @@
+import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs";
+
+export default config;
diff --git a/packages/shared/filter-commons/package.json b/packages/shared/filter-commons/package.json
new file mode 100644
index 0000000000..4efaf21ebf
--- /dev/null
+++ b/packages/shared/filter-commons/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@mendix/filter-commons",
+ "version": "0.1.0",
+ "description": "Common filter utilities and types for filter widgets",
+ "copyright": "© Mendix Technology BV 2025. All rights reserved.",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/mendix/web-widgets.git"
+ },
+ "type": "module",
+ "files": [
+ "dist",
+ "!*.map"
+ ],
+ "exports": {
+ "./*": "./dist/*.js"
+ },
+ "typesVersions": {
+ "*": {
+ "*": [
+ "dist/*"
+ ]
+ }
+ },
+ "scripts": {
+ "build": "tsc",
+ "dev": "tsc --watch",
+ "format": "prettier --write .",
+ "lint": "eslint src/ package.json",
+ "prepare": "tsc"
+ },
+ "dependencies": {
+ "mendix": "^10.16.49747"
+ },
+ "peerDependencies": {
+ "mobx": "6.12.3",
+ "mobx-react-lite": "4.0.7"
+ },
+ "devDependencies": {
+ "@mendix/eslint-config-web-widgets": "workspace:*",
+ "@mendix/prettier-config-web-widgets": "workspace:*",
+ "@mendix/tsconfig-web-widgets": "workspace:*",
+ "@swc/core": "^1.7.26"
+ }
+}
diff --git a/packages/shared/filter-commons/src/condition-utils.ts b/packages/shared/filter-commons/src/condition-utils.ts
new file mode 100644
index 0000000000..976da416b5
--- /dev/null
+++ b/packages/shared/filter-commons/src/condition-utils.ts
@@ -0,0 +1,224 @@
+import {
+ AndCondition,
+ ContainsCondition,
+ EqualsCondition,
+ FilterCondition,
+ LiteralExpression,
+ OrCondition
+} from "mendix/filters";
+import { and, literal, notEqual } from "mendix/filters/builders";
+
+type BinaryExpression = T extends { arg1: unknown; arg2: object } ? T : never;
+type Func = T extends { name: infer Fn } ? Fn : never;
+type FilterFunction = Func;
+
+const hasOwn = (o: object, k: PropertyKey): boolean => Object.hasOwn(o, k);
+
+export function isBinary(cond: FilterCondition): cond is BinaryExpression {
+ return hasOwn(cond, "arg1") && hasOwn(cond, "arg2");
+}
+
+export function isAnd(exp: FilterCondition): exp is AndCondition {
+ return exp.type === "function" && exp.name === "and";
+}
+
+export function isOr(exp: FilterCondition): exp is OrCondition {
+ return exp.type === "function" && exp.name === "or";
+}
+
+export function isEmptyExp(exp: FilterCondition): boolean {
+ return isBinary(exp) && exp.arg2.type === "literal" && exp.name === "=" && exp.arg2.valueType === "undefined";
+}
+
+export function isNotEmptyExp(exp: FilterCondition): boolean {
+ return isBinary(exp) && exp.arg2.type === "literal" && exp.name === "!=" && exp.arg2.valueType === "undefined";
+}
+
+interface TagName {
+ readonly type: "literal";
+ readonly value: string;
+ readonly valueType: "string";
+}
+
+const MARKER = "#";
+
+interface TagMarker {
+ readonly type: "literal";
+ readonly value: typeof MARKER;
+ readonly valueType: "string";
+}
+
+interface TagCond {
+ readonly type: "function";
+ readonly name: "!=";
+ readonly arg1: TagName;
+ readonly arg2: TagMarker;
+}
+
+export function tag(name: string): TagCond {
+ return notEqual(literal(name), literal(MARKER)) as TagCond;
+}
+
+export function isTag(cond: FilterCondition): cond is TagCond {
+ return (
+ cond.name === "!=" &&
+ cond.arg1.type === "literal" &&
+ cond.arg2.type === "literal" &&
+ /string/i.test(cond.arg1.valueType) &&
+ /string/i.test(cond.arg2.valueType) &&
+ cond.arg2.value === MARKER
+ );
+}
+
+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 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,
+ val: (exp: LiteralExpression) => V
+): null | [Fn, V] | [Fn, V, V] {
+ // Or - condition build for multiple attrs, get state from the first one.
+ if (isOr(cond)) {
+ return inputStateFromCond(cond.args[0], fn, val);
+ }
+
+ // Between
+ if (isAnd(cond)) {
+ return betweenToState(cond, fn, val);
+ }
+
+ return singularToState(cond, fn, val);
+}
+
+export function betweenToState(
+ cond: AndCondition,
+ fn: (func: "between") => Fn,
+ val: (exp: LiteralExpression) => V
+): null | [Fn, V, V] {
+ const [exp1, exp2] = cond.args;
+ const [v1, v2] = [expValue(exp1, val), expValue(exp2, val)];
+ if (v1 && v2) {
+ return [fn("between"), v1, v2];
+ }
+ return null;
+}
+
+export function singularToState(
+ cond: FilterCondition,
+ fn: (func: FilterFunction | "between" | "empty" | "notEmpty") => Fn,
+ val: (exp: LiteralExpression) => V
+): null | [Fn, V] {
+ const value = expValue(cond, val);
+ if (value === null) {
+ return null;
+ }
+
+ if (isEmptyExp(cond)) {
+ return [fn("empty"), value];
+ }
+ if (isNotEmptyExp(cond)) {
+ return [fn("notEmpty"), value];
+ }
+
+ return [fn(cond.name), value];
+}
+
+export function expValue(exp: FilterCondition, val: (exp: LiteralExpression) => V): null | V {
+ if (!isBinary(exp)) {
+ return null;
+ }
+ if (exp.arg2.type !== "literal") {
+ return null;
+ }
+ return val(exp.arg2);
+}
+
+export function selectedFromCond(
+ cond: FilterCondition,
+ val: (exp: LiteralExpression) => V | undefined
+): V[] {
+ const reduce = (acc: V[], cond: FilterCondition): V[] => {
+ if (cond.name === "or") {
+ return cond.args.reduce(reduce, acc);
+ }
+
+ if (cond.name === "=" || cond.name === "contains") {
+ const item = expValue(cond, val);
+ if (item != null) {
+ acc.push(item);
+ }
+ }
+
+ return acc;
+ };
+
+ return [cond].reduce(reduce, []);
+}
+
+export function flattenRefCond(cond: FilterCondition): Array {
+ return [cond].flatMap(exp => {
+ if (exp.name === "or") {
+ return exp.args.flatMap(flattenRefCond);
+ }
+ if (exp.name === "=" || exp.name === "contains") {
+ return [exp];
+ }
+ return [];
+ });
+}
diff --git a/packages/shared/filter-commons/src/typings/FilterFunctions.ts b/packages/shared/filter-commons/src/typings/FilterFunctions.ts
new file mode 100644
index 0000000000..13cc4f228a
--- /dev/null
+++ b/packages/shared/filter-commons/src/typings/FilterFunctions.ts
@@ -0,0 +1,9 @@
+export type FilterFunctionNonValue = "empty" | "notEmpty";
+
+export type FilterFunctionGeneric = "equal" | "notEqual" | "greater" | "greaterEqual" | "smaller" | "smallerEqual";
+
+export type FilterFunctionBinary = "between"; // | "betweenEqRight" | "betweenEqLeft" | "betweenEqBoth";
+
+export type FilterFunctionString = "contains" | "startsWith" | "endsWith";
+
+export type AllFunctions = FilterFunctionNonValue | FilterFunctionGeneric | FilterFunctionBinary | FilterFunctionString;
diff --git a/packages/shared/filter-commons/src/typings/IJSActionsControlled.ts b/packages/shared/filter-commons/src/typings/IJSActionsControlled.ts
new file mode 100644
index 0000000000..50117d47ba
--- /dev/null
+++ b/packages/shared/filter-commons/src/typings/IJSActionsControlled.ts
@@ -0,0 +1,11 @@
+export interface IJSActionsControlled {
+ handleResetValue: ResetHandler;
+ handleSetValue: SetValueHandler;
+}
+
+export type ResetHandler = (useDefaultValue: boolean) => void;
+
+export type SetValueHandler = (
+ useDefaultValue: boolean,
+ params: { operators: any; stringValue: string; numberValue: Big.Big; dateTimeValue: Date; dateTimeValue2: Date }
+) => void;
diff --git a/packages/shared/filter-commons/src/typings/mendix.ts b/packages/shared/filter-commons/src/typings/mendix.ts
new file mode 100644
index 0000000000..528a614adf
--- /dev/null
+++ b/packages/shared/filter-commons/src/typings/mendix.ts
@@ -0,0 +1,3 @@
+import { FilterCondition } from "mendix/filters";
+
+export type FilterName = FilterCondition extends { name: infer Name } ? Name : never;
diff --git a/packages/shared/filter-commons/src/typings/settings.ts b/packages/shared/filter-commons/src/typings/settings.ts
new file mode 100644
index 0000000000..1b6cb7a373
--- /dev/null
+++ b/packages/shared/filter-commons/src/typings/settings.ts
@@ -0,0 +1,9 @@
+import { AllFunctions } from "./FilterFunctions";
+
+export type InputData = [Fn, string | null, string | null];
+
+export type SelectData = string[];
+
+export type FilterData = InputData | SelectData | null | undefined;
+
+export type FiltersSettingsMap = Map;
diff --git a/packages/shared/filter-commons/tsconfig.json b/packages/shared/filter-commons/tsconfig.json
new file mode 100644
index 0000000000..052cc1cee7
--- /dev/null
+++ b/packages/shared/filter-commons/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "@mendix/tsconfig-web-widgets/esm-library-with-jsx",
+ "include": ["./src/**/*"],
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "resolveJsonModule": true
+ }
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/.prettierrc.cjs b/packages/shared/widget-plugin-dropdown-filter/.prettierrc.cjs
new file mode 100644
index 0000000000..0892704ab0
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/.prettierrc.cjs
@@ -0,0 +1 @@
+module.exports = require("@mendix/prettier-config-web-widgets");
diff --git a/packages/shared/widget-plugin-dropdown-filter/eslint.config.mjs b/packages/shared/widget-plugin-dropdown-filter/eslint.config.mjs
new file mode 100644
index 0000000000..ed68ae9e78
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/eslint.config.mjs
@@ -0,0 +1,3 @@
+import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs";
+
+export default config;
diff --git a/packages/shared/widget-plugin-dropdown-filter/jest.config.cjs b/packages/shared/widget-plugin-dropdown-filter/jest.config.cjs
new file mode 100644
index 0000000000..3f605bedaa
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/jest.config.cjs
@@ -0,0 +1,26 @@
+module.exports = {
+ modulePathIgnorePatterns: ["/dist/"],
+ transform: {
+ "^.+\\.(t|j)sx?$": [
+ "@swc/jest",
+ {
+ jsc: {
+ transform: {
+ react: {
+ runtime: "automatic"
+ }
+ }
+ }
+ }
+ ]
+ },
+ moduleDirectories: ["node_modules", "src"],
+ moduleNameMapper: {
+ "big.js": "big.js",
+ "(.+)\\.js": "$1"
+ },
+ extensionsToTreatAsEsm: [".ts"],
+ testEnvironment: "jsdom",
+ collectCoverage: !process.env.CI,
+ coverageProvider: "v8"
+};
diff --git a/packages/shared/widget-plugin-dropdown-filter/package.json b/packages/shared/widget-plugin-dropdown-filter/package.json
new file mode 100644
index 0000000000..a211bbd560
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@mendix/widget-plugin-dropdown-filter",
+ "version": "1.0.0",
+ "description": "Drop-down filter widget plugin",
+ "copyright": "© Mendix Technology BV 2025. All rights reserved.",
+ "license": "Apache-2.0",
+ "type": "module",
+ "files": [
+ "dist",
+ "!*.map"
+ ],
+ "exports": {
+ "./*": "./dist/*.js"
+ },
+ "typesVersions": {
+ "*": {
+ "*": [
+ "dist/*"
+ ]
+ }
+ },
+ "scripts": {
+ "build": "tsc",
+ "dev": "tsc --watch",
+ "format": "prettier --write .",
+ "lint": "eslint src/ package.json",
+ "test": "jest"
+ },
+ "dependencies": {
+ "@mendix/widget-plugin-mobx-kit": "workspace:^",
+ "downshift": "^9.0.9",
+ "mobx": "6.12.3",
+ "mobx-react-lite": "4.0.7"
+ },
+ "devDependencies": {
+ "@mendix/eslint-config-web-widgets": "workspace:*",
+ "@mendix/prettier-config-web-widgets": "workspace:*",
+ "@mendix/tsconfig-web-widgets": "workspace:*"
+ }
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerBaseController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerBaseController.ts
new file mode 100644
index 0000000000..47e5cf5661
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerBaseController.ts
@@ -0,0 +1,62 @@
+import { ActionValue, EditableValue } from "mendix";
+import { OptionsSerializer } from "../stores/OptionsSerializer";
+import { IJSActionsControlled, ResetHandler, SetValueHandler } from "../typings/IJSActionsControlled";
+import { OptionWithState } from "../typings/OptionWithState";
+import { PickerChangeHelper } from "./PickerChangeHelper";
+import { PickerJSActionsHelper } from "./PickerJSActionsHelper";
+
+interface FilterStore {
+ reset: () => void;
+ clear: () => void;
+ setSelected: (value: Iterable) => void;
+ selected: Set;
+ options: OptionWithState[];
+}
+
+export class PickerBaseController implements IJSActionsControlled {
+ protected actionHelper: PickerJSActionsHelper;
+ protected changeHelper: PickerChangeHelper;
+ protected defaultValue?: Iterable;
+ protected serializer: OptionsSerializer;
+ filterStore: S;
+ multiselect: boolean;
+
+ constructor(props: PickerBaseControllerProps) {
+ this.filterStore = props.filterStore;
+ this.multiselect = props.multiselect;
+ this.serializer = new OptionsSerializer({ store: this.filterStore });
+ this.defaultValue = this.parseDefaultValue(props.defaultValue);
+ this.actionHelper = new PickerJSActionsHelper({
+ filterStore: props.filterStore,
+ parse: value => this.serializer.fromStorableValue(value) ?? [],
+ multiselect: props.multiselect
+ });
+ this.changeHelper = new PickerChangeHelper(props, () => this.serializer.value);
+ }
+
+ parseDefaultValue = (value: string | undefined): Iterable | undefined => {
+ const defaultValue = this.serializer.fromStorableValue(value);
+ if (!defaultValue) {
+ return undefined;
+ }
+ const arr = Array.from(defaultValue);
+ return this.multiselect ? arr : arr.slice(0, 1);
+ };
+
+ handleSetValue = (...args: Parameters): void => {
+ this.actionHelper.handleSetValue(...args);
+ };
+
+ handleResetValue = (...args: Parameters): void => {
+ this.actionHelper.handleResetValue(...args);
+ };
+}
+
+export interface PickerBaseControllerProps {
+ defaultValue?: string;
+ filterStore: S;
+ multiselect: boolean;
+ onChange?: ActionValue;
+ valueAttribute?: EditableValue;
+ emptyCaption?: string;
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts
new file mode 100644
index 0000000000..f4a51110f3
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts
@@ -0,0 +1,35 @@
+import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
+import { ActionValue, EditableValue } from "mendix";
+import { IReactionDisposer, reaction } from "mobx";
+
+interface Props {
+ valueAttribute?: EditableValue;
+ onChange?: ActionValue;
+}
+
+export class PickerChangeHelper {
+ private onChange?: ActionValue;
+ private valueAttribute?: EditableValue;
+ private valueFn: () => string | undefined;
+
+ constructor(props: Props, valueFn: () => string | undefined) {
+ this.onChange = props.onChange;
+ this.valueAttribute = props.valueAttribute;
+ this.valueFn = valueFn;
+ }
+
+ setup(): IReactionDisposer {
+ const effect = (value: string | undefined): void => {
+ this.valueAttribute?.setValue(value);
+
+ executeAction(this.onChange);
+ };
+
+ return reaction(this.valueFn, effect);
+ }
+
+ updateProps(props: Props): void {
+ this.onChange = props.onChange;
+ this.valueAttribute = props.valueAttribute;
+ }
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerJSActionsHelper.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerJSActionsHelper.ts
new file mode 100644
index 0000000000..c71b741cb7
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerJSActionsHelper.ts
@@ -0,0 +1,41 @@
+import { IJSActionsControlled, ResetHandler, SetValueHandler } from "../typings/IJSActionsControlled";
+
+interface FilterStore {
+ reset: () => void;
+ clear: () => void;
+ setSelected: (value: Iterable) => void;
+}
+
+type Parse = (value: string) => Iterable;
+
+export class PickerJSActionsHelper implements IJSActionsControlled {
+ private filterStore: FilterStore;
+ private parse: Parse;
+ private multiselect: boolean;
+
+ constructor({ filterStore, parse, multiselect }: { filterStore: FilterStore; parse: Parse; multiselect: boolean }) {
+ this.filterStore = filterStore;
+ this.parse = parse;
+ this.multiselect = multiselect;
+ }
+
+ handleResetValue: ResetHandler = (useDefaultValue): void => {
+ if (useDefaultValue) {
+ this.filterStore.reset();
+ return;
+ }
+ this.filterStore.clear();
+ };
+
+ handleSetValue: SetValueHandler = (useDefaultValue, params): void => {
+ if (useDefaultValue) {
+ this.filterStore.reset();
+ return;
+ }
+ let value = Array.from(this.parse(params.stringValue));
+ if (!this.multiselect) {
+ value = value.slice(0, 1);
+ }
+ this.filterStore.setSelected(value);
+ };
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefBaseController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefBaseController.ts
new file mode 100644
index 0000000000..44568ca35c
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefBaseController.ts
@@ -0,0 +1,40 @@
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { ActionValue, EditableValue } from "mendix";
+import { action, makeObservable } from "mobx";
+import { RefFilterStore } from "../stores/RefFilterStore";
+import { PickerBaseController } from "./PickerBaseController";
+
+export class RefBaseController extends PickerBaseController {
+ constructor(props: RefBaseControllerProps) {
+ super(props);
+ makeObservable(this, {
+ updateProps: action
+ });
+ }
+
+ setup(): () => void {
+ const [add, disposeAll] = disposeBatch();
+
+ add(this.changeHelper.setup());
+
+ if (this.defaultValue) {
+ this.filterStore.setDefaultSelected(this.defaultValue);
+ }
+
+ return disposeAll;
+ }
+
+ updateProps(props: RefBaseControllerProps): void {
+ this.changeHelper.updateProps(props);
+ }
+}
+
+export interface RefBaseControllerProps {
+ defaultValue?: string;
+ filterStore: RefFilterStore;
+ multiselect: boolean;
+ onChange?: ActionValue;
+ valueAttribute?: EditableValue;
+ emptyCaption?: string;
+ placeholder?: string;
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefSelectController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefSelectController.ts
new file mode 100644
index 0000000000..8156e6aa22
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefSelectController.ts
@@ -0,0 +1,18 @@
+import { RefBaseController, RefBaseControllerProps } from "./RefBaseController";
+import { SelectControllerMixin } from "./SelectControllerMixin";
+
+export class RefSelectController extends SelectControllerMixin(RefBaseController) {
+ constructor(props: RefBaseControllerProps) {
+ super(props);
+ this.emptyOption.caption = props.emptyCaption || "None";
+ this.placeholder = props.placeholder || "Search";
+ }
+
+ handleFocus = (): void => {
+ this.filterStore.setFetchReady(true);
+ };
+
+ handleMenuScrollEnd = (): void => {
+ this.filterStore.loadMore();
+ };
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/SelectControllerMixin.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/SelectControllerMixin.ts
new file mode 100644
index 0000000000..cfb49bb3f8
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/SelectControllerMixin.ts
@@ -0,0 +1,102 @@
+import { useSelect, UseSelectProps } from "downshift";
+import { action, computed, makeObservable } from "mobx";
+import { OptionWithState } from "../typings/OptionWithState";
+import { GConstructor } from "../typings/type-utils";
+
+export interface FilterStore {
+ toggle: (value: string) => void;
+ clear: () => void;
+ setSelected: (value: Iterable) => void;
+ selected: Set;
+ options: OptionWithState[];
+ selectedOptions: OptionWithState[];
+}
+
+type BaseController = GConstructor<{
+ filterStore: FilterStore;
+ multiselect: boolean;
+}>;
+
+const none = "[[__none__]]" as const;
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+export function SelectControllerMixin(Base: TBase) {
+ return class SelectControllerMixin extends Base {
+ placeholder = "Select";
+
+ readonly emptyOption = {
+ value: none,
+ caption: "None",
+ selected: false
+ };
+
+ constructor(...args: any[]) {
+ super(...args);
+ makeObservable(this, {
+ options: computed,
+ isEmpty: computed,
+ value: computed,
+ handleClear: action
+ });
+ }
+
+ get options(): OptionWithState[] {
+ return [this.emptyOption, ...this.filterStore.options];
+ }
+
+ get isEmpty(): boolean {
+ return this.filterStore.selected.size === 0;
+ }
+
+ get value(): string {
+ const selected = this.filterStore.selectedOptions;
+
+ if (selected.length < 1) {
+ return this.placeholder;
+ }
+
+ return selected.map(option => option.caption).join(", ");
+ }
+
+ handleClear = (): void => {
+ this.filterStore.clear();
+ };
+
+ useSelectProps = (): UseSelectProps => {
+ const props: UseSelectProps = {
+ items: this.options,
+ itemToKey: item => item?.value,
+ itemToString: item => item?.caption ?? "",
+ onSelectedItemChange: ({ selectedItem }) => {
+ if (!selectedItem) {
+ return;
+ }
+ if (selectedItem.value === none) {
+ this.filterStore.clear();
+ } else if (this.multiselect) {
+ this.filterStore.toggle(selectedItem.value);
+ } else {
+ this.filterStore.setSelected([selectedItem.value]);
+ }
+ }
+ };
+
+ if (this.multiselect) {
+ props.stateReducer = (state, { changes, type }) => {
+ switch (type) {
+ case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
+ case useSelect.stateChangeTypes.ItemClick:
+ return {
+ ...changes,
+ isOpen: true,
+ highlightedIndex: state.highlightedIndex
+ };
+ default:
+ return changes;
+ }
+ };
+ }
+ return props;
+ };
+ };
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticBaseController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticBaseController.ts
new file mode 100644
index 0000000000..55ec3ab492
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticBaseController.ts
@@ -0,0 +1,65 @@
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { ActionValue, DynamicValue, EditableValue } from "mendix";
+import { action, autorun, makeObservable, observable } from "mobx";
+import { StaticSelectFilterStore } from "../stores/StaticSelectFilterStore";
+import { PickerBaseController } from "./PickerBaseController";
+
+export class StaticBaseController extends PickerBaseController {
+ filterOptions: Array>>;
+
+ constructor(props: StaticBaseControllerProps) {
+ super(props);
+ this.filterOptions = props.filterOptions;
+ makeObservable(this, {
+ updateProps: action,
+ filterOptions: observable.struct
+ });
+ }
+
+ setup(): () => void {
+ const [addDisposer, dispose] = disposeBatch();
+
+ addDisposer(this.changeHelper.setup());
+
+ addDisposer(
+ autorun(() => {
+ if (this.filterOptions.length > 0) {
+ const options = this.filterOptions.map(this.toStoreOption);
+ this.filterStore.setCustomOptions(options);
+ }
+ })
+ );
+
+ if (this.defaultValue) {
+ this.filterStore.setDefaultSelected(this.defaultValue);
+ }
+
+ return dispose;
+ }
+
+ updateProps(props: StaticBaseControllerProps): void {
+ this.filterOptions = props.filterOptions;
+ this.changeHelper.updateProps(props);
+ }
+
+ toStoreOption = (opt: CustomOption>): CustomOption => ({
+ caption: `${opt.caption?.value}`,
+ value: `${opt.value?.value}`
+ });
+}
+
+export interface StaticBaseControllerProps {
+ defaultValue?: string;
+ filterOptions: Array>>;
+ filterStore: StaticSelectFilterStore;
+ multiselect: boolean;
+ onChange?: ActionValue;
+ valueAttribute?: EditableValue;
+ emptyCaption?: string;
+ placeholder?: string;
+}
+
+export interface CustomOption {
+ caption: T;
+ value: T;
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticSelectController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticSelectController.ts
new file mode 100644
index 0000000000..49088d8efd
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticSelectController.ts
@@ -0,0 +1,10 @@
+import { SelectControllerMixin } from "./SelectControllerMixin";
+import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController";
+
+export class StaticSelectController extends SelectControllerMixin(StaticBaseController) {
+ constructor(props: StaticBaseControllerProps) {
+ super(props);
+ this.emptyOption.caption = props.emptyCaption || "None";
+ this.placeholder = props.emptyCaption || "Select";
+ }
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/TagPickerControllerMixin.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/TagPickerControllerMixin.ts
new file mode 100644
index 0000000000..9366807e61
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/TagPickerControllerMixin.ts
@@ -0,0 +1,172 @@
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { useCombobox, UseComboboxProps, useMultipleSelection, UseMultipleSelectionProps } from "downshift";
+import { action, autorun, computed, makeObservable, observable } from "mobx";
+import { SearchStore } from "../stores/SearchStore";
+import { OptionWithState } from "../typings/OptionWithState";
+import { GConstructor } from "../typings/type-utils";
+
+export interface FilterStore {
+ toggle: (value: string) => void;
+ clear: () => void;
+ setSelected: (value: Iterable) => void;
+ selected: Set;
+ options: OptionWithState[];
+ selectedOptions: OptionWithState[];
+ search: SearchStore;
+}
+
+type BaseController = GConstructor<{
+ filterStore: FilterStore;
+ multiselect: boolean;
+ setup(): () => void;
+}>;
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+export function TagPickerControllerMixin(Base: TBase) {
+ return class TagPickerControllerMixin extends Base {
+ touched = false;
+ inputPlaceholder = "Search";
+ filterSelectedOptions = false;
+ inputValue = "";
+
+ constructor(...args: any[]) {
+ super(...args);
+ makeObservable(this, {
+ touched: observable,
+ inputValue: observable,
+ setTouched: action,
+ setInputValue: action,
+ handleBlur: action,
+ handleClear: action,
+ options: computed,
+ selectedIndex: computed,
+ selectedOptions: computed,
+ isEmpty: computed
+ });
+ }
+
+ setup(): () => void {
+ const [add, disposeAll] = disposeBatch();
+
+ add(super.setup());
+ add(autorun(...this.searchSyncFx()));
+
+ return disposeAll;
+ }
+
+ searchSyncFx(): Parameters {
+ const effect = (): void => {
+ const { touched, inputValue } = this;
+ if (touched) {
+ this.filterStore.search.setBuffer(inputValue);
+ } else {
+ this.filterStore.search.clear();
+ }
+ };
+
+ return [effect];
+ }
+
+ get options(): OptionWithState[] {
+ const options = this.filterStore.options;
+ return this.filterSelectedOptions ? options.filter(option => !option.selected) : options;
+ }
+
+ get isEmpty(): boolean {
+ return this.filterStore.selected.size === 0;
+ }
+
+ get selectedIndex(): number {
+ const index = this.filterStore.options.findIndex(option => option.selected);
+ return Math.max(index, 0);
+ }
+
+ get selectedOptions(): OptionWithState[] {
+ return this.filterStore.selectedOptions;
+ }
+
+ setTouched(touched: boolean): void {
+ this.touched = touched;
+ }
+
+ setInputValue(value: string): void {
+ this.inputValue = value;
+ }
+
+ handleBlur = (): void => {
+ this.setInputValue("");
+ this.setTouched(false);
+ this.filterStore.search.clear();
+ };
+
+ handleClear = (): void => {
+ this.setTouched(false);
+ this.setInputValue("");
+ this.filterStore.clear();
+ };
+
+ useComboboxProps = (): UseComboboxProps => {
+ const props: UseComboboxProps = {
+ items: this.options,
+ itemToKey: item => item?.value,
+ itemToString: item => item?.caption ?? "",
+ inputValue: this.inputValue,
+ defaultHighlightedIndex: this.selectedIndex,
+ onInputValueChange: changes => {
+ // Blur is handled by handleBlur;
+ if (changes.type === useCombobox.stateChangeTypes.InputBlur) {
+ return;
+ }
+ if (changes.type === useCombobox.stateChangeTypes.InputChange) {
+ this.setTouched(true);
+ }
+ this.setInputValue(changes.inputValue);
+ },
+ onSelectedItemChange: ({ selectedItem, type }) => {
+ if (
+ type === useCombobox.stateChangeTypes.InputBlur ||
+ type === useCombobox.stateChangeTypes.InputKeyDownEscape ||
+ !selectedItem
+ ) {
+ return;
+ }
+ this.filterStore.toggle(selectedItem.value);
+ },
+ stateReducer(state, { changes, type }) {
+ switch (type) {
+ case useCombobox.stateChangeTypes.ItemClick:
+ return {
+ ...changes,
+ isOpen: true,
+ highlightedIndex: state.highlightedIndex
+ };
+ default:
+ return changes;
+ }
+ }
+ };
+ return props;
+ };
+
+ useMultipleSelectionProps = (): UseMultipleSelectionProps => {
+ const props: UseMultipleSelectionProps = {
+ selectedItems: this.selectedOptions,
+ onStateChange: ({ selectedItems: newSelectedItems, type }) => {
+ newSelectedItems ??= [];
+ switch (type) {
+ case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
+ case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
+ case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
+ case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
+ this.filterStore.setSelected(newSelectedItems.map(item => item.value));
+ break;
+ default:
+ break;
+ }
+ }
+ };
+
+ return props;
+ };
+ };
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/stores/BaseSelectStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/BaseSelectStore.ts
new file mode 100644
index 0000000000..7ae086f7fc
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/stores/BaseSelectStore.ts
@@ -0,0 +1,71 @@
+import { FilterData, InputData } from "@mendix/filter-commons/typings/settings";
+import { action, makeObservable, observable } from "mobx";
+
+export class BaseSelectStore {
+ protected defaultSelected: Iterable = [];
+ protected blockSetDefaults = false;
+ selected = new Set();
+
+ constructor() {
+ makeObservable(this, {
+ selected: observable.struct,
+ clear: action,
+ reset: action,
+ toggle: action,
+ setSelected: action,
+ fromJSON: action
+ });
+ }
+
+ setSelected(selected: Iterable): void {
+ this.selected = new Set(selected);
+ }
+
+ clear(): void {
+ this.setSelected([]);
+ }
+
+ reset(): void {
+ this.setSelected(this.defaultSelected);
+ }
+
+ toggle(value: string): void {
+ const next = new Set(this.selected);
+ this.setSelected(next.delete(value) ? next : next.add(value));
+ }
+
+ toJSON(): string[] {
+ return [...this.selected];
+ }
+
+ fromJSON(json: FilterData): void {
+ if (json === undefined || json === null || isInputData(json)) {
+ return;
+ }
+ this.setSelected(json);
+ this.blockSetDefaults = true;
+ }
+}
+
+const fnNames = new Set([
+ "empty",
+ "notEmpty",
+ "equal",
+ "notEqual",
+ "greater",
+ "greaterEqual",
+ "smaller",
+ "smallerEqual",
+ "between",
+ "contains",
+ "startsWith",
+ "endsWith"
+]);
+
+export function isInputData(data: unknown): data is InputData {
+ if (Array.isArray(data)) {
+ const [name] = data;
+ return fnNames.has(name);
+ }
+ return false;
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/stores/OptionsSerializer.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/OptionsSerializer.ts
new file mode 100644
index 0000000000..16ccb1b939
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/stores/OptionsSerializer.ts
@@ -0,0 +1,42 @@
+import { computed, makeObservable } from "mobx";
+
+interface Params {
+ store: Store;
+}
+
+interface Store {
+ /** @reactive */
+ selected: Iterable;
+}
+
+export class OptionsSerializer {
+ private store: Store;
+
+ constructor(params: Params) {
+ makeObservable(this, {
+ value: computed
+ });
+
+ this.store = params.store;
+ }
+
+ get value(): string | undefined {
+ const selected = [...this.store.selected];
+ return this.toStorableValue(selected);
+ }
+
+ fromStorableValue(value: string | undefined): Iterable | undefined {
+ if (!value) {
+ return undefined;
+ }
+ return value.split(",");
+ }
+
+ toStorableValue(selected: string[]): string | undefined {
+ if (selected.length > 0) {
+ return selected.join(",");
+ }
+
+ return undefined;
+ }
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/stores/RefFilterStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/RefFilterStore.ts
new file mode 100644
index 0000000000..ab2d08b67c
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/stores/RefFilterStore.ts
@@ -0,0 +1,245 @@
+import { flattenRefCond, selectedFromCond } from "@mendix/filter-commons/condition-utils";
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { ListAttributeValue, ListReferenceSetValue, ListReferenceValue, ListValue, ObjectItem } from "mendix";
+import { ContainsCondition, EqualsCondition, FilterCondition, LiteralExpression } from "mendix/filters";
+import { association, attribute, contains, empty, equals, literal, or } from "mendix/filters/builders";
+import { action, autorun, computed, makeObservable, observable, reaction, runInAction, when } from "mobx";
+import { OptionWithState } from "../typings/OptionWithState";
+import { BaseSelectStore } from "./BaseSelectStore";
+import { SearchStore } from "./SearchStore";
+
+type ListAttributeId = ListAttributeValue["id"];
+
+export interface RefFilterStoreProps {
+ ref: ListReferenceValue | ListReferenceSetValue;
+ datasource: ListValue;
+ searchAttrId?: ListAttributeId;
+ fetchOptionsLazy?: boolean;
+ caption: CaptionAccessor;
+}
+
+interface CaptionAccessor {
+ get: (obj: ObjectItem) => { value: string | undefined };
+}
+
+export class RefFilterStore extends BaseSelectStore {
+ readonly storeType = "refselect";
+ readonly optionsFilterable: boolean;
+
+ private datasource: ListValue;
+ private listRef: ListReferenceValue | ListReferenceSetValue;
+ private caption: CaptionAccessor;
+ private searchAttrId?: ListAttributeId;
+ private readonly initCondArray: Array;
+ private readonly pageSize = 20;
+ private readonly searchSize = 100;
+ private fetchReady = false;
+
+ selectedItems: ObjectItem[] = [];
+ lazyMode: boolean;
+ search: SearchStore;
+
+ constructor(props: RefFilterStoreProps, initCond: FilterCondition | null) {
+ super();
+ this.caption = props.caption;
+ this.datasource = props.datasource;
+ this.listRef = props.ref;
+ this.lazyMode = props.fetchOptionsLazy ?? true;
+ this.searchAttrId = props.searchAttrId;
+ this.initCondArray = initCond ? flattenRefCond(initCond) : [];
+ this.search = new SearchStore();
+ this.optionsFilterable = !!this.searchAttrId;
+
+ if (this.lazyMode) {
+ this.datasource.setLimit(0);
+ }
+
+ makeObservable(this, {
+ datasource: observable.ref,
+ listRef: observable.ref,
+ caption: observable.ref,
+ searchAttrId: observable.ref,
+ options: computed,
+ hasMore: computed,
+ isLoading: computed,
+ condition: computed,
+ updateProps: action,
+ fromViewState: action,
+ fetchReady: observable,
+ setFetchReady: action,
+ setDefaultSelected: action,
+ selectedItems: observable.struct,
+ selectedOptions: computed
+ });
+
+ if (initCond) {
+ this.fromViewState(initCond);
+ }
+ }
+
+ get hasMore(): boolean {
+ return this.datasource.hasMoreItems ?? false;
+ }
+
+ get isLoading(): boolean {
+ return this.datasource.status === "loading";
+ }
+
+ get options(): OptionWithState[] {
+ const items = this.datasource.items ?? [];
+ return items.map(obj => this.toOption(obj));
+ }
+
+ get selectedOptions(): OptionWithState[] {
+ return this.selectedItems.map(obj => this.toOption(obj));
+ }
+
+ toOption(obj: ObjectItem): OptionWithState {
+ return {
+ caption: `${this.caption.get(obj).value}`,
+ value: `${obj.id}`,
+ selected: this.selected.has(obj.id)
+ };
+ }
+
+ get condition(): FilterCondition | undefined {
+ if (this.selected.size < 1) {
+ return undefined;
+ }
+
+ const exp = (guid: string): FilterCondition[] => {
+ const obj = this.selectedItems.find(o => o.id === guid);
+
+ if (obj && this.listRef.type === "Reference") {
+ return [refEquals(this.listRef, obj)];
+ } else if (obj && this.listRef.type === "ReferenceSet") {
+ return [refContains(this.listRef, [obj])];
+ }
+
+ const viewExp = this.initCondArray.find(e => {
+ if (e.arg2.type !== "literal") {
+ return false;
+ }
+ if (e.arg2.valueType === "Reference") {
+ return e.arg2.value === guid;
+ }
+ if (e.arg2.valueType === "ReferenceSet") {
+ return e.arg2.value.at(0) === guid;
+ }
+ return false;
+ });
+ return viewExp ? [viewExp] : [];
+ };
+
+ const cond = [...this.selected].flatMap(exp);
+
+ if (cond.length > 1) {
+ return or(...cond);
+ }
+
+ return cond[0];
+ }
+
+ setup(): () => void {
+ const [add, dispose] = disposeBatch();
+
+ add(this.search.setup());
+ add(reaction(...this.searchChangeFx()));
+ add(autorun(...this.computeSelectedItemsFx()));
+
+ if (this.lazyMode) {
+ add(
+ when(
+ () => this.fetchReady,
+ () => this.loadMore()
+ )
+ );
+ } else {
+ this.setFetchReady(true);
+ this.loadMore();
+ }
+
+ return dispose;
+ }
+
+ searchChangeFx(): Parameters {
+ const data = (): string => this.search.value;
+
+ const effect = (search: string): void => {
+ if (!this.searchAttrId) {
+ return;
+ }
+ const cond =
+ typeof search === "string" && search !== ""
+ ? contains(attribute(this.searchAttrId), literal(search))
+ : undefined;
+ this.datasource.setFilter(cond);
+ this.datasource.setLimit(this.searchSize);
+ };
+
+ return [data, effect, { delay: 300 }];
+ }
+
+ computeSelectedItemsFx(): Parameters {
+ const compute = (): void => {
+ const allObjects = [...this.selectedItems, ...(this.datasource.items ?? [])];
+ const map = new Map(allObjects.map(o => [o.id, o]));
+ // Note: keep selected inside current block, so autorun can react to it.
+ const selectedItems = [...this.selected].flatMap(guid => map.get(guid) ?? []);
+ runInAction(() => (this.selectedItems = selectedItems));
+ };
+
+ return [compute];
+ }
+
+ setFetchReady(fetchReady: boolean): void {
+ this.fetchReady ||= fetchReady;
+ }
+
+ setDefaultSelected(defaultSelected?: Iterable): void {
+ if (!this.blockSetDefaults && defaultSelected) {
+ this.defaultSelected = defaultSelected;
+ this.blockSetDefaults = true;
+ this.setSelected(defaultSelected);
+ }
+ }
+
+ updateProps(props: RefFilterStoreProps): void {
+ this.listRef = props.ref;
+ this.datasource = props.datasource;
+ this.caption = props.caption;
+ }
+
+ loadMore(): void {
+ this.datasource.setLimit(this.datasource.limit + this.pageSize);
+ }
+
+ fromViewState(cond: FilterCondition): void {
+ const val = (exp: LiteralExpression): string | undefined => {
+ switch (exp.valueType) {
+ case "Reference":
+ return exp.value;
+ case "ReferenceSet":
+ return exp.value.at(0);
+ default:
+ return undefined;
+ }
+ };
+
+ const selected = selectedFromCond(cond, val);
+
+ if (selected.length > 0) {
+ this.setSelected(selected);
+ this.blockSetDefaults = true;
+ }
+ }
+}
+
+export function refEquals(associationValue: ListReferenceValue, value: ObjectItem): EqualsCondition {
+ return equals(association(associationValue.id), literal(value));
+}
+
+export function refContains(associationValue: ListReferenceSetValue, value: ObjectItem[]): ContainsCondition {
+ const v = value.length ? literal(value.slice()) : empty();
+ return contains(association(associationValue.id), v);
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/stores/SearchStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/SearchStore.ts
new file mode 100644
index 0000000000..fbbd726573
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/stores/SearchStore.ts
@@ -0,0 +1,46 @@
+import { action, autorun, makeAutoObservable, runInAction } from "mobx";
+
+export class SearchStore {
+ private delay: number;
+ readonly disposers = [] as Array<() => void>;
+ readonly defaultValue: string;
+ value: string;
+ buffer: string;
+
+ constructor({ defaultValue = "", delay = 0 }: { defaultValue?: string; delay?: number } = {}) {
+ this.defaultValue = defaultValue;
+ this.buffer = this.value = this.defaultValue;
+ this.delay = delay;
+
+ makeAutoObservable(this, {
+ setBuffer: action,
+ clear: action,
+ reset: action
+ });
+ }
+
+ setBuffer(value: string): void {
+ this.buffer = value;
+ }
+
+ reset(): void {
+ this.buffer = this.value = this.defaultValue;
+ }
+
+ clear(): void {
+ this.buffer = this.value = "";
+ }
+
+ setup(): () => void {
+ this.disposers.push(
+ autorun(
+ () => {
+ const value = this.buffer;
+ runInAction(() => (this.value = value));
+ },
+ { delay: this.delay }
+ )
+ );
+ return () => this.disposers.forEach(dispose => dispose());
+ }
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/stores/StaticSelectFilterStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/StaticSelectFilterStore.ts
new file mode 100644
index 0000000000..2501db5882
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/stores/StaticSelectFilterStore.ts
@@ -0,0 +1,179 @@
+import { selectedFromCond } from "@mendix/filter-commons/condition-utils";
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { ListAttributeValue } from "mendix";
+import { FilterCondition, LiteralExpression } from "mendix/filters";
+import { attribute, equals, literal, or } from "mendix/filters/builders";
+import { action, computed, makeObservable, observable } from "mobx";
+import { OptionWithState } from "../typings/OptionWithState";
+import { BaseSelectStore } from "./BaseSelectStore";
+import { SearchStore } from "./SearchStore";
+
+interface CustomOption {
+ caption: string;
+ value: string;
+}
+
+export class StaticSelectFilterStore extends BaseSelectStore {
+ readonly storeType = "select";
+ _attributes: ListAttributeValue[] = [];
+ _customOptions: CustomOption[] = [];
+ search: SearchStore;
+
+ constructor(attributes: ListAttributeValue[], initCond: FilterCondition | null) {
+ super();
+ this.search = new SearchStore();
+ this._attributes = attributes;
+
+ makeObservable(this, {
+ _attributes: observable.struct,
+ _customOptions: observable.struct,
+ allOptions: computed,
+ options: computed,
+ selectedOptions: computed,
+ universe: computed,
+ condition: computed,
+ setCustomOptions: action,
+ setDefaultSelected: action,
+ updateProps: action,
+ fromViewState: action
+ });
+
+ if (initCond) {
+ this.fromViewState(initCond);
+ }
+ }
+
+ get allOptions(): OptionWithState[] {
+ const selected = this.selected;
+
+ if (this._customOptions.length > 0) {
+ return this._customOptions.map(opt => ({
+ ...opt,
+ selected: selected.has(opt.value)
+ }));
+ }
+
+ const options = this._attributes.flatMap(attr =>
+ Array.from(attr.universe ?? [], value => {
+ const stringValue = `${value}`;
+ return {
+ caption: attr.formatter.format(value),
+ value: stringValue,
+ selected: selected.has(stringValue)
+ };
+ })
+ );
+
+ return options;
+ }
+
+ get options(): OptionWithState[] {
+ if (!this.search.value) {
+ return this.allOptions;
+ }
+
+ return this.allOptions.filter(opt => opt.caption.toLowerCase().includes(this.search.value.toLowerCase()));
+ }
+
+ get selectedOptions(): OptionWithState[] {
+ return [...this.selected].flatMap(value => {
+ const option = this.allOptions.find(opt => opt.value === value);
+ return option ? [option] : [];
+ });
+ }
+
+ get universe(): Set {
+ return new Set(this._attributes.flatMap(attr => Array.from(attr.universe ?? [], value => `${value}`)));
+ }
+
+ get condition(): FilterCondition | undefined {
+ const selected = this.selected;
+ const conditions = this._attributes.flatMap(attr => {
+ const cond = getFilterCondition(attr, selected);
+ return cond ? [cond] : [];
+ });
+ return conditions.length > 1 ? or(...conditions) : conditions[0];
+ }
+
+ setup(): () => void {
+ const [add, dispose] = disposeBatch();
+ add(this.search.setup());
+ return dispose;
+ }
+
+ setCustomOptions(options: CustomOption[]): void {
+ this._customOptions = options;
+ }
+
+ setDefaultSelected(defaultSelected?: Iterable): void {
+ if (!this.blockSetDefaults && defaultSelected) {
+ this.defaultSelected = defaultSelected;
+ this.setSelected(defaultSelected);
+ this.blockSetDefaults = true;
+ }
+ }
+
+ updateProps(attributes: ListAttributeValue[]): void {
+ this._attributes = attributes;
+ }
+
+ checkAttrs(): TypeError | null {
+ const isValidAttr = (attr: ListAttributeValue): boolean => /Enum|Boolean/.test(attr.type);
+
+ if (this._attributes.every(isValidAttr)) {
+ return null;
+ }
+
+ return new TypeError("StaticSelectFilterStore: invalid attribute found. Check widget configuration.");
+ }
+
+ isValidValue(value: string): boolean {
+ return this.universe.has(value);
+ }
+
+ fromViewState(cond: FilterCondition): void {
+ const val = (exp: LiteralExpression): string | undefined =>
+ exp.valueType === "string"
+ ? exp.value
+ : exp.valueType === "boolean"
+ ? exp.value
+ ? "true"
+ : "false"
+ : undefined;
+
+ const selected = selectedFromCond(cond, val);
+
+ if (selected.length < 1) {
+ return;
+ }
+
+ this.setSelected(selected);
+ this.blockSetDefaults = true;
+ }
+}
+
+function getFilterCondition(
+ listAttribute: ListAttributeValue | undefined,
+ selected: Set
+): FilterCondition | undefined {
+ if (!listAttribute) {
+ return undefined;
+ }
+ if (selected.size < 1) {
+ return undefined;
+ }
+
+ const attrExp = attribute(listAttribute.id);
+ const conditions = Array.from(selected, value =>
+ equals(attrExp, literal(universeValue(listAttribute.type, value)))
+ );
+
+ return conditions.length > 1 ? or(...conditions) : conditions[0];
+}
+
+function universeValue(type: ListAttributeValue["type"], value: string): boolean | string {
+ if (type === "Boolean") {
+ return value === "true";
+ }
+ return value;
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/typings/IJSActionsControlled.ts b/packages/shared/widget-plugin-dropdown-filter/src/typings/IJSActionsControlled.ts
new file mode 100644
index 0000000000..50117d47ba
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/typings/IJSActionsControlled.ts
@@ -0,0 +1,11 @@
+export interface IJSActionsControlled {
+ handleResetValue: ResetHandler;
+ handleSetValue: SetValueHandler;
+}
+
+export type ResetHandler = (useDefaultValue: boolean) => void;
+
+export type SetValueHandler = (
+ useDefaultValue: boolean,
+ params: { operators: any; stringValue: string; numberValue: Big.Big; dateTimeValue: Date; dateTimeValue2: Date }
+) => void;
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/typings/OptionWithState.ts b/packages/shared/widget-plugin-dropdown-filter/src/typings/OptionWithState.ts
new file mode 100644
index 0000000000..084554c218
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/typings/OptionWithState.ts
@@ -0,0 +1,5 @@
+export interface OptionWithState {
+ caption: string;
+ value: string;
+ selected: boolean;
+}
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/typings/PickerFilterStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/typings/PickerFilterStore.ts
new file mode 100644
index 0000000000..02d0cb0d86
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/typings/PickerFilterStore.ts
@@ -0,0 +1,4 @@
+import { RefFilterStore } from "../stores/RefFilterStore";
+import { StaticSelectFilterStore } from "../stores/StaticSelectFilterStore";
+
+export type PickerFilterStore = RefFilterStore | StaticSelectFilterStore;
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/typings/type-utils.ts b/packages/shared/widget-plugin-dropdown-filter/src/typings/type-utils.ts
new file mode 100644
index 0000000000..443afb18a9
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/typings/type-utils.ts
@@ -0,0 +1 @@
+export type GConstructor = new (...args: any[]) => T;
diff --git a/packages/shared/widget-plugin-dropdown-filter/tsconfig.json b/packages/shared/widget-plugin-dropdown-filter/tsconfig.json
new file mode 100644
index 0000000000..052cc1cee7
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "@mendix/tsconfig-web-widgets/esm-library-with-jsx",
+ "include": ["./src/**/*"],
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "resolveJsonModule": true
+ }
+}
From b2c64c3f99fe576a6aaff77e49c7c5096ba4afea Mon Sep 17 00:00:00 2001
From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com>
Date: Fri, 18 Apr 2025 10:08:47 +0200
Subject: [PATCH 03/29] chore: fix imports and remove old modules
---
.../widget-plugin-filtering/package.json | 2 +
.../src/condition-utils.ts | 224 ------------------
.../widget-plugin-filtering/src/context.ts | 12 +-
.../controllers/generic/PickerChangeHelper.ts | 35 ---
.../generic/PickerJSActionsHelper.ts | 41 ----
.../picker/PickerBaseController.ts | 62 -----
.../controllers/picker/RefBaseController.ts | 40 ----
.../picker/RefComboboxController.ts | 18 --
.../controllers/picker/RefSelectController.ts | 18 --
.../picker/RefTagPickerController.ts | 31 ---
.../picker/StaticBaseController.ts | 65 -----
.../picker/StaticComboboxController.ts | 9 -
.../picker/StaticSelectController.ts | 10 -
.../picker/StaticTagPickerController.ts | 23 --
.../picker/mixins/ComboboxControllerMixin.ts | 176 --------------
.../picker/mixins/SelectControllerMixin.ts | 102 --------
.../picker/mixins/TagPickerControllerMixin.ts | 177 --------------
.../widget-plugin-filtering/src/index.ts | 1 -
.../widget-plugin-filtering/src/mobx-utils.ts | 10 -
.../src/stores/picker/BaseSelectStore.ts | 49 ----
.../src/stores/picker/DropdownFilterStore.ts | 138 -----------
.../src/stores/picker/OptionsSerializer.ts | 42 ----
.../src/stores/picker/SearchStore.ts | 46 ----
.../src/typings/FilterFunctions.ts | 6 -
.../src/typings/FilterObserver.ts | 2 +-
.../src/typings/IJSActionsControlled.ts | 11 -
.../src/typings/InputFilterInterface.ts | 2 +-
.../src/typings/OptionListFilterInterface.ts | 27 ---
.../src/typings/OptionWithState.ts | 5 -
.../src/typings/PickerFilterStore.ts | 4 -
.../src/typings/mendix.ts | 4 -
.../src/typings/settings.ts | 9 -
.../src/typings/type-utils.ts | 5 -
.../widget-plugin-filtering/tsconfig.json | 2 +-
34 files changed, 11 insertions(+), 1397 deletions(-)
delete mode 100644 packages/shared/widget-plugin-filtering/src/condition-utils.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/generic/PickerChangeHelper.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/generic/PickerJSActionsHelper.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/StaticComboboxController.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/StaticSelectController.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/index.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/mobx-utils.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/stores/picker/DropdownFilterStore.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/stores/picker/OptionsSerializer.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/stores/picker/SearchStore.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/typings/FilterFunctions.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/typings/IJSActionsControlled.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/typings/OptionListFilterInterface.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/typings/OptionWithState.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/typings/PickerFilterStore.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/typings/mendix.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/typings/settings.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/typings/type-utils.ts
diff --git a/packages/shared/widget-plugin-filtering/package.json b/packages/shared/widget-plugin-filtering/package.json
index e92778dc25..17597b8ddd 100644
--- a/packages/shared/widget-plugin-filtering/package.json
+++ b/packages/shared/widget-plugin-filtering/package.json
@@ -34,6 +34,8 @@
"dependencies": {
"@floating-ui/react": "^0.26.27",
"@floating-ui/react-dom": "^2.1.2",
+ "@mendix/filter-commons": "workspace:*",
+ "@mendix/widget-plugin-dropdown-filter": "workspace:*",
"@mendix/widget-plugin-external-events": "workspace:*",
"@mendix/widget-plugin-hooks": "workspace:*",
"@mendix/widget-plugin-mobx-kit": "workspace:^",
diff --git a/packages/shared/widget-plugin-filtering/src/condition-utils.ts b/packages/shared/widget-plugin-filtering/src/condition-utils.ts
deleted file mode 100644
index 976da416b5..0000000000
--- a/packages/shared/widget-plugin-filtering/src/condition-utils.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-import {
- AndCondition,
- ContainsCondition,
- EqualsCondition,
- FilterCondition,
- LiteralExpression,
- OrCondition
-} from "mendix/filters";
-import { and, literal, notEqual } from "mendix/filters/builders";
-
-type BinaryExpression = T extends { arg1: unknown; arg2: object } ? T : never;
-type Func = T extends { name: infer Fn } ? Fn : never;
-type FilterFunction = Func;
-
-const hasOwn = (o: object, k: PropertyKey): boolean => Object.hasOwn(o, k);
-
-export function isBinary(cond: FilterCondition): cond is BinaryExpression {
- return hasOwn(cond, "arg1") && hasOwn(cond, "arg2");
-}
-
-export function isAnd(exp: FilterCondition): exp is AndCondition {
- return exp.type === "function" && exp.name === "and";
-}
-
-export function isOr(exp: FilterCondition): exp is OrCondition {
- return exp.type === "function" && exp.name === "or";
-}
-
-export function isEmptyExp(exp: FilterCondition): boolean {
- return isBinary(exp) && exp.arg2.type === "literal" && exp.name === "=" && exp.arg2.valueType === "undefined";
-}
-
-export function isNotEmptyExp(exp: FilterCondition): boolean {
- return isBinary(exp) && exp.arg2.type === "literal" && exp.name === "!=" && exp.arg2.valueType === "undefined";
-}
-
-interface TagName {
- readonly type: "literal";
- readonly value: string;
- readonly valueType: "string";
-}
-
-const MARKER = "#";
-
-interface TagMarker {
- readonly type: "literal";
- readonly value: typeof MARKER;
- readonly valueType: "string";
-}
-
-interface TagCond {
- readonly type: "function";
- readonly name: "!=";
- readonly arg1: TagName;
- readonly arg2: TagMarker;
-}
-
-export function tag(name: string): TagCond {
- return notEqual(literal(name), literal(MARKER)) as TagCond;
-}
-
-export function isTag(cond: FilterCondition): cond is TagCond {
- return (
- cond.name === "!=" &&
- cond.arg1.type === "literal" &&
- cond.arg2.type === "literal" &&
- /string/i.test(cond.arg1.valueType) &&
- /string/i.test(cond.arg2.valueType) &&
- cond.arg2.value === MARKER
- );
-}
-
-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 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,
- val: (exp: LiteralExpression) => V
-): null | [Fn, V] | [Fn, V, V] {
- // Or - condition build for multiple attrs, get state from the first one.
- if (isOr(cond)) {
- return inputStateFromCond(cond.args[0], fn, val);
- }
-
- // Between
- if (isAnd(cond)) {
- return betweenToState(cond, fn, val);
- }
-
- return singularToState(cond, fn, val);
-}
-
-export function betweenToState(
- cond: AndCondition,
- fn: (func: "between") => Fn,
- val: (exp: LiteralExpression) => V
-): null | [Fn, V, V] {
- const [exp1, exp2] = cond.args;
- const [v1, v2] = [expValue(exp1, val), expValue(exp2, val)];
- if (v1 && v2) {
- return [fn("between"), v1, v2];
- }
- return null;
-}
-
-export function singularToState(
- cond: FilterCondition,
- fn: (func: FilterFunction | "between" | "empty" | "notEmpty") => Fn,
- val: (exp: LiteralExpression) => V
-): null | [Fn, V] {
- const value = expValue(cond, val);
- if (value === null) {
- return null;
- }
-
- if (isEmptyExp(cond)) {
- return [fn("empty"), value];
- }
- if (isNotEmptyExp(cond)) {
- return [fn("notEmpty"), value];
- }
-
- return [fn(cond.name), value];
-}
-
-export function expValue(exp: FilterCondition, val: (exp: LiteralExpression) => V): null | V {
- if (!isBinary(exp)) {
- return null;
- }
- if (exp.arg2.type !== "literal") {
- return null;
- }
- return val(exp.arg2);
-}
-
-export function selectedFromCond(
- cond: FilterCondition,
- val: (exp: LiteralExpression) => V | undefined
-): V[] {
- const reduce = (acc: V[], cond: FilterCondition): V[] => {
- if (cond.name === "or") {
- return cond.args.reduce(reduce, acc);
- }
-
- if (cond.name === "=" || cond.name === "contains") {
- const item = expValue(cond, val);
- if (item != null) {
- acc.push(item);
- }
- }
-
- return acc;
- };
-
- return [cond].reduce(reduce, []);
-}
-
-export function flattenRefCond(cond: FilterCondition): Array {
- return [cond].flatMap(exp => {
- if (exp.name === "or") {
- return exp.args.flatMap(flattenRefCond);
- }
- if (exp.name === "=" || exp.name === "contains") {
- return [exp];
- }
- return [];
- });
-}
diff --git a/packages/shared/widget-plugin-filtering/src/context.ts b/packages/shared/widget-plugin-filtering/src/context.ts
index e34c9f5375..981b934025 100644
--- a/packages/shared/widget-plugin-filtering/src/context.ts
+++ b/packages/shared/widget-plugin-filtering/src/context.ts
@@ -1,10 +1,10 @@
-import { FilterCondition } from "mendix/filters/index.js";
+import { PickerFilterStore } from "@mendix/widget-plugin-dropdown-filter/typings/PickerFilterStore";
+import { FilterCondition } from "mendix/filters";
import { Context, createContext, useContext } from "react";
-import { APIError, ENOCONTEXT } from "./errors.js";
-import { Result, error, value } from "./result-meta.js";
-import { FilterObserver } from "./typings/FilterObserver.js";
-import { InputFilterInterface } from "./typings/InputFilterInterface.js";
-import { PickerFilterStore } from "./typings/PickerFilterStore.js";
+import { APIError, ENOCONTEXT } from "./errors";
+import { Result, error, value } from "./result-meta";
+import { FilterObserver } from "./typings/FilterObserver";
+import { InputFilterInterface } from "./typings/InputFilterInterface";
export interface FilterAPI {
version: 3;
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerChangeHelper.ts b/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerChangeHelper.ts
deleted file mode 100644
index f4a51110f3..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerChangeHelper.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
-import { ActionValue, EditableValue } from "mendix";
-import { IReactionDisposer, reaction } from "mobx";
-
-interface Props {
- valueAttribute?: EditableValue;
- onChange?: ActionValue;
-}
-
-export class PickerChangeHelper {
- private onChange?: ActionValue;
- private valueAttribute?: EditableValue;
- private valueFn: () => string | undefined;
-
- constructor(props: Props, valueFn: () => string | undefined) {
- this.onChange = props.onChange;
- this.valueAttribute = props.valueAttribute;
- this.valueFn = valueFn;
- }
-
- setup(): IReactionDisposer {
- const effect = (value: string | undefined): void => {
- this.valueAttribute?.setValue(value);
-
- executeAction(this.onChange);
- };
-
- return reaction(this.valueFn, effect);
- }
-
- updateProps(props: Props): void {
- this.onChange = props.onChange;
- this.valueAttribute = props.valueAttribute;
- }
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerJSActionsHelper.ts b/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerJSActionsHelper.ts
deleted file mode 100644
index 3cd56a159e..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerJSActionsHelper.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { IJSActionsControlled, ResetHandler, SetValueHandler } from "../../typings/IJSActionsControlled";
-
-interface FilterStore {
- reset: () => void;
- clear: () => void;
- setSelected: (value: Iterable) => void;
-}
-
-type Parse = (value: string) => Iterable;
-
-export class PickerJSActionsHelper implements IJSActionsControlled {
- private filterStore: FilterStore;
- private parse: Parse;
- private multiselect: boolean;
-
- constructor({ filterStore, parse, multiselect }: { filterStore: FilterStore; parse: Parse; multiselect: boolean }) {
- this.filterStore = filterStore;
- this.parse = parse;
- this.multiselect = multiselect;
- }
-
- handleResetValue: ResetHandler = (useDefaultValue): void => {
- if (useDefaultValue) {
- this.filterStore.reset();
- return;
- }
- this.filterStore.clear();
- };
-
- handleSetValue: SetValueHandler = (useDefaultValue, params): void => {
- if (useDefaultValue) {
- this.filterStore.reset();
- return;
- }
- let value = Array.from(this.parse(params.stringValue));
- if (!this.multiselect) {
- value = value.slice(0, 1);
- }
- this.filterStore.setSelected(value);
- };
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts
deleted file mode 100644
index a762277879..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { ActionValue, EditableValue } from "mendix";
-import { OptionsSerializer } from "../../stores/picker/OptionsSerializer";
-import { IJSActionsControlled, ResetHandler, SetValueHandler } from "../../typings/IJSActionsControlled";
-import { OptionWithState } from "../../typings/OptionWithState";
-import { PickerChangeHelper } from "../generic/PickerChangeHelper";
-import { PickerJSActionsHelper } from "../generic/PickerJSActionsHelper";
-
-interface FilterStore {
- reset: () => void;
- clear: () => void;
- setSelected: (value: Iterable) => void;
- selected: Set;
- options: OptionWithState[];
-}
-
-export class PickerBaseController implements IJSActionsControlled {
- protected actionHelper: PickerJSActionsHelper;
- protected changeHelper: PickerChangeHelper;
- protected defaultValue?: Iterable;
- protected serializer: OptionsSerializer;
- filterStore: S;
- multiselect: boolean;
-
- constructor(props: PickerBaseControllerProps) {
- this.filterStore = props.filterStore;
- this.multiselect = props.multiselect;
- this.serializer = new OptionsSerializer({ store: this.filterStore });
- this.defaultValue = this.parseDefaultValue(props.defaultValue);
- this.actionHelper = new PickerJSActionsHelper({
- filterStore: props.filterStore,
- parse: value => this.serializer.fromStorableValue(value) ?? [],
- multiselect: props.multiselect
- });
- this.changeHelper = new PickerChangeHelper(props, () => this.serializer.value);
- }
-
- parseDefaultValue = (value: string | undefined): Iterable | undefined => {
- const defaultValue = this.serializer.fromStorableValue(value);
- if (!defaultValue) {
- return undefined;
- }
- const arr = Array.from(defaultValue);
- return this.multiselect ? arr : arr.slice(0, 1);
- };
-
- handleSetValue = (...args: Parameters): void => {
- this.actionHelper.handleSetValue(...args);
- };
-
- handleResetValue = (...args: Parameters): void => {
- this.actionHelper.handleResetValue(...args);
- };
-}
-
-export interface PickerBaseControllerProps {
- defaultValue?: string;
- filterStore: S;
- multiselect: boolean;
- onChange?: ActionValue;
- valueAttribute?: EditableValue;
- emptyCaption?: string;
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts
deleted file mode 100644
index c355a2e530..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { ActionValue, EditableValue } from "mendix";
-import { action, makeObservable } from "mobx";
-import { disposeFx } from "../../mobx-utils";
-import { RefFilterStore } from "../../stores/picker/RefFilterStore";
-import { PickerBaseController } from "./PickerBaseController";
-
-export class RefBaseController extends PickerBaseController {
- constructor(props: RefBaseControllerProps) {
- super(props);
- makeObservable(this, {
- updateProps: action
- });
- }
-
- setup(): () => void {
- const [disposers, dispose] = disposeFx();
-
- disposers.push(this.changeHelper.setup());
-
- if (this.defaultValue) {
- this.filterStore.setDefaultSelected(this.defaultValue);
- }
-
- return dispose;
- }
-
- updateProps(props: RefBaseControllerProps): void {
- this.changeHelper.updateProps(props);
- }
-}
-
-export interface RefBaseControllerProps {
- defaultValue?: string;
- filterStore: RefFilterStore;
- multiselect: boolean;
- onChange?: ActionValue;
- valueAttribute?: EditableValue;
- emptyCaption?: string;
- placeholder?: string;
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts
deleted file mode 100644
index d211132988..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { ComboboxControllerMixin } from "./mixins/ComboboxControllerMixin";
-import { RefBaseController, RefBaseControllerProps } from "./RefBaseController";
-
-export class RefComboboxController extends ComboboxControllerMixin(RefBaseController) {
- constructor(props: RefBaseControllerProps) {
- super({ ...props, multiselect: false });
- this.inputPlaceholder = props.placeholder ?? "Search";
- }
-
- handleFocus = (event: React.FocusEvent): void => {
- super.handleFocus(event);
- this.filterStore.setFetchReady(true);
- };
-
- handleMenuScrollEnd = (): void => {
- this.filterStore.loadMore();
- };
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts
deleted file mode 100644
index 6d5c46280c..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { RefBaseController, RefBaseControllerProps } from "./RefBaseController";
-import { SelectControllerMixin } from "./mixins/SelectControllerMixin";
-
-export class RefSelectController extends SelectControllerMixin(RefBaseController) {
- constructor(props: RefBaseControllerProps) {
- super(props);
- this.emptyOption.caption = props.emptyCaption || "None";
- this.placeholder = props.placeholder || "Search";
- }
-
- handleFocus = (): void => {
- this.filterStore.setFetchReady(true);
- };
-
- handleMenuScrollEnd = (): void => {
- this.filterStore.loadMore();
- };
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts
deleted file mode 100644
index 0595766f8d..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { RefBaseController, RefBaseControllerProps } from "./RefBaseController";
-import { TagPickerControllerMixin } from "./mixins/TagPickerControllerMixin";
-
-type SelectionMethodEnum = "checkbox" | "rowClick";
-type SelectedItemsStyleEnum = "text" | "boxes";
-
-interface Props extends RefBaseControllerProps {
- selectionMethod: SelectionMethodEnum;
- selectedItemsStyle: SelectedItemsStyleEnum;
-}
-
-export class RefTagPickerController extends TagPickerControllerMixin(RefBaseController) {
- selectionMethod: SelectionMethodEnum;
- selectedStyle: SelectedItemsStyleEnum;
-
- constructor(props: Props) {
- super(props);
- this.inputPlaceholder = props.placeholder ?? "Search";
- this.filterSelectedOptions = props.selectionMethod === "rowClick";
- this.selectedStyle = props.selectedItemsStyle;
- this.selectionMethod = this.selectedStyle === "boxes" ? props.selectionMethod : "checkbox";
- }
-
- handleFocus = (): void => {
- this.filterStore.setFetchReady(true);
- };
-
- handleMenuScrollEnd = (): void => {
- this.filterStore.loadMore();
- };
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts
deleted file mode 100644
index b9506dcd99..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { ActionValue, DynamicValue, EditableValue } from "mendix";
-import { action, autorun, makeObservable, observable } from "mobx";
-import { disposeFx } from "../../mobx-utils";
-import { StaticSelectFilterStore } from "../../stores/picker/StaticSelectFilterStore";
-import { PickerBaseController } from "./PickerBaseController";
-
-export class StaticBaseController extends PickerBaseController {
- filterOptions: Array>>;
-
- constructor(props: StaticBaseControllerProps) {
- super(props);
- this.filterOptions = props.filterOptions;
- makeObservable(this, {
- updateProps: action,
- filterOptions: observable.struct
- });
- }
-
- setup(): () => void {
- const [disposers, dispose] = disposeFx();
-
- disposers.push(this.changeHelper.setup());
-
- disposers.push(
- autorun(() => {
- if (this.filterOptions.length > 0) {
- const options = this.filterOptions.map(this.toStoreOption);
- this.filterStore.setCustomOptions(options);
- }
- })
- );
-
- if (this.defaultValue) {
- this.filterStore.setDefaultSelected(this.defaultValue);
- }
-
- return dispose;
- }
-
- updateProps(props: StaticBaseControllerProps): void {
- this.filterOptions = props.filterOptions;
- this.changeHelper.updateProps(props);
- }
-
- toStoreOption = (opt: CustomOption>): CustomOption => ({
- caption: `${opt.caption?.value}`,
- value: `${opt.value?.value}`
- });
-}
-
-export interface StaticBaseControllerProps {
- defaultValue?: string;
- filterOptions: Array>>;
- filterStore: StaticSelectFilterStore;
- multiselect: boolean;
- onChange?: ActionValue;
- valueAttribute?: EditableValue;
- emptyCaption?: string;
- placeholder?: string;
-}
-
-export interface CustomOption {
- caption: T;
- value: T;
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticComboboxController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticComboboxController.ts
deleted file mode 100644
index c6410a631c..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticComboboxController.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController";
-import { ComboboxControllerMixin } from "./mixins/ComboboxControllerMixin";
-
-export class StaticComboboxController extends ComboboxControllerMixin(StaticBaseController) {
- constructor(props: StaticBaseControllerProps) {
- super({ ...props, multiselect: false });
- this.inputPlaceholder = props.placeholder ?? "Search";
- }
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticSelectController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticSelectController.ts
deleted file mode 100644
index 8174fbb975..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticSelectController.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController";
-import { SelectControllerMixin } from "./mixins/SelectControllerMixin";
-
-export class StaticSelectController extends SelectControllerMixin(StaticBaseController) {
- constructor(props: StaticBaseControllerProps) {
- super(props);
- this.emptyOption.caption = props.emptyCaption || "None";
- this.placeholder = props.emptyCaption || "Select";
- }
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts
deleted file mode 100644
index 11bc90fd30..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController";
-import { TagPickerControllerMixin } from "./mixins/TagPickerControllerMixin";
-
-type SelectionMethodEnum = "checkbox" | "rowClick";
-type SelectedItemsStyleEnum = "text" | "boxes";
-
-interface Props extends StaticBaseControllerProps {
- selectionMethod: SelectionMethodEnum;
- selectedItemsStyle: SelectedItemsStyleEnum;
-}
-
-export class StaticTagPickerController extends TagPickerControllerMixin(StaticBaseController) {
- selectionMethod: SelectionMethodEnum;
- selectedStyle: SelectedItemsStyleEnum;
-
- constructor(props: Props) {
- super(props);
- this.inputPlaceholder = props.placeholder ?? "Search";
- this.filterSelectedOptions = props.selectionMethod === "rowClick";
- this.selectedStyle = props.selectedItemsStyle;
- this.selectionMethod = this.selectedStyle === "boxes" ? props.selectionMethod : "checkbox";
- }
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts
deleted file mode 100644
index 0364d9331f..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts
+++ /dev/null
@@ -1,176 +0,0 @@
-import { useCombobox, UseComboboxProps } from "downshift";
-import { action, autorun, computed, makeObservable, observable, reaction } from "mobx";
-import { disposeFx } from "../../../mobx-utils";
-import { SearchStore } from "../../../stores/picker/SearchStore";
-import { OptionWithState } from "../../../typings/OptionWithState";
-import { GConstructor } from "../../../typings/type-utils";
-
-export interface FilterStore {
- clear: () => void;
- setSelected: (value: Iterable) => void;
- selected: Set;
- options: OptionWithState[];
- selectedOptions: OptionWithState[];
- search: SearchStore;
-}
-
-type BaseController = GConstructor<{
- filterStore: FilterStore;
- multiselect: boolean;
- setup(): () => void;
-}>;
-
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function ComboboxControllerMixin(Base: TBase) {
- return class ComboboxControllerMixin extends Base {
- touched = false;
- inputValue = "";
- inputPlaceholder = "";
-
- constructor(...args: any[]) {
- super(...args);
-
- makeObservable(this, {
- inputValue: observable,
- setInputValue: action,
- touched: observable,
- setTouched: action,
- selectedIndex: computed,
- selectedOption: computed,
- isEmpty: computed,
- options: computed,
- handleBlur: action,
- handleClear: action
- });
- }
-
- setup(): () => void {
- const [disposers, dispose] = disposeFx();
- disposers.push(autorun(...this.searchSyncFx()));
-
- // Set input when store state changes
- disposers.push(reaction(...this.storeSyncFx()));
-
- disposers.push(super.setup());
- this.setInputValue(this.inputInitValue);
- return dispose;
- }
-
- searchSyncFx(): Parameters {
- const effect = (): void => {
- const { touched, inputValue } = this;
- if (touched) {
- this.filterStore.search.setBuffer(inputValue);
- } else {
- this.filterStore.search.clear();
- }
- };
-
- return [effect];
- }
-
- storeSyncFx(): Parameters {
- const data = (): string => this.selectedOption?.caption ?? "";
- const effect = (caption: string): void => {
- if (!this.touched) {
- this.setInputValue(caption);
- }
- };
- return [data, effect];
- }
-
- get options(): OptionWithState[] {
- return this.filterStore.options;
- }
-
- get isEmpty(): boolean {
- return this.filterStore.selected.size === 0;
- }
-
- get selectedIndex(): number {
- const index = this.filterStore.options.findIndex(option => option.selected);
- return Math.max(index, 0);
- }
-
- get selectedOption(): OptionWithState | null {
- return this.filterStore.selectedOptions.at(0) ?? null;
- }
-
- get inputInitValue(): string {
- if (this.selectedOption) {
- return this.selectedOption.caption;
- }
- if (this.filterStore.selected.size === 0) {
- return "";
- } else {
- return "1 item selected (but not applied)";
- }
- }
-
- setTouched(value: boolean): void {
- this.touched = value;
- }
-
- setInputValue(value: string): void {
- this.inputValue = value;
- }
-
- handleFocus(event: React.FocusEvent): void {
- event.target.select();
- }
-
- handleBlur = (): void => {
- this.setTouched(false);
- this.setInputValue(this.selectedOption?.caption ?? "");
- this.filterStore.search.clear();
- };
-
- handleClear = (): void => {
- this.setTouched(false);
- this.setInputValue("");
- this.filterStore.clear();
- };
-
- useComboboxProps = (): UseComboboxProps => {
- const props: UseComboboxProps = {
- items: this.filterStore.options,
- itemToKey: item => item?.value,
- itemToString: item => item?.caption ?? "",
- inputValue: this.inputValue,
- defaultHighlightedIndex: this.selectedIndex,
- onInputValueChange: changes => {
- // Blur is handled by handleBlur;
- if (changes.type === useCombobox.stateChangeTypes.InputBlur) {
- return;
- }
- if (changes.type === useCombobox.stateChangeTypes.InputKeyDownEscape) {
- this.handleClear();
- return;
- }
- if (changes.type === useCombobox.stateChangeTypes.InputChange) {
- this.setTouched(true);
- }
- this.setInputValue(changes.inputValue);
- },
- onSelectedItemChange: ({ selectedItem, type }) => {
- if (
- type === useCombobox.stateChangeTypes.InputBlur ||
- type === useCombobox.stateChangeTypes.InputKeyDownEscape
- ) {
- return;
- }
-
- this.setTouched(false);
- this.filterStore.setSelected(selectedItem ? [selectedItem.value] : []);
- },
- stateReducer(state, { changes }) {
- return {
- ...changes,
- highlightedIndex: changes.inputValue !== state.inputValue ? 0 : changes.highlightedIndex
- };
- }
- };
- return props;
- };
- };
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts
deleted file mode 100644
index c765116ed8..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { useSelect, UseSelectProps } from "downshift";
-import { action, computed, makeObservable } from "mobx";
-import { OptionWithState } from "../../../typings/OptionWithState";
-import { GConstructor } from "../../../typings/type-utils";
-
-export interface FilterStore {
- toggle: (value: string) => void;
- clear: () => void;
- setSelected: (value: Iterable) => void;
- selected: Set;
- options: OptionWithState[];
- selectedOptions: OptionWithState[];
-}
-
-type BaseController = GConstructor<{
- filterStore: FilterStore;
- multiselect: boolean;
-}>;
-
-const none = "[[__none__]]" as const;
-
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function SelectControllerMixin(Base: TBase) {
- return class SelectControllerMixin extends Base {
- placeholder = "Select";
-
- readonly emptyOption = {
- value: none,
- caption: "None",
- selected: false
- };
-
- constructor(...args: any[]) {
- super(...args);
- makeObservable(this, {
- options: computed,
- isEmpty: computed,
- value: computed,
- handleClear: action
- });
- }
-
- get options(): OptionWithState[] {
- return [this.emptyOption, ...this.filterStore.options];
- }
-
- get isEmpty(): boolean {
- return this.filterStore.selected.size === 0;
- }
-
- get value(): string {
- const selected = this.filterStore.selectedOptions;
-
- if (selected.length < 1) {
- return this.placeholder;
- }
-
- return selected.map(option => option.caption).join(", ");
- }
-
- handleClear = (): void => {
- this.filterStore.clear();
- };
-
- useSelectProps = (): UseSelectProps => {
- const props: UseSelectProps = {
- items: this.options,
- itemToKey: item => item?.value,
- itemToString: item => item?.caption ?? "",
- onSelectedItemChange: ({ selectedItem }) => {
- if (!selectedItem) {
- return;
- }
- if (selectedItem.value === none) {
- this.filterStore.clear();
- } else if (this.multiselect) {
- this.filterStore.toggle(selectedItem.value);
- } else {
- this.filterStore.setSelected([selectedItem.value]);
- }
- }
- };
-
- if (this.multiselect) {
- props.stateReducer = (state, { changes, type }) => {
- switch (type) {
- case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
- case useSelect.stateChangeTypes.ItemClick:
- return {
- ...changes,
- isOpen: true,
- highlightedIndex: state.highlightedIndex
- };
- default:
- return changes;
- }
- };
- }
- return props;
- };
- };
-}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts
deleted file mode 100644
index 42ed406a37..0000000000
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import { useCombobox, UseComboboxProps, useMultipleSelection, UseMultipleSelectionProps } from "downshift";
-import { action, autorun, computed, makeObservable, observable } from "mobx";
-import { disposeFx } from "../../../mobx-utils";
-import { SearchStore } from "../../../stores/picker/SearchStore";
-import { OptionWithState } from "../../../typings/OptionWithState";
-import { GConstructor } from "../../../typings/type-utils";
-
-export interface FilterStore {
- toggle: (value: string) => void;
- clear: () => void;
- setSelected: (value: Iterable) => void;
- selected: Set;
- options: OptionWithState[];
- selectedOptions: OptionWithState[];
- search: SearchStore;
-}
-
-type BaseController = GConstructor<{
- filterStore: FilterStore;
- multiselect: boolean;
- setup(): () => void;
-}>;
-
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function TagPickerControllerMixin(Base: TBase) {
- return class TagPickerControllerMixin extends Base {
- touched = false;
- inputValue = "";
- inputPlaceholder = "";
- filterSelectedOptions = false;
-
- constructor(...args: any[]) {
- super(...args);
-
- makeObservable(this, {
- inputValue: observable,
- setInputValue: action,
- touched: observable,
- setTouched: action,
- selectedIndex: computed,
- selectedOptions: computed,
- handleBlur: action,
- handleClear: action,
- isEmpty: computed,
- options: computed
- });
- }
-
- setup(): () => void {
- const [disposers, dispose] = disposeFx();
- disposers.push(autorun(...this.searchSyncFx()));
- disposers.push(super.setup());
- return dispose;
- }
-
- searchSyncFx(): Parameters {
- const effect = (): void => {
- const { touched, inputValue } = this;
- if (touched) {
- this.filterStore.search.setBuffer(inputValue);
- } else {
- this.filterStore.search.clear();
- }
- };
-
- return [effect];
- }
-
- get options(): OptionWithState[] {
- const options = this.filterStore.options;
- return this.filterSelectedOptions ? options.filter(option => !option.selected) : options;
- }
-
- get isEmpty(): boolean {
- return this.filterStore.selected.size === 0;
- }
-
- get selectedIndex(): number {
- const index = this.filterStore.options.findIndex(option => option.selected);
- return Math.max(index, 0);
- }
-
- get selectedOptions(): OptionWithState[] {
- return this.filterStore.selectedOptions;
- }
-
- setTouched(value: boolean): void {
- this.touched = value;
- }
-
- setInputValue(value: string): void {
- this.inputValue = value;
- }
-
- handleBlur = (): void => {
- this.setTouched(false);
- this.setInputValue("");
- this.filterStore.search.clear();
- };
-
- handleClear = (): void => {
- this.setTouched(false);
- this.setInputValue("");
- this.filterStore.clear();
- };
-
- useComboboxProps = (): UseComboboxProps => {
- const props: UseComboboxProps = {
- items: this.options,
- itemToKey: item => item?.value,
- itemToString: item => item?.caption ?? "",
- inputValue: this.inputValue,
- defaultHighlightedIndex: this.selectedIndex,
- onInputValueChange: changes => {
- // Blur is handled by handleBlur;
- if (changes.type === useCombobox.stateChangeTypes.InputBlur) {
- return;
- }
- if (changes.type === useCombobox.stateChangeTypes.InputChange) {
- this.setTouched(true);
- }
- this.setInputValue(changes.inputValue);
- },
- onSelectedItemChange: ({ selectedItem, type }) => {
- if (
- type === useCombobox.stateChangeTypes.InputBlur ||
- type === useCombobox.stateChangeTypes.InputKeyDownEscape ||
- !selectedItem
- ) {
- return;
- }
- this.filterStore.toggle(selectedItem.value);
- this.setInputValue("");
- },
- stateReducer(state, { changes, type }) {
- switch (type) {
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
- case useCombobox.stateChangeTypes.ItemClick:
- return {
- ...changes,
- isOpen: true,
- highlightedIndex: state.highlightedIndex,
- inputValue: state.inputValue
- };
- default:
- return {
- ...changes,
- highlightedIndex: changes.inputValue !== state.inputValue ? 0 : changes.highlightedIndex
- };
- }
- }
- };
- return props;
- };
-
- useMultipleSelectionProps = (): UseMultipleSelectionProps => {
- const props: UseMultipleSelectionProps = {
- selectedItems: this.selectedOptions,
- onStateChange: ({ selectedItems: newSelectedItems, type }) => {
- newSelectedItems ??= [];
- switch (type) {
- case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
- case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
- case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
- case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
- this.filterStore.setSelected(newSelectedItems.map(item => item.value));
- break;
- default:
- break;
- }
- }
- };
-
- return props;
- };
- };
-}
diff --git a/packages/shared/widget-plugin-filtering/src/index.ts b/packages/shared/widget-plugin-filtering/src/index.ts
deleted file mode 100644
index e01bd3faca..0000000000
--- a/packages/shared/widget-plugin-filtering/src/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./custom-filter-api/DropdownStoreProvider";
diff --git a/packages/shared/widget-plugin-filtering/src/mobx-utils.ts b/packages/shared/widget-plugin-filtering/src/mobx-utils.ts
deleted file mode 100644
index 0b5ab0649a..0000000000
--- a/packages/shared/widget-plugin-filtering/src/mobx-utils.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export function disposeFx(): [disposers: Array<() => void>, dispose: () => void] {
- const disposers: Array<() => void> = [];
- return [
- disposers,
- () => {
- disposers.forEach(dispose => dispose());
- disposers.length = 0;
- }
- ];
-}
diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts b/packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts
deleted file mode 100644
index ca1b324fd0..0000000000
--- a/packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { action, makeObservable, observable } from "mobx";
-import { FilterData } from "../../typings/settings";
-import { isInputData } from "../utils/is-input-data";
-
-export class BaseSelectStore {
- protected defaultSelected: Iterable = [];
- protected blockSetDefaults = false;
- selected = new Set();
-
- constructor() {
- makeObservable(this, {
- selected: observable.struct,
- clear: action,
- reset: action,
- toggle: action,
- setSelected: action,
- fromJSON: action
- });
- }
-
- setSelected(selected: Iterable): void {
- this.selected = new Set(selected);
- }
-
- clear(): void {
- this.setSelected([]);
- }
-
- reset(): void {
- this.setSelected(this.defaultSelected);
- }
-
- toggle(value: string): void {
- const next = new Set(this.selected);
- this.setSelected(next.delete(value) ? next : next.add(value));
- }
-
- toJSON(): string[] {
- return [...this.selected];
- }
-
- fromJSON(json: FilterData): void {
- if (json == null || isInputData(json)) {
- return;
- }
- this.setSelected(json);
- this.blockSetDefaults = true;
- }
-}
diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/DropdownFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/picker/DropdownFilterStore.ts
deleted file mode 100644
index 977d2c8876..0000000000
--- a/packages/shared/widget-plugin-filtering/src/stores/picker/DropdownFilterStore.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { ListAttributeValue } from "mendix";
-import { FilterCondition, LiteralExpression } from "mendix/filters";
-import { attribute, equals, literal, or } from "mendix/filters/builders";
-import { action, computed, makeObservable, observable } from "mobx";
-import { selectedFromCond } from "../../condition-utils";
-import { disposeFx } from "../../mobx-utils";
-import { OptionWithState } from "../../typings/OptionWithState";
-import { BaseSelectStore } from "./BaseSelectStore";
-import { SearchStore } from "./SearchStore";
-
-export class DropdownFilterStore extends BaseSelectStore {
- readonly storeType = "select";
- _attributes: ListAttributeValue[] = [];
- _customOptions: Array<{ caption: string; value: string }> = [];
- search: SearchStore;
-
- constructor(attributes: ListAttributeValue[], initCond: FilterCondition | null) {
- super();
- this.search = new SearchStore();
- this._attributes = attributes;
-
- makeObservable(this, {
- _attributes: observable.struct,
- _customOptions: observable.struct,
- allOptions: computed,
- options: computed,
- selectedOptions: computed,
- universe: computed,
- condition: computed,
- setCustomOptions: action,
- setDefaultSelected: action,
- updateProps: action,
- fromViewState: action
- });
-
- if (initCond) {
- this.fromViewState(initCond);
- }
- }
-
- get allOptions(): OptionWithState[] {
- const selected = this.selected;
-
- if (this._customOptions.length > 0) {
- return this._customOptions.map(opt => ({ ...opt, selected: selected.has(opt.value) }));
- }
-
- const options = this._attributes.flatMap(attr =>
- Array.from(attr.universe ?? [], value => {
- const stringValue = `${value}`;
- return {
- caption: attr.formatter.format(value),
- value: stringValue,
- selected: selected.has(stringValue)
- };
- })
- );
-
- return options;
- }
-
- get options(): OptionWithState[] {
- if (!this.search.value) {
- return this.allOptions;
- }
-
- return this.allOptions.filter(opt => opt.caption.toLowerCase().includes(this.search.value.toLowerCase()));
- }
-
- get selectedOptions(): OptionWithState[] {
- return [...this.selected].flatMap(value => {
- const option = this.allOptions.find(opt => opt.value === value);
- return option ? [option] : [];
- });
- }
-
- get universe(): Set {
- return new Set(this._attributes.flatMap(attr => Array.from(attr.universe ?? [], value => `${value}`)));
- }
-
- get condition(): FilterCondition | undefined {
- const selected = this.selected;
- if (selected.size === 0) {
- return undefined;
- }
-
- const conditions = this._attributes.flatMap(attr => {
- const values = [...selected]
- .map(value => attr.formatter.parse(value))
- .filter(result => result.valid)
- .map(result => (result as { valid: true; value: any }).value);
-
- return values.length > 0 ? [or(...values.map(value => equals(attribute(attr.id), literal(value))))] : [];
- });
-
- return conditions.length > 1 ? or(...conditions) : conditions[0];
- }
-
- setup(): () => void {
- const [disposers, dispose] = disposeFx();
- disposers.push(this.search.setup());
- return dispose;
- }
-
- setCustomOptions(options: Array<{ caption: string; value: string }>): void {
- this._customOptions = options;
- }
-
- setDefaultSelected(defaultSelected?: Iterable): void {
- if (!this.blockSetDefaults && defaultSelected) {
- this.defaultSelected = defaultSelected;
- this.setSelected(defaultSelected);
- this.blockSetDefaults = true;
- }
- }
-
- updateProps(attributes: ListAttributeValue[]): void {
- this._attributes = attributes;
- }
-
- isValidValue(value: string): boolean {
- return this.universe.has(value);
- }
-
- fromViewState(cond: FilterCondition): void {
- const val = (exp: LiteralExpression): string | undefined =>
- exp.valueType === "string" ? exp.value : exp.valueType === "boolean" ? String(exp.value) : undefined;
-
- const selected = selectedFromCond(cond, val);
-
- if (selected.length < 1) {
- return;
- }
-
- this.setSelected(selected);
- this.blockSetDefaults = true;
- }
-}
diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/OptionsSerializer.ts b/packages/shared/widget-plugin-filtering/src/stores/picker/OptionsSerializer.ts
deleted file mode 100644
index 16ccb1b939..0000000000
--- a/packages/shared/widget-plugin-filtering/src/stores/picker/OptionsSerializer.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { computed, makeObservable } from "mobx";
-
-interface Params {
- store: Store;
-}
-
-interface Store {
- /** @reactive */
- selected: Iterable;
-}
-
-export class OptionsSerializer {
- private store: Store;
-
- constructor(params: Params) {
- makeObservable(this, {
- value: computed
- });
-
- this.store = params.store;
- }
-
- get value(): string | undefined {
- const selected = [...this.store.selected];
- return this.toStorableValue(selected);
- }
-
- fromStorableValue(value: string | undefined): Iterable | undefined {
- if (!value) {
- return undefined;
- }
- return value.split(",");
- }
-
- toStorableValue(selected: string[]): string | undefined {
- if (selected.length > 0) {
- return selected.join(",");
- }
-
- return undefined;
- }
-}
diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/SearchStore.ts b/packages/shared/widget-plugin-filtering/src/stores/picker/SearchStore.ts
deleted file mode 100644
index fbbd726573..0000000000
--- a/packages/shared/widget-plugin-filtering/src/stores/picker/SearchStore.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { action, autorun, makeAutoObservable, runInAction } from "mobx";
-
-export class SearchStore {
- private delay: number;
- readonly disposers = [] as Array<() => void>;
- readonly defaultValue: string;
- value: string;
- buffer: string;
-
- constructor({ defaultValue = "", delay = 0 }: { defaultValue?: string; delay?: number } = {}) {
- this.defaultValue = defaultValue;
- this.buffer = this.value = this.defaultValue;
- this.delay = delay;
-
- makeAutoObservable(this, {
- setBuffer: action,
- clear: action,
- reset: action
- });
- }
-
- setBuffer(value: string): void {
- this.buffer = value;
- }
-
- reset(): void {
- this.buffer = this.value = this.defaultValue;
- }
-
- clear(): void {
- this.buffer = this.value = "";
- }
-
- setup(): () => void {
- this.disposers.push(
- autorun(
- () => {
- const value = this.buffer;
- runInAction(() => (this.value = value));
- },
- { delay: this.delay }
- )
- );
- return () => this.disposers.forEach(dispose => dispose());
- }
-}
diff --git a/packages/shared/widget-plugin-filtering/src/typings/FilterFunctions.ts b/packages/shared/widget-plugin-filtering/src/typings/FilterFunctions.ts
deleted file mode 100644
index 7fc85213c1..0000000000
--- a/packages/shared/widget-plugin-filtering/src/typings/FilterFunctions.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export type FilterFunctionNonValue = "empty" | "notEmpty";
-export type FilterFunctionGeneric = "equal" | "notEqual" | "greater" | "greaterEqual" | "smaller" | "smallerEqual";
-export type FilterFunctionBinary = "between"; // | "betweenEqRight" | "betweenEqLeft" | "betweenEqBoth";
-export type FilterFunctionString = "contains" | "startsWith" | "endsWith";
-
-export type AllFunctions = FilterFunctionNonValue | FilterFunctionGeneric | FilterFunctionBinary | FilterFunctionString;
diff --git a/packages/shared/widget-plugin-filtering/src/typings/FilterObserver.ts b/packages/shared/widget-plugin-filtering/src/typings/FilterObserver.ts
index c572ed5d38..f9a9a672ff 100644
--- a/packages/shared/widget-plugin-filtering/src/typings/FilterObserver.ts
+++ b/packages/shared/widget-plugin-filtering/src/typings/FilterObserver.ts
@@ -1,5 +1,5 @@
+import { FilterData, FiltersSettingsMap } from "@mendix/filter-commons/typings/settings";
import { FilterCondition } from "mendix/filters";
-import { FilterData, FiltersSettingsMap } from "./settings";
export interface Filter {
toJSON(): FilterData;
diff --git a/packages/shared/widget-plugin-filtering/src/typings/IJSActionsControlled.ts b/packages/shared/widget-plugin-filtering/src/typings/IJSActionsControlled.ts
deleted file mode 100644
index 50117d47ba..0000000000
--- a/packages/shared/widget-plugin-filtering/src/typings/IJSActionsControlled.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export interface IJSActionsControlled {
- handleResetValue: ResetHandler;
- handleSetValue: SetValueHandler;
-}
-
-export type ResetHandler = (useDefaultValue: boolean) => void;
-
-export type SetValueHandler = (
- useDefaultValue: boolean,
- params: { operators: any; stringValue: string; numberValue: Big.Big; dateTimeValue: Date; dateTimeValue2: Date }
-) => void;
diff --git a/packages/shared/widget-plugin-filtering/src/typings/InputFilterInterface.ts b/packages/shared/widget-plugin-filtering/src/typings/InputFilterInterface.ts
index c58a879fea..8063c3ae3e 100644
--- a/packages/shared/widget-plugin-filtering/src/typings/InputFilterInterface.ts
+++ b/packages/shared/widget-plugin-filtering/src/typings/InputFilterInterface.ts
@@ -4,7 +4,7 @@ import {
FilterFunctionGeneric,
FilterFunctionNonValue,
FilterFunctionString
-} from "./FilterFunctions";
+} from "@mendix/filter-commons/typings/FilterFunctions";
import {
ArgumentInterface,
DateArgumentInterface,
diff --git a/packages/shared/widget-plugin-filtering/src/typings/OptionListFilterInterface.ts b/packages/shared/widget-plugin-filtering/src/typings/OptionListFilterInterface.ts
deleted file mode 100644
index 22f50c16a3..0000000000
--- a/packages/shared/widget-plugin-filtering/src/typings/OptionListFilterInterface.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { OptionWithState } from "./OptionWithState";
-
-export interface CustomOption {
- caption: string;
- value: string;
-}
-
-export interface OptionListFilterInterface {
- type: "refselect" | "select";
- storeType: "optionlist" | "refselect" | "select";
- options: OptionWithState[];
- selected: string[];
- isLoading: boolean;
- hasMore: boolean;
- canSearch: boolean;
-
- replace(value: string[]): void;
- toggle(value: string): void;
- loadMore(): void;
- setSearch(term: string | undefined): void;
- isValidValue(value: string): boolean;
- reset(): void;
- clear(): void;
- UNSAFE_setDefaults(value?: string[]): void;
- setup?(): () => void | void;
- setCustomOptions(options: CustomOption[]): void;
-}
diff --git a/packages/shared/widget-plugin-filtering/src/typings/OptionWithState.ts b/packages/shared/widget-plugin-filtering/src/typings/OptionWithState.ts
deleted file mode 100644
index 084554c218..0000000000
--- a/packages/shared/widget-plugin-filtering/src/typings/OptionWithState.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface OptionWithState {
- caption: string;
- value: string;
- selected: boolean;
-}
diff --git a/packages/shared/widget-plugin-filtering/src/typings/PickerFilterStore.ts b/packages/shared/widget-plugin-filtering/src/typings/PickerFilterStore.ts
deleted file mode 100644
index d64f165603..0000000000
--- a/packages/shared/widget-plugin-filtering/src/typings/PickerFilterStore.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { RefFilterStore } from "../stores/picker/RefFilterStore";
-import { StaticSelectFilterStore } from "../stores/picker/StaticSelectFilterStore";
-
-export type PickerFilterStore = RefFilterStore | StaticSelectFilterStore;
diff --git a/packages/shared/widget-plugin-filtering/src/typings/mendix.ts b/packages/shared/widget-plugin-filtering/src/typings/mendix.ts
deleted file mode 100644
index 2544d74ee0..0000000000
--- a/packages/shared/widget-plugin-filtering/src/typings/mendix.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { FilterCondition } from "mendix/filters";
-import { FnName } from "./type-utils";
-
-export type FilterFunction = FnName;
diff --git a/packages/shared/widget-plugin-filtering/src/typings/settings.ts b/packages/shared/widget-plugin-filtering/src/typings/settings.ts
deleted file mode 100644
index 1b6cb7a373..0000000000
--- a/packages/shared/widget-plugin-filtering/src/typings/settings.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { AllFunctions } from "./FilterFunctions";
-
-export type InputData = [Fn, string | null, string | null];
-
-export type SelectData = string[];
-
-export type FilterData = InputData | SelectData | null | undefined;
-
-export type FiltersSettingsMap = Map;
diff --git a/packages/shared/widget-plugin-filtering/src/typings/type-utils.ts b/packages/shared/widget-plugin-filtering/src/typings/type-utils.ts
deleted file mode 100644
index a61d66ebf5..0000000000
--- a/packages/shared/widget-plugin-filtering/src/typings/type-utils.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export type FnName = T extends { name: infer Name } ? Name : never;
-
-export type Dispose = () => void;
-
-export type GConstructor = new (...args: any[]) => T;
diff --git a/packages/shared/widget-plugin-filtering/tsconfig.json b/packages/shared/widget-plugin-filtering/tsconfig.json
index 052cc1cee7..53827483c5 100644
--- a/packages/shared/widget-plugin-filtering/tsconfig.json
+++ b/packages/shared/widget-plugin-filtering/tsconfig.json
@@ -1,6 +1,6 @@
{
"extends": "@mendix/tsconfig-web-widgets/esm-library-with-jsx",
- "include": ["./src/**/*"],
+ "include": ["./src/**/*", "../widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts"],
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
From 2fa5a14fbb25b38fc2289a3b919fac9c631f5a34 Mon Sep 17 00:00:00 2001
From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com>
Date: Fri, 18 Apr 2025 10:27:43 +0200
Subject: [PATCH 04/29] chore: move tests
---
.../__mocks__/mendix/filters/builders.js | 1 +
.../shared/filter-commons/jest.config.cjs | 26 +++++++++++++++++++
packages/shared/filter-commons/package.json | 4 ++-
.../src/__tests__/condition-utils.spec.ts | 0
4 files changed, 30 insertions(+), 1 deletion(-)
create mode 100644 packages/shared/filter-commons/__mocks__/mendix/filters/builders.js
create mode 100644 packages/shared/filter-commons/jest.config.cjs
rename packages/shared/{widget-plugin-filtering => filter-commons}/src/__tests__/condition-utils.spec.ts (100%)
diff --git a/packages/shared/filter-commons/__mocks__/mendix/filters/builders.js b/packages/shared/filter-commons/__mocks__/mendix/filters/builders.js
new file mode 100644
index 0000000000..28fe810c06
--- /dev/null
+++ b/packages/shared/filter-commons/__mocks__/mendix/filters/builders.js
@@ -0,0 +1 @@
+module.exports = require("@mendix/widget-plugin-test-utils/__mocks__/mendix/filters/builders.js");
diff --git a/packages/shared/filter-commons/jest.config.cjs b/packages/shared/filter-commons/jest.config.cjs
new file mode 100644
index 0000000000..3f605bedaa
--- /dev/null
+++ b/packages/shared/filter-commons/jest.config.cjs
@@ -0,0 +1,26 @@
+module.exports = {
+ modulePathIgnorePatterns: ["/dist/"],
+ transform: {
+ "^.+\\.(t|j)sx?$": [
+ "@swc/jest",
+ {
+ jsc: {
+ transform: {
+ react: {
+ runtime: "automatic"
+ }
+ }
+ }
+ }
+ ]
+ },
+ moduleDirectories: ["node_modules", "src"],
+ moduleNameMapper: {
+ "big.js": "big.js",
+ "(.+)\\.js": "$1"
+ },
+ extensionsToTreatAsEsm: [".ts"],
+ testEnvironment: "jsdom",
+ collectCoverage: !process.env.CI,
+ coverageProvider: "v8"
+};
diff --git a/packages/shared/filter-commons/package.json b/packages/shared/filter-commons/package.json
index 4efaf21ebf..b12356a45b 100644
--- a/packages/shared/filter-commons/package.json
+++ b/packages/shared/filter-commons/package.json
@@ -28,7 +28,8 @@
"dev": "tsc --watch",
"format": "prettier --write .",
"lint": "eslint src/ package.json",
- "prepare": "tsc"
+ "prepare": "tsc",
+ "test": "jest"
},
"dependencies": {
"mendix": "^10.16.49747"
@@ -41,6 +42,7 @@
"@mendix/eslint-config-web-widgets": "workspace:*",
"@mendix/prettier-config-web-widgets": "workspace:*",
"@mendix/tsconfig-web-widgets": "workspace:*",
+ "@mendix/widget-plugin-test-utils": "workspace:*",
"@swc/core": "^1.7.26"
}
}
diff --git a/packages/shared/widget-plugin-filtering/src/__tests__/condition-utils.spec.ts b/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/__tests__/condition-utils.spec.ts
rename to packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts
From e9922327f2db0295d0a01e28e6dd565d1df683bc Mon Sep 17 00:00:00 2001
From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com>
Date: Fri, 18 Apr 2025 11:28:57 +0200
Subject: [PATCH 05/29] build: fix all type issues
---
.../package.json | 1 +
.../src/__tests__/RefFilterStore.spec.ts | 2 +-
.../src/controls/base/ClearButton.tsx | 0
.../src/controls/base/OptionsWrapper.tsx | 0
.../src/controls/combobox/Combobox.tsx | 0
.../src/controls/hooks/useFloatingMenu.tsx | 0
.../src/controls/picker-primitives.tsx | 0
.../src/controls/select/Select.tsx | 0
.../src/controls/tag-picker/TagPicker.tsx | 0
.../widget-plugin-filtering/package.json | 3 +-
.../__tests__/DateInputFilterStore.spec.ts | 8 +-
.../input/NumberInputController.ts | 6 +-
.../input/StringInputController.ts | 10 +-
.../controls/filter-selector/useSelect.tsx | 2 +-
.../src/controls/input/InputWithFilters.tsx | 2 +-
.../src/controls/input/typings.ts | 2 +-
.../custom-filter-api/BaseStoreProvider.ts | 2 +-
.../DropdownStoreProvider.ts | 42 ---
.../src/custom-filter-api/index.ts | 6 -
.../src/helpers/useDateSync.ts | 6 +-
.../src/helpers/useNumberFilterController.ts | 2 +-
.../src/helpers/usePickerJSActions.ts | 2 +-
.../src/helpers/useSelectFilterAPI.ts | 2 +-
.../src/helpers/useStringFilterController.ts | 2 +-
.../src/providers/LegacyPv.ts | 6 +-
.../src/stores/generic/CustomFilterHost.ts | 4 +-
.../src/stores/generic/HeaderFiltersStore.ts | 2 +-
.../src/stores/input/BaseInputFilterStore.ts | 4 +-
.../src/stores/input/DateInputFilterStore.ts | 21 +-
.../stores/input/NumberInputFilterStore.ts | 10 +-
.../stores/input/StringInputFilterStore.ts | 12 +-
.../src/stores/input/fn-mappers.ts | 10 +-
.../src/stores/input/store-utils.ts | 2 +-
.../src/stores/picker/RefFilterStore.ts | 250 ------------------
.../stores/picker/StaticSelectFilterStore.ts | 183 -------------
.../src/stores/utils/is-input-data.ts | 24 --
.../widget-plugin-filtering/tsconfig.json | 2 +-
.../widget-plugin-test-utils/package.json | 2 +-
38 files changed, 77 insertions(+), 555 deletions(-)
rename packages/shared/{widget-plugin-filtering => widget-plugin-dropdown-filter}/src/__tests__/RefFilterStore.spec.ts (99%)
rename packages/shared/{widget-plugin-filtering => widget-plugin-dropdown-filter}/src/controls/base/ClearButton.tsx (100%)
rename packages/shared/{widget-plugin-filtering => widget-plugin-dropdown-filter}/src/controls/base/OptionsWrapper.tsx (100%)
rename packages/shared/{widget-plugin-filtering => widget-plugin-dropdown-filter}/src/controls/combobox/Combobox.tsx (100%)
rename packages/shared/{widget-plugin-filtering => widget-plugin-dropdown-filter}/src/controls/hooks/useFloatingMenu.tsx (100%)
rename packages/shared/{widget-plugin-filtering => widget-plugin-dropdown-filter}/src/controls/picker-primitives.tsx (100%)
rename packages/shared/{widget-plugin-filtering => widget-plugin-dropdown-filter}/src/controls/select/Select.tsx (100%)
rename packages/shared/{widget-plugin-filtering => widget-plugin-dropdown-filter}/src/controls/tag-picker/TagPicker.tsx (100%)
delete mode 100644 packages/shared/widget-plugin-filtering/src/custom-filter-api/DropdownStoreProvider.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/custom-filter-api/index.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/stores/picker/RefFilterStore.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/stores/picker/StaticSelectFilterStore.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/stores/utils/is-input-data.ts
diff --git a/packages/shared/widget-plugin-dropdown-filter/package.json b/packages/shared/widget-plugin-dropdown-filter/package.json
index a211bbd560..41ffc4217b 100644
--- a/packages/shared/widget-plugin-dropdown-filter/package.json
+++ b/packages/shared/widget-plugin-dropdown-filter/package.json
@@ -29,6 +29,7 @@
"dependencies": {
"@mendix/widget-plugin-mobx-kit": "workspace:^",
"downshift": "^9.0.9",
+ "mendix": "^10.21.64362",
"mobx": "6.12.3",
"mobx-react-lite": "4.0.7"
},
diff --git a/packages/shared/widget-plugin-filtering/src/__tests__/RefFilterStore.spec.ts b/packages/shared/widget-plugin-dropdown-filter/src/__tests__/RefFilterStore.spec.ts
similarity index 99%
rename from packages/shared/widget-plugin-filtering/src/__tests__/RefFilterStore.spec.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/__tests__/RefFilterStore.spec.ts
index de11ebf61e..8ade5c23e8 100644
--- a/packages/shared/widget-plugin-filtering/src/__tests__/RefFilterStore.spec.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/__tests__/RefFilterStore.spec.ts
@@ -1,7 +1,7 @@
import { cases, list, listExpression, listReference, ListValueBuilder, obj } from "@mendix/widget-plugin-test-utils";
import { ObjectItem } from "mendix";
import { _resetGlobalState, autorun } from "mobx";
-import { RefFilterStore, RefFilterStoreProps } from "../stores/picker/RefFilterStore";
+import { RefFilterStore, RefFilterStoreProps } from "../stores/RefFilterStore";
describe("RefFilterStore", () => {
afterEach(() => _resetGlobalState());
diff --git a/packages/shared/widget-plugin-filtering/src/controls/base/ClearButton.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/base/ClearButton.tsx
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/controls/base/ClearButton.tsx
rename to packages/shared/widget-plugin-dropdown-filter/src/controls/base/ClearButton.tsx
diff --git a/packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/base/OptionsWrapper.tsx
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx
rename to packages/shared/widget-plugin-dropdown-filter/src/controls/base/OptionsWrapper.tsx
diff --git a/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/combobox/Combobox.tsx
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx
rename to packages/shared/widget-plugin-dropdown-filter/src/controls/combobox/Combobox.tsx
diff --git a/packages/shared/widget-plugin-filtering/src/controls/hooks/useFloatingMenu.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/hooks/useFloatingMenu.tsx
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/controls/hooks/useFloatingMenu.tsx
rename to packages/shared/widget-plugin-dropdown-filter/src/controls/hooks/useFloatingMenu.tsx
diff --git a/packages/shared/widget-plugin-filtering/src/controls/picker-primitives.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/picker-primitives.tsx
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/controls/picker-primitives.tsx
rename to packages/shared/widget-plugin-dropdown-filter/src/controls/picker-primitives.tsx
diff --git a/packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/select/Select.tsx
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx
rename to packages/shared/widget-plugin-dropdown-filter/src/controls/select/Select.tsx
diff --git a/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/tag-picker/TagPicker.tsx
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx
rename to packages/shared/widget-plugin-dropdown-filter/src/controls/tag-picker/TagPicker.tsx
diff --git a/packages/shared/widget-plugin-filtering/package.json b/packages/shared/widget-plugin-filtering/package.json
index 17597b8ddd..8f077978c2 100644
--- a/packages/shared/widget-plugin-filtering/package.json
+++ b/packages/shared/widget-plugin-filtering/package.json
@@ -28,7 +28,6 @@
"dev": "tsc --watch",
"format": "prettier --write .",
"lint": "eslint src/ package.json",
- "prepare": "tsc",
"test": "jest"
},
"dependencies": {
@@ -41,7 +40,7 @@
"@mendix/widget-plugin-mobx-kit": "workspace:^",
"@mendix/widget-plugin-platform": "workspace:*",
"downshift": "^9.0.8",
- "mendix": "^10.16.49747",
+ "mendix": "^10.21.64362",
"mobx": "6.12.3",
"mobx-react-lite": "4.0.7"
},
diff --git a/packages/shared/widget-plugin-filtering/src/__tests__/DateInputFilterStore.spec.ts b/packages/shared/widget-plugin-filtering/src/__tests__/DateInputFilterStore.spec.ts
index 25ba577ab6..b54f27e74a 100644
--- a/packages/shared/widget-plugin-filtering/src/__tests__/DateInputFilterStore.spec.ts
+++ b/packages/shared/widget-plugin-filtering/src/__tests__/DateInputFilterStore.spec.ts
@@ -1,5 +1,5 @@
jest.mock("mendix/filters/builders");
-import { attrId, listAttr } from "@mendix/widget-plugin-test-utils";
+import { attrId, listAttribute } from "@mendix/widget-plugin-test-utils";
import { ListAttributeValue } from "mendix";
import {
and,
@@ -28,7 +28,7 @@ describe("DateInputFilterStore", () => {
let store: DateInputFilterStore;
beforeEach(() => {
- attr = listAttr(() => new Date());
+ attr = listAttribute(() => new Date());
attr.id = attrId("attr_unset");
store = new DateInputFilterStore([attr], null);
});
@@ -125,7 +125,7 @@ describe("DateInputFilterStore", () => {
});
it("uses 'or' when have multiple attributes", () => {
- const [attr1, attr2] = [listAttr(() => new Date()), listAttr(() => new Date())];
+ const [attr1, attr2] = [listAttribute(() => new Date()), listAttribute(() => new Date())];
store = new DateInputFilterStore([attr1, attr2], null);
const date1 = new Date("2024-09-17T01:01:01.000Z");
store.filterFunction = "equal";
@@ -146,7 +146,7 @@ describe("DateInputFilterStore", () => {
let store: DateInputFilterStore;
beforeEach(() => {
- attr = listAttr(() => new Date());
+ attr = listAttribute(() => new Date());
store = new DateInputFilterStore([attr], null);
expect(store.filterFunction).toBe("equal");
});
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/input/NumberInputController.ts b/packages/shared/widget-plugin-filtering/src/controllers/input/NumberInputController.ts
index 3edbd1e4f9..685846dacc 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/input/NumberInputController.ts
+++ b/packages/shared/widget-plugin-filtering/src/controllers/input/NumberInputController.ts
@@ -1,8 +1,12 @@
+import {
+ FilterFunctionBinary,
+ FilterFunctionGeneric,
+ FilterFunctionNonValue
+} from "@mendix/filter-commons/typings/FilterFunctions";
import { debounce } from "@mendix/widget-plugin-platform/utils/debounce";
import { action, autorun, computed, makeObservable, reaction, runInAction } from "mobx";
import { createRef } from "react";
import { InputStore } from "../../stores/input/InputStore";
-import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue } from "../../typings/FilterFunctions";
import { FilterV, Number_InputFilterInterface } from "../../typings/InputFilterInterface";
export type Params = {
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/input/StringInputController.ts b/packages/shared/widget-plugin-filtering/src/controllers/input/StringInputController.ts
index 91aef3be74..6f58b6730b 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/input/StringInputController.ts
+++ b/packages/shared/widget-plugin-filtering/src/controllers/input/StringInputController.ts
@@ -1,13 +1,13 @@
-import { debounce } from "@mendix/widget-plugin-platform/utils/debounce";
-import { action, autorun, computed, makeObservable, reaction, runInAction } from "mobx";
-import { createRef } from "react";
-import { InputStore } from "../../stores/input/InputStore";
import {
FilterFunctionBinary,
FilterFunctionGeneric,
FilterFunctionNonValue,
FilterFunctionString
-} from "../../typings/FilterFunctions";
+} from "@mendix/filter-commons/typings/FilterFunctions";
+import { debounce } from "@mendix/widget-plugin-platform/utils/debounce";
+import { action, autorun, computed, makeObservable, reaction, runInAction } from "mobx";
+import { createRef } from "react";
+import { InputStore } from "../../stores/input/InputStore";
import { FilterV, String_InputFilterInterface } from "../../typings/InputFilterInterface";
export type Params = {
diff --git a/packages/shared/widget-plugin-filtering/src/controls/filter-selector/useSelect.tsx b/packages/shared/widget-plugin-filtering/src/controls/filter-selector/useSelect.tsx
index c589f08c33..c8e901ea87 100644
--- a/packages/shared/widget-plugin-filtering/src/controls/filter-selector/useSelect.tsx
+++ b/packages/shared/widget-plugin-filtering/src/controls/filter-selector/useSelect.tsx
@@ -36,7 +36,7 @@ export function useSelect(props: useSelectProps): ViewProps {
items: props.options,
selectedItem,
itemToString,
- onSelectedItemChange: ({ selectedItem }) => props.onSelect(selectedItem.value)
+ onSelectedItemChange: ({ selectedItem }) => props.onSelect(selectedItem?.value ?? null)
});
const { refs, floatingStyles } = useFloating({
diff --git a/packages/shared/widget-plugin-filtering/src/controls/input/InputWithFilters.tsx b/packages/shared/widget-plugin-filtering/src/controls/input/InputWithFilters.tsx
index dd779c096f..5c203b23bf 100644
--- a/packages/shared/widget-plugin-filtering/src/controls/input/InputWithFilters.tsx
+++ b/packages/shared/widget-plugin-filtering/src/controls/input/InputWithFilters.tsx
@@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
import classNames from "classnames";
import { FilterSelector } from "../filter-selector/FilterSelector";
import { InputComponentProps } from "./typings";
-import { AllFunctions } from "../../typings/FilterFunctions";
+import { AllFunctions } from "@mendix/filter-commons/typings/FilterFunctions";
export function InputWithFiltersComponent(props: InputComponentProps): React.ReactElement {
const {
diff --git a/packages/shared/widget-plugin-filtering/src/controls/input/typings.ts b/packages/shared/widget-plugin-filtering/src/controls/input/typings.ts
index becf0608be..be875f3474 100644
--- a/packages/shared/widget-plugin-filtering/src/controls/input/typings.ts
+++ b/packages/shared/widget-plugin-filtering/src/controls/input/typings.ts
@@ -1,5 +1,5 @@
+import { AllFunctions } from "@mendix/filter-commons/typings/FilterFunctions";
import { InputStore } from "../../stores/input/InputStore";
-import { AllFunctions } from "../../typings/FilterFunctions";
import { InputFilterInterface } from "../../typings/InputFilterInterface";
export interface BaseProps {
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 883290b619..7cd0dfc276 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,7 @@
+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 { isAnd, isTag } from "../condition-utils";
import { FilterAPI } from "../context";
import { Filter } from "../typings/FilterObserver";
diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/DropdownStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/DropdownStoreProvider.ts
deleted file mode 100644
index abeef3dd15..0000000000
--- a/packages/shared/widget-plugin-filtering/src/custom-filter-api/DropdownStoreProvider.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { ListAttributeValue } from "mendix";
-import { FilterAPI } from "../context";
-import { StaticSelectFilterStore } from "../stores/picker/StaticSelectFilterStore";
-import { PickerFilterStore } from "../typings/PickerFilterStore";
-import { BaseStoreProvider } from "./BaseStoreProvider";
-import { FilterSpec } from "./typings";
-
-export class DropdownStoreProvider extends BaseStoreProvider {
- protected _store: StaticSelectFilterStore;
- protected filterAPI: FilterAPI;
- readonly dataKey: string;
-
- constructor(filterAPI: FilterAPI, spec: FilterSpec) {
- super();
- this.filterAPI = filterAPI;
- this.dataKey = spec.dataKey;
-
- // Convert AttributeMetaData to ListAttributeValue
- const attributes = spec.attributes.map(attr => {
- const defaultFormatter = {
- format: (value: any) => String(value),
- parse: (value: string) => ({ valid: true, value })
- };
-
- return {
- ...attr,
- isList: false,
- get: (obj: any) => obj[attr.id],
- formatter: defaultFormatter
- } as ListAttributeValue;
- });
-
- this._store = new StaticSelectFilterStore(
- attributes,
- this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey)
- );
- }
-
- get store(): PickerFilterStore {
- return this._store;
- }
-}
diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/index.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/index.ts
deleted file mode 100644
index 45c14f0e4d..0000000000
--- a/packages/shared/widget-plugin-filtering/src/custom-filter-api/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export * from "./BaseStoreProvider";
-export * from "./DateStoreProvider";
-export * from "./DropdownStoreProvider";
-export * from "./NumberStoreProvider";
-export * from "./StringStoreProvider";
-export * from "./typings";
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useDateSync.ts b/packages/shared/widget-plugin-filtering/src/helpers/useDateSync.ts
index cc09e597a3..b1363eb67a 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useDateSync.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useDateSync.ts
@@ -1,7 +1,11 @@
+import {
+ FilterFunctionBinary,
+ FilterFunctionGeneric,
+ FilterFunctionNonValue
+} from "@mendix/filter-commons/typings/FilterFunctions";
import { ActionValue, EditableValue } from "mendix";
import { reaction } from "mobx";
import { useEffect, useRef } from "react";
-import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue } from "../typings/FilterFunctions";
import { InputFilterInterface } from "../typings/InputFilterInterface";
interface Props {
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts b/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts
index 9100b1fd16..96d710c6b1 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts
@@ -1,7 +1,7 @@
+import { AllFunctions } from "@mendix/filter-commons/typings/FilterFunctions";
import { useEffect, useState } from "react";
import { NumberFilterController, Params } from "../controllers/input/NumberInputController";
import { ArgumentInterface } from "../typings/ArgumentInterface";
-import { AllFunctions } from "../typings/FilterFunctions";
import { FilterFn, InputFilterBaseInterface } from "../typings/InputFilterInterface";
export function useNumberFilterController<
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/usePickerJSActions.ts b/packages/shared/widget-plugin-filtering/src/helpers/usePickerJSActions.ts
index ee7a00dc1a..6c76d13a76 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/usePickerJSActions.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/usePickerJSActions.ts
@@ -1,5 +1,5 @@
+import { IJSActionsControlled } from "@mendix/filter-commons/typings/IJSActionsControlled";
import { useOnResetValueEvent, useOnSetValueEvent } from "@mendix/widget-plugin-external-events/hooks";
-import { IJSActionsControlled } from "../typings/IJSActionsControlled";
export function usePickerJSActions(
controller: IJSActionsControlled,
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts b/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts
index 24c7d9fa57..f27fd6d6cd 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts
@@ -1,8 +1,8 @@
+import { PickerFilterStore } from "@mendix/widget-plugin-dropdown-filter/typings/PickerFilterStore";
import { useRef } from "react";
import { FilterType, getFilterStore, useFilterContextValue } from "../context";
import { APIError, EMISSINGSTORE, EStoreTypeMisMatch, OPTIONS_NOT_FILTERABLE } from "../errors";
import { Result, error, value } from "../result-meta";
-import { PickerFilterStore } from "../typings/PickerFilterStore";
export interface Select_FilterAPIv2 {
filterStore: PickerFilterStore;
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts b/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts
index 35c26ad8c4..dbe9e83fb7 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts
@@ -1,7 +1,7 @@
+import { AllFunctions } from "@mendix/filter-commons/typings/FilterFunctions";
import { useEffect, useState } from "react";
import { Params, StringFilterController } from "../controllers/input/StringInputController";
import { ArgumentInterface } from "../typings/ArgumentInterface";
-import { AllFunctions } from "../typings/FilterFunctions";
import { FilterFn, InputFilterBaseInterface } from "../typings/InputFilterInterface";
export function useStringFilterController<
diff --git a/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts b/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts
index d9cb4f3b14..d81a80e6c0 100644
--- a/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts
+++ b/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts
@@ -1,3 +1,6 @@
+import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings";
+import { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore";
+import { PickerFilterStore } from "@mendix/widget-plugin-dropdown-filter/typings/PickerFilterStore";
import { ListAttributeValue } from "mendix";
import { FilterCondition } from "mendix/filters";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
@@ -5,10 +8,7 @@ import { FilterType as Ft, LegacyProvider } from "../context";
import { DateInputFilterStore } from "../stores/input/DateInputFilterStore";
import { NumberInputFilterStore } from "../stores/input/NumberInputFilterStore";
import { StringInputFilterStore } from "../stores/input/StringInputFilterStore";
-import { StaticSelectFilterStore } from "../stores/picker/StaticSelectFilterStore";
import { InputFilterInterface } from "../typings/InputFilterInterface";
-import { PickerFilterStore } from "../typings/PickerFilterStore";
-import { FiltersSettingsMap } from "../typings/settings";
type FilterMap = {
[Ft.STRING]: StringInputFilterStore | null;
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 b798603b73..a999e31901 100644
--- a/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts
+++ b/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts
@@ -1,9 +1,9 @@
+import { tag } from "@mendix/filter-commons/condition-utils";
+import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings";
import { FilterCondition } from "mendix/filters";
import { and } from "mendix/filters/builders";
import { autorun, makeAutoObservable } from "mobx";
-import { tag } from "../../condition-utils";
import { Filter, FilterObserver } from "../../typings/FilterObserver";
-import { FiltersSettingsMap } from "../../typings/settings";
export class CustomFilterHost implements FilterObserver {
private filters: Map = new Map();
diff --git a/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts b/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts
index f627172731..c09a1d540e 100644
--- a/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts
+++ b/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts
@@ -1,3 +1,4 @@
+import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings";
import { ListAttributeValue } from "mendix";
import { FilterCondition } from "mendix/filters";
import { computed, makeObservable } from "mobx";
@@ -6,7 +7,6 @@ import { APIError } from "../../errors";
import { LegacyPv } from "../../providers/LegacyPv";
import { Result, value } from "../../result-meta";
import { FilterObserver } from "../../typings/FilterObserver";
-import { FiltersSettingsMap } from "../../typings/settings";
export interface FilterListType {
filter: ListAttributeValue;
diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts
index 08c21e5cfc..f599e940cf 100644
--- a/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts
+++ b/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts
@@ -1,3 +1,5 @@
+import { AllFunctions } from "@mendix/filter-commons/typings/FilterFunctions";
+import { FilterData, InputData } from "@mendix/filter-commons/typings/settings";
import { Big } from "big.js";
import { AttributeMetaData } from "mendix";
import { FilterCondition } from "mendix/filters";
@@ -17,8 +19,6 @@ import {
startsWith
} from "mendix/filters/builders";
import { action, computed, makeObservable, observable } from "mobx";
-import { AllFunctions } from "../../typings/FilterFunctions";
-import { FilterData, InputData } from "../../typings/settings";
import { Argument } from "./Argument";
type StateTuple = [Fn] | [Fn, V] | [Fn, V, V];
diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts
index 7a8807a803..b7fbcfcbf5 100644
--- a/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts
+++ b/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts
@@ -1,3 +1,18 @@
+import {
+ betweenToState,
+ isAnd,
+ isEmptyExp,
+ isNotEmptyExp,
+ isOr,
+ singularToState
+} from "@mendix/filter-commons/condition-utils";
+import {
+ FilterFunctionBinary,
+ FilterFunctionGeneric,
+ FilterFunctionNonValue
+} from "@mendix/filter-commons/typings/FilterFunctions";
+import { FilterName } from "@mendix/filter-commons/typings/mendix";
+import { FilterData, InputData } from "@mendix/filter-commons/typings/settings";
import { AttributeMetaData, DateTimeFormatter, ListAttributeValue, SimpleFormatter } from "mendix";
import { AndCondition, FilterCondition, LiteralExpression } from "mendix/filters";
import {
@@ -15,11 +30,7 @@ import {
or
} from "mendix/filters/builders";
import { action, comparer, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
-import { betweenToState, isAnd, isEmptyExp, isNotEmptyExp, isOr, singularToState } from "../../condition-utils";
-import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue } from "../../typings/FilterFunctions";
import { Date_InputFilterInterface } from "../../typings/InputFilterInterface";
-import { FilterFunction } from "../../typings/mendix";
-import { FilterData, InputData } from "../../typings/settings";
import { DateArgument } from "./Argument";
import { BaseInputFilterStore } from "./BaseInputFilterStore";
@@ -249,7 +260,7 @@ export class DateInputFilterStore
}
}
- private mapFn = (name: FilterFunction | "between" | "empty" | "notEmpty"): DateFns => {
+ private mapFn = (name: FilterName | "between" | "empty" | "notEmpty"): DateFns => {
switch (name) {
case "day:=":
return "equal";
diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts
index 18a302d02d..7985c66ba9 100644
--- a/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts
+++ b/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts
@@ -1,11 +1,15 @@
+import { inputStateFromCond } from "@mendix/filter-commons/condition-utils";
+import {
+ FilterFunctionBinary,
+ FilterFunctionGeneric,
+ FilterFunctionNonValue
+} from "@mendix/filter-commons/typings/FilterFunctions";
+import { FilterData, InputData } from "@mendix/filter-commons/typings/settings";
import { Big } from "big.js";
import { AttributeMetaData, ListAttributeValue, SimpleFormatter } from "mendix";
import { FilterCondition } from "mendix/filters";
import { action, comparer, makeObservable } from "mobx";
-import { inputStateFromCond } from "../../condition-utils";
-import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue } from "../../typings/FilterFunctions";
import { Number_InputFilterInterface } from "../../typings/InputFilterInterface";
-import { FilterData, InputData } from "../../typings/settings";
import { NumberArgument } from "./Argument";
import { BaseInputFilterStore } from "./BaseInputFilterStore";
import { baseNames } from "./fn-mappers";
diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts
index 31c720798b..3bae4046c5 100644
--- a/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts
+++ b/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts
@@ -1,15 +1,15 @@
-import { AttributeMetaData, ListAttributeValue, SimpleFormatter } from "mendix";
-import { FilterCondition } from "mendix/filters";
-import { action, comparer, makeObservable } from "mobx";
-import { inputStateFromCond } from "../../condition-utils";
+import { inputStateFromCond } from "@mendix/filter-commons/condition-utils";
import {
FilterFunctionBinary,
FilterFunctionGeneric,
FilterFunctionNonValue,
FilterFunctionString
-} from "../../typings/FilterFunctions";
+} from "@mendix/filter-commons/typings/FilterFunctions";
+import { FilterData, InputData } from "@mendix/filter-commons/typings/settings";
+import { AttributeMetaData, ListAttributeValue, SimpleFormatter } from "mendix";
+import { FilterCondition } from "mendix/filters";
+import { action, comparer, makeObservable } from "mobx";
import { String_InputFilterInterface } from "../../typings/InputFilterInterface";
-import { FilterData, InputData } from "../../typings/settings";
import { StringArgument } from "./Argument";
import { BaseInputFilterStore } from "./BaseInputFilterStore";
import { baseNames } from "./fn-mappers";
diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/fn-mappers.ts b/packages/shared/widget-plugin-filtering/src/stores/input/fn-mappers.ts
index 71894c6d81..8ad3c58361 100644
--- a/packages/shared/widget-plugin-filtering/src/stores/input/fn-mappers.ts
+++ b/packages/shared/widget-plugin-filtering/src/stores/input/fn-mappers.ts
@@ -1,8 +1,12 @@
-import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue } from "../../typings/FilterFunctions";
-import { FilterFunction } from "../../typings/mendix";
+import {
+ FilterFunctionBinary,
+ FilterFunctionGeneric,
+ FilterFunctionNonValue
+} from "@mendix/filter-commons/typings/FilterFunctions";
+import { FilterName } from "@mendix/filter-commons/typings/mendix";
export function baseNames(
- fn: FilterFunction | "between" | "empty" | "notEmpty"
+ fn: FilterName | "between" | "empty" | "notEmpty"
): FilterFunctionGeneric | FilterFunctionNonValue | FilterFunctionBinary {
switch (fn) {
case "=":
diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/store-utils.ts b/packages/shared/widget-plugin-filtering/src/stores/input/store-utils.ts
index 63f382dea3..542391c3eb 100644
--- a/packages/shared/widget-plugin-filtering/src/stores/input/store-utils.ts
+++ b/packages/shared/widget-plugin-filtering/src/stores/input/store-utils.ts
@@ -1,3 +1,4 @@
+import { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore";
import { ListAttributeValue } from "mendix";
import { FilterCondition } from "mendix/filters";
import {
@@ -9,7 +10,6 @@ import {
import { DateInputFilterStore } from "../input/DateInputFilterStore";
import { NumberInputFilterStore } from "../input/NumberInputFilterStore";
import { StringInputFilterStore } from "../input/StringInputFilterStore";
-import { StaticSelectFilterStore } from "../picker/StaticSelectFilterStore";
export type InputFilterStore = StringInputFilterStore | NumberInputFilterStore | DateInputFilterStore;
diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/RefFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/picker/RefFilterStore.ts
deleted file mode 100644
index c545fbb634..0000000000
--- a/packages/shared/widget-plugin-filtering/src/stores/picker/RefFilterStore.ts
+++ /dev/null
@@ -1,250 +0,0 @@
-import { ListAttributeValue, ListReferenceSetValue, ListReferenceValue, ListValue, ObjectItem } from "mendix";
-import { ContainsCondition, EqualsCondition, FilterCondition, LiteralExpression } from "mendix/filters";
-import { association, attribute, contains, empty, equals, literal, or } from "mendix/filters/builders";
-import { action, autorun, computed, makeObservable, observable, reaction, runInAction, when } from "mobx";
-import { flattenRefCond, selectedFromCond } from "../../condition-utils";
-import { disposeFx } from "../../mobx-utils";
-import { OptionWithState } from "../../typings/OptionWithState";
-import { BaseSelectStore } from "./BaseSelectStore";
-import { SearchStore } from "./SearchStore";
-
-type ListAttributeId = ListAttributeValue["id"];
-
-export interface RefFilterStoreProps {
- ref: ListReferenceValue | ListReferenceSetValue;
- datasource: ListValue;
- searchAttrId?: ListAttributeId;
- fetchOptionsLazy?: boolean;
- caption: CaptionAccessor;
-}
-
-interface CaptionAccessor {
- get: (obj: ObjectItem) => { value: string | undefined };
-}
-
-export class RefFilterStore extends BaseSelectStore {
- readonly storeType = "refselect";
- readonly optionsFilterable: boolean;
-
- private datasource: ListValue;
- private listRef: ListReferenceValue | ListReferenceSetValue;
- private caption: CaptionAccessor;
- private searchAttrId?: ListAttributeId;
- /**
- * As Ref filter fetch options lazily,
- * we just keep condition and
- * return it if options not loaded yet.
- */
- private readonly initCondArray: Array;
- private readonly pageSize = 20;
- private readonly searchSize = 100;
- private fetchReady = false;
-
- selectedItems: ObjectItem[] = [];
- lazyMode: boolean;
- search: SearchStore;
-
- constructor(props: RefFilterStoreProps, initCond: FilterCondition | null) {
- super();
- this.caption = props.caption;
- this.datasource = props.datasource;
- this.listRef = props.ref;
- this.lazyMode = props.fetchOptionsLazy ?? true;
- this.searchAttrId = props.searchAttrId;
- this.initCondArray = initCond ? flattenRefCond(initCond) : [];
- this.search = new SearchStore();
- this.optionsFilterable = !!this.searchAttrId;
-
- if (this.lazyMode) {
- this.datasource.setLimit(0);
- }
-
- makeObservable(this, {
- datasource: observable.ref,
- listRef: observable.ref,
- caption: observable.ref,
- searchAttrId: observable.ref,
- options: computed,
- hasMore: computed,
- isLoading: computed,
- condition: computed,
- updateProps: action,
- fromViewState: action,
- fetchReady: observable,
- setFetchReady: action,
- setDefaultSelected: action,
- selectedItems: observable.struct,
- selectedOptions: computed
- });
-
- if (initCond) {
- this.fromViewState(initCond);
- }
- }
-
- get hasMore(): boolean {
- return this.datasource.hasMoreItems ?? false;
- }
-
- get isLoading(): boolean {
- return this.datasource.status === "loading";
- }
-
- get options(): OptionWithState[] {
- const items = this.datasource.items ?? [];
- return items.map(obj => this.toOption(obj));
- }
-
- get selectedOptions(): OptionWithState[] {
- return this.selectedItems.map(obj => this.toOption(obj));
- }
-
- toOption(obj: ObjectItem): OptionWithState {
- return {
- caption: `${this.caption.get(obj).value}`,
- value: `${obj.id}`,
- selected: this.selected.has(obj.id)
- };
- }
-
- get condition(): FilterCondition | undefined {
- if (this.selected.size < 1) {
- return undefined;
- }
-
- const exp = (guid: string): FilterCondition[] => {
- const obj = this.selectedItems.find(o => o.id === guid);
-
- if (obj && this.listRef.type === "Reference") {
- return [refEquals(this.listRef, obj)];
- } else if (obj && this.listRef.type === "ReferenceSet") {
- return [refContains(this.listRef, [obj])];
- }
-
- const viewExp = this.initCondArray.find(e => {
- if (e.arg2.type !== "literal") {
- return false;
- }
- if (e.arg2.valueType === "Reference") {
- return e.arg2.value === guid;
- }
- if (e.arg2.valueType === "ReferenceSet") {
- return e.arg2.value.at(0) === guid;
- }
- return false;
- });
- return viewExp ? [viewExp] : [];
- };
-
- const cond = [...this.selected].flatMap(exp);
-
- if (cond.length > 1) {
- return or(...cond);
- }
-
- return cond[0];
- }
-
- setup(): () => void {
- const [disposers, dispose] = disposeFx();
-
- disposers.push(this.search.setup());
- disposers.push(reaction(...this.searchChangeFx()));
- disposers.push(autorun(...this.computeSelectedItemsFx()));
-
- if (this.lazyMode) {
- disposers.push(
- when(
- () => this.fetchReady,
- () => this.loadMore()
- )
- );
- } else {
- this.setFetchReady(true);
- this.loadMore();
- }
-
- return dispose;
- }
-
- searchChangeFx(): Parameters {
- const data = (): string => this.search.value;
-
- const effect = (search: string): void => {
- if (!this.searchAttrId) {
- return;
- }
- const cond =
- typeof search === "string" && search !== ""
- ? contains(attribute(this.searchAttrId), literal(search))
- : undefined;
- this.datasource.setFilter(cond);
- this.datasource.setLimit(this.searchSize);
- };
-
- return [data, effect, { delay: 300 }];
- }
-
- computeSelectedItemsFx(): Parameters {
- const compute = (): void => {
- const allObjects = [...this.selectedItems, ...(this.datasource.items ?? [])];
- const map = new Map(allObjects.map(o => [o.id, o]));
- // Note: keep selected inside current block, so autorun can react to it.
- const selectedItems = [...this.selected].flatMap(guid => map.get(guid) ?? []);
- runInAction(() => (this.selectedItems = selectedItems));
- };
-
- return [compute];
- }
-
- setFetchReady(fetchReady: boolean): void {
- this.fetchReady ||= fetchReady;
- }
-
- setDefaultSelected(defaultSelected?: Iterable): void {
- if (!this.blockSetDefaults && defaultSelected) {
- this.defaultSelected = defaultSelected;
- this.blockSetDefaults = true;
- this.setSelected(defaultSelected);
- }
- }
-
- updateProps(props: RefFilterStoreProps): void {
- this.listRef = props.ref;
- this.datasource = props.datasource;
- this.caption = props.caption;
- }
-
- loadMore(): void {
- this.datasource.setLimit(this.datasource.limit + this.pageSize);
- }
-
- fromViewState(cond: FilterCondition): void {
- const val = (exp: LiteralExpression): string | undefined => {
- switch (exp.valueType) {
- case "Reference":
- return exp.value;
- case "ReferenceSet":
- return exp.value.at(0);
- default:
- return undefined;
- }
- };
-
- const selected = selectedFromCond(cond, val);
-
- if (selected.length > 0) {
- this.setSelected(selected);
- this.blockSetDefaults = true;
- }
- }
-}
-
-export function refEquals(associationValue: ListReferenceValue, value: ObjectItem): EqualsCondition {
- return equals(association(associationValue.id), literal(value));
-}
-
-export function refContains(associationValue: ListReferenceSetValue, value: ObjectItem[]): ContainsCondition {
- const v = value.length ? literal(value.slice()) : empty();
- return contains(association(associationValue.id), v);
-}
diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/StaticSelectFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/picker/StaticSelectFilterStore.ts
deleted file mode 100644
index 97b27353b9..0000000000
--- a/packages/shared/widget-plugin-filtering/src/stores/picker/StaticSelectFilterStore.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-import { ListAttributeValue } from "mendix";
-import { FilterCondition, LiteralExpression } from "mendix/filters";
-import { attribute, equals, literal, or } from "mendix/filters/builders";
-import { action, computed, makeObservable, observable } from "mobx";
-import { selectedFromCond } from "../../condition-utils";
-import { disposeFx } from "../../mobx-utils";
-import { OptionWithState } from "../../typings/OptionWithState";
-import { BaseSelectStore } from "./BaseSelectStore";
-import { SearchStore } from "./SearchStore";
-
-interface CustomOption {
- caption: string;
- value: string;
-}
-
-export class StaticSelectFilterStore extends BaseSelectStore {
- readonly storeType = "select";
- _attributes: ListAttributeValue[] = [];
- _customOptions: CustomOption[] = [];
- search: SearchStore;
-
- constructor(attributes: ListAttributeValue[], initCond: FilterCondition | null) {
- super();
- this.search = new SearchStore();
- this._attributes = attributes;
-
- makeObservable(this, {
- _attributes: observable.struct,
- _customOptions: observable.struct,
- allOptions: computed,
- options: computed,
- selectedOptions: computed,
- universe: computed,
- condition: computed,
- setCustomOptions: action,
- setDefaultSelected: action,
- updateProps: action,
- fromViewState: action
- });
-
- if (initCond) {
- this.fromViewState(initCond);
- }
- }
-
- get allOptions(): OptionWithState[] {
- const selected = this.selected;
-
- if (this._customOptions.length > 0) {
- return this._customOptions.map(opt => ({ ...opt, selected: selected.has(opt.value) }));
- }
-
- const options = this._attributes.flatMap(attr =>
- Array.from(attr.universe ?? [], value => {
- const stringValue = `${value}`;
- return {
- caption: attr.formatter.format(value),
- value: stringValue,
- selected: selected.has(stringValue)
- };
- })
- );
-
- return options;
- }
-
- get options(): OptionWithState[] {
- if (!this.search.value) {
- return this.allOptions;
- }
-
- return this.allOptions.filter(opt => opt.caption.toLowerCase().includes(this.search.value.toLowerCase()));
- }
-
- get selectedOptions(): OptionWithState[] {
- return [...this.selected].flatMap(value => {
- const option = this.allOptions.find(opt => opt.value === value);
- return option ? [option] : [];
- });
- }
-
- get universe(): Set {
- return new Set(this._attributes.flatMap(attr => Array.from(attr.universe ?? [], value => `${value}`)));
- }
-
- get condition(): FilterCondition | undefined {
- const selected = this.selected;
- const conditions = this._attributes.flatMap(attr => {
- const cond = getFilterCondition(attr, selected);
- return cond ? [cond] : [];
- });
- return conditions.length > 1 ? or(...conditions) : conditions[0];
- }
-
- setup(): () => void {
- const [disposers, dispose] = disposeFx();
- disposers.push(this.search.setup());
- return dispose;
- }
-
- setCustomOptions(options: CustomOption[]): void {
- this._customOptions = options;
- }
-
- setDefaultSelected(defaultSelected?: Iterable): void {
- if (!this.blockSetDefaults && defaultSelected) {
- this.defaultSelected = defaultSelected;
- this.setSelected(defaultSelected);
- this.blockSetDefaults = true;
- }
- }
-
- updateProps(attributes: ListAttributeValue[]): void {
- this._attributes = attributes;
- }
-
- checkAttrs(): TypeError | null {
- const isValidAttr = (attr: ListAttributeValue): boolean => /Enum|Boolean/.test(attr.type);
-
- if (this._attributes.every(isValidAttr)) {
- return null;
- }
-
- return new TypeError("StaticSelectFilterStore: invalid attribute found. Check widget configuration.");
- }
-
- isValidValue(value: string): boolean {
- return this.universe.has(value);
- }
-
- fromViewState(cond: FilterCondition): void {
- const val = (exp: LiteralExpression): string | undefined =>
- exp.valueType === "string"
- ? exp.value
- : exp.valueType === "boolean"
- ? exp.value
- ? "true"
- : "false"
- : undefined;
-
- const selected = selectedFromCond(cond, val);
-
- if (selected.length < 1) {
- return;
- }
-
- this.setSelected(selected);
- this.blockSetDefaults = true;
- }
-}
-
-function getFilterCondition(
- listAttribute: ListAttributeValue | undefined,
- selected: Set
-): FilterCondition | undefined {
- if (!listAttribute || !listAttribute.filterable || selected.size === 0) {
- return undefined;
- }
-
- const { id, type } = listAttribute;
- const filterAttribute = attribute(id);
-
- const filters = [...selected]
- .filter(value => listAttribute.universe?.includes(universeValue(listAttribute.type, value)))
- .map(value => equals(filterAttribute, literal(universeValue(type, value))));
-
- if (filters.length > 1) {
- return or(...filters);
- }
-
- const [filterValue] = filters;
- return filterValue;
-}
-
-function universeValue(type: ListAttributeValue["type"], value: string): boolean | string {
- if (type === "Boolean") {
- if (value !== "true" && value !== "false") {
- return value;
- }
- return value === "true";
- }
- return value;
-}
diff --git a/packages/shared/widget-plugin-filtering/src/stores/utils/is-input-data.ts b/packages/shared/widget-plugin-filtering/src/stores/utils/is-input-data.ts
deleted file mode 100644
index ce484a9507..0000000000
--- a/packages/shared/widget-plugin-filtering/src/stores/utils/is-input-data.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { InputData } from "../../typings/settings";
-
-const fnNames = new Set([
- "empty",
- "notEmpty",
- "equal",
- "notEqual",
- "greater",
- "greaterEqual",
- "smaller",
- "smallerEqual",
- "between",
- "contains",
- "startsWith",
- "endsWith"
-]);
-
-export function isInputData(data: unknown): data is InputData {
- if (Array.isArray(data)) {
- const [name] = data;
- return fnNames.has(name);
- }
- return false;
-}
diff --git a/packages/shared/widget-plugin-filtering/tsconfig.json b/packages/shared/widget-plugin-filtering/tsconfig.json
index 53827483c5..052cc1cee7 100644
--- a/packages/shared/widget-plugin-filtering/tsconfig.json
+++ b/packages/shared/widget-plugin-filtering/tsconfig.json
@@ -1,6 +1,6 @@
{
"extends": "@mendix/tsconfig-web-widgets/esm-library-with-jsx",
- "include": ["./src/**/*", "../widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts"],
+ "include": ["./src/**/*"],
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
diff --git a/packages/shared/widget-plugin-test-utils/package.json b/packages/shared/widget-plugin-test-utils/package.json
index 889942b8ce..8b2c5d0021 100644
--- a/packages/shared/widget-plugin-test-utils/package.json
+++ b/packages/shared/widget-plugin-test-utils/package.json
@@ -30,7 +30,7 @@
"test": "jest"
},
"dependencies": {
- "mendix": "^10.16.49747"
+ "mendix": "^10.21.64362"
},
"devDependencies": {
"@mendix/eslint-config-web-widgets": "workspace:*",
From e1423269e32200c59c4b395ca871f2529bd442fd Mon Sep 17 00:00:00 2001
From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com>
Date: Fri, 18 Apr 2025 14:31:03 +0200
Subject: [PATCH 06/29] refactor(!): remove legacy provider
---
.../widget-plugin-filtering/src/context.ts | 36 +----
.../src/helpers/useDateFilterAPI.ts | 6 +-
.../src/helpers/useNumberFilterAPI.ts | 6 +-
.../src/helpers/useNumberFilterController.ts | 16 +-
.../src/helpers/useSelectFilterAPI.ts | 23 +--
.../src/helpers/useSetup.ts | 11 --
.../src/helpers/useSetupUpdate.ts | 15 --
.../src/helpers/useStringFilterAPI.ts | 6 +-
.../src/helpers/useStringFilterController.ts | 15 +-
.../src/providers/LegacyPv.ts | 139 ------------------
.../src/stores/generic/HeaderFiltersStore.ts | 83 -----------
11 files changed, 29 insertions(+), 327 deletions(-)
delete mode 100644 packages/shared/widget-plugin-filtering/src/helpers/useSetup.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/helpers/useSetupUpdate.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts
delete mode 100644 packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts
diff --git a/packages/shared/widget-plugin-filtering/src/context.ts b/packages/shared/widget-plugin-filtering/src/context.ts
index 981b934025..55796118a7 100644
--- a/packages/shared/widget-plugin-filtering/src/context.ts
+++ b/packages/shared/widget-plugin-filtering/src/context.ts
@@ -1,4 +1,4 @@
-import { PickerFilterStore } from "@mendix/widget-plugin-dropdown-filter/typings/PickerFilterStore";
+import { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore";
import { FilterCondition } from "mendix/filters";
import { Context, createContext, useContext } from "react";
import { APIError, ENOCONTEXT } from "./errors";
@@ -9,34 +9,25 @@ import { InputFilterInterface } from "./typings/InputFilterInterface";
export interface FilterAPI {
version: 3;
parentChannelName: string;
- provider: Result;
+ provider: Result;
filterObserver: FilterObserver;
sharedInitFilter: Array;
}
-/** @deprecated */
-export enum FilterType {
- STRING = "string",
- NUMBER = "number",
- ENUMERATION = "enum",
- DATE = "date"
-}
-
-export type FilterStoreProvider = DirectProvider | LegacyProvider;
-
-export type FilterStore = InputFilterInterface | PickerFilterStore;
+export type FilterStore = InputFilterInterface | StaticSelectFilterStore;
interface DirectProvider {
type: "direct";
store: FilterStore | null;
}
-/** @deprecated */
-export interface LegacyProvider {
- type: "legacy";
- get: (type: FilterType) => FilterStore | null;
+interface ProviderStub {
+ type: "stub";
+ hint: "No filter store available";
}
+export const PROVIDER_STUB = Object.freeze({ type: "stub", hint: "No filter store available" } as const);
+
type FilterAPIContext = Context;
const CONTEXT_OBJECT_PATH = "com.mendix.widgets.web.filterable.filterContext.v2" as const;
@@ -64,14 +55,3 @@ export function useFilterAPI(): Result {
/** @deprecated This hook is renamed, use `useFilterAPI` instead. */
export const useFilterContextValue = useFilterAPI;
-
-export function getFilterStore(provider: FilterStoreProvider, legacyType: FilterType): FilterStore | null {
- switch (provider.type) {
- case "direct":
- return provider.store;
- case "legacy":
- return provider.get(legacyType);
- default:
- return null;
- }
-}
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts b/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts
index d6cdab88bf..6805b94642 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts
@@ -1,5 +1,5 @@
import { useRef } from "react";
-import { FilterType, getFilterStore, useFilterContextValue } from "../context";
+import { useFilterAPI } from "../context";
import { APIError, EMISSINGSTORE, EStoreTypeMisMatch } from "../errors";
import { error, Result, value } from "../result-meta";
import { isDateFilter } from "../stores/input/store-utils";
@@ -11,7 +11,7 @@ export interface Date_FilterAPIv2 {
}
export function useDateFilterAPI(): Result {
- const ctx = useFilterContextValue();
+ const ctx = useFilterAPI();
const dateAPI = useRef();
if (ctx.hasError) {
@@ -24,7 +24,7 @@ export function useDateFilterAPI(): Result {
return error(api.provider.error);
}
- const store = getFilterStore(api.provider.value, FilterType.DATE);
+ const store = api.provider.value.type === "direct" ? api.provider.value.store : null;
if (store === null) {
return error(EMISSINGSTORE);
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterAPI.ts b/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterAPI.ts
index c1790cc83c..ca908bbcb9 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterAPI.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterAPI.ts
@@ -1,5 +1,5 @@
import { useRef } from "react";
-import { FilterType, getFilterStore, useFilterContextValue } from "../context";
+import { useFilterAPI } from "../context";
import { APIError, EMISSINGSTORE, EStoreTypeMisMatch } from "../errors";
import { error, Result, value } from "../result-meta";
import { isNumberFilter } from "../stores/input/store-utils";
@@ -11,7 +11,7 @@ export interface Number_FilterAPIv2 {
}
export function useNumberFilterAPI(): Result {
- const ctx = useFilterContextValue();
+ const ctx = useFilterAPI();
const numAPI = useRef();
if (ctx.hasError) {
@@ -24,7 +24,7 @@ export function useNumberFilterAPI(): Result {
return error(api.provider.error);
}
- const store = getFilterStore(api.provider.value, FilterType.NUMBER);
+ const store = api.provider.value.type === "direct" ? api.provider.value.store : null;
if (store === null) {
return error(EMISSINGSTORE);
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts b/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts
index 96d710c6b1..b40927efb9 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts
@@ -1,17 +1,7 @@
-import { AllFunctions } from "@mendix/filter-commons/typings/FilterFunctions";
-import { useEffect, useState } from "react";
+import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup";
import { NumberFilterController, Params } from "../controllers/input/NumberInputController";
-import { ArgumentInterface } from "../typings/ArgumentInterface";
-import { FilterFn, InputFilterBaseInterface } from "../typings/InputFilterInterface";
-
-export function useNumberFilterController<
- F extends InputFilterBaseInterface,
- A extends ArgumentInterface,
- Fn extends AllFunctions = FilterFn
->(params: Params): NumberFilterController {
- const [ctrl] = useState(() => new NumberFilterController(params));
-
- useEffect(() => ctrl.setup(), [ctrl]);
+export function useNumberFilterController(params: Params): NumberFilterController {
+ const ctrl = useSetup(() => new NumberFilterController(params));
return ctrl;
}
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts b/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts
index f27fd6d6cd..c38954477c 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts
@@ -1,7 +1,7 @@
import { PickerFilterStore } from "@mendix/widget-plugin-dropdown-filter/typings/PickerFilterStore";
import { useRef } from "react";
-import { FilterType, getFilterStore, useFilterContextValue } from "../context";
-import { APIError, EMISSINGSTORE, EStoreTypeMisMatch, OPTIONS_NOT_FILTERABLE } from "../errors";
+import { useFilterAPI } from "../context";
+import { APIError, EMISSINGSTORE, EStoreTypeMisMatch } from "../errors";
import { Result, error, value } from "../result-meta";
export interface Select_FilterAPIv2 {
@@ -9,12 +9,8 @@ export interface Select_FilterAPIv2 {
parentChannelName?: string;
}
-interface Props {
- filterable: boolean;
-}
-
-export function useSelectFilterAPI({ filterable }: Props): Result {
- const ctx = useFilterContextValue();
+export function useSelectFilterAPI(): Result {
+ const ctx = useFilterAPI();
const slctAPI = useRef();
if (ctx.hasError) {
@@ -27,22 +23,15 @@ export function useSelectFilterAPI({ filterable }: Props): Result void);
-}
-
-export function useSetup(props: () => T): T {
- const [obj] = useState(props);
- useEffect(() => obj.setup(), [obj]);
- return obj;
-}
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useSetupUpdate.ts b/packages/shared/widget-plugin-filtering/src/helpers/useSetupUpdate.ts
deleted file mode 100644
index 1c22ec422d..0000000000
--- a/packages/shared/widget-plugin-filtering/src/helpers/useSetupUpdate.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { useEffect } from "react";
-import { Setupable, useSetup } from "./useSetup";
-
-interface IUpdateModel extends Setupable {
- updateProps: (props: P) => void;
-}
-
-export function useSetupUpdate, P>(setup: () => T, props: P): T {
- const obj = useSetup(setup);
-
- // NOTE: Don't use dependencies.
- // Model should be updated on every render.
- useEffect(() => obj.updateProps(props));
- return obj;
-}
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterAPI.ts b/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterAPI.ts
index f3c11fbcda..9e5b49dcd7 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterAPI.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterAPI.ts
@@ -1,5 +1,5 @@
import { useRef } from "react";
-import { FilterType, getFilterStore, useFilterContextValue } from "../context";
+import { useFilterAPI } from "../context";
import { APIError, EMISSINGSTORE, EStoreTypeMisMatch } from "../errors";
import { error, Result, value } from "../result-meta";
import { isStringFilter } from "../stores/input/store-utils";
@@ -11,7 +11,7 @@ export interface String_FilterAPIv2 {
}
export function useStringFilterAPI(): Result {
- const ctx = useFilterContextValue();
+ const ctx = useFilterAPI();
const strAPI = useRef();
if (ctx.hasError) {
@@ -24,7 +24,7 @@ export function useStringFilterAPI(): Result {
return error(api.provider.error);
}
- const store = getFilterStore(api.provider.value, FilterType.STRING);
+ const store = api.provider.value.type === "direct" ? api.provider.value.store : null;
if (store === null) {
return error(EMISSINGSTORE);
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts b/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts
index dbe9e83fb7..0316703559 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts
@@ -1,17 +1,8 @@
-import { AllFunctions } from "@mendix/filter-commons/typings/FilterFunctions";
-import { useEffect, useState } from "react";
+import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup";
import { Params, StringFilterController } from "../controllers/input/StringInputController";
-import { ArgumentInterface } from "../typings/ArgumentInterface";
-import { FilterFn, InputFilterBaseInterface } from "../typings/InputFilterInterface";
-export function useStringFilterController<
- F extends InputFilterBaseInterface,
- A extends ArgumentInterface,
- Fn extends AllFunctions = FilterFn
->(params: Params): StringFilterController {
- const [ctrl] = useState(() => new StringFilterController(params));
-
- useEffect(() => ctrl.setup(), [ctrl]);
+export function useStringFilterController(params: Params): StringFilterController {
+ const ctrl = useSetup(() => new StringFilterController(params));
return ctrl;
}
diff --git a/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts b/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts
deleted file mode 100644
index d81a80e6c0..0000000000
--- a/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings";
-import { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore";
-import { PickerFilterStore } from "@mendix/widget-plugin-dropdown-filter/typings/PickerFilterStore";
-import { ListAttributeValue } from "mendix";
-import { FilterCondition } from "mendix/filters";
-import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
-import { FilterType as Ft, LegacyProvider } from "../context";
-import { DateInputFilterStore } from "../stores/input/DateInputFilterStore";
-import { NumberInputFilterStore } from "../stores/input/NumberInputFilterStore";
-import { StringInputFilterStore } from "../stores/input/StringInputFilterStore";
-import { InputFilterInterface } from "../typings/InputFilterInterface";
-
-type FilterMap = {
- [Ft.STRING]: StringInputFilterStore | null;
- [Ft.NUMBER]: NumberInputFilterStore | null;
- [Ft.DATE]: DateInputFilterStore | null;
- [Ft.ENUMERATION]: StaticSelectFilterStore | null;
-};
-
-type FilterList = [
- StringInputFilterStore | null,
- NumberInputFilterStore | null,
- DateInputFilterStore | null,
- StaticSelectFilterStore | null
-];
-
-export class LegacyPv implements LegacyProvider {
- readonly type = "legacy";
- private _attrs: ListAttributeValue[];
- dispose?: () => void;
- filterMap: FilterMap;
- filterList: FilterList;
-
- constructor(attrs: ListAttributeValue[], dsViewState: Array | null) {
- this._attrs = attrs;
- const map = (this.filterMap = createMap(attrs, dsViewState));
- this.filterList = [map[Ft.STRING], map[Ft.NUMBER], map[Ft.DATE], map[Ft.ENUMERATION]];
- makeObservable(this, {
- _attrs: observable.ref,
- conditions: computed,
- settings: computed,
- updateProps: action
- });
- }
-
- get conditions(): Array {
- return this.filterList.map(store => (store ? store.condition : undefined));
- }
-
- get settings(): FiltersSettingsMap {
- return new Map();
- }
-
- set settings(_: unknown) {}
-
- get = (type: Ft): InputFilterInterface | PickerFilterStore | null => {
- return this.filterMap[type];
- };
-
- updateProps(attrs: ListAttributeValue[]): void {
- this._attrs = attrs;
- }
-
- updateFilters(attrs: ListAttributeValue[]): void {
- const [str, num, dte, enm] = groupByType(attrs);
- const [s1, s2, s3, s4] = this.filterList;
- s1?.updateProps(str);
- s2?.updateProps(num);
- s3?.updateProps(dte);
- s4?.updateProps(enm);
- }
-
- setup(): () => void {
- const disposers: Array<() => void> = [];
- this.filterList.forEach(store => {
- if (store && "setup" in store) {
- disposers.push(store.setup());
- }
- });
- disposers.push(
- reaction(
- () => this._attrs,
- attrs => this.updateFilters(attrs),
- { equals: comparer.shallow }
- )
- );
-
- return (this.dispose = () => disposers.forEach(dispose => dispose()));
- }
-}
-
-function createMap(attrs: ListAttributeValue[], dsViewState: Array | null): FilterMap {
- const [ini1 = null, ini2 = null, ini3 = null, ini4 = null] = dsViewState ?? [];
- const [str, num, dte, enm] = groupByType(attrs);
-
- const r: FilterMap = {
- [Ft.STRING]: str.length > 0 ? new StringInputFilterStore(str, ini1) : null,
- [Ft.NUMBER]: num.length > 0 ? new NumberInputFilterStore(num, ini2) : null,
- [Ft.DATE]: dte.length > 0 ? new DateInputFilterStore(dte, ini3) : null,
- [Ft.ENUMERATION]: enm.length > 0 ? new StaticSelectFilterStore(enm, ini4) : null
- };
-
- return r;
-}
-
-function groupByType(
- attrs: ListAttributeValue[]
-): [
- Array>,
- Array>,
- Array>,
- Array>
-] {
- return [
- attrs.filter(isStringAttr),
- attrs.filter(isNumberAttr),
- attrs.filter(isDateAttr),
- attrs.filter((a): a is ListAttributeValue => isBoolAttr(a) || isEnumAttr(a))
- ];
-}
-
-function isDateAttr(attr: ListAttributeValue): attr is ListAttributeValue {
- return attr.type === "DateTime";
-}
-
-function isBoolAttr(attr: ListAttributeValue): attr is ListAttributeValue {
- return attr.type === "Boolean";
-}
-
-function isEnumAttr(attr: ListAttributeValue): attr is ListAttributeValue {
- return attr.type === "Enum";
-}
-
-function isNumberAttr(attr: ListAttributeValue): attr is ListAttributeValue {
- return attr.type === "Long" || attr.type === "Decimal" || attr.type === "Integer" || attr.type === "AutoNumber";
-}
-function isStringAttr(attr: ListAttributeValue): attr is ListAttributeValue {
- return attr.type === "HashString" || attr.type === "String";
-}
diff --git a/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts b/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts
deleted file mode 100644
index c09a1d540e..0000000000
--- a/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings";
-import { ListAttributeValue } from "mendix";
-import { FilterCondition } from "mendix/filters";
-import { computed, makeObservable } from "mobx";
-import { FilterAPI } from "../../context";
-import { APIError } from "../../errors";
-import { LegacyPv } from "../../providers/LegacyPv";
-import { Result, value } from "../../result-meta";
-import { FilterObserver } from "../../typings/FilterObserver";
-
-export interface FilterListType {
- filter: ListAttributeValue;
-}
-
-export interface HeaderFiltersStoreSpec {
- filterList: FilterListType[];
- filterChannelName: string;
- headerInitFilter: Array;
- sharedInitFilter: Array;
- customFilterHost: FilterObserver;
-}
-
-export class HeaderFiltersStore {
- private provider: Result;
- context: FilterAPI;
-
- constructor({
- filterList,
- filterChannelName,
- headerInitFilter,
- sharedInitFilter,
- customFilterHost: filterObserver
- }: HeaderFiltersStoreSpec) {
- this.provider = this.createProvider(filterList, headerInitFilter);
- this.context = {
- version: 3,
- parentChannelName: filterChannelName,
- provider: this.provider,
- sharedInitFilter,
- filterObserver
- };
- makeObservable(this, {
- conditions: computed,
- settings: computed
- });
- }
-
- get conditions(): Array {
- return this.provider.hasError ? [] : this.provider.value.conditions;
- }
-
- get settings(): FiltersSettingsMap {
- return this.provider.hasError ? new Map() : this.provider.value.settings;
- }
-
- set settings(value: FiltersSettingsMap | undefined) {
- if (this.provider.hasError) {
- return;
- }
-
- this.provider.value.settings = value;
- }
-
- createProvider(
- filterList: FilterListType[],
- initFilter: Array
- ): Result {
- return value(
- new LegacyPv(
- filterList.map(f => f.filter),
- initFilter
- )
- );
- }
-
- setup(): (() => void) | void {
- if (this.provider.hasError) {
- return;
- }
-
- return this.provider.value.setup();
- }
-}
From 3df8535278aeb9dcfc71d336d725858138bb2f31 Mon Sep 17 00:00:00 2001
From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com>
Date: Tue, 22 Apr 2025 15:13:28 +0200
Subject: [PATCH 07/29] chore: move hook
---
.../src/helpers/usePickerJSActions.ts | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename packages/shared/{widget-plugin-filtering => widget-plugin-dropdown-filter}/src/helpers/usePickerJSActions.ts (100%)
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/usePickerJSActions.ts b/packages/shared/widget-plugin-dropdown-filter/src/helpers/usePickerJSActions.ts
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/helpers/usePickerJSActions.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/helpers/usePickerJSActions.ts
From d2882aee33048150c7c8d469025ceaf2dd480840 Mon Sep 17 00:00:00 2001
From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com>
Date: Tue, 22 Apr 2025 15:27:58 +0200
Subject: [PATCH 08/29] refactor: remove header filter
---
.../controllers/DatasourceParamsController.ts | 31 ++++++-------------
.../src/helpers/state/RootGridStore.ts | 15 +--------
2 files changed, 11 insertions(+), 35 deletions(-)
diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts
index 9a79deb4e0..0ea99f6487 100644
--- a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts
@@ -1,4 +1,4 @@
-import { compactArray, fromCompactArray, isAnd } from "@mendix/widget-plugin-filtering/condition-utils";
+import { compactArray, fromCompactArray, isAnd } from "@mendix/filter-commons/condition-utils.js";
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";
@@ -19,20 +19,17 @@ interface FiltersInput {
type DatasourceParamsControllerSpec = {
query: QueryController;
columns: Columns;
- header: FiltersInput;
customFilters: FiltersInput;
};
export class DatasourceParamsController implements ReactiveController {
private columns: Columns;
- private header: FiltersInput;
private query: QueryController;
private customFilters: FiltersInput;
constructor(host: ReactiveControllerHost, spec: DatasourceParamsControllerSpec) {
host.addController(this);
this.columns = spec.columns;
- this.header = spec.header;
this.query = spec.query;
this.customFilters = spec.customFilters;
@@ -40,13 +37,9 @@ export class DatasourceParamsController implements ReactiveController {
}
private get derivedFilter(): FilterCondition | undefined {
- const { columns, header, customFilters } = this;
+ const { columns, customFilters } = this;
- return and(
- compactArray(columns.conditions),
- compactArray(header.conditions),
- compactArray(customFilters.conditions)
- );
+ return and(compactArray(columns.conditions), compactArray(customFilters.conditions));
}
private get derivedSortOrder(): SortInstruction[] | undefined {
@@ -75,22 +68,18 @@ export class DatasourceParamsController implements ReactiveController {
static unzipFilter(
filter?: FilterCondition
- ): [
- columns: Array,
- header: Array,
- sharedFilter: Array
- ] {
+ ): [columns: Array, sharedFilter: Array] {
if (!filter) {
- return [[], [], []];
+ return [[], []];
}
if (!isAnd(filter)) {
- return [[], [], []];
+ return [[], []];
}
- if (filter.args.length !== 3) {
- return [[], [], []];
+ if (filter.args.length !== 2) {
+ return [[], []];
}
- const [columns, header, shared] = filter.args;
- return [fromCompactArray(columns), fromCompactArray(header), fromCompactArray(shared)];
+ const [columns, shared] = filter.args;
+ return [fromCompactArray(columns), fromCompactArray(shared)];
}
}
diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts
index 1e03c6b941..94b9d6faeb 100644
--- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts
@@ -1,5 +1,4 @@
import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost";
-import { HeaderFiltersStore } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore";
import { BaseControllerHost } from "@mendix/widget-plugin-mobx-kit/BaseControllerHost";
import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
@@ -25,7 +24,6 @@ type Spec = {
export class RootGridStore extends BaseControllerHost {
columnsStore: ColumnGroupStore;
- headerFiltersStore: HeaderFiltersStore;
settingsStore: GridPersonalizationStore;
staticInfo: StaticInfo;
exportProgressCtrl: ProgressStore;
@@ -38,9 +36,7 @@ export class RootGridStore extends BaseControllerHost {
super();
const { props } = gate;
- const [columnsInitFilter, headerInitFilter, sharedInitFilter] = DatasourceParamsController.unzipFilter(
- props.datasource.filter
- );
+ const [columnsInitFilter, sharedInitFilter] = DatasourceParamsController.unzipFilter(props.datasource.filter);
this.gate = gate;
this.staticInfo = {
@@ -53,13 +49,6 @@ export class RootGridStore extends BaseControllerHost {
customFilterHost,
sharedInitFilter
}));
- const header = (this.headerFiltersStore = new HeaderFiltersStore({
- filterList: props.filterList,
- filterChannelName: this.staticInfo.filtersChannelName,
- headerInitFilter,
- sharedInitFilter,
- customFilterHost
- }));
this.settingsStore = new GridPersonalizationStore(props, this.columnsStore, customFilterHost);
this.paginationCtrl = new PaginationController(this, { gate, query });
this.exportProgressCtrl = exportCtrl;
@@ -67,7 +56,6 @@ export class RootGridStore extends BaseControllerHost {
new DatasourceParamsController(this, {
query,
columns,
- header,
customFilters: customFilterHost
});
@@ -87,7 +75,6 @@ export class RootGridStore extends BaseControllerHost {
const [add, disposeAll] = disposeBatch();
add(super.setup());
add(this.columnsStore.setup());
- add(this.headerFiltersStore.setup());
add(() => this.settingsStore.dispose());
add(autorun(() => this.updateProps(this.gate.props)));
From df50c5221ed4c0179e1c8929eeec92ceb4c78790 Mon Sep 17 00:00:00 2001
From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com>
Date: Wed, 23 Apr 2025 10:13:15 +0200
Subject: [PATCH 09/29] refactor: remove header filter
---
.../datagrid-web/src/Datagrid.xml | 38 ----------
.../src/components/WidgetHeaderContext.tsx | 12 +--
.../src/helpers/state/RootGridStore.ts | 3 +
.../state/column/ColumnFilterStore.tsx | 75 ++++---------------
.../datagrid-web/typings/DatagridProps.d.ts | 16 +---
packages/shared/filter-commons/package.json | 2 +-
6 files changed, 24 insertions(+), 122 deletions(-)
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
index 3f00e78f9c..92e6bd9e90 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
@@ -121,44 +121,6 @@