;
+ name: string;
+}
+
+export function withLinkedEnumStore(
+ Component: FC
+): FC
{
+ return function ProviderHost(props) {
+ const { store } = useSetup(
+ () => new EnumStoreProvider(props.filterAPI, { attributes: [props.attr], dataKey: props.name })
+ );
+ return ;
+ };
+}
diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedRefStore.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedRefStore.tsx
new file mode 100644
index 0000000000..e07634f95d
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedRefStore.tsx
@@ -0,0 +1,90 @@
+import { useEffect, createElement } from "react";
+import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
+import { RefFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/RefFilterStore";
+import { FilterAPI, useFilterAPI } from "@mendix/widget-plugin-filtering/context";
+import { BaseStoreProvider } from "@mendix/widget-plugin-filtering/custom-filter-api/BaseStoreProvider";
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
+import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider";
+import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
+import { ListValue, ListAttributeValue, AssociationMetaData } from "mendix";
+import { DatagridDropdownFilterContainerProps } from "../../typings/DatagridDropdownFilterProps";
+import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup";
+import { RefFilterProps } from "../components/typings";
+
+type WidgetProps = Pick;
+
+export interface RequiredProps {
+ name: string;
+ refEntity: AssociationMetaData;
+ refOptions: ListValue;
+ refCaption: ListAttributeValue;
+ searchAttrId: ListAttributeValue["id"];
+}
+
+type Component = (props: P) => React.ReactElement;
+
+export function withLinkedRefStore
(Cmp: Component
): Component
{
+ function StoreProvider(props: P & { filterAPI: FilterAPI }): React.ReactElement {
+ const gate = useGate(props);
+ const provider = useSetup(() => new RefStoreProvider(props.filterAPI, gate));
+ return ;
+ }
+ return function FilterAPIProvider(props) {
+ const api = useFilterAPI();
+
+ if (api.hasError) {
+ return {api.error.message} ;
+ }
+
+ return ;
+ };
+}
+
+function mapProps(props: WidgetProps): RequiredProps {
+ if (!props.refEntity) {
+ throw new Error("RefFilterStoreProvider: refEntity is required");
+ }
+
+ if (!props.refOptions) {
+ throw new Error("RefFilterStoreProvider: refOptions is required");
+ }
+
+ if (!props.refCaption) {
+ throw new Error("RefFilterStoreProvider: refCaption is required");
+ }
+ return {
+ name: props.name,
+ refEntity: props.refEntity,
+ refOptions: props.refOptions,
+ refCaption: props.refCaption,
+ searchAttrId: props.refCaption.id
+ };
+}
+
+function useGate(props: WidgetProps): DerivedPropsGate {
+ const gp = useConst(() => new GateProvider(mapProps(props)));
+ useEffect(() => {
+ gp.setProps(mapProps(props));
+ });
+ return gp.gate;
+}
+
+class RefStoreProvider extends BaseStoreProvider {
+ protected _store: RefFilterStore;
+ protected filterAPI: FilterAPI;
+ readonly dataKey: string;
+
+ constructor(filterAPI: FilterAPI, gate: DerivedPropsGate) {
+ super();
+ this.filterAPI = filterAPI;
+ this.dataKey = gate.props.name;
+ this._store = new RefFilterStore({
+ gate,
+ initCond: this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey)
+ });
+ }
+
+ get store(): RefFilterStore {
+ return this._store;
+ }
+}
diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withParentProvidedEnumStore.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withParentProvidedEnumStore.tsx
new file mode 100644
index 0000000000..e80b89aed2
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withParentProvidedEnumStore.tsx
@@ -0,0 +1,48 @@
+import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
+import { useRef, createElement } from "react";
+import { useFilterAPI } from "@mendix/widget-plugin-filtering/context";
+import { APIError, EMISSINGSTORE, EStoreTypeMisMatch } from "@mendix/widget-plugin-filtering/errors";
+import { Result, error, value } from "@mendix/widget-plugin-filtering/result-meta";
+import { EnumFilterProps } from "../components/typings";
+
+export function withParentProvidedEnumStore(
+ Component: (props: P & EnumFilterProps) => React.ReactElement
+): (props: P) => React.ReactElement {
+ return function FilterAPIProvider(props: P): React.ReactElement {
+ const api = useEnumFilterAPI();
+ if (api.hasError) {
+ return {api.error.message} ;
+ }
+
+ return (
+
+ );
+ };
+}
+
+function useEnumFilterAPI(): Result {
+ const ctx = useFilterAPI();
+ const slctAPI = useRef();
+
+ if (ctx.hasError) {
+ return error(ctx.error);
+ }
+
+ const api = ctx.value;
+
+ if (api.provider.hasError) {
+ return error(api.provider.error);
+ }
+
+ const store = api.provider.value.type === "direct" ? api.provider.value.store : null;
+
+ if (store === null) {
+ return error(EMISSINGSTORE);
+ }
+
+ if (store.storeType !== "select") {
+ return error(EStoreTypeMisMatch("dropdown filter", store.arg1.type));
+ }
+
+ return value((slctAPI.current ??= { filterStore: store, parentChannelName: api.parentChannelName }));
+}
diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withSelectFilterAPI.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withSelectFilterAPI.tsx
deleted file mode 100644
index f8f6a74f88..0000000000
--- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withSelectFilterAPI.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
-import { Select_FilterAPIv2, useSelectFilterAPI } from "@mendix/widget-plugin-filtering/helpers/useSelectFilterAPI";
-import { createElement } from "react";
-
-export { Select_FilterAPIv2 };
-
-export function withSelectFilterAPI(
- Component: (props: P & Select_FilterAPIv2) => React.ReactElement
-): (props: P) => React.ReactElement {
- return function FilterAPIProvider(props: P): React.ReactElement {
- const api = useSelectFilterAPI(props);
- if (api.hasError) {
- return {api.error.message} ;
- }
-
- return (
-
- );
- };
-}
diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/package.xml b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/package.xml
index 800d1df062..fc38a0b891 100644
--- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/package.xml
+++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/package.xml
@@ -1,6 +1,6 @@
-
+
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..a443bd6589 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, AssociationMetaData, AttributeMetaData, DynamicValue, EditableValue, ListValue, ListAttributeValue } from "mendix";
+
+export type BaseTypeEnum = "attr" | "ref";
+
+export type AttrChoiceEnum = "auto" | "linked";
export interface FilterOptionsType {
caption: DynamicValue;
@@ -25,9 +29,16 @@ export interface DatagridDropdownFilterContainerProps {
class: string;
style?: CSSProperties;
tabIndex?: number;
+ baseType: BaseTypeEnum;
+ attrChoice: AttrChoiceEnum;
+ attr: AttributeMetaData;
auto: boolean;
- defaultValue?: DynamicValue;
filterOptions: FilterOptionsType[];
+ refEntity: AssociationMetaData;
+ refOptions?: ListValue;
+ refCaption?: ListAttributeValue;
+ fetchOptionsLazy: boolean;
+ defaultValue?: DynamicValue;
filterable: boolean;
multiSelect: boolean;
emptyOptionCaption?: DynamicValue;
@@ -50,9 +61,16 @@ 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[];
+ refEntity: string;
+ refOptions: {} | { caption: string } | { type: string } | null;
+ refCaption: string;
+ fetchOptionsLazy: boolean;
+ defaultValue: string;
filterable: boolean;
multiSelect: boolean;
emptyOptionCaption: string;
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/package.json b/packages/pluggableWidgets/datagrid-number-filter-web/package.json
index 81c719e2e9..869959239e 100644
--- a/packages/pluggableWidgets/datagrid-number-filter-web/package.json
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/package.json
@@ -1,7 +1,7 @@
{
"name": "@mendix/datagrid-number-filter-web",
"widgetName": "DatagridNumberFilter",
- "version": "2.9.2",
+ "version": "3.0.0",
"description": "",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.editorConfig.ts b/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.editorConfig.ts
index 4c52aa7205..a6c2f1d30f 100644
--- a/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.editorConfig.ts
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.editorConfig.ts
@@ -1,10 +1,4 @@
-import {
- ContainerProps,
- ImageProps,
- structurePreviewPalette,
- StructurePreviewProps,
- text
-} from "@mendix/widget-plugin-platform/preview/structure-preview-api";
+import { hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools";
import {
emptyIcon,
emptyIconDark,
@@ -23,25 +17,26 @@ import {
smallerThanIcon,
smallerThanIconDark
} from "@mendix/widget-plugin-filtering/preview/editor-preview-icons";
-import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools";
+import {
+ ContainerProps,
+ ImageProps,
+ structurePreviewPalette,
+ StructurePreviewProps,
+ text
+} from "@mendix/widget-plugin-platform/preview/structure-preview-api";
import { DatagridNumberFilterPreviewProps, DefaultFilterEnum } from "../typings/DatagridNumberFilterProps";
-export function getProperties(
- values: DatagridNumberFilterPreviewProps,
- defaultProperties: Properties,
- platform: "web" | "desktop"
-): Properties {
+export function getProperties(values: DatagridNumberFilterPreviewProps, defaultProperties: Properties): Properties {
if (!values.adjustable) {
hidePropertyIn(defaultProperties, values, "screenReaderButtonCaption");
}
- if (platform === "web") {
- if (!values.advanced) {
- hidePropertiesIn(defaultProperties, values, ["onChange", "valueAttribute"]);
- }
- } else {
- hidePropertyIn(defaultProperties, values, "advanced");
+
+ if (values.attrChoice === "auto") {
+ hidePropertyIn(defaultProperties, values, "attributes");
+ hidePropertyIn(defaultProperties, {} as { linkedDs: unknown }, "linkedDs");
}
+
return defaultProperties;
}
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.tsx b/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.tsx
index f306da389c..8743b68cdd 100644
--- a/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.tsx
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.tsx
@@ -4,10 +4,18 @@ import { DatagridNumberFilterContainerProps } from "../typings/DatagridNumberFil
import { NumberFilterContainer } from "./components/NumberFilterContainer";
import { isLoadingDefaultValues } from "./utils/widget-utils";
import { withNumberFilterAPI } from "./hocs/withNumberFilterAPI";
+import { withLinkedAttributes } from "./hocs/withLinkedAttributes";
const container = withPreloader(NumberFilterContainer, isLoadingDefaultValues);
-const Widget = withNumberFilterAPI(container);
+const FilterAuto = withNumberFilterAPI(container);
+const FilterLinked = withLinkedAttributes(container);
export default function DatagridNumberFilter(props: DatagridNumberFilterContainerProps): ReactElement {
- return ;
+ const isAuto = props.attrChoice === "auto";
+
+ if (isAuto) {
+ return ;
+ }
+
+ return ;
}
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.xml b/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.xml
index 5c024b076f..e628073200 100644
--- a/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.xml
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/DatagridNumberFilter.xml
@@ -8,9 +8,35 @@
-
- Enable advanced options
+
+ Filter attributes
+
+ Auto
+ Custom
+
+
+
+ Datasource to Filter
+
+
+
+ Attributes
+ Select the attributes that the end-user may use for filtering.
+
+
+
+ Attribute
+
+
+
+
+
+
+
+
+
+
Default value
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx b/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx
index 413fef4513..abfb3a38a3 100644
--- a/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/components/__tests__/DatagridNumberFilter.spec.tsx
@@ -1,9 +1,9 @@
import "@testing-library/jest-dom";
-import { FilterAPIv2 } from "@mendix/widget-plugin-filtering/context";
+import { FilterAPI } from "@mendix/widget-plugin-filtering/context";
import { requirePlugin } from "@mendix/widget-plugin-external-events/plugin";
import {
HeaderFiltersStore,
- HeaderFiltersStoreProps
+ HeaderFiltersStoreSpec
} from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore";
import {
actionValue,
@@ -20,11 +20,7 @@ import DatagridNumberFilter from "../../DatagridNumberFilter";
import { Big } from "big.js";
import { DatagridNumberFilterContainerProps } from "../../../typings/DatagridNumberFilterProps";
import { resetIdCounter } from "downshift";
-
-interface StaticInfo {
- name: string;
- filtersChannelName: string;
-}
+import { FilterObserver } from "@mendix/widget-plugin-filtering/typings/FilterObserver";
const commonProps: DatagridNumberFilterContainerProps = {
class: "filter-custom-class",
@@ -36,13 +32,17 @@ const commonProps: DatagridNumberFilterContainerProps = {
delay: 1000
};
-const headerFilterStoreInfo: StaticInfo = {
- name: commonProps.name,
- filtersChannelName: "datagrid1"
-};
-
jest.useFakeTimers();
+const mockSpec = (spec: Partial): HeaderFiltersStoreSpec => ({
+ filterList: [],
+ filterChannelName: "datagrid1",
+ headerInitFilter: [],
+ sharedInitFilter: [],
+ customFilterHost: {} as FilterObserver,
+ ...spec
+});
+
beforeEach(() => {
jest.spyOn(console, "warn").mockImplementation(() => {
// noop
@@ -60,23 +60,23 @@ describe("Number Filter", () => {
describe("with single attribute", () => {
beforeEach(() => {
- const props: HeaderFiltersStoreProps = {
- filterList: [
- {
- filter: new ListAttributeValueBuilder()
- .withType("Long")
- .withFormatter(
- value => (value ? value.toString() : ""),
- (value: string) => ({ valid: true, value })
- )
- .withFilterable(true)
- .build()
- }
- ],
- parentChannelName: headerFilterStoreInfo.filtersChannelName
- };
- const headerFilterStore = new HeaderFiltersStore(props, headerFilterStoreInfo, null);
- (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
+ const headerFilterStore = new HeaderFiltersStore(
+ mockSpec({
+ filterList: [
+ {
+ filter: new ListAttributeValueBuilder()
+ .withType("Long")
+ .withFormatter(
+ value => (value ? value.toString() : ""),
+ (value: string) => ({ valid: true, value })
+ )
+ .withFilterable(true)
+ .build()
+ }
+ ]
+ })
+ );
+ (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
headerFilterStore.context
);
});
@@ -194,39 +194,40 @@ describe("Number Filter", () => {
describe("with multiple attributes", () => {
beforeEach(() => {
- const props: HeaderFiltersStoreProps = {
- filterList: [
- {
- filter: new ListAttributeValueBuilder()
- .withId("attribute1")
- .withType("Long")
- .withFormatter(
- value => value,
- () => {
- // noop
- }
- )
- .withFilterable(true)
- .build()
- },
- {
- filter: new ListAttributeValueBuilder()
- .withId("attribute2")
- .withType("Decimal")
- .withFormatter(
- value => value,
- () => {
- // noop
- }
- )
- .withFilterable(true)
- .build()
- }
- ],
- parentChannelName: headerFilterStoreInfo.filtersChannelName
- };
- const headerFilterStore = new HeaderFiltersStore(props, headerFilterStoreInfo, null);
- (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
+ const headerFilterStore = new HeaderFiltersStore(
+ mockSpec({
+ filterList: [
+ {
+ filter: new ListAttributeValueBuilder()
+ .withId("attribute1")
+ .withType("Long")
+ .withFormatter(
+ value => value,
+ () => {
+ // noop
+ }
+ )
+ .withFilterable(true)
+ .build()
+ },
+ {
+ filter: new ListAttributeValueBuilder()
+ .withId("attribute2")
+ .withType("Decimal")
+ .withFormatter(
+ value => value,
+ () => {
+ // noop
+ }
+ )
+ .withFilterable(true)
+ .build()
+ }
+ ]
+ })
+ );
+
+ (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
headerFilterStore.context
);
});
@@ -296,13 +297,14 @@ describe("Number Filter", () => {
describe("with wrong attribute's type", () => {
beforeAll(() => {
- const props: HeaderFiltersStoreProps = {
+ const spec = mockSpec({
filterList: [
{ filter: new ListAttributeValueBuilder().withType("Boolean").withFilterable(true).build() }
]
- };
- const headerFilterStore = new HeaderFiltersStore(props, headerFilterStoreInfo, null);
- (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
+ });
+
+ const headerFilterStore = new HeaderFiltersStore(spec);
+ (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
headerFilterStore.context
);
});
@@ -320,7 +322,7 @@ describe("Number Filter", () => {
describe("with wrong multiple attributes' types", () => {
beforeAll(() => {
- const props: HeaderFiltersStoreProps = {
+ const spec = mockSpec({
filterList: [
{
filter: new ListAttributeValueBuilder()
@@ -337,9 +339,10 @@ describe("Number Filter", () => {
.build()
}
]
- };
- const headerFilterStore = new HeaderFiltersStore(props, headerFilterStoreInfo, null);
- (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
+ });
+
+ const headerFilterStore = new HeaderFiltersStore(spec);
+ (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
headerFilterStore.context
);
});
@@ -370,7 +373,7 @@ describe("Number Filter", () => {
describe("with multiple instances", () => {
beforeEach(() => {
- const props: HeaderFiltersStoreProps = {
+ const spec = mockSpec({
filterList: [
{
filter: new ListAttributeValueBuilder()
@@ -385,9 +388,9 @@ describe("Number Filter", () => {
.build()
}
]
- };
- const headerFilterStore = new HeaderFiltersStore(props, headerFilterStoreInfo, null);
- (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
+ });
+ const headerFilterStore = new HeaderFiltersStore(spec);
+ (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
headerFilterStore.context
);
});
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/hocs/withLinkedAttributes.tsx b/packages/pluggableWidgets/datagrid-number-filter-web/src/hocs/withLinkedAttributes.tsx
new file mode 100644
index 0000000000..3ec11b8f0d
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/hocs/withLinkedAttributes.tsx
@@ -0,0 +1,72 @@
+import { createElement } from "react";
+import { AttributeMetaData } from "mendix";
+import { useFilterAPI } from "@mendix/widget-plugin-filtering/context";
+import { APIError } from "@mendix/widget-plugin-filtering/errors";
+import { error, value, Result } from "@mendix/widget-plugin-filtering/result-meta";
+import { Number_InputFilterInterface } from "@mendix/widget-plugin-filtering/typings/InputFilterInterface";
+import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
+import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
+import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup";
+import { NumberStoreProvider } from "@mendix/widget-plugin-filtering/custom-filter-api/NumberStoreProvider";
+import { ISetupable } from "@mendix/widget-plugin-mobx-kit/setupable";
+import { Big } from "big.js";
+
+interface RequiredProps {
+ attributes: Array<{
+ attribute: AttributeMetaData;
+ }>;
+ name: string;
+}
+
+interface StoreProvider extends ISetupable {
+ store: Number_InputFilterInterface;
+}
+
+type Component = (props: P) => React.ReactElement;
+
+export function withLinkedAttributes
(
+ 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: Number_InputFilterInterface;
+ 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 NumberStoreProvider(filterAPI.value, {
+ attributes: props.attributes.map(obj => obj.attribute),
+ dataKey: props.name
+ }),
+ channel: filterAPI.value.parentChannelName
+ });
+ });
+}
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/hocs/withNumberFilterAPI.tsx b/packages/pluggableWidgets/datagrid-number-filter-web/src/hocs/withNumberFilterAPI.tsx
index e937dfd70a..73c78450ea 100644
--- a/packages/pluggableWidgets/datagrid-number-filter-web/src/hocs/withNumberFilterAPI.tsx
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/hocs/withNumberFilterAPI.tsx
@@ -6,7 +6,7 @@ export function withNumberFilterAPI
(
Component: (props: P & Number_FilterAPIv2) => React.ReactElement
): (props: P) => React.ReactElement {
return function FilterAPIProvider(props) {
- const api = useNumberFilterAPI("");
+ const api = useNumberFilterAPI();
if (api.hasError) {
return {api.error.message} ;
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/src/package.xml b/packages/pluggableWidgets/datagrid-number-filter-web/src/package.xml
index e34fd6e137..e039d03a35 100644
--- a/packages/pluggableWidgets/datagrid-number-filter-web/src/package.xml
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/src/package.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/packages/pluggableWidgets/datagrid-number-filter-web/typings/DatagridNumberFilterProps.d.ts b/packages/pluggableWidgets/datagrid-number-filter-web/typings/DatagridNumberFilterProps.d.ts
index 0c2f405af5..8b9a872dff 100644
--- a/packages/pluggableWidgets/datagrid-number-filter-web/typings/DatagridNumberFilterProps.d.ts
+++ b/packages/pluggableWidgets/datagrid-number-filter-web/typings/DatagridNumberFilterProps.d.ts
@@ -4,17 +4,28 @@
* @author Mendix Widgets Framework Team
*/
import { CSSProperties } from "react";
-import { ActionValue, DynamicValue, EditableValue } from "mendix";
+import { ActionValue, AttributeMetaData, DynamicValue, EditableValue } from "mendix";
import { Big } from "big.js";
+export type AttrChoiceEnum = "auto" | "linked";
+
+export interface AttributesType {
+ attribute: AttributeMetaData;
+}
+
export type DefaultFilterEnum = "greater" | "greaterEqual" | "equal" | "notEqual" | "smaller" | "smallerEqual" | "empty" | "notEmpty";
+export interface AttributesPreviewType {
+ attribute: string;
+}
+
export interface DatagridNumberFilterContainerProps {
name: string;
class: string;
style?: CSSProperties;
tabIndex?: number;
- advanced: boolean;
+ attrChoice: AttrChoiceEnum;
+ attributes: AttributesType[];
defaultValue?: DynamicValue;
defaultFilter: DefaultFilterEnum;
placeholder?: DynamicValue;
@@ -37,7 +48,8 @@ export interface DatagridNumberFilterPreviewProps {
readOnly: boolean;
renderMode: "design" | "xray" | "structure";
translate: (text: string) => string;
- advanced: boolean;
+ attrChoice: AttrChoiceEnum;
+ attributes: AttributesPreviewType[];
defaultValue: string;
defaultFilter: DefaultFilterEnum;
placeholder: string;
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/package.json b/packages/pluggableWidgets/datagrid-text-filter-web/package.json
index 0aecea7bf8..6073ee1aa4 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/package.json
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/package.json
@@ -1,7 +1,7 @@
{
"name": "@mendix/datagrid-text-filter-web",
"widgetName": "DatagridTextFilter",
- "version": "2.9.1",
+ "version": "3.0.0",
"description": "",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
@@ -45,6 +45,7 @@
"@mendix/widget-plugin-external-events": "workspace:*",
"@mendix/widget-plugin-filtering": "workspace:*",
"@mendix/widget-plugin-hooks": "workspace:*",
+ "@mendix/widget-plugin-mobx-kit": "workspace:^",
"@mendix/widget-plugin-platform": "workspace:*",
"classnames": "^2.3.2",
"mobx": "6.12.3",
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.editorConfig.ts b/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.editorConfig.ts
index 2414af2eae..787ae0b8c9 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.editorConfig.ts
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.editorConfig.ts
@@ -1,10 +1,4 @@
-import {
- ContainerProps,
- ImageProps,
- StructurePreviewProps,
- text,
- structurePreviewPalette
-} from "@mendix/widget-plugin-platform/preview/structure-preview-api";
+import { hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools";
import {
containsIcon,
containsIconDark,
@@ -29,25 +23,27 @@ import {
startsWithIcon,
startsWithIconDark
} from "@mendix/widget-plugin-filtering/preview/editor-preview-icons";
-import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools";
+import {
+ ContainerProps,
+ ImageProps,
+ structurePreviewPalette,
+ StructurePreviewProps,
+ text
+} from "@mendix/widget-plugin-platform/preview/structure-preview-api";
import { DatagridTextFilterPreviewProps, DefaultFilterEnum } from "../typings/DatagridTextFilterProps";
-export function getProperties(
- values: DatagridTextFilterPreviewProps,
- defaultProperties: Properties,
- platform: "web" | "desktop"
-): Properties {
+export function getProperties(values: DatagridTextFilterPreviewProps, defaultProperties: Properties): Properties {
if (!values.adjustable) {
hidePropertyIn(defaultProperties, values, "screenReaderButtonCaption");
}
- if (platform === "web") {
- if (!values.advanced) {
- hidePropertiesIn(defaultProperties, values, ["onChange", "valueAttribute"]);
- }
- } else {
- hidePropertyIn(defaultProperties, values, "advanced");
+
+ if (values.attrChoice === "auto") {
+ hidePropertyIn(defaultProperties, values, "attributes");
}
+
+ hidePropertyIn(defaultProperties, {} as { linkedDs: unknown }, "linkedDs");
+
return defaultProperties;
}
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.tsx b/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.tsx
index 5772f0eb2f..f47d4f1bcc 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.tsx
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.tsx
@@ -4,10 +4,18 @@ import { DatagridTextFilterContainerProps } from "../typings/DatagridTextFilterP
import { TextFilterContainer } from "./components/TextFilterContainer";
import { withTextFilterAPI } from "./hocs/withTextFilterAPI";
import { isLoadingDefaultValues } from "./utils/widget-utils";
+import { withLinkedAttributes } from "./hocs/withLinkedAttributes";
const container = withPreloader(TextFilterContainer, isLoadingDefaultValues);
-const Widget = withTextFilterAPI(container);
+const FilterAuto = withTextFilterAPI(container);
+const FilterLinked = withLinkedAttributes(container);
export default function DatagridTextFilter(props: DatagridTextFilterContainerProps): ReactElement {
- return ;
+ const isAuto = props.attrChoice === "auto";
+
+ if (isAuto) {
+ return ;
+ }
+
+ return ;
}
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.xml b/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.xml
index d8ed8c4cfc..893a20f660 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.xml
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/DatagridTextFilter.xml
@@ -8,9 +8,32 @@
-
- Enable advanced options
+
+ Filter attributes
+
+ Auto
+ Custom
+
+
+
+ Datasource to Filter
+
+
+
+ Attributes
+ Select the attributes that the end-user may use for filtering.
+
+
+
+ Attribute
+
+
+
+
+
+
+
Default value
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx b/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx
index 7726d55c48..3b8ae18f20 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/components/__tests__/DatagridTextFilter.spec.tsx
@@ -1,8 +1,7 @@
import "@testing-library/jest-dom";
-import { FilterAPIv2 } from "@mendix/widget-plugin-filtering/context";
import {
HeaderFiltersStore,
- HeaderFiltersStoreProps
+ HeaderFiltersStoreSpec
} from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore";
import {
actionValue,
@@ -17,11 +16,8 @@ import { createContext, createElement } from "react";
import DatagridTextFilter from "../../DatagridTextFilter";
import { DatagridTextFilterContainerProps } from "../../../typings/DatagridTextFilterProps";
import { resetIdCounter } from "downshift";
-
-interface StaticInfo {
- name: string;
- filtersChannelName: string;
-}
+import { FilterObserver } from "@mendix/widget-plugin-filtering/typings/FilterObserver";
+import { FilterAPI } from "@mendix/widget-plugin-filtering/context";
const commonProps: DatagridTextFilterContainerProps = {
class: "filter-custom-class",
@@ -29,13 +25,9 @@ const commonProps: DatagridTextFilterContainerProps = {
name: "filter-test",
defaultFilter: "equal" as const,
adjustable: true,
- advanced: false,
- delay: 1000
-};
-
-const headerFilterStoreInfo: StaticInfo = {
- name: commonProps.name,
- filtersChannelName: "datagrid1"
+ delay: 1000,
+ attrChoice: "auto",
+ attributes: []
};
jest.useFakeTimers();
@@ -57,7 +49,7 @@ describe("Text Filter", () => {
describe("with defaultValue prop", () => {
beforeEach(() => {
- const props: HeaderFiltersStoreProps = {
+ const spec: HeaderFiltersStoreSpec = {
filterList: [
{
filter: new ListAttributeValueBuilder()
@@ -70,10 +62,13 @@ describe("Text Filter", () => {
.build()
}
],
- parentChannelName: "datagrid1"
+ filterChannelName: "datagrid1",
+ sharedInitFilter: [],
+ headerInitFilter: [],
+ customFilterHost: {} as FilterObserver
};
- const headerFilterStore = new HeaderFiltersStore(props, headerFilterStoreInfo, null);
- (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
+ const headerFilterStore = new HeaderFiltersStore(spec);
+ (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
headerFilterStore.context
);
});
@@ -170,7 +165,7 @@ describe("Text Filter", () => {
describe("with single attribute", () => {
beforeAll(() => {
- const props: HeaderFiltersStoreProps = {
+ const spec: HeaderFiltersStoreSpec = {
filterList: [
{
filter: new ListAttributeValueBuilder()
@@ -183,10 +178,13 @@ describe("Text Filter", () => {
.build()
}
],
- parentChannelName: "datagrid1"
+ filterChannelName: "datagrid1",
+ sharedInitFilter: [],
+ headerInitFilter: [],
+ customFilterHost: {} as FilterObserver
};
- const headerFilterStore = new HeaderFiltersStore(props, headerFilterStoreInfo, null);
- (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
+ const headerFilterStore = new HeaderFiltersStore(spec);
+ (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
headerFilterStore.context
);
});
@@ -256,7 +254,7 @@ describe("Text Filter", () => {
describe("with multiple attributes", () => {
beforeAll(() => {
- const props: HeaderFiltersStoreProps = {
+ const spec: HeaderFiltersStoreSpec = {
filterList: [
{
filter: new ListAttributeValueBuilder()
@@ -284,10 +282,14 @@ describe("Text Filter", () => {
.withFilterable(true)
.build()
}
- ]
+ ],
+ filterChannelName: "datagrid1",
+ sharedInitFilter: [],
+ headerInitFilter: [],
+ customFilterHost: {} as FilterObserver
};
- const headerFilterStore = new HeaderFiltersStore(props, headerFilterStoreInfo, null);
- (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
+ const headerFilterStore = new HeaderFiltersStore(spec);
+ (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
headerFilterStore.context
);
});
@@ -305,13 +307,17 @@ describe("Text Filter", () => {
describe("with wrong attribute's type", () => {
beforeAll(() => {
- const props: HeaderFiltersStoreProps = {
+ const spec: HeaderFiltersStoreSpec = {
filterList: [
{ filter: new ListAttributeValueBuilder().withType("Decimal").withFilterable(true).build() }
- ]
+ ],
+ filterChannelName: "datagrid1",
+ sharedInitFilter: [],
+ headerInitFilter: [],
+ customFilterHost: {} as FilterObserver
};
- const headerFilterStore = new HeaderFiltersStore(props, headerFilterStoreInfo, null);
- (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
+ const headerFilterStore = new HeaderFiltersStore(spec);
+ (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
headerFilterStore.context
);
});
@@ -329,7 +335,7 @@ describe("Text Filter", () => {
describe("with wrong multiple attributes' types", () => {
beforeAll(() => {
- const props: HeaderFiltersStoreProps = {
+ const spec: HeaderFiltersStoreSpec = {
filterList: [
{
filter: new ListAttributeValueBuilder()
@@ -345,10 +351,14 @@ describe("Text Filter", () => {
.withFilterable(true)
.build()
}
- ]
+ ],
+ filterChannelName: "datagrid1",
+ sharedInitFilter: [],
+ headerInitFilter: [],
+ customFilterHost: {} as FilterObserver
};
- const headerFilterStore = new HeaderFiltersStore(props, headerFilterStoreInfo, null);
- (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
+ const headerFilterStore = new HeaderFiltersStore(spec);
+ (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
headerFilterStore.context
);
});
@@ -379,7 +389,7 @@ describe("Text Filter", () => {
describe("with multiple instances", () => {
beforeAll(() => {
- const props: HeaderFiltersStoreProps = {
+ const spec: HeaderFiltersStoreSpec = {
filterList: [
{
filter: new ListAttributeValueBuilder()
@@ -393,10 +403,14 @@ describe("Text Filter", () => {
.withFilterable(true)
.build()
}
- ]
+ ],
+ filterChannelName: "datagrid1",
+ sharedInitFilter: [],
+ headerInitFilter: [],
+ customFilterHost: {} as FilterObserver
};
- const headerFilterStore = new HeaderFiltersStore(props, headerFilterStoreInfo, null);
- (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
+ const headerFilterStore = new HeaderFiltersStore(spec);
+ (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext(
headerFilterStore.context
);
});
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/hocs/withLinkedAttributes.tsx b/packages/pluggableWidgets/datagrid-text-filter-web/src/hocs/withLinkedAttributes.tsx
new file mode 100644
index 0000000000..839892a368
--- /dev/null
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/hocs/withLinkedAttributes.tsx
@@ -0,0 +1,71 @@
+import { createElement } from "react";
+import { AttributeMetaData } from "mendix";
+import { useFilterAPI } from "@mendix/widget-plugin-filtering/context";
+import { APIError } from "@mendix/widget-plugin-filtering/errors";
+import { error, value, Result } from "@mendix/widget-plugin-filtering/result-meta";
+import { String_InputFilterInterface } from "@mendix/widget-plugin-filtering/typings/InputFilterInterface";
+import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
+import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
+import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup";
+import { StringStoreProvider } from "@mendix/widget-plugin-filtering/custom-filter-api/StringStoreProvider";
+import { ISetupable } from "@mendix/widget-plugin-mobx-kit/setupable";
+
+interface RequiredProps {
+ attributes: Array<{
+ attribute: AttributeMetaData;
+ }>;
+ name: string;
+}
+
+interface StoreProvider extends ISetupable {
+ store: String_InputFilterInterface;
+}
+
+type Component = (props: P) => React.ReactElement;
+
+export function withLinkedAttributes
(
+ 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: String_InputFilterInterface;
+ 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 StringStoreProvider(filterAPI.value, {
+ attributes: props.attributes.map(obj => obj.attribute),
+ dataKey: props.name
+ }),
+ channel: filterAPI.value.parentChannelName
+ });
+ });
+}
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/hocs/withTextFilterAPI.tsx b/packages/pluggableWidgets/datagrid-text-filter-web/src/hocs/withTextFilterAPI.tsx
index c54deea135..726f91c054 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/hocs/withTextFilterAPI.tsx
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/hocs/withTextFilterAPI.tsx
@@ -6,7 +6,7 @@ export function withTextFilterAPI
(
Component: (props: P & String_FilterAPIv2) => React.ReactElement
): (props: P) => React.ReactElement {
return function FilterAPIProvider(props) {
- const api = useStringFilterAPI("");
+ const api = useStringFilterAPI();
if (api.hasError) {
return {api.error.message} ;
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/src/package.xml b/packages/pluggableWidgets/datagrid-text-filter-web/src/package.xml
index 144d3e9453..48e78f1246 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/src/package.xml
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/src/package.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/packages/pluggableWidgets/datagrid-text-filter-web/typings/DatagridTextFilterProps.d.ts b/packages/pluggableWidgets/datagrid-text-filter-web/typings/DatagridTextFilterProps.d.ts
index dfd4193e6a..170eb35aa8 100644
--- a/packages/pluggableWidgets/datagrid-text-filter-web/typings/DatagridTextFilterProps.d.ts
+++ b/packages/pluggableWidgets/datagrid-text-filter-web/typings/DatagridTextFilterProps.d.ts
@@ -4,16 +4,27 @@
* @author Mendix Widgets Framework Team
*/
import { CSSProperties } from "react";
-import { ActionValue, DynamicValue, EditableValue } from "mendix";
+import { ActionValue, AttributeMetaData, DynamicValue, EditableValue } from "mendix";
+
+export type AttrChoiceEnum = "auto" | "linked";
+
+export interface AttributesType {
+ attribute: AttributeMetaData;
+}
export type DefaultFilterEnum = "contains" | "startsWith" | "endsWith" | "greater" | "greaterEqual" | "equal" | "notEqual" | "smaller" | "smallerEqual" | "empty" | "notEmpty";
+export interface AttributesPreviewType {
+ attribute: string;
+}
+
export interface DatagridTextFilterContainerProps {
name: string;
class: string;
style?: CSSProperties;
tabIndex?: number;
- advanced: boolean;
+ attrChoice: AttrChoiceEnum;
+ attributes: AttributesType[];
defaultValue?: DynamicValue;
defaultFilter: DefaultFilterEnum;
placeholder?: DynamicValue;
@@ -36,7 +47,8 @@ export interface DatagridTextFilterPreviewProps {
readOnly: boolean;
renderMode: "design" | "xray" | "structure";
translate: (text: string) => string;
- advanced: boolean;
+ attrChoice: AttrChoiceEnum;
+ attributes: AttributesPreviewType[];
defaultValue: string;
defaultFilter: DefaultFilterEnum;
placeholder: string;
diff --git a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js
index 5402ff52cd..321616b7d9 100644
--- a/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js
+++ b/packages/pluggableWidgets/datagrid-web/e2e/DataGrid.spec.js
@@ -113,14 +113,14 @@ test.describe("capabilities: hiding", () => {
const textAreaValue = await textArea.inputValue();
expect(JSON.parse(textAreaValue)).toEqual({
name: "datagrid5",
- schemaVersion: 2,
+ schemaVersion: 3,
settingsHash: "1530160614",
columns: [
{ columnId: "0", hidden: true },
{ columnId: "1", hidden: false }
],
columnFilters: [],
- groupFilters: [],
+ customFilters: [],
sortOrder: [],
columnOrder: ["0", "1"]
});
diff --git a/packages/pluggableWidgets/datagrid-web/package.json b/packages/pluggableWidgets/datagrid-web/package.json
index 0c7fb50a83..d166365111 100644
--- a/packages/pluggableWidgets/datagrid-web/package.json
+++ b/packages/pluggableWidgets/datagrid-web/package.json
@@ -1,7 +1,7 @@
{
"name": "@mendix/datagrid-web",
"widgetName": "Datagrid",
- "version": "2.30.4",
+ "version": "3.0.0",
"description": "",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts
index bdf7464d68..411f28f843 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts
@@ -74,21 +74,6 @@ export function getProperties(
"hidable"
]);
}
-
- if (!column.filterAssociation) {
- hideNestedPropertiesIn(defaultProperties, values, "columns", index, [
- "filterAssociationOptions",
- "filterAssociationOptionLabel",
- "fetchOptionsLazy",
- "filterCaptionType",
- "filterAssociationOptionLabelAttr"
- ]);
- }
- if (column.filterCaptionType === "attribute") {
- hidePropertyIn(defaultProperties, values, "columns", index, "filterAssociationOptionLabel");
- } else {
- hidePropertyIn(defaultProperties, values, "columns", index, "filterAssociationOptionLabelAttr");
- }
});
if (values.pagination === "buttons") {
hidePropertyIn(defaultProperties, values, "showNumberOfRows");
@@ -152,8 +137,6 @@ export function getProperties(
"columnsHidable",
"configurationAttribute",
"onConfigurationChange",
- "filterList",
- "filtersPlaceholder",
"filterSectionTitle"
]);
}
@@ -209,11 +192,6 @@ export const getPreview = (
draggable: false,
dynamicText: "Dynamic text",
filter: { widgetCount: 0, renderer: () => null },
- filterAssociation: "",
- filterAssociationOptionLabel: "",
- filterAssociationOptionLabelAttr: "",
- filterAssociationOptions: {},
- filterCaptionType: "attribute",
header: "Column",
hidable: "no",
resizable: false,
@@ -227,8 +205,7 @@ export const getPreview = (
minWidth: "auto",
minWidthLimit: 100,
allowEventPropagation: true,
- exportValue: "",
- fetchOptionsLazy: true
+ exportValue: ""
}
];
const columns = rowLayout({
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx
index 1bef235a15..ba1df3f098 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx
@@ -35,11 +35,7 @@ const initColumns: ColumnsPreviewType[] = [
draggable: false,
dynamicText: "Dynamic Text",
filter: { renderer: () =>
, widgetCount: 0 },
- filterAssociation: "",
- filterAssociationOptionLabel: "",
- filterAssociationOptionLabelAttr: "",
- filterAssociationOptions: {},
- filterCaptionType: "expression",
+
header: "Column",
hidable: "no",
resizable: false,
@@ -53,8 +49,7 @@ const initColumns: ColumnsPreviewType[] = [
minWidth: "auto",
minWidthLimit: 100,
allowEventPropagation: true,
- exportValue: "",
- fetchOptionsLazy: true
+ exportValue: ""
}
];
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
index 0178fe3ea8..018b3a94d4 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx
@@ -82,7 +82,7 @@ const Container = observer((props: Props): ReactElement => {
headerTitle={props.filterSectionTitle?.value}
headerContent={
props.filtersPlaceholder && (
-
+
{props.filtersPlaceholder}
)
diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
index 3f00e78f9c..b403c9134c 100644
--- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
+++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml
@@ -121,44 +121,6 @@
-
-
- Entity
- Set the entity to enable filtering over association with the Drop-down filter widget.
-
-
-
-
-
-
- Selectable objects
- The options to show in the Drop-down filter widget.
-
-
- Use lazy load
- Lazy loading enables faster data grid loading, but with personalization enabled, value restoration will be limited.
-
-
- Option caption type
-
-
- Attribute
- Expression
-
-
-
- Option caption
-
-
-
-
- Option caption
-
-
-
-
-
-
Can sort
@@ -316,6 +278,10 @@
On selection change
+
+ Filters placeholder
+
+
@@ -363,36 +329,6 @@
-
-
-
- Filters
- The list of attributes is used by the filter widgets that are placed in the placeholder above the data grid. This enables filtering across multiple attributes or columns instead of limiting to a single column.
-
-
-
- Filter attribute
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Filters placeholder
-
-
-
-
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/StickySentinel.tsx b/packages/pluggableWidgets/datagrid-web/src/components/StickySentinel.tsx
deleted file mode 100644
index cde33fdd50..0000000000
--- a/packages/pluggableWidgets/datagrid-web/src/components/StickySentinel.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import classNames from "classnames";
-import { createElement, ReactElement, useState, useEffect, useRef } from "react";
-
-/**
- * StickySentinel - A small hidden element that uses "IntersectionObserver"
- * to detect the "scrolled" state of the grid. By toggling the "container-stuck" class
- * on this element, we can force "position: sticky" for column headers.
- */
-export function StickySentinel(): ReactElement {
- const sentinelRef = useRef(null);
- const [ratio, setRatio] = useState(1);
-
- useEffect(() => {
- const target = sentinelRef.current;
-
- if (target === null) {
- return;
- }
-
- return createObserver(target, setRatio);
- }, []);
-
- return (
-
- );
-}
-
-function createObserver(target: Element, onIntersectionChange: (ratio: number) => void): () => void {
- const options = { threshold: [0, 1] };
-
- const observer = new IntersectionObserver(([entry]) => {
- if (entry.intersectionRatio === 0 || entry.intersectionRatio === 1) {
- onIntersectionChange(entry.intersectionRatio);
- }
- }, options);
-
- observer.observe(target);
-
- return () => observer.unobserve(target);
-}
diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx
index 840cf55d2d..651c04806a 100644
--- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx
@@ -1,35 +1,27 @@
import { getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context";
-import { HeaderFiltersStore } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore";
import {
getGlobalSelectionContext,
SelectionHelper,
useCreateSelectionContextValue
} from "@mendix/widget-plugin-grid/selection";
import { createElement, memo, ReactElement, ReactNode } from "react";
+import { RootGridStore } from "../helpers/state/RootGridStore";
interface WidgetHeaderContextProps {
children?: ReactNode;
- filtersStore: HeaderFiltersStore;
selectionHelper?: SelectionHelper;
+ rootStore: RootGridStore;
}
const SelectionContext = getGlobalSelectionContext();
const FilterContext = getGlobalFilterContextObject();
-function FilterAPIProvider(props: { filtersStore: HeaderFiltersStore; children?: ReactNode }): ReactElement {
- return {props.children} ;
-}
-
-function SelectionStatusProvider(props: { selectionHelper?: SelectionHelper; children?: ReactNode }): ReactElement {
- const value = useCreateSelectionContextValue(props.selectionHelper);
- return {props.children} ;
-}
-
function HeaderContainer(props: WidgetHeaderContextProps): ReactElement {
+ const selectionContext = useCreateSelectionContextValue(props.selectionHelper);
return (
-
- {props.children}
-
+
+ {props.children}
+
);
}
diff --git a/packages/pluggableWidgets/datagrid-web/src/consistency-check.ts b/packages/pluggableWidgets/datagrid-web/src/consistency-check.ts
index bd6a615c83..de02c76a80 100644
--- a/packages/pluggableWidgets/datagrid-web/src/consistency-check.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/consistency-check.ts
@@ -4,13 +4,7 @@ import { ColumnsPreviewType, DatagridPreviewProps } from "../typings/DatagridPro
export function check(values: DatagridPreviewProps): Problem[] {
const errors: Problem[] = [];
- const columnChecks = [
- checkAssociationSettings,
- checkFilteringSettings,
- checkDisplaySettings,
- checkSortingSettings,
- checkHidableSettings
- ];
+ const columnChecks = [checkDisplaySettings, checkSortingSettings, checkHidableSettings];
values.columns.forEach((column: ColumnsPreviewType, index) => {
for (const check of columnChecks) {
@@ -28,51 +22,6 @@ export function check(values: DatagridPreviewProps): Problem[] {
const columnPropPath = (prop: string, index: number): string => `columns/${index + 1}/${prop}`;
-const checkAssociationSettings = (
- values: DatagridPreviewProps,
- column: ColumnsPreviewType,
- index: number
-): Problem | undefined => {
- if (!values.columnsFilterable) {
- return;
- }
-
- if (!column.filterAssociation) {
- return;
- }
-
- if (column.filterCaptionType === "expression" && !column.filterAssociationOptionLabel) {
- return {
- property: columnPropPath("filterAssociationOptionLabel", index),
- message: `A caption is required when using associations. Please set 'Option caption' property for column (${column.header})`
- };
- }
-
- if (column.filterCaptionType === "attribute" && !column.filterAssociationOptionLabelAttr) {
- return {
- property: columnPropPath("filterAssociationOptionLabelAttr", index),
- message: `A caption is required when using associations. Please set 'Option caption' property for column (${column.header})`
- };
- }
-};
-
-const checkFilteringSettings = (
- values: DatagridPreviewProps,
- column: ColumnsPreviewType,
- index: number
-): Problem | undefined => {
- if (!values.columnsFilterable) {
- return;
- }
-
- if (!column.attribute && !column.filterAssociation) {
- return {
- property: columnPropPath("attribute", index),
- message: `An attribute or reference is required when filtering is enabled. Please select 'Attribute' or 'Reference' property for column (${column.header})`
- };
- }
-};
-
const checkDisplaySettings = (
_values: DatagridPreviewProps,
column: ColumnsPreviewType,
diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/StateSyncController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts
similarity index 69%
rename from packages/pluggableWidgets/datagrid-web/src/controllers/StateSyncController.ts
rename to packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts
index 397c73e3c6..d99e7f2d06 100644
--- a/packages/pluggableWidgets/datagrid-web/src/controllers/StateSyncController.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts
@@ -1,45 +1,45 @@
-import { compactArray, fromCompactArray, isAnd } from "@mendix/widget-plugin-filtering/condition-utils";
+import { compactArray, fromCompactArray, isAnd } from "@mendix/filter-commons/condition-utils";
+import { QueryController } from "@mendix/widget-plugin-grid/query/query-controller";
import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller";
import { FilterCondition } from "mendix/filters";
import { and } from "mendix/filters/builders";
import { makeAutoObservable, reaction } from "mobx";
import { SortInstruction } from "../typings/sorting";
-import { QueryController } from "./query-controller";
interface Columns {
conditions: Array;
sortInstructions: SortInstruction[] | undefined;
}
-interface Header {
+interface FiltersInput {
conditions: Array;
}
-type StateSyncControllerSpec = {
+type DatasourceParamsControllerSpec = {
query: QueryController;
columns: Columns;
- header: Header;
+ customFilters: FiltersInput;
};
-export class StateSyncController implements ReactiveController {
+export class DatasourceParamsController implements ReactiveController {
private columns: Columns;
- private header: Header;
private query: QueryController;
+ private customFilters: FiltersInput;
- constructor(host: ReactiveControllerHost, spec: StateSyncControllerSpec) {
+ constructor(host: ReactiveControllerHost, spec: DatasourceParamsControllerSpec) {
host.addController(this);
this.columns = spec.columns;
- this.header = spec.header;
this.query = spec.query;
+ this.customFilters = spec.customFilters;
makeAutoObservable(this, { setup: false });
}
private get derivedFilter(): FilterCondition | undefined {
- const { columns, header } = this;
+ const { columns, customFilters } = this;
- return and(compactArray(columns.conditions), compactArray(header.conditions));
+ return and(compactArray(columns.conditions), compactArray(customFilters.conditions));
}
private get derivedSortOrder(): SortInstruction[] | undefined {
@@ -68,7 +68,7 @@ export class StateSyncController implements ReactiveController {
static unzipFilter(
filter?: FilterCondition
- ): [columns: Array, header: Array] {
+ ): [columns: Array, sharedFilter: Array] {
if (!filter) {
return [[], []];
}
@@ -78,7 +78,8 @@ export class StateSyncController implements ReactiveController {
if (filter.args.length !== 2) {
return [[], []];
}
- const [columns, header] = filter.args;
- return [fromCompactArray(columns), fromCompactArray(header)];
+
+ const [columns, shared] = filter.args;
+ return [fromCompactArray(columns), fromCompactArray(shared)];
}
}
diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/PaginationController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/PaginationController.ts
index 28a966e029..454c47a674 100644
--- a/packages/pluggableWidgets/datagrid-web/src/controllers/PaginationController.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/controllers/PaginationController.ts
@@ -1,7 +1,7 @@
+import { QueryController } from "@mendix/widget-plugin-grid/query/query-controller";
import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller";
import { PaginationEnum, ShowPagingButtonsEnum } from "../../typings/DatagridProps";
-import { QueryController } from "./query-controller";
type Gate = DerivedPropsGate<{
pageSize: number;
diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts
index efc8dd67ad..b3660c0767 100644
--- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts
@@ -1,5 +1,5 @@
-import { disposeFx } from "@mendix/widget-plugin-filtering/mobx-utils";
-import { FiltersSettingsMap } from "@mendix/widget-plugin-filtering/typings/settings";
+import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings";
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
import { FilterCondition } from "mendix/filters";
import { action, computed, makeObservable, observable } from "mobx";
import { DatagridContainerProps } from "../../../typings/DatagridProps";
@@ -13,7 +13,7 @@ import {
sortInstructionsToSortRules,
sortRulesToSortInstructions
} from "./ColumnsSortingStore";
-import { ColumnFilterStore } from "./column/ColumnFilterStore";
+import { ColumnFilterStore, ObserverBag } from "./column/ColumnFilterStore";
import { ColumnStore } from "./column/ColumnStore";
export interface IColumnGroupStore {
@@ -46,17 +46,18 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore {
constructor(
props: Pick,
info: StaticInfo,
- dsViewState: Array | null
+ initFilter: Array,
+ observerBag: ObserverBag
) {
this._allColumns = [];
this.columnFilters = [];
props.columns.forEach((columnProps, i) => {
- const initCond = dsViewState?.at(i) ?? null;
+ const initCond = initFilter.at(i) ?? null;
const column = new ColumnStore(i, columnProps, this);
this._allColumnsById.set(column.columnId, column);
this._allColumns[i] = column;
- this.columnFilters[i] = new ColumnFilterStore(columnProps, info, initCond);
+ this.columnFilters[i] = new ColumnFilterStore(columnProps, info, initCond, observerBag);
});
this.sorting = new ColumnsSortingStore(
@@ -82,9 +83,9 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore {
}
setup(): () => void {
- const [disposers, dispose] = disposeFx();
+ const [add, dispose] = disposeBatch();
for (const filter of this.columnFilters) {
- disposers.push(filter.setup());
+ add(filter.setup());
}
return dispose;
}
@@ -92,7 +93,6 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore {
updateProps(props: Pick): void {
props.columns.forEach((columnProps, i) => {
this._allColumns[i].updateProps(columnProps);
- this.columnFilters[i].updateProps(columnProps);
});
if (this.visibleColumns.length < 1) {
@@ -144,7 +144,7 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore {
get conditions(): Array {
return this.columnFilters.map((store, index) => {
- return this._allColumns[index].isHidden ? undefined : store.condition2;
+ return this._allColumns[index].isHidden ? undefined : store.condition;
});
}
diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts
index 21c8024aca..801798b61e 100644
--- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts
@@ -1,6 +1,6 @@
+import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings";
import { error, Result, value } from "@mendix/widget-plugin-filtering/result-meta";
-import { HeaderFiltersStore } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore";
-import { FiltersSettingsMap } from "@mendix/widget-plugin-filtering/typings/settings";
+import { ObservableFilterHost } from "@mendix/widget-plugin-filtering/typings/ObservableFilterHost";
import { action, comparer, computed, IReactionDisposer, makeObservable, reaction } from "mobx";
import { DatagridContainerProps } from "../../../typings/DatagridProps";
import { ColumnId } from "../../typings/GridColumn";
@@ -17,7 +17,7 @@ import { ColumnGroupStore } from "./ColumnGroupStore";
export class GridPersonalizationStore {
private readonly gridName: string;
private readonly gridColumnsHash: string;
- private readonly schemaVersion: GridPersonalizationStorageSettings["schemaVersion"] = 2;
+ private readonly schemaVersion: GridPersonalizationStorageSettings["schemaVersion"] = 3;
private readonly storeFilters: boolean;
private storage: PersonalizationStorage;
@@ -27,7 +27,7 @@ export class GridPersonalizationStore {
constructor(
props: DatagridContainerProps,
private columnsStore: ColumnGroupStore,
- private headerFilters: HeaderFiltersStore
+ private customFilters: ObservableFilterHost
) {
this.gridName = props.name;
this.gridColumnsHash = getHash(this.columnsStore._allColumns, this.gridName);
@@ -35,7 +35,6 @@ export class GridPersonalizationStore {
makeObservable(this, {
settings: computed,
-
applySettings: action
});
@@ -95,6 +94,7 @@ export class GridPersonalizationStore {
private applySettings(settings: GridPersonalizationStorageSettings): void {
this.columnsStore.setColumnSettings(toColumnSettings(settings));
this.columnsStore.setColumnFilterSettings(settings.columnFilters);
+ this.customFilters.settings = new Map(settings.customFilters);
}
private readSettings(
@@ -137,7 +137,7 @@ export class GridPersonalizationStore {
this.gridColumnsHash,
this.columnsStore.columnSettings,
this.storeFilters ? this.columnsStore.filterSettings : new Map(),
- this.storeFilters ? this.headerFilters.settings : new Map()
+ this.storeFilters ? this.customFilters.settings : new Map()
);
}
}
@@ -164,7 +164,7 @@ function toStorageFormat(
gridColumnsHash: string,
columnsSettings: ColumnPersonalizationSettings[],
columnFilters: FiltersSettingsMap,
- groupFilters: FiltersSettingsMap
+ customFilters: FiltersSettingsMap
): GridPersonalizationStorageSettings {
const sortOrder = columnsSettings
.filter(c => c.sortDir && c.sortWeight !== undefined)
@@ -175,7 +175,7 @@ function toStorageFormat(
return {
name: gridName,
- schemaVersion: 2,
+ schemaVersion: 3,
settingsHash: gridColumnsHash,
columns: columnsSettings.map(c => ({
columnId: c.columnId,
@@ -185,7 +185,7 @@ function toStorageFormat(
})),
columnFilters: Array.from(columnFilters),
- groupFilters: Array.from(groupFilters),
+ customFilters: Array.from(customFilters),
sortOrder,
columnOrder
diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts
index a98cbf7eff..cc7e718a08 100644
--- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts
@@ -1,15 +1,16 @@
-import { HeaderFiltersStore } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore";
+import { createContextWithStub, FilterAPI } from "@mendix/widget-plugin-filtering/context";
+import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost";
+import { DatasourceController } from "@mendix/widget-plugin-grid/query/DatasourceController";
+import { RefreshController } from "@mendix/widget-plugin-grid/query/RefreshController";
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";
import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid";
-import { autorun, computed } from "mobx";
+import { autorun } from "mobx";
import { DatagridContainerProps } from "../../../typings/DatagridProps";
-import { DatasourceController } from "../../controllers/DatasourceController";
+import { DatasourceParamsController } from "../../controllers/DatasourceParamsController";
import { DerivedLoaderController } from "../../controllers/DerivedLoaderController";
import { PaginationController } from "../../controllers/PaginationController";
-import { RefreshController } from "../../controllers/RefreshController";
-import { StateSyncController } from "../../controllers/StateSyncController";
import { ProgressStore } from "../../features/data-export/ProgressStore";
import { StaticInfo } from "../../typings/static-info";
import { ColumnGroupStore } from "./ColumnGroupStore";
@@ -24,12 +25,12 @@ type Spec = {
export class RootGridStore extends BaseControllerHost {
columnsStore: ColumnGroupStore;
- headerFiltersStore: HeaderFiltersStore;
settingsStore: GridPersonalizationStore;
staticInfo: StaticInfo;
exportProgressCtrl: ProgressStore;
loaderCtrl: DerivedLoaderController;
paginationCtrl: PaginationController;
+ readonly autonomousFilterAPI: FilterAPI;
private gate: Gate;
@@ -37,28 +38,36 @@ export class RootGridStore extends BaseControllerHost {
super();
const { props } = gate;
- const [columnsViewState, headerViewState] = StateSyncController.unzipFilter(props.datasource.filter);
+ const [columnsInitFilter, sharedInitFilter] = DatasourceParamsController.unzipFilter(props.datasource.filter);
this.gate = gate;
this.staticInfo = {
name: props.name,
filtersChannelName: `datagrid/${generateUUID()}`
};
+ const customFilterHost = new CustomFilterHost();
const query = new DatasourceController(this, { gate });
- const columns = (this.columnsStore = new ColumnGroupStore(props, this.staticInfo, columnsViewState));
- const header = (this.headerFiltersStore = new HeaderFiltersStore(props, this.staticInfo, headerViewState));
- this.settingsStore = new GridPersonalizationStore(props, this.columnsStore, this.headerFiltersStore);
+ this.autonomousFilterAPI = createContextWithStub({
+ filterObserver: customFilterHost,
+ sharedInitFilter,
+ parentChannelName: this.staticInfo.filtersChannelName
+ });
+ const columns = (this.columnsStore = new ColumnGroupStore(props, this.staticInfo, columnsInitFilter, {
+ customFilterHost,
+ sharedInitFilter
+ }));
+ this.settingsStore = new GridPersonalizationStore(props, this.columnsStore, customFilterHost);
this.paginationCtrl = new PaginationController(this, { gate, query });
this.exportProgressCtrl = exportCtrl;
- new StateSyncController(this, {
+ new DatasourceParamsController(this, {
query,
columns,
- header
+ customFilters: customFilterHost
});
new RefreshController(this, {
- query: computed(() => query.computedCopy),
+ query: query.derivedQuery,
delay: props.refreshInterval * 1000
});
@@ -73,7 +82,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)));
diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx b/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx
index 9d0d9ae7cb..fcf274a8aa 100644
--- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx
@@ -1,90 +1,51 @@
-import { FilterAPIv2, getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context";
-import { RefFilterStore, RefFilterStoreProps } from "@mendix/widget-plugin-filtering/stores/picker/RefFilterStore";
-import { StaticSelectFilterStore } from "@mendix/widget-plugin-filtering/stores/picker/StaticSelectFilterStore";
+import { FilterAPI, getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context";
+import { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore";
import { InputFilterStore, attrgroupFilterStore } from "@mendix/widget-plugin-filtering/stores/input/store-utils";
-import { ensure } from "@mendix/widget-plugin-platform/utils/ensure";
+import { FilterData } from "@mendix/filter-commons/typings/settings";
+import { value } from "@mendix/widget-plugin-filtering/result-meta";
+import { ObservableFilterHost } from "@mendix/widget-plugin-filtering/typings/ObservableFilterHost";
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { ListAttributeListValue, ListAttributeValue } from "mendix";
import { FilterCondition } from "mendix/filters";
-import { ListAttributeValue, ListAttributeListValue } from "mendix";
-import { action, computed, makeObservable } from "mobx";
+import { computed, makeObservable } from "mobx";
import { ReactNode, createElement } from "react";
import { ColumnsType } from "../../../../typings/DatagridProps";
import { StaticInfo } from "../../../typings/static-info";
-import { FilterData } from "@mendix/widget-plugin-filtering/typings/settings";
-import { value } from "@mendix/widget-plugin-filtering/result-meta";
-import { disposeFx } from "@mendix/widget-plugin-filtering/mobx-utils";
+
export interface IColumnFilterStore {
renderFilterWidgets(): ReactNode;
}
-type FilterStore = InputFilterStore | StaticSelectFilterStore | RefFilterStore;
+type FilterStore = InputFilterStore | StaticSelectFilterStore;
const { Provider } = getGlobalFilterContextObject();
export class ColumnFilterStore implements IColumnFilterStore {
private _widget: ReactNode;
private _filterStore: FilterStore | null = null;
- private _context: FilterAPIv2;
+ private _context: FilterAPI;
+ private _observerBag: ObserverBag;
- constructor(props: ColumnsType, info: StaticInfo, dsViewState: FilterCondition | null) {
+ constructor(props: ColumnsType, info: StaticInfo, dsViewState: FilterCondition | null, observerBag: ObserverBag) {
+ this._observerBag = observerBag;
this._widget = props.filter;
this._filterStore = this.createFilterStore(props, dsViewState);
this._context = this.createContext(this._filterStore, info);
- makeObservable(this, {
- _updateStore: action,
- condition2: computed,
- updateProps: action
+ makeObservable(this, {
+ condition: computed
});
}
setup(): () => void {
- const [disposers, dispose] = disposeFx();
+ const [add, disposeAll] = disposeBatch();
if (this._filterStore && "setup" in this._filterStore) {
- disposers.push(this._filterStore.setup());
- }
- return dispose;
- }
-
- updateProps(props: ColumnsType): void {
- this._widget = props.filter;
- this._updateStore(props);
- }
-
- private _updateStore(props: ColumnsType): void {
- const store = this._filterStore;
-
- if (store === null) {
- return;
+ add(this._filterStore.setup());
}
-
- if (store.storeType === "refselect") {
- store.updateProps(this.toRefselectProps(props));
- } else if (isListAttributeValue(props.attribute)) {
- store.updateProps([props.attribute]);
- }
- }
-
- private toRefselectProps(props: ColumnsType): RefFilterStoreProps {
- const searchAttrId = props.filterAssociationOptionLabelAttr?.id;
- const caption =
- props.filterCaptionType === "expression"
- ? ensure(props.filterAssociationOptionLabel, errorMessage("filterAssociationOptionLabel"))
- : ensure(props.filterAssociationOptionLabelAttr, errorMessage("filterAssociationOptionLabelAttr"));
-
- return {
- ref: ensure(props.filterAssociation, errorMessage("filterAssociation")),
- datasource: ensure(props.filterAssociationOptions, errorMessage("filterAssociationOptions")),
- searchAttrId,
- fetchOptionsLazy: props.fetchOptionsLazy,
- caption
- };
+ return disposeAll;
}
private createFilterStore(props: ColumnsType, dsViewState: FilterCondition | null): FilterStore | null {
- if (props.filterAssociation) {
- return new RefFilterStore(this.toRefselectProps(props), dsViewState);
- }
-
if (isListAttributeValue(props.attribute)) {
return attrgroupFilterStore(props.attribute.type, [props.attribute], dsViewState);
}
@@ -92,14 +53,16 @@ export class ColumnFilterStore implements IColumnFilterStore {
return null;
}
- private createContext(store: FilterStore | null, info: StaticInfo): FilterAPIv2 {
+ private createContext(store: FilterStore | null, info: StaticInfo): FilterAPI {
return {
- version: 2,
+ version: 3,
parentChannelName: info.filtersChannelName,
provider: value({
type: "direct",
store
- })
+ }),
+ filterObserver: this._observerBag.customFilterHost,
+ sharedInitFilter: this._observerBag.sharedInitFilter
};
}
@@ -107,7 +70,7 @@ export class ColumnFilterStore implements IColumnFilterStore {
return {this._widget} ;
}
- get condition2(): FilterCondition | undefined {
+ get condition(): FilterCondition | undefined {
return this._filterStore ? this._filterStore.condition : undefined;
}
@@ -130,5 +93,7 @@ const isListAttributeValue = (
return !!(attribute && attribute.isList === false);
};
-const errorMessage = (propName: string): string =>
- `Can't map ColumnsType to AssociationProperties: ${propName} is undefined`;
+export interface ObserverBag {
+ customFilterHost: ObservableFilterHost;
+ sharedInitFilter: Array;
+}
diff --git a/packages/pluggableWidgets/datagrid-web/src/package.xml b/packages/pluggableWidgets/datagrid-web/src/package.xml
index c9f6330fb2..8bf5807932 100644
--- a/packages/pluggableWidgets/datagrid-web/src/package.xml
+++ b/packages/pluggableWidgets/datagrid-web/src/package.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/packages/pluggableWidgets/datagrid-web/src/typings/personalization-settings.ts b/packages/pluggableWidgets/datagrid-web/src/typings/personalization-settings.ts
index f51a4f9b4e..663dcb3b32 100644
--- a/packages/pluggableWidgets/datagrid-web/src/typings/personalization-settings.ts
+++ b/packages/pluggableWidgets/datagrid-web/src/typings/personalization-settings.ts
@@ -1,4 +1,4 @@
-import { FilterData } from "@mendix/widget-plugin-filtering/typings/settings";
+import { FilterData } from "@mendix/filter-commons/typings/settings";
import { ColumnId } from "./GridColumn";
import { SortDirection, SortRule } from "./sorting";
@@ -19,14 +19,14 @@ interface ColumnPersonalizationStorageSettings {
export type ColumnFilterSettings = Array<[key: ColumnId, data: FilterData]>;
-export type GroupFilterSettings = Array<[key: string, data: FilterData]>;
+export type CustomFilterSettings = Array<[key: string, data: FilterData]>;
export interface GridPersonalizationStorageSettings {
name: string;
- schemaVersion: 2;
+ schemaVersion: 3;
settingsHash: string;
columns: ColumnPersonalizationStorageSettings[];
- groupFilters: GroupFilterSettings;
+ customFilters: CustomFilterSettings;
columnFilters: ColumnFilterSettings;
columnOrder: ColumnId[];
sortOrder: SortRule[];
diff --git a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx
index 3a9a345e20..9a347d20aa 100644
--- a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx
+++ b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx
@@ -29,9 +29,7 @@ export const column = (header = "Test", patch?: (col: ColumnsType) => void): Col
visible: dynamicValue(true),
minWidth: "auto",
minWidthLimit: 100,
- allowEventPropagation: true,
- fetchOptionsLazy: true,
- filterCaptionType: "attribute"
+ allowEventPropagation: true
};
if (patch) {
diff --git a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts
index 3749768048..262d9ab434 100644
--- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts
+++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts
@@ -4,7 +4,7 @@
* @author Mendix Widgets Framework Team
*/
import { ComponentType, CSSProperties, ReactNode } from "react";
-import { ActionValue, DynamicValue, EditableValue, ListValue, ListActionValue, ListAttributeValue, ListAttributeListValue, ListExpressionValue, ListReferenceValue, ListReferenceSetValue, ListWidgetValue, SelectionSingleValue, SelectionMultiValue } from "mendix";
+import { ActionValue, DynamicValue, EditableValue, ListValue, ListActionValue, ListAttributeValue, ListAttributeListValue, ListExpressionValue, ListWidgetValue, SelectionSingleValue, SelectionMultiValue } from "mendix";
import { Big } from "big.js";
export type ItemSelectionMethodEnum = "checkbox" | "rowClick";
@@ -15,8 +15,6 @@ export type LoadingTypeEnum = "spinner" | "skeleton";
export type ShowContentAsEnum = "attribute" | "dynamicText" | "customContent";
-export type FilterCaptionTypeEnum = "attribute" | "expression";
-
export type HidableEnum = "yes" | "hidden" | "no";
export type WidthEnum = "autoFill" | "autoFit" | "manual";
@@ -35,12 +33,6 @@ export interface ColumnsType {
tooltip?: ListExpressionValue;
filter?: ReactNode;
visible: DynamicValue;
- filterAssociation?: ListReferenceValue | ListReferenceSetValue;
- filterAssociationOptions?: ListValue;
- fetchOptionsLazy: boolean;
- filterCaptionType: FilterCaptionTypeEnum;
- filterAssociationOptionLabel?: ListExpressionValue;
- filterAssociationOptionLabelAttr?: ListAttributeValue;
sortable: boolean;
resizable: boolean;
draggable: boolean;
@@ -67,10 +59,6 @@ export type OnClickTriggerEnum = "single" | "double";
export type ConfigurationStorageTypeEnum = "attribute" | "localStorage";
-export interface FilterListType {
- filter: ListAttributeValue;
-}
-
export interface ColumnsPreviewType {
showContentAs: ShowContentAsEnum;
attribute: string;
@@ -81,12 +69,6 @@ export interface ColumnsPreviewType {
tooltip: string;
filter: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> };
visible: string;
- filterAssociation: string;
- filterAssociationOptions: {} | { caption: string } | { type: string } | null;
- fetchOptionsLazy: boolean;
- filterCaptionType: FilterCaptionTypeEnum;
- filterAssociationOptionLabel: string;
- filterAssociationOptionLabelAttr: string;
sortable: boolean;
resizable: boolean;
draggable: boolean;
@@ -101,10 +83,6 @@ export interface ColumnsPreviewType {
wrapText: boolean;
}
-export interface FilterListPreviewType {
- filter: string;
-}
-
export interface DatagridContainerProps {
name: string;
class: string;
@@ -132,6 +110,7 @@ export interface DatagridContainerProps {
onClickTrigger: OnClickTriggerEnum;
onClick?: ListActionValue;
onSelectionChange?: ActionValue;
+ filtersPlaceholder?: ReactNode;
columnsSortable: boolean;
columnsResizable: boolean;
columnsDraggable: boolean;
@@ -139,8 +118,6 @@ export interface DatagridContainerProps {
configurationStorageType: ConfigurationStorageTypeEnum;
configurationAttribute?: EditableValue;
storeFiltersInPersonalization: boolean;
- filterList: FilterListType[];
- filtersPlaceholder?: ReactNode;
filterSectionTitle?: DynamicValue;
exportDialogLabel?: DynamicValue;
cancelExportLabel?: DynamicValue;
@@ -180,6 +157,7 @@ export interface DatagridPreviewProps {
onClickTrigger: OnClickTriggerEnum;
onClick: {} | null;
onSelectionChange: {} | null;
+ filtersPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> };
columnsSortable: boolean;
columnsResizable: boolean;
columnsDraggable: boolean;
@@ -188,8 +166,6 @@ export interface DatagridPreviewProps {
configurationAttribute: string;
storeFiltersInPersonalization: boolean;
onConfigurationChange: {} | null;
- filterList: FilterListPreviewType[];
- filtersPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> };
filterSectionTitle: string;
exportDialogLabel: string;
cancelExportLabel: string;
diff --git a/packages/pluggableWidgets/dropdown-sort-web/package.json b/packages/pluggableWidgets/dropdown-sort-web/package.json
index 2070899fb4..7115ffcdce 100644
--- a/packages/pluggableWidgets/dropdown-sort-web/package.json
+++ b/packages/pluggableWidgets/dropdown-sort-web/package.json
@@ -1,7 +1,7 @@
{
"name": "@mendix/dropdown-sort-web",
"widgetName": "DropdownSort",
- "version": "1.2.2",
+ "version": "3.0.0",
"description": "",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
diff --git a/packages/pluggableWidgets/dropdown-sort-web/src/DropdownSort.tsx b/packages/pluggableWidgets/dropdown-sort-web/src/DropdownSort.tsx
index 1caef896bf..2a35f534d6 100644
--- a/packages/pluggableWidgets/dropdown-sort-web/src/DropdownSort.tsx
+++ b/packages/pluggableWidgets/dropdown-sort-web/src/DropdownSort.tsx
@@ -1,14 +1,16 @@
import { observer } from "mobx-react-lite";
-import { createElement, ReactElement, useRef } from "react";
+import { createElement, ReactElement } from "react";
import { useSortControl } from "@mendix/widget-plugin-sorting/helpers/useSortControl";
-import { SortingStoreInterface } from "@mendix/widget-plugin-sorting/typings";
+import { SortingStoreInterface } from "@mendix/widget-plugin-sorting/SortingStoreInterface";
import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid";
import { DropdownSortContainerProps } from "../typings/DropdownSortProps";
import { SortComponent } from "./components/SortComponent";
-import { withSortStore } from "./hocs/withSortStore";
+import { withLinkedSortStore, withSortAPI } from "./hocs/withLinkedSortStore";
+import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
function Container(props: DropdownSortContainerProps & { sortStore: SortingStoreInterface }): ReactElement {
- const id = (useRef().current ??= `DropdownSort${generateUUID()}`);
+ const id = useConst(() => `DropdownSort${generateUUID()}`);
+
const sortProps = useSortControl(
{ ...props, emptyOptionCaption: props.emptyOptionCaption?.value },
props.sortStore
@@ -28,8 +30,4 @@ function Container(props: DropdownSortContainerProps & { sortStore: SortingStore
);
}
-const Widget = withSortStore(observer(Container));
-
-export function DropdownSort(props: DropdownSortContainerProps): ReactElement {
- return ;
-}
+export const DropdownSort = withSortAPI(withLinkedSortStore(observer(Container)));
diff --git a/packages/pluggableWidgets/dropdown-sort-web/src/DropdownSort.xml b/packages/pluggableWidgets/dropdown-sort-web/src/DropdownSort.xml
index 09af998512..9dbb977d8a 100644
--- a/packages/pluggableWidgets/dropdown-sort-web/src/DropdownSort.xml
+++ b/packages/pluggableWidgets/dropdown-sort-web/src/DropdownSort.xml
@@ -8,6 +8,36 @@
+
+ Datasource to sort
+
+
+
+ Attributes
+ Select the attributes that the end-user may use for sorting
+
+
+
+ Attribute
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Caption
+
+
+
+
+
Empty option caption
diff --git a/packages/pluggableWidgets/dropdown-sort-web/src/components/SortComponent.tsx b/packages/pluggableWidgets/dropdown-sort-web/src/components/SortComponent.tsx
index 723c659a9c..fc89199d11 100644
--- a/packages/pluggableWidgets/dropdown-sort-web/src/components/SortComponent.tsx
+++ b/packages/pluggableWidgets/dropdown-sort-web/src/components/SortComponent.tsx
@@ -125,6 +125,7 @@ export function SortComponent(props: SortComponentProps): ReactElement {
aria-expanded={show}
aria-controls={`${props.id}-dropdown-list`}
aria-label={props.screenReaderInputCaption}
+ onChange={() => {}}
/>
(
- headerFilterStore.context
-);
+const createAPI = (): SortAPI => ({
+ version: 1,
+ sortObserver: new SortStoreHost()
+});
-const sortList: SortListType[] = [
+const mockAttributes = (): AttributesType[] => [
{
attribute: new ListAttributeValueBuilder().withId("attribute1").withType("String").withSortable(true).build(),
caption: dynamicValue("Option 1")
@@ -48,22 +29,28 @@ const sortList: SortListType[] = [
caption: dynamicValue("Option 2")
}
];
-const sortProvider = new SortAPIProvider({
- datasource: { sortOrder: [[sortList[0].attribute.id as ListAttributeId, "asc"]] } as ListValue,
- sortList
-});
-(window as any)["com.mendix.widgets.web.sortable.sortContext"] = createContext(sortProvider.context);
-// END CONTEXT
+
+function renderWithSortAPI(elt: React.ReactElement, api: SortAPI): ReturnType {
+ const SortAPI = getGlobalSortContext();
+ return render({elt} );
+}
describe("Dropdown Sort", () => {
describe("with single instance", () => {
- afterEach(() => {
+ beforeEach(() => {
delete (global as any)["com.mendix.widgets.web.UUID"];
});
describe("with correct context", () => {
+ let attributes: DropdownSortContainerProps["attributes"];
+ let api: SortAPI;
+ beforeEach(() => {
+ api = createAPI();
+ api.sortObserver.sortOrder = [["attribute1" as ListAttributeId, "asc"]];
+ attributes = mockAttributes();
+ });
it("loads correct values from attributes", () => {
- const filter = render( );
+ const filter = renderWithSortAPI( , api);
fireEvent.click(filter.getByRole("textbox"));
const items = filter.getAllByRole("menuitem");
@@ -77,14 +64,24 @@ describe("Dropdown Sort", () => {
});
it("renders correctly", () => {
- const { asFragment } = render( );
+ const { asFragment } = renderWithSortAPI(
+ ,
+ api
+ );
expect(asFragment()).toMatchSnapshot();
});
});
describe("with view state", () => {
+ let attributes: DropdownSortContainerProps["attributes"];
+ let api: SortAPI;
+ beforeEach(() => {
+ api = createAPI();
+ api.sortObserver.sortOrder = [["attribute1" as ListAttributeId, "asc"]];
+ attributes = mockAttributes();
+ });
it("loads correct default option", () => {
- const filter = render( );
+ const filter = renderWithSortAPI( , api);
fireEvent.click(filter.getByRole("textbox"));
expect(filter.getByRole("textbox").getAttribute("value")).toStrictEqual("Option 1");
@@ -104,24 +101,27 @@ describe("Dropdown Sort", () => {
});
describe("with multiple instances", () => {
- beforeAll(() => {
- (window as any)["com.mendix.widgets.web.sortable.sortContext"] = createContext(
- sortProvider.context
- );
+ let attributes: DropdownSortContainerProps["attributes"];
+ let api: SortAPI;
+ beforeEach(() => {
+ delete (global as any)["com.mendix.widgets.web.UUID"];
+ api = createAPI();
+ attributes = mockAttributes();
});
it("renders with a unique id", () => {
- const { asFragment: fragment1 } = render( );
- const { asFragment: fragment2 } = render( );
+ const { asFragment: fragment1 } = renderWithSortAPI(
+ ,
+ api
+ );
+ const { asFragment: fragment2 } = renderWithSortAPI(
+ ,
+ api
+ );
expect(fragment1().querySelector("input")?.getAttribute("aria-controls")).not.toBe(
fragment2().querySelector("input")?.getAttribute("aria-controls")
);
});
-
- afterAll(() => {
- (window as any)["com.mendix.widgets.web.sortable.sortContext"] = undefined;
- delete (global as any)["com.mendix.widgets.web.UUID"];
- });
});
});
diff --git a/packages/pluggableWidgets/dropdown-sort-web/src/hocs/withLinkedSortStore.tsx b/packages/pluggableWidgets/dropdown-sort-web/src/hocs/withLinkedSortStore.tsx
new file mode 100644
index 0000000000..3cf1168dae
--- /dev/null
+++ b/packages/pluggableWidgets/dropdown-sort-web/src/hocs/withLinkedSortStore.tsx
@@ -0,0 +1,41 @@
+import { DynamicValue, AttributeMetaData } from "mendix";
+import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
+import { createElement, FC } from "react";
+import { SortingStoreInterface } from "@mendix/widget-plugin-sorting/SortingStoreInterface";
+import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup";
+import { SortAPI, useSortAPI } from "@mendix/widget-plugin-sorting/context";
+import { SortStoreProvider } from "@mendix/widget-plugin-sorting/controllers/SortStoreProvider";
+
+interface RequiredProps {
+ attributes: Array<{
+ attribute: AttributeMetaData;
+ caption?: DynamicValue;
+ }>;
+ name: string;
+}
+
+export function withSortAPI(Component: FC
): FC
{
+ return function SortAPIProvider(props) {
+ const sortAPI = useSortAPI();
+ if (sortAPI.hasError) {
+ return {sortAPI.error.message} ;
+ }
+ return ;
+ };
+}
+
+export function withLinkedSortStore
(
+ Component: FC
+): FC
{
+ return function SortStoreProviderHost(props) {
+ const { store } = useSetup(
+ () =>
+ new SortStoreProvider(props.sortAPI.sortObserver, {
+ options: props.attributes,
+ name: props.name
+ })
+ );
+
+ return ;
+ };
+}
diff --git a/packages/pluggableWidgets/dropdown-sort-web/src/hocs/withSortStore.tsx b/packages/pluggableWidgets/dropdown-sort-web/src/hocs/withSortStore.tsx
deleted file mode 100644
index 8ce9dbcf69..0000000000
--- a/packages/pluggableWidgets/dropdown-sort-web/src/hocs/withSortStore.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
-import { createElement } from "react";
-import { SortingStoreInterface } from "@mendix/widget-plugin-sorting/typings";
-import { useSortingStore } from "@mendix/widget-plugin-sorting/helpers/useSortingStore";
-
-export function withSortStore
(
- Component: (props: P & { sortStore: SortingStoreInterface }) => React.ReactElement
-): (props: P) => React.ReactElement {
- return function SortStoreProvider(props: P) {
- const store = useSortingStore();
- if (store.hasError) {
- return {store.error.message} ;
- }
- return ;
- };
-}
diff --git a/packages/pluggableWidgets/dropdown-sort-web/src/package.xml b/packages/pluggableWidgets/dropdown-sort-web/src/package.xml
index 9b93f18cf2..7ec50ce079 100644
--- a/packages/pluggableWidgets/dropdown-sort-web/src/package.xml
+++ b/packages/pluggableWidgets/dropdown-sort-web/src/package.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/packages/pluggableWidgets/dropdown-sort-web/typings/DropdownSortProps.d.ts b/packages/pluggableWidgets/dropdown-sort-web/typings/DropdownSortProps.d.ts
index 1ffef90866..b61b12893b 100644
--- a/packages/pluggableWidgets/dropdown-sort-web/typings/DropdownSortProps.d.ts
+++ b/packages/pluggableWidgets/dropdown-sort-web/typings/DropdownSortProps.d.ts
@@ -4,13 +4,25 @@
* @author Mendix Widgets Framework Team
*/
import { CSSProperties } from "react";
-import { DynamicValue } from "mendix";
+import { AttributeMetaData, DynamicValue } from "mendix";
+import { Big } from "big.js";
+
+export interface AttributesType {
+ attribute: AttributeMetaData;
+ caption: DynamicValue;
+}
+
+export interface AttributesPreviewType {
+ attribute: string;
+ caption: string;
+}
export interface DropdownSortContainerProps {
name: string;
class: string;
style?: CSSProperties;
tabIndex?: number;
+ attributes: AttributesType[];
emptyOptionCaption?: DynamicValue;
screenReaderButtonCaption?: DynamicValue;
screenReaderInputCaption?: DynamicValue;
@@ -27,6 +39,7 @@ export interface DropdownSortPreviewProps {
readOnly: boolean;
renderMode: "design" | "xray" | "structure";
translate: (text: string) => string;
+ attributes: AttributesPreviewType[];
emptyOptionCaption: string;
screenReaderButtonCaption: string;
screenReaderInputCaption: string;
diff --git a/packages/pluggableWidgets/gallery-web/package.json b/packages/pluggableWidgets/gallery-web/package.json
index 86577461ce..bec70b13ff 100644
--- a/packages/pluggableWidgets/gallery-web/package.json
+++ b/packages/pluggableWidgets/gallery-web/package.json
@@ -1,7 +1,7 @@
{
"name": "@mendix/gallery-web",
"widgetName": "Gallery",
- "version": "1.14.0",
+ "version": "3.0.0",
"description": "A flexible gallery widget that renders columns, rows and layouts.",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
@@ -43,6 +43,7 @@
"dependencies": {
"@mendix/widget-plugin-external-events": "workspace:*",
"@mendix/widget-plugin-filtering": "workspace:*",
+ "@mendix/widget-plugin-mobx-kit": "workspace:^",
"@mendix/widget-plugin-sorting": "workspace:*",
"classnames": "^2.3.2",
"mobx": "6.12.3",
diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts b/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts
index 00c6f2d42b..136ddf7cac 100644
--- a/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts
+++ b/packages/pluggableWidgets/gallery-web/src/Gallery.editorConfig.ts
@@ -1,25 +1,15 @@
+import { hidePropertiesIn, hidePropertyIn, Problem, Properties } from "@mendix/pluggable-widgets-tools";
import {
ContainerProps,
+ datasource,
DropZoneProps,
RowLayoutProps,
- StructurePreviewProps,
- datasource,
- structurePreviewPalette
+ structurePreviewPalette,
+ StructurePreviewProps
} from "@mendix/widget-plugin-platform/preview/structure-preview-api";
-import {
- hidePropertiesIn,
- hidePropertyIn,
- Problem,
- Properties,
- transformGroupsIntoTabs
-} from "@mendix/pluggable-widgets-tools";
import { GalleryPreviewProps } from "../typings/GalleryProps";
-export function getProperties(
- values: GalleryPreviewProps,
- defaultProperties: Properties,
- platform: "web" | "desktop"
-): Properties {
+export function getProperties(values: GalleryPreviewProps, defaultProperties: Properties): Properties {
if (values.pagination !== "buttons") {
hidePropertyIn(defaultProperties, values, "pagingPosition");
}
@@ -32,26 +22,8 @@ export function getProperties(
hidePropertiesIn(defaultProperties, values, ["onSelectionChange", "itemSelectionMode"]);
}
- if (platform === "web") {
- if (!values.advanced) {
- hidePropertiesIn(defaultProperties, values, [
- "pagination",
- "pagingPosition",
- "showEmptyPlaceholder",
- "emptyPlaceholder",
- "itemClass",
- "filtersPlaceholder",
- "filterList",
- "sortList",
- "emptyMessageTitle",
- "filterSectionTitle"
- ]);
- }
-
- transformGroupsIntoTabs(defaultProperties);
- } else {
- hidePropertyIn(defaultProperties, values, "advanced");
- }
+ // Hide scrolling settings for now.
+ hidePropertiesIn(defaultProperties, values, ["showPagingButtons", "showTotalCount"]);
return defaultProperties;
}
diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx
index 9f9caf1ffc..bd0b6cf89f 100644
--- a/packages/pluggableWidgets/gallery-web/src/Gallery.tsx
+++ b/packages/pluggableWidgets/gallery-web/src/Gallery.tsx
@@ -10,34 +10,13 @@ import { useItemEventsController } from "./features/item-interaction/ItemEventsC
import { GridPositionsProps, useGridPositions } from "./features/useGridPositions";
import { useItemHelper } from "./helpers/ItemHelper";
import { useItemSelectHelper } from "./helpers/useItemSelectHelper";
-import { useRootGalleryStore } from "./helpers/useRootGalleryStore";
-import { RootGalleryStore } from "./stores/RootGalleryStore";
-import { HeaderContainer } from "./components/HeaderContainer";
+import { useGalleryStore } from "./helpers/useGalleryStore";
+import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
+import { GalleryRootScope, GalleryContext, useGalleryRootScope } from "./helpers/root-context";
+import { HeaderWidgetsHost } from "./components/HeaderWidgetsHost";
-interface RootAPI {
- rootStore: RootGalleryStore;
-}
-
-function Container(props: GalleryContainerProps & RootAPI): ReactElement {
- const isInfiniteLoad = props.pagination === "virtualScrolling";
- const currentPage = isInfiniteLoad
- ? props.datasource.limit / props.pageSize
- : props.datasource.offset / props.pageSize;
- const setPage = useCallback(
- (computePage: (prevPage: number) => number) => {
- const newPage = computePage(currentPage);
-
- if (isInfiniteLoad) {
- props.datasource.setLimit(newPage * props.pageSize);
- } else {
- props.datasource.setOffset(newPage * props.pageSize);
- }
- },
- [props.datasource, props.pageSize, isInfiniteLoad, currentPage]
- );
-
- const selection = useSelectionHelper(props.itemSelection, props.datasource, props.onSelectionChange);
- const selectHelper = useItemSelectHelper(props.itemSelection, selection);
+const Container = observer(function GalleryContainer(props: GalleryContainerProps): ReactElement {
+ const { rootStore, itemSelectHelper } = useGalleryRootScope();
const items = props.datasource.items ?? [];
const config: GridPositionsProps = {
desktopItems: props.desktopItems,
@@ -56,23 +35,26 @@ function Container(props: GalleryContainerProps & RootAPI): ReactElement {
columns: numberOfColumns,
pageSize: props.pageSize
});
+
const clickActionHelper = useClickActionHelper({ onClick: props.onClick, onClickTrigger: props.onClickTrigger });
+
const itemEventsController = useItemEventsController(
- selectHelper,
+ itemSelectHelper,
clickActionHelper,
focusController,
numberOfColumns,
props.itemSelectionMode
);
- const showHeader = props.filterList.length > 0 || props.sortList.length > 0 || selection?.type === "Multi";
const itemHelper = useItemHelper({
classValue: props.itemClass,
contentValue: props.content,
clickValue: props.onClick
});
- useOnResetFiltersEvent(props.rootStore.staticInfo.name, props.rootStore.staticInfo.filtersChannelName);
+ useOnResetFiltersEvent(rootStore.name, rootStore.id);
+
+ const header = {props.filtersPlaceholder} ;
return (
- {props.filtersPlaceholder}
-
- )
- }
+ header={header}
headerTitle={props.filterSectionTitle?.value}
ariaLabelListBox={props.ariaLabelListBox?.value}
- showHeader={showHeader}
+ showHeader={!!props.filtersPlaceholder}
hasMoreItems={props.datasource.hasMoreItems ?? false}
items={items}
itemHelper={itemHelper}
numberOfItems={props.datasource.totalCount}
- page={currentPage}
+ page={rootStore.paging.currentPage}
pageSize={props.pageSize}
paging={props.pagination === "buttons"}
paginationPosition={props.pagingPosition}
phoneItems={props.phoneItems}
- setPage={setPage}
+ setPage={rootStore.paging.setPage}
style={props.style}
tabletItems={props.tabletItems}
tabIndex={props.tabIndex}
- selectHelper={selectHelper}
+ selectHelper={itemSelectHelper}
itemEventsController={itemEventsController}
focusController={focusController}
getPosition={getPositionCallback}
/>
);
-}
+});
-const Widget = observer(function RootStoreProvider(props: GalleryContainerProps) {
- const store = useRootGalleryStore(props);
+function useCreateGalleryScope(props: GalleryContainerProps): GalleryRootScope {
+ const rootStore = useGalleryStore(props);
+ const selectionHelper = useSelectionHelper(props.itemSelection, props.datasource, props.onSelectionChange);
+ const itemSelectHelper = useItemSelectHelper(props.itemSelection, selectionHelper);
- return ;
-});
+ return useConst({
+ rootStore,
+ selectionHelper,
+ itemSelectHelper
+ });
+}
export function Gallery(props: GalleryContainerProps): ReactElement {
- return ;
+ const scope = useCreateGalleryScope(props);
+
+ return (
+
+
+
+ );
}
diff --git a/packages/pluggableWidgets/gallery-web/src/Gallery.xml b/packages/pluggableWidgets/gallery-web/src/Gallery.xml
index 878b66faa9..eaf37b9a51 100644
--- a/packages/pluggableWidgets/gallery-web/src/Gallery.xml
+++ b/packages/pluggableWidgets/gallery-web/src/Gallery.xml
@@ -8,8 +8,8 @@
-
- Enable advanced options
+
+ Filters placeholder
@@ -73,6 +73,18 @@
Above grid
+
+ Show paging buttons
+
+
+ Always
+ Auto
+
+
+
+ Show total count
+
+
Empty message
@@ -110,66 +122,6 @@
-
-
-
- Filters
-
-
-
-
- Filter attribute
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Filters placeholder
-
-
-
-
-
-
-
- Sort attributes
-
-
-
-
- Attribute
-
-
-
-
-
-
-
-
-
-
-
-
-
- Caption
-
-
-
-
-
-
-
diff --git a/packages/pluggableWidgets/gallery-web/src/components/HeaderContainer.tsx b/packages/pluggableWidgets/gallery-web/src/components/HeaderContainer.tsx
deleted file mode 100644
index fe26bc8a98..0000000000
--- a/packages/pluggableWidgets/gallery-web/src/components/HeaderContainer.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { createElement } from "react";
-import { getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context";
-import { getGlobalSortContext } from "@mendix/widget-plugin-sorting/context";
-import { SortAPIProvider } from "@mendix/widget-plugin-sorting/providers/SortAPIProvider";
-import { HeaderFiltersStore } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore";
-import {
- getGlobalSelectionContext,
- SelectionHelper,
- useCreateSelectionContextValue
-} from "@mendix/widget-plugin-grid/selection";
-const SelectionContext = getGlobalSelectionContext();
-const FilterContext = getGlobalFilterContextObject();
-const SortAPIContext = getGlobalSortContext();
-
-function FilterAPIProvider(props: React.PropsWithChildren<{ filtersStore: HeaderFiltersStore }>): React.ReactElement {
- return {props.children} ;
-}
-
-function SelectionStatusProvider(
- props: React.PropsWithChildren<{ selectionHelper?: SelectionHelper }>
-): React.ReactElement {
- const value = useCreateSelectionContextValue(props.selectionHelper);
- return {props.children} ;
-}
-
-interface HeaderContainerProps
- extends React.PropsWithChildren<{
- filtersStore: HeaderFiltersStore;
- sortProvider: SortAPIProvider;
- selectionHelper?: SelectionHelper;
- }> {}
-
-export function HeaderContainer(props: HeaderContainerProps): React.ReactElement {
- return (
-
-
-
- {props.children}
-
-
-
- );
-}
diff --git a/packages/pluggableWidgets/gallery-web/src/components/HeaderWidgetsHost.tsx b/packages/pluggableWidgets/gallery-web/src/components/HeaderWidgetsHost.tsx
new file mode 100644
index 0000000000..656aa708d4
--- /dev/null
+++ b/packages/pluggableWidgets/gallery-web/src/components/HeaderWidgetsHost.tsx
@@ -0,0 +1,22 @@
+import { createElement } from "react";
+import { getGlobalSortContext } from "@mendix/widget-plugin-sorting/context";
+import { getGlobalSelectionContext, useCreateSelectionContextValue } from "@mendix/widget-plugin-grid/selection";
+import { useGalleryRootScope } from "../helpers/root-context";
+import { getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context";
+
+const SelectionContext = getGlobalSelectionContext();
+const SortAPI = getGlobalSortContext();
+const FilterAPI = getGlobalFilterContextObject();
+
+export function HeaderWidgetsHost(props: { children?: React.ReactNode }): React.ReactElement {
+ const { selectionHelper, rootStore } = useGalleryRootScope();
+ const selectionContext = useCreateSelectionContextValue(selectionHelper);
+
+ return (
+
+
+ {props.children}
+
+
+ );
+}
diff --git a/packages/pluggableWidgets/gallery-web/src/controllers/QueryParamsController.ts b/packages/pluggableWidgets/gallery-web/src/controllers/QueryParamsController.ts
new file mode 100644
index 0000000000..cb918618b2
--- /dev/null
+++ b/packages/pluggableWidgets/gallery-web/src/controllers/QueryParamsController.ts
@@ -0,0 +1,65 @@
+import { compactArray, fromCompactArray, isAnd } from "@mendix/filter-commons/condition-utils";
+import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost";
+import { DatasourceController } from "@mendix/widget-plugin-grid/query/DatasourceController";
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller";
+import { ObservableSortStoreHost } from "@mendix/widget-plugin-sorting/ObservableSortStoreHost";
+import { ListValue } from "mendix";
+import { FilterCondition } from "mendix/filters";
+import { makeAutoObservable, reaction } from "mobx";
+
+export class QueryParamsController implements ReactiveController {
+ private readonly _query: DatasourceController;
+ private readonly _filters: CustomFilterHost;
+ private readonly _sort: ObservableSortStoreHost;
+
+ constructor(
+ host: ReactiveControllerHost,
+ query: DatasourceController,
+ filters: CustomFilterHost,
+ sort: ObservableSortStoreHost
+ ) {
+ host.addController(this);
+
+ this._query = query;
+ this._filters = filters;
+ this._sort = sort;
+
+ makeAutoObservable(this, { setup: false });
+ }
+
+ private get _derivedSortOrder(): ListValue["sortOrder"] {
+ return this._sort.sortOrder;
+ }
+
+ private get _derivedFilter(): FilterCondition {
+ return compactArray(this._filters.conditions);
+ }
+
+ setup(): () => void {
+ const [add, disposeAll] = disposeBatch();
+ add(
+ reaction(
+ () => this._derivedSortOrder,
+ sortOrder => this._query.setSortOrder(sortOrder),
+ { fireImmediately: true }
+ )
+ );
+ add(
+ reaction(
+ () => this._derivedFilter,
+ filter => this._query.setFilter(filter),
+ { fireImmediately: true }
+ )
+ );
+
+ return disposeAll;
+ }
+
+ unzipFilter(filter?: FilterCondition): Array {
+ if (!filter || !isAnd(filter)) {
+ return [];
+ }
+ return fromCompactArray(filter);
+ }
+}
diff --git a/packages/pluggableWidgets/gallery-web/src/helpers/root-context.ts b/packages/pluggableWidgets/gallery-web/src/helpers/root-context.ts
new file mode 100644
index 0000000000..8669738bfa
--- /dev/null
+++ b/packages/pluggableWidgets/gallery-web/src/helpers/root-context.ts
@@ -0,0 +1,19 @@
+import { SelectActionHandler, SelectionHelper } from "@mendix/widget-plugin-grid/selection";
+import { createContext, useContext } from "react";
+import { GalleryStore } from "../stores/GalleryStore";
+
+export interface GalleryRootScope {
+ rootStore: GalleryStore;
+ selectionHelper: SelectionHelper | undefined;
+ itemSelectHelper: SelectActionHandler;
+}
+
+export const GalleryContext = createContext(null);
+
+export const useGalleryRootScope = (): GalleryRootScope => {
+ const contextValue = useContext(GalleryContext);
+ if (!contextValue) {
+ throw new Error("useGalleryRootContext must be used within a root context provider");
+ }
+ return contextValue;
+};
diff --git a/packages/pluggableWidgets/gallery-web/src/helpers/useGalleryStore.ts b/packages/pluggableWidgets/gallery-web/src/helpers/useGalleryStore.ts
new file mode 100644
index 0000000000..cad395cbc0
--- /dev/null
+++ b/packages/pluggableWidgets/gallery-web/src/helpers/useGalleryStore.ts
@@ -0,0 +1,20 @@
+import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider";
+import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
+import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup";
+import { useEffect } from "react";
+import { GalleryContainerProps } from "../../typings/GalleryProps";
+import { GalleryPropsGate, GalleryStore } from "../stores/GalleryStore";
+
+export function useGalleryStore(props: GalleryContainerProps): GalleryStore {
+ const gate = useGate(props);
+ const store = useSetup(
+ () => new GalleryStore({ gate, ...props, showPagingButtons: "auto", showTotalCount: false })
+ );
+ return store;
+}
+
+function useGate(props: GalleryContainerProps): GalleryPropsGate {
+ const gateProvider = useConst(() => new GateProvider(props));
+ useEffect(() => gateProvider.setProps(props));
+ return gateProvider.gate;
+}
diff --git a/packages/pluggableWidgets/gallery-web/src/helpers/useRootGalleryStore.ts b/packages/pluggableWidgets/gallery-web/src/helpers/useRootGalleryStore.ts
deleted file mode 100644
index afd9802a29..0000000000
--- a/packages/pluggableWidgets/gallery-web/src/helpers/useRootGalleryStore.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { IReactionDisposer, reaction } from "mobx";
-import { useState, useEffect, useRef } from "react";
-import { RootGalleryStore } from "../stores/RootGalleryStore";
-import { GalleryContainerProps } from "../../typings/GalleryProps";
-
-export function useRootGalleryStore(props: GalleryContainerProps): RootGalleryStore {
- const datasourceRef = useRef(props.datasource);
- const [rootStore] = useState(() => {
- return new RootGalleryStore(props);
- });
-
- useEffect(() => rootStore.setup(), [rootStore]);
-
- useEffect(() => {
- rootStore.updateProps(props);
- datasourceRef.current = props.datasource;
- });
-
- useEffect(() => {
- const disposers: IReactionDisposer[] = [];
-
- // apply sorting
- disposers.push(
- reaction(
- () => rootStore.sortOrder,
- order => {
- datasourceRef.current.setSortOrder(order);
- }
- )
- );
-
- // apply filters
- disposers.push(
- reaction(
- () => rootStore.conditions,
- filter => {
- datasourceRef.current.setFilter(filter);
- },
- { fireImmediately: true }
- )
- );
-
- return () => {
- disposers.forEach(d => d());
- };
- }, [rootStore]);
-
- return rootStore;
-}
diff --git a/packages/pluggableWidgets/gallery-web/src/package.xml b/packages/pluggableWidgets/gallery-web/src/package.xml
index ddd28660e9..81ce386e38 100644
--- a/packages/pluggableWidgets/gallery-web/src/package.xml
+++ b/packages/pluggableWidgets/gallery-web/src/package.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts
new file mode 100644
index 0000000000..f0b73a6760
--- /dev/null
+++ b/packages/pluggableWidgets/gallery-web/src/stores/GalleryStore.ts
@@ -0,0 +1,79 @@
+import { createContextWithStub, FilterAPI } from "@mendix/widget-plugin-filtering/context";
+import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost";
+import { DatasourceController } from "@mendix/widget-plugin-grid/query/DatasourceController";
+import { PaginationController } from "@mendix/widget-plugin-grid/query/PaginationController";
+import { RefreshController } from "@mendix/widget-plugin-grid/query/RefreshController";
+import { BaseControllerHost } from "@mendix/widget-plugin-mobx-kit/BaseControllerHost";
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
+import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid";
+import { SortAPI } from "@mendix/widget-plugin-sorting/context";
+import { SortStoreHost } from "@mendix/widget-plugin-sorting/controllers/SortStoreHost";
+import { ListValue } from "mendix";
+import { PaginationEnum } from "../../typings/GalleryProps";
+import { QueryParamsController } from "../controllers/QueryParamsController";
+
+interface DynamicProps {
+ datasource: ListValue;
+}
+
+interface StaticProps {
+ pagination: PaginationEnum;
+ showPagingButtons: "always" | "auto";
+ showTotalCount: boolean;
+ pageSize: number;
+ name: string;
+}
+
+export type GalleryPropsGate = DerivedPropsGate;
+
+type GalleryStoreSpec = StaticProps & {
+ gate: GalleryPropsGate;
+};
+
+export class GalleryStore extends BaseControllerHost {
+ private readonly _query: DatasourceController;
+
+ readonly id: string = `GalleryStore@${generateUUID()}`;
+ readonly name: string;
+ readonly paging: PaginationController;
+ readonly filterAPI: FilterAPI;
+ readonly sortAPI: SortAPI;
+
+ constructor(spec: GalleryStoreSpec) {
+ super();
+
+ this.name = spec.name;
+
+ this._query = new DatasourceController(this, { gate: spec.gate });
+
+ this.paging = new PaginationController(this, {
+ query: this._query,
+ pageSize: spec.pageSize,
+ pagination: spec.pagination,
+ showPagingButtons: spec.showPagingButtons,
+ showTotalCount: spec.showTotalCount
+ });
+
+ const filterObserver = new CustomFilterHost();
+
+ const sortObserver = new SortStoreHost();
+
+ const paramCtrl = new QueryParamsController(this, this._query, filterObserver, sortObserver);
+
+ this.filterAPI = createContextWithStub({
+ filterObserver,
+ parentChannelName: this.id,
+ sharedInitFilter: paramCtrl.unzipFilter(spec.gate.props.datasource.filter)
+ });
+
+ this.sortAPI = {
+ version: 1,
+ sortObserver
+ };
+
+ new RefreshController(this, {
+ delay: 0,
+ query: this._query.derivedQuery
+ });
+ }
+}
diff --git a/packages/pluggableWidgets/gallery-web/src/stores/RootGalleryStore.ts b/packages/pluggableWidgets/gallery-web/src/stores/RootGalleryStore.ts
deleted file mode 100644
index 0f9bf1ebf6..0000000000
--- a/packages/pluggableWidgets/gallery-web/src/stores/RootGalleryStore.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { compactArray, fromCompactArray } from "@mendix/widget-plugin-filtering/condition-utils";
-import { HeaderFiltersStore } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore";
-import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid";
-import { SortAPIProvider } from "@mendix/widget-plugin-sorting/providers/SortAPIProvider";
-import { ListValue } from "mendix";
-import { FilterCondition } from "mendix/filters";
-import { GalleryContainerProps } from "../../typings/GalleryProps";
-
-type SortInstruction = ListValue["sortOrder"] extends Array ? T : never;
-
-interface StaticInfo {
- name: string;
- filtersChannelName: string;
-}
-
-export class RootGalleryStore {
- headerFiltersStore: HeaderFiltersStore;
- sortProvider: SortAPIProvider;
- staticInfo: StaticInfo;
-
- constructor(props: GalleryContainerProps) {
- this.setInitParams(props);
- this.staticInfo = {
- name: props.name,
- filtersChannelName: `datagrid/${generateUUID()}`
- };
-
- const headerViewState = this.getDsViewState(props);
- this.headerFiltersStore = new HeaderFiltersStore(props, this.staticInfo, headerViewState);
- this.sortProvider = new SortAPIProvider(props);
- }
-
- get conditions(): FilterCondition {
- return compactArray(this.headerFiltersStore.conditions);
- }
-
- get sortOrder(): SortInstruction[] {
- return this.sortProvider.sortOrder;
- }
-
- setup(): (() => void) | void {
- return this.headerFiltersStore.setup();
- }
-
- updateProps(_: GalleryContainerProps): void {}
-
- private setInitParams(props: GalleryContainerProps): void {
- if (props.pagination === "buttons") {
- props.datasource.requestTotalCount(true);
- }
-
- // Set initial limit
- props.datasource.setLimit(props.pageSize);
- }
-
- // Mirror operation from "condition";
- private getDsViewState({ datasource }: GalleryContainerProps): Array {
- if (!datasource.filter) {
- return [];
- }
-
- return fromCompactArray(datasource.filter);
- }
-}
diff --git a/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts b/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts
index a50bfea37f..0abfb13abb 100644
--- a/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts
+++ b/packages/pluggableWidgets/gallery-web/typings/GalleryProps.d.ts
@@ -4,8 +4,7 @@
* @author Mendix Widgets Framework Team
*/
import { ComponentType, CSSProperties, ReactNode } from "react";
-import { ActionValue, DynamicValue, ListValue, ListActionValue, ListAttributeValue, ListExpressionValue, ListWidgetValue, SelectionSingleValue, SelectionMultiValue } from "mendix";
-import { Big } from "big.js";
+import { ActionValue, DynamicValue, ListValue, ListActionValue, ListExpressionValue, ListWidgetValue, SelectionSingleValue, SelectionMultiValue } from "mendix";
export type ItemSelectionModeEnum = "toggle" | "clear";
@@ -13,34 +12,18 @@ export type PaginationEnum = "buttons" | "virtualScrolling";
export type PagingPositionEnum = "below" | "above";
+export type ShowPagingButtonsEnum = "always" | "auto";
+
export type ShowEmptyPlaceholderEnum = "none" | "custom";
export type OnClickTriggerEnum = "single" | "double";
-export interface FilterListType {
- filter: ListAttributeValue;
-}
-
-export interface SortListType {
- attribute: ListAttributeValue;
- caption: DynamicValue;
-}
-
-export interface FilterListPreviewType {
- filter: string;
-}
-
-export interface SortListPreviewType {
- attribute: string;
- caption: string;
-}
-
export interface GalleryContainerProps {
name: string;
class: string;
style?: CSSProperties;
tabIndex?: number;
- advanced: boolean;
+ filtersPlaceholder?: ReactNode;
datasource: ListValue;
itemSelection?: SelectionSingleValue | SelectionMultiValue;
itemSelectionMode: ItemSelectionModeEnum;
@@ -51,15 +34,14 @@ export interface GalleryContainerProps {
pageSize: number;
pagination: PaginationEnum;
pagingPosition: PagingPositionEnum;
+ showPagingButtons: ShowPagingButtonsEnum;
+ showTotalCount: boolean;
showEmptyPlaceholder: ShowEmptyPlaceholderEnum;
emptyPlaceholder?: ReactNode;
itemClass?: ListExpressionValue;
onClickTrigger: OnClickTriggerEnum;
onClick?: ListActionValue;
onSelectionChange?: ActionValue;
- filterList: FilterListType[];
- filtersPlaceholder?: ReactNode;
- sortList: SortListType[];
filterSectionTitle?: DynamicValue;
emptyMessageTitle?: DynamicValue;
ariaLabelListBox?: DynamicValue;
@@ -76,7 +58,7 @@ export interface GalleryPreviewProps {
readOnly: boolean;
renderMode: "design" | "xray" | "structure";
translate: (text: string) => string;
- advanced: boolean;
+ filtersPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> };
datasource: {} | { caption: string } | { type: string } | null;
itemSelection: "None" | "Single" | "Multi";
itemSelectionMode: ItemSelectionModeEnum;
@@ -87,15 +69,14 @@ export interface GalleryPreviewProps {
pageSize: number | null;
pagination: PaginationEnum;
pagingPosition: PagingPositionEnum;
+ showPagingButtons: ShowPagingButtonsEnum;
+ showTotalCount: boolean;
showEmptyPlaceholder: ShowEmptyPlaceholderEnum;
emptyPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> };
itemClass: string;
onClickTrigger: OnClickTriggerEnum;
onClick: {} | null;
onSelectionChange: {} | null;
- filterList: FilterListPreviewType[];
- filtersPlaceholder: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> };
- sortList: SortListPreviewType[];
filterSectionTitle: string;
emptyMessageTitle: string;
ariaLabelListBox: string;
diff --git a/packages/pluggableWidgets/selection-helper-web/package.json b/packages/pluggableWidgets/selection-helper-web/package.json
index 3a8aa20ba9..f6f9c0f981 100644
--- a/packages/pluggableWidgets/selection-helper-web/package.json
+++ b/packages/pluggableWidgets/selection-helper-web/package.json
@@ -1,7 +1,7 @@
{
"name": "@mendix/selection-helper-web",
"widgetName": "SelectionHelper",
- "version": "1.0.3",
+ "version": "3.0.0",
"description": "",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
diff --git a/packages/pluggableWidgets/selection-helper-web/src/package.xml b/packages/pluggableWidgets/selection-helper-web/src/package.xml
index f49b54fe09..f81afeb6e4 100644
--- a/packages/pluggableWidgets/selection-helper-web/src/package.xml
+++ b/packages/pluggableWidgets/selection-helper-web/src/package.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/packages/pluggableWidgets/tree-node-web/package.json b/packages/pluggableWidgets/tree-node-web/package.json
index c884bdc7e4..ed0f0ebae2 100644
--- a/packages/pluggableWidgets/tree-node-web/package.json
+++ b/packages/pluggableWidgets/tree-node-web/package.json
@@ -1,7 +1,7 @@
{
"name": "@mendix/tree-node-web",
"widgetName": "TreeNode",
- "version": "1.2.1",
+ "version": "3.0.0",
"description": "A Mendix pluggable widget to display a tree view structure.",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
diff --git a/packages/pluggableWidgets/tree-node-web/src/package.xml b/packages/pluggableWidgets/tree-node-web/src/package.xml
index 93ea74c1bb..433644de84 100644
--- a/packages/pluggableWidgets/tree-node-web/src/package.xml
+++ b/packages/pluggableWidgets/tree-node-web/src/package.xml
@@ -1,6 +1,6 @@
-
+
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/__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/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/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
new file mode 100644
index 0000000000..9405077fae
--- /dev/null
+++ b/packages/shared/filter-commons/package.json
@@ -0,0 +1,48 @@
+{
+ "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",
+ "test": "jest"
+ },
+ "dependencies": {
+ "mendix": "^10.21.64362"
+ },
+ "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:*",
+ "@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 67%
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
index 5e2484ec08..39e062ec82 100644
--- a/packages/shared/widget-plugin-filtering/src/__tests__/condition-utils.spec.ts
+++ b/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts
@@ -1,27 +1,33 @@
jest.mock("mendix/filters/builders");
+import { AndCondition } from "mendix/filters";
import { equals, literal } from "mendix/filters/builders";
import { compactArray, fromCompactArray, tag } from "../condition-utils";
-import { AndCondition } from "mendix/filters";
describe("condition-utils", () => {
describe("compactArray", () => {
- it("returns 'and' condition for zero array", () => {
+ it("returns 'tag' condition for zero array", () => {
const result = compactArray([]);
- expect(result).toMatchObject({ name: "and", type: "function" });
- expect((result as AndCondition).args).toHaveLength(2);
+ expect(result).toMatchObject({
+ name: "!=",
+ type: "function",
+ arg1: { value: "[0,[]]", valueType: "String" }
+ });
});
- it("returns 'and' condition for empty array", () => {
+ it("returns 'tag' condition for array of undefined", () => {
const result = compactArray([undefined, undefined, undefined]);
- expect(result).toMatchObject({ name: "and", type: "function" });
- expect((result as AndCondition).args).toHaveLength(2);
+ expect(result).toMatchObject({
+ name: "!=",
+ type: "function",
+ arg1: { value: "[3,[]]", valueType: "String" }
+ });
});
- it("returns 'and' condition with 4 args", () => {
+ it("returns 'and' condition with 3 args", () => {
const result = compactArray([tag("0"), undefined, tag("2")]);
expect(result).toMatchObject({ name: "and", type: "function" });
- expect((result as AndCondition).args).toHaveLength(4);
+ expect((result as AndCondition).args).toHaveLength(3);
});
});
diff --git a/packages/shared/widget-plugin-filtering/src/condition-utils.ts b/packages/shared/filter-commons/src/condition-utils.ts
similarity index 87%
rename from packages/shared/widget-plugin-filtering/src/condition-utils.ts
rename to packages/shared/filter-commons/src/condition-utils.ts
index 7f14ecbdc4..976da416b5 100644
--- a/packages/shared/widget-plugin-filtering/src/condition-utils.ts
+++ b/packages/shared/filter-commons/src/condition-utils.ts
@@ -1,12 +1,12 @@
import {
- FilterCondition,
AndCondition,
- OrCondition,
- LiteralExpression,
ContainsCondition,
- EqualsCondition
+ EqualsCondition,
+ FilterCondition,
+ LiteralExpression,
+ OrCondition
} from "mendix/filters";
-import { equals, literal, and } from "mendix/filters/builders";
+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;
@@ -40,25 +40,33 @@ interface TagName {
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 name: "!=";
readonly arg1: TagName;
- readonly arg2: TagName;
+ readonly arg2: TagMarker;
}
export function tag(name: string): TagCond {
- return equals(literal(name), literal(name)) as TagCond;
+ return notEqual(literal(name), literal(MARKER)) as TagCond;
}
export function isTag(cond: FilterCondition): cond is TagCond {
return (
- cond.name === "=" &&
+ cond.name === "!=" &&
cond.arg1.type === "literal" &&
cond.arg2.type === "literal" &&
/string/i.test(cond.arg1.valueType) &&
/string/i.test(cond.arg2.valueType) &&
- cond.arg1.value === cond.arg2.value
+ cond.arg2.value === MARKER
);
}
@@ -88,20 +96,19 @@ function shrink(array: Array): [indexes: number[], items: T[]]
export function compactArray(input: Array): FilterCondition {
const [indexes, items] = shrink(input);
- const arrayMeta = [input.length, indexes] as const;
- const metaTag = tag(arrayTag(arrayMeta));
- // As 'and' requires at least 2 args, we add a placeholder
- const placeholder = tag("_");
- return and(metaTag, placeholder, ...items);
+ 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 {
- if (!isAnd(cond)) {
- return [];
- }
+ const tag = isAnd(cond) ? cond.args[0] : cond;
- const [metaTag] = cond.args;
- const arrayMeta = isTag(metaTag) ? fromArrayTag(metaTag.arg1.value) : undefined;
+ const arrayMeta = isTag(tag) ? fromArrayTag(tag.arg1.value) : undefined;
if (!arrayMeta) {
return [];
@@ -109,7 +116,12 @@ export function fromCompactArray(cond: FilterCondition): Array = Array(length).fill(undefined);
- cond.args.slice(2).forEach((cond, i) => {
+
+ if (!isAnd(cond)) {
+ return arr;
+ }
+
+ cond.args.slice(1).forEach((cond, i) => {
arr[indexes[i]] = cond;
});
diff --git a/packages/shared/widget-plugin-filtering/src/typings/FilterFunctions.ts b/packages/shared/filter-commons/src/typings/FilterFunctions.ts
similarity index 99%
rename from packages/shared/widget-plugin-filtering/src/typings/FilterFunctions.ts
rename to packages/shared/filter-commons/src/typings/FilterFunctions.ts
index 7fc85213c1..13cc4f228a 100644
--- a/packages/shared/widget-plugin-filtering/src/typings/FilterFunctions.ts
+++ b/packages/shared/filter-commons/src/typings/FilterFunctions.ts
@@ -1,6 +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/widget-plugin-filtering/src/typings/IJSActionsControlled.ts b/packages/shared/filter-commons/src/typings/IJSActionsControlled.ts
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/typings/IJSActionsControlled.ts
rename to packages/shared/filter-commons/src/typings/IJSActionsControlled.ts
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/widget-plugin-filtering/src/typings/settings.ts b/packages/shared/filter-commons/src/typings/settings.ts
similarity index 76%
rename from packages/shared/widget-plugin-filtering/src/typings/settings.ts
rename to packages/shared/filter-commons/src/typings/settings.ts
index 7f4f3b6614..1b6cb7a373 100644
--- a/packages/shared/widget-plugin-filtering/src/typings/settings.ts
+++ b/packages/shared/filter-commons/src/typings/settings.ts
@@ -4,6 +4,6 @@ export type InputData = [Fn, string | null, string | null];
export type SelectData = string[];
-export type FilterData = InputData | SelectData | null;
+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..41ffc4217b
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/package.json
@@ -0,0 +1,41 @@
+{
+ "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",
+ "mendix": "^10.21.64362",
+ "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/__tests__/RefFilterStore.spec.ts b/packages/shared/widget-plugin-dropdown-filter/src/__tests__/RefFilterStore.spec.ts
new file mode 100644
index 0000000000..c7b800b61f
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/__tests__/RefFilterStore.spec.ts
@@ -0,0 +1,253 @@
+// describe("RefFilterStore", () => {
+// afterEach(() => _resetGlobalState());
+
+// describe("get options()", () => {
+// let items: ObjectItem[];
+// let mapItem: (item: ObjectItem) => string;
+// let store: RefFilterStore;
+// beforeEach(() => {
+// const [a, b, c] = [obj("id3n"), obj("f8x3"), obj("932c")];
+// items = [a, b, c];
+// mapItem = cases([a, "Alice"], [b, "Bob"], [c, "Chuck"], [undefined, "No caption"]);
+// store = new RefFilterStore(
+// {
+// ref: listReference(),
+// datasource: list(items),
+// caption: listExpression(mapItem)
+// },
+// null
+// );
+// });
+
+// it("returns array of options", () => {
+// expect(store.options).toMatchInlineSnapshot(`
+// [
+// {
+// "caption": "Alice",
+// "selected": false,
+// "value": "obj_id3n",
+// },
+// {
+// "caption": "Bob",
+// "selected": false,
+// "value": "obj_f8x3",
+// },
+// {
+// "caption": "Chuck",
+// "selected": false,
+// "value": "obj_932c",
+// },
+// ]
+// `);
+// });
+// });
+
+// describe("get condition()", () => {
+// it.todo("return filter condition using equals");
+// });
+
+// describe("toJSON()", () => {
+// let store: RefFilterStore;
+// beforeEach(() => {
+// store = new RefFilterStore(
+// {
+// ref: listReference(),
+// datasource: list(3),
+// caption: listExpression(() => "[string]")
+// },
+// null
+// );
+// });
+
+// it("returns state as JS array", () => {
+// expect(store.toJSON()).toEqual([]);
+// });
+
+// it("save state with one item", () => {
+// const guid = store.options[0].value;
+// store.toggle(guid);
+// expect(store.toJSON()).toEqual([guid]);
+// });
+
+// it("save state with multiple items", () => {
+// const [guid1, guid2] = [store.options[0].value, store.options[1].value];
+// store.toggle(guid1);
+// store.toggle(guid2);
+// const output = store.toJSON();
+
+// expect(output).toHaveLength(2);
+// expect(output).toContain(guid1);
+// expect(output).toContain(guid2);
+// });
+// });
+
+// describe("fromJSON()", () => {
+// let store: RefFilterStore;
+// beforeEach(() => {
+// store = new RefFilterStore(
+// {
+// ref: listReference(),
+// datasource: list([obj("id3n"), obj("f8x3"), obj("932c")]),
+// caption: listExpression(() => "[string]")
+// },
+// null
+// );
+// });
+
+// it("restore state from JS array", () => {
+// const output: Array = [];
+// autorun(() => output.push(store.options));
+
+// // Check that every option is unselected
+// expect(output[0].map(option => option.selected)).toEqual([false, false, false]);
+// // Restore state
+// store.fromJSON(["obj_id3n"]);
+// // Check that first option is selected
+// expect(output[1].map(option => option.selected)).toEqual([true, false, false]);
+// });
+
+// it("overrides any previous state", () => {
+// const output: Array = [];
+
+// // Select each option
+// store.options.forEach(option => {
+// store.toggle(option.value);
+// });
+// // Start observing
+// autorun(() => output.push(store.options));
+// // Check the state
+// expect(output[0].map(option => option.selected)).toEqual([true, true, true]);
+// store.fromJSON(["obj_932c"]);
+// expect(output[1].map(option => option.selected)).toEqual([false, false, true]);
+// });
+
+// it("should not change state if json is null", () => {
+// const state = store.toJSON();
+// store.fromJSON(null);
+// expect(store.toJSON()).toEqual(state);
+// });
+// });
+
+// describe("toggle()", () => {
+// let store: RefFilterStore;
+// beforeEach(() => {
+// store = new RefFilterStore(
+// {
+// ref: listReference(),
+// datasource: list([obj("id3n"), obj("f8x3"), obj("932c")]),
+// caption: listExpression(() => "[string]")
+// },
+// null
+// );
+// });
+
+// it("compute new options", () => {
+// const output: Array = [];
+// const [x, y] = store.options;
+
+// autorun(() => output.push(store.options));
+// expect(output[0].map(opt => opt.selected)).toEqual([false, false, false]);
+
+// store.toggle(x.value);
+// store.toggle(y.value);
+// expect(output[2].map(opt => opt.selected)).toEqual([true, true, false]);
+
+// store.toggle(x.value);
+// expect(output[3].map(opt => opt.selected)).toEqual([false, true, false]);
+// });
+// });
+
+// describe("with 'fetchOptionsLazy' flag", () => {
+// it("should set limit to 0 on options datasource", () => {
+// const datasource = list.loading();
+// const props = {
+// ref: listReference(),
+// datasource,
+// caption: listExpression(() => "[string]"),
+// fetchOptionsLazy: true
+// };
+
+// new RefFilterStore(props, null);
+
+// expect(datasource.setLimit).toHaveBeenLastCalledWith(0);
+// });
+
+// it("should not change limit if flag is false", () => {
+// const datasource = list.loading();
+// const props = {
+// ref: listReference(),
+// datasource,
+// caption: listExpression(() => "[string]"),
+// fetchOptionsLazy: false
+// };
+
+// new RefFilterStore(props, null);
+
+// expect(datasource.setLimit).not.toHaveBeenCalled();
+// });
+// });
+
+// describe("when datasource changed", () => {
+// it("compute new options", () => {
+// const a = obj("id3n");
+// const props = {
+// ref: listReference(),
+// datasource: list.loading(),
+// caption: listExpression(cases([a, "Alice"], [undefined, "No caption"]))
+// };
+// const store = new RefFilterStore(props, null);
+// const output: any[] = [];
+// autorun(() => output.push(store.options));
+
+// expect(output[0]).toEqual([]);
+// store.updateProps({ ...props, datasource: list([a]) });
+// expect(output[1]).toEqual([
+// {
+// caption: "Alice",
+// selected: false,
+// value: "obj_id3n"
+// }
+// ]);
+// });
+// });
+
+// describe("json data filtering", () => {
+// const savedJson = ["obj_xx", "obj_xiii", "obj_deleted", "obj_yy", "obj_unknown"];
+// let a: ObjectItem;
+// let b: ObjectItem;
+// let props: Omit;
+// beforeEach(() => {
+// a = obj("xx");
+// b = obj("yy");
+// props = {
+// ref: listReference(),
+// caption: listExpression(() => "[string]")
+// };
+// });
+
+// it("skip filtering if has just part of the list", () => {
+// const store = new RefFilterStore({ ...props, datasource: list.loading() }, null);
+// // Restore state
+// store.fromJSON(savedJson);
+// // Check state
+// expect(store.toJSON()).toEqual(savedJson);
+// // Update with partial data
+// const datasource = new ListValueBuilder().withItems([a, b]).withHasMore(true).build();
+// store.updateProps({ ...props, datasource });
+// // Check that state still has unknown GUIDs
+// expect(store.toJSON()).toEqual(savedJson);
+// });
+
+// it("allows to restore any state if datasource is loading", () => {
+// const store = new RefFilterStore({ ...props, datasource: list.loading() }, null);
+// // Restore state
+// store.fromJSON(savedJson);
+// // Check state
+// expect(store.toJSON()).toEqual(savedJson);
+// });
+// });
+// });
+
+describe("RefFilterStore", () => {
+ it.todo("should be implemented");
+});
diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilterContainer.tsx b/packages/shared/widget-plugin-dropdown-filter/src/containers/RefFilterContainer.tsx
similarity index 70%
rename from packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilterContainer.tsx
rename to packages/shared/widget-plugin-dropdown-filter/src/containers/RefFilterContainer.tsx
index 5b332e833e..fe4a658567 100644
--- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilterContainer.tsx
+++ b/packages/shared/widget-plugin-dropdown-filter/src/containers/RefFilterContainer.tsx
@@ -1,18 +1,22 @@
-import { RefSelectController } from "@mendix/widget-plugin-filtering/controllers/picker/RefSelectController";
-import { RefComboboxController } from "@mendix/widget-plugin-filtering/controllers/picker/RefComboboxController";
-import { RefTagPickerController } from "@mendix/widget-plugin-filtering/controllers/picker/RefTagPickerController";
-import { Select } from "@mendix/widget-plugin-filtering/controls/select/Select";
-import { Combobox } from "@mendix/widget-plugin-filtering/controls/combobox/Combobox";
-import { TagPicker } from "@mendix/widget-plugin-filtering/controls/tag-picker/TagPicker";
-import { usePickerJSActions } from "@mendix/widget-plugin-filtering/helpers/usePickerJSActions";
-import { RefFilterStore } from "@mendix/widget-plugin-filtering/stores/picker/RefFilterStore";
+import { RefSelectController } from "../controllers/RefSelectController";
+import { RefComboboxController } from "../controllers/RefComboboxController";
+import { RefTagPickerController } from "../controllers/RefTagPickerController";
+import { Select } from "../controls/select/Select";
+import { Combobox } from "../controls/combobox/Combobox";
+import { TagPicker } from "../controls/tag-picker/TagPicker";
+import { usePickerJSActions } from "../helpers/usePickerJSActions";
+import { RefFilterStore } from "../stores/RefFilterStore";
import { ActionValue, EditableValue } from "mendix";
import { observer } from "mobx-react-lite";
-import { createElement, CSSProperties } from "react";
-import { useSetupUpdate } from "@mendix/widget-plugin-filtering/helpers/useSetupUpdate";
-import { useFrontendType } from "../hooks/useFrontendType";
-import { SelectedItemsStyleEnum, SelectionMethodEnum } from "../../typings/DatagridDropdownFilterProps";
+import { createElement, CSSProperties, useEffect } from "react";
+
+import { useFrontendType } from "../helpers/useFrontendType";
import { useOnScrollBottom } from "@mendix/widget-plugin-hooks/useOnScrollBottom";
+import { SelectedItemsStyleEnum, SelectionMethodEnum } from "../typings/widget";
+import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup";
+import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
+import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider";
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
export interface RefFilterContainerProps {
ariaLabel?: string;
@@ -48,7 +52,8 @@ function Container(props: RefFilterContainerProps): React.ReactElement {
}
const SelectWidget = observer(function SelectWidget(props: RefFilterContainerProps): React.ReactElement {
- const ctrl1 = useSetupUpdate(() => new RefSelectController(props), props);
+ const gate = useGate(props);
+ const ctrl1 = useSetup(() => new RefSelectController({ gate }));
const handleMenuScroll = useOnScrollBottom(ctrl1.handleMenuScrollEnd, { triggerZoneHeight: 100 });
usePickerJSActions(ctrl1, props);
@@ -71,7 +76,8 @@ const SelectWidget = observer(function SelectWidget(props: RefFilterContainerPro
});
const ComboboxWidget = observer(function ComboboxWidget(props: RefFilterContainerProps): React.ReactElement {
- const ctrl2 = useSetupUpdate(() => new RefComboboxController(props), props);
+ const gate = useGate(props);
+ const ctrl2 = useSetup(() => new RefComboboxController({ gate }));
const handleMenuScroll = useOnScrollBottom(ctrl2.handleMenuScrollEnd, { triggerZoneHeight: 100 });
usePickerJSActions(ctrl2, props);
@@ -93,7 +99,8 @@ const ComboboxWidget = observer(function ComboboxWidget(props: RefFilterContaine
});
const TagPickerWidget = observer(function TagPickerWidget(props: RefFilterContainerProps): React.ReactElement {
- const ctrl3 = useSetupUpdate(() => new RefTagPickerController(props), props);
+ const gate = useGate(props);
+ const ctrl3 = useSetup(() => new RefTagPickerController({ gate }));
const handleMenuScroll = useOnScrollBottom(ctrl3.handleMenuScrollEnd, { triggerZoneHeight: 100 });
usePickerJSActions(ctrl3, props);
@@ -119,3 +126,9 @@ const TagPickerWidget = observer(function TagPickerWidget(props: RefFilterContai
});
export const RefFilterContainer = Container;
+
+function useGate(props: RefFilterContainerProps): DerivedPropsGate {
+ const gp = useConst(() => new GateProvider(props));
+ useEffect(() => gp.setProps(props));
+ return gp.gate;
+}
diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/StaticFilterContainer.tsx b/packages/shared/widget-plugin-dropdown-filter/src/containers/StaticFilterContainer.tsx
similarity index 66%
rename from packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/StaticFilterContainer.tsx
rename to packages/shared/widget-plugin-dropdown-filter/src/containers/StaticFilterContainer.tsx
index cd0e25c46f..f06728949d 100644
--- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/StaticFilterContainer.tsx
+++ b/packages/shared/widget-plugin-dropdown-filter/src/containers/StaticFilterContainer.tsx
@@ -1,22 +1,21 @@
-import { StaticSelectController } from "@mendix/widget-plugin-filtering/controllers/picker/StaticSelectController";
-import { StaticComboboxController } from "@mendix/widget-plugin-filtering/controllers/picker/StaticComboboxController";
-import { Select } from "@mendix/widget-plugin-filtering/controls/select/Select";
-import { Combobox } from "@mendix/widget-plugin-filtering/controls/combobox/Combobox";
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
+import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst";
+import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider";
import { ActionValue, EditableValue } from "mendix";
import { observer } from "mobx-react-lite";
-import { createElement, CSSProperties } from "react";
-import {
- FilterOptionsType,
- SelectedItemsStyleEnum,
- SelectionMethodEnum
-} from "../../typings/DatagridDropdownFilterProps";
+import { createElement, CSSProperties, useEffect } from "react";
+import { StaticComboboxController } from "../controllers/StaticComboboxController";
+import { StaticSelectController } from "../controllers/StaticSelectController";
+import { StaticTagPickerController } from "../controllers/StaticTagPickerController";
+import { Combobox } from "../controls/combobox/Combobox";
+import { Select } from "../controls/select/Select";
+import { TagPicker } from "../controls/tag-picker/TagPicker";
+import { useFrontendType } from "../helpers/useFrontendType";
+import { usePickerJSActions } from "../helpers/usePickerJSActions";
import { withCustomOptionsGuard } from "../hocs/withCustomOptionsGuard";
-import { StaticSelectFilterStore } from "@mendix/widget-plugin-filtering/stores/picker/StaticSelectFilterStore";
-import { StaticTagPickerController } from "@mendix/widget-plugin-filtering/controllers/picker/StaticTagPickerController";
-import { TagPicker } from "@mendix/widget-plugin-filtering/controls/tag-picker/TagPicker";
-import { useSetupUpdate } from "@mendix/widget-plugin-filtering/helpers/useSetupUpdate";
-import { usePickerJSActions } from "@mendix/widget-plugin-filtering/helpers/usePickerJSActions";
-import { useFrontendType } from "../hooks/useFrontendType";
+import { StaticSelectFilterStore } from "../stores/StaticSelectFilterStore";
+import { FilterOptionsType, SelectedItemsStyleEnum, SelectionMethodEnum } from "../typings/widget";
+import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup";
export interface StaticFilterContainerProps {
ariaLabel?: string;
@@ -53,7 +52,8 @@ function Container(props: StaticFilterContainerProps): React.ReactElement {
}
const SelectWidget = observer(function SelectWidget(props: StaticFilterContainerProps): React.ReactElement {
- const ctrl1 = useSetupUpdate(() => new StaticSelectController(props), props);
+ const gate = useGate(props);
+ const ctrl1 = useSetup(() => new StaticSelectController({ gate }));
usePickerJSActions(ctrl1, props);
@@ -73,7 +73,8 @@ const SelectWidget = observer(function SelectWidget(props: StaticFilterContainer
});
const ComboboxWidget = observer(function ComboboxWidget(props: StaticFilterContainerProps): React.ReactElement {
- const ctrl2 = useSetupUpdate(() => new StaticComboboxController(props), props);
+ const gate = useGate(props);
+ const ctrl2 = useSetup(() => new StaticComboboxController({ gate }));
usePickerJSActions(ctrl2, props);
@@ -93,7 +94,8 @@ const ComboboxWidget = observer(function ComboboxWidget(props: StaticFilterConta
});
const TagPickerWidget = observer(function TagPickerWidget(props: StaticFilterContainerProps): React.ReactElement {
- const ctrl3 = useSetupUpdate(() => new StaticTagPickerController(props), props);
+ const gate = useGate(props);
+ const ctrl3 = useSetup(() => new StaticTagPickerController({ gate }));
usePickerJSActions(ctrl3, props);
@@ -116,3 +118,9 @@ const TagPickerWidget = observer(function TagPickerWidget(props: StaticFilterCon
});
export const StaticFilterContainer = withCustomOptionsGuard(Container);
+
+function useGate(props: StaticFilterContainerProps): DerivedPropsGate {
+ const gp = useConst(() => new GateProvider(props));
+ useEffect(() => gp.setProps(props));
+ return gp.gate;
+}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerBaseController.ts
similarity index 73%
rename from packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerBaseController.ts
index a762277879..a834478824 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerBaseController.ts
@@ -1,9 +1,10 @@
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
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";
+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;
@@ -13,6 +14,17 @@ interface FilterStore {
options: OptionWithState[];
}
+export interface PickerBaseControllerProps {
+ defaultValue?: string;
+ filterStore: S;
+ multiselect: boolean;
+ onChange?: ActionValue;
+ valueAttribute?: EditableValue;
+ emptyCaption?: string;
+}
+
+type Gate = DerivedPropsGate>;
+
export class PickerBaseController implements IJSActionsControlled {
protected actionHelper: PickerJSActionsHelper;
protected changeHelper: PickerChangeHelper;
@@ -21,9 +33,10 @@ export class PickerBaseController implements IJSActionsCo
filterStore: S;
multiselect: boolean;
- constructor(props: PickerBaseControllerProps) {
+ constructor({ gate, multiselect }: { gate: Gate; multiselect: boolean }) {
+ const props = gate.props;
this.filterStore = props.filterStore;
- this.multiselect = props.multiselect;
+ this.multiselect = multiselect;
this.serializer = new OptionsSerializer({ store: this.filterStore });
this.defaultValue = this.parseDefaultValue(props.defaultValue);
this.actionHelper = new PickerJSActionsHelper({
@@ -31,7 +44,7 @@ export class PickerBaseController implements IJSActionsCo
parse: value => this.serializer.fromStorableValue(value) ?? [],
multiselect: props.multiselect
});
- this.changeHelper = new PickerChangeHelper(props, () => this.serializer.value);
+ this.changeHelper = new PickerChangeHelper(gate, () => this.serializer.value);
}
parseDefaultValue = (value: string | undefined): Iterable | undefined => {
@@ -51,12 +64,3 @@ export class PickerBaseController implements IJSActionsCo
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/generic/PickerChangeHelper.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts
similarity index 54%
rename from packages/shared/widget-plugin-filtering/src/controllers/generic/PickerChangeHelper.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts
index f4a51110f3..d74bfdce29 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerChangeHelper.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts
@@ -1,3 +1,4 @@
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
import { ActionValue, EditableValue } from "mendix";
import { IReactionDisposer, reaction } from "mobx";
@@ -8,28 +9,21 @@ interface Props {
}
export class PickerChangeHelper {
- private onChange?: ActionValue;
- private valueAttribute?: EditableValue;
+ private readonly gate: DerivedPropsGate;
private valueFn: () => string | undefined;
- constructor(props: Props, valueFn: () => string | undefined) {
- this.onChange = props.onChange;
- this.valueAttribute = props.valueAttribute;
+ constructor(gate: DerivedPropsGate, valueFn: () => string | undefined) {
+ this.gate = gate;
this.valueFn = valueFn;
}
setup(): IReactionDisposer {
const effect = (value: string | undefined): void => {
- this.valueAttribute?.setValue(value);
-
- executeAction(this.onChange);
+ const { valueAttribute, onChange } = this.gate.props;
+ valueAttribute?.setValue(value);
+ executeAction(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-dropdown-filter/src/controllers/PickerJSActionsHelper.ts
similarity index 96%
rename from packages/shared/widget-plugin-filtering/src/controllers/generic/PickerJSActionsHelper.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerJSActionsHelper.ts
index 3cd56a159e..c71b741cb7 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerJSActionsHelper.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerJSActionsHelper.ts
@@ -1,4 +1,4 @@
-import { IJSActionsControlled, ResetHandler, SetValueHandler } from "../../typings/IJSActionsControlled";
+import { IJSActionsControlled, ResetHandler, SetValueHandler } from "../typings/IJSActionsControlled";
interface FilterStore {
reset: () => void;
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefBaseController.ts
similarity index 52%
rename from packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/RefBaseController.ts
index c355a2e530..eabd9ad9df 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefBaseController.ts
@@ -1,31 +1,24 @@
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
import { ActionValue, EditableValue } from "mendix";
-import { action, makeObservable } from "mobx";
-import { disposeFx } from "../../mobx-utils";
-import { RefFilterStore } from "../../stores/picker/RefFilterStore";
+import { RefFilterStore } from "../stores/RefFilterStore";
import { PickerBaseController } from "./PickerBaseController";
export class RefBaseController extends PickerBaseController {
- constructor(props: RefBaseControllerProps) {
- super(props);
- makeObservable(this, {
- updateProps: action
- });
+ constructor({ gate, multiselect }: { gate: DerivedPropsGate; multiselect: boolean }) {
+ super({ gate, multiselect });
}
setup(): () => void {
- const [disposers, dispose] = disposeFx();
+ const [add, disposeAll] = disposeBatch();
- disposers.push(this.changeHelper.setup());
+ add(this.changeHelper.setup());
if (this.defaultValue) {
this.filterStore.setDefaultSelected(this.defaultValue);
}
- return dispose;
- }
-
- updateProps(props: RefBaseControllerProps): void {
- this.changeHelper.updateProps(props);
+ return disposeAll;
}
}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefComboboxController.ts
similarity index 64%
rename from packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/RefComboboxController.ts
index d211132988..57b956400f 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefComboboxController.ts
@@ -1,10 +1,11 @@
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
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";
+ constructor({ gate }: { gate: DerivedPropsGate }) {
+ super({ gate, multiselect: false });
+ this.inputPlaceholder = gate.props.placeholder ?? "Search";
}
handleFocus = (event: React.FocusEvent): void => {
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefSelectController.ts
similarity index 54%
rename from packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/RefSelectController.ts
index 6d5c46280c..4b178af430 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefSelectController.ts
@@ -1,11 +1,12 @@
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
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";
+ constructor({ gate }: { gate: DerivedPropsGate }) {
+ super({ gate, multiselect: gate.props.multiselect });
+ this.emptyOption.caption = gate.props.emptyCaption || "None";
+ this.placeholder = gate.props.placeholder || "Search";
}
handleFocus = (): void => {
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefTagPickerController.ts
similarity index 64%
rename from packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/RefTagPickerController.ts
index 0595766f8d..e238ebd173 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefTagPickerController.ts
@@ -1,3 +1,4 @@
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
import { RefBaseController, RefBaseControllerProps } from "./RefBaseController";
import { TagPickerControllerMixin } from "./mixins/TagPickerControllerMixin";
@@ -13,12 +14,12 @@ export class RefTagPickerController extends TagPickerControllerMixin(RefBaseCont
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";
+ constructor({ gate }: { gate: DerivedPropsGate }) {
+ super({ gate, multiselect: gate.props.multiselect });
+ this.inputPlaceholder = gate.props.placeholder ?? "Search";
+ this.filterSelectedOptions = gate.props.selectionMethod === "rowClick";
+ this.selectedStyle = gate.props.selectedItemsStyle;
+ this.selectionMethod = this.selectedStyle === "boxes" ? gate.props.selectionMethod : "checkbox";
}
handleFocus = (): void => {
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticBaseController.ts
similarity index 53%
rename from packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticBaseController.ts
index b9506dcd99..0a3bc88b69 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticBaseController.ts
@@ -1,31 +1,34 @@
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
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 { autorun, computed, makeObservable } from "mobx";
+import { StaticSelectFilterStore } from "../stores/StaticSelectFilterStore";
import { PickerBaseController } from "./PickerBaseController";
export class StaticBaseController extends PickerBaseController {
- filterOptions: Array>>;
+ private readonly gate: DerivedPropsGate;
- constructor(props: StaticBaseControllerProps) {
- super(props);
- this.filterOptions = props.filterOptions;
- makeObservable(this, {
- updateProps: action,
- filterOptions: observable.struct
+ constructor({ gate, multiselect }: { gate: DerivedPropsGate; multiselect: boolean }) {
+ super({ gate, multiselect });
+ this.gate = gate;
+ makeObservable(this, {
+ filterOptions: computed.struct
});
}
+ private get filterOptions(): Array> {
+ return this.gate.props.filterOptions.map(this.toStoreOption);
+ }
+
setup(): () => void {
- const [disposers, dispose] = disposeFx();
+ const [addDisposer, dispose] = disposeBatch();
- disposers.push(this.changeHelper.setup());
+ addDisposer(this.changeHelper.setup());
- disposers.push(
+ addDisposer(
autorun(() => {
if (this.filterOptions.length > 0) {
- const options = this.filterOptions.map(this.toStoreOption);
- this.filterStore.setCustomOptions(options);
+ this.filterStore.setCustomOptions(this.filterOptions);
}
})
);
@@ -37,11 +40,6 @@ export class StaticBaseController extends PickerBaseController>): CustomOption => ({
caption: `${opt.caption?.value}`,
value: `${opt.value?.value}`
diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticComboboxController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticComboboxController.ts
new file mode 100644
index 0000000000..af85ad482d
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticComboboxController.ts
@@ -0,0 +1,10 @@
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
+import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController";
+import { ComboboxControllerMixin } from "./mixins/ComboboxControllerMixin";
+
+export class StaticComboboxController extends ComboboxControllerMixin(StaticBaseController) {
+ constructor({ gate }: { gate: DerivedPropsGate }) {
+ super({ gate, multiselect: false });
+ this.inputPlaceholder = gate.props.placeholder ?? "Search";
+ }
+}
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..48fe7a9909
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticSelectController.ts
@@ -0,0 +1,11 @@
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
+import { SelectControllerMixin } from "./mixins/SelectControllerMixin";
+import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController";
+
+export class StaticSelectController extends SelectControllerMixin(StaticBaseController) {
+ constructor({ gate }: { gate: DerivedPropsGate }) {
+ super({ gate, multiselect: gate.props.multiselect });
+ this.emptyOption.caption = gate.props.emptyCaption || "None";
+ this.placeholder = gate.props.emptyCaption || "Select";
+ }
+}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticTagPickerController.ts
similarity index 59%
rename from packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticTagPickerController.ts
index 11bc90fd30..7863520b51 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticTagPickerController.ts
@@ -1,3 +1,4 @@
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController";
import { TagPickerControllerMixin } from "./mixins/TagPickerControllerMixin";
@@ -13,11 +14,11 @@ export class StaticTagPickerController extends TagPickerControllerMixin(StaticBa
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";
+ constructor({ gate }: { gate: DerivedPropsGate }) {
+ super({ gate, multiselect: gate.props.multiselect });
+ this.inputPlaceholder = gate.props.placeholder ?? "Search";
+ this.filterSelectedOptions = gate.props.selectionMethod === "rowClick";
+ this.selectedStyle = gate.props.selectedItemsStyle;
+ this.selectionMethod = this.selectedStyle === "boxes" ? gate.props.selectionMethod : "checkbox";
}
}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/ComboboxControllerMixin.ts
similarity index 92%
rename from packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/ComboboxControllerMixin.ts
index 0364d9331f..12282fa36a 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/ComboboxControllerMixin.ts
@@ -1,9 +1,9 @@
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
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";
+import { SearchStore } from "../../stores/SearchStore";
+import { OptionWithState } from "../../typings/OptionWithState";
+import { GConstructor } from "../../typings/type-utils";
export interface FilterStore {
clear: () => void;
@@ -45,13 +45,13 @@ export function ComboboxControllerMixin(Base: TBas
}
setup(): () => void {
- const [disposers, dispose] = disposeFx();
- disposers.push(autorun(...this.searchSyncFx()));
+ const [add, dispose] = disposeBatch();
+ add(autorun(...this.searchSyncFx()));
// Set input when store state changes
- disposers.push(reaction(...this.storeSyncFx()));
+ add(reaction(...this.storeSyncFx()));
- disposers.push(super.setup());
+ add(super.setup());
this.setInputValue(this.inputInitValue);
return dispose;
}
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/SelectControllerMixin.ts
similarity index 96%
rename from packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/SelectControllerMixin.ts
index c765116ed8..929bb1d743 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/SelectControllerMixin.ts
@@ -1,7 +1,7 @@
import { useSelect, UseSelectProps } from "downshift";
import { action, computed, makeObservable } from "mobx";
-import { OptionWithState } from "../../../typings/OptionWithState";
-import { GConstructor } from "../../../typings/type-utils";
+import { OptionWithState } from "../../typings/OptionWithState";
+import { GConstructor } from "../../typings/type-utils";
export interface FilterStore {
toggle: (value: string) => void;
diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/TagPickerControllerMixin.ts
similarity index 94%
rename from packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/TagPickerControllerMixin.ts
index 42ed406a37..be215ca8dc 100644
--- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/TagPickerControllerMixin.ts
@@ -1,9 +1,9 @@
+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 { disposeFx } from "../../../mobx-utils";
-import { SearchStore } from "../../../stores/picker/SearchStore";
-import { OptionWithState } from "../../../typings/OptionWithState";
-import { GConstructor } from "../../../typings/type-utils";
+import { SearchStore } from "../../stores/SearchStore";
+import { OptionWithState } from "../../typings/OptionWithState";
+import { GConstructor } from "../../typings/type-utils";
export interface FilterStore {
toggle: (value: string) => void;
@@ -47,9 +47,9 @@ export function TagPickerControllerMixin(Base: TBa
}
setup(): () => void {
- const [disposers, dispose] = disposeFx();
- disposers.push(autorun(...this.searchSyncFx()));
- disposers.push(super.setup());
+ const [add, dispose] = disposeBatch();
+ add(autorun(...this.searchSyncFx()));
+ add(super.setup());
return dispose;
}
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/pluggableWidgets/datagrid-dropdown-filter-web/src/hooks/useFrontendType.ts b/packages/shared/widget-plugin-dropdown-filter/src/helpers/useFrontendType.ts
similarity index 100%
rename from packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hooks/useFrontendType.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/helpers/useFrontendType.ts
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 85%
rename from packages/shared/widget-plugin-filtering/src/helpers/usePickerJSActions.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/helpers/usePickerJSActions.ts
index ee7a00dc1a..6c76d13a76 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/usePickerJSActions.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/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/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withCustomOptionsGuard.tsx b/packages/shared/widget-plugin-dropdown-filter/src/hocs/withCustomOptionsGuard.tsx
similarity index 82%
rename from packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withCustomOptionsGuard.tsx
rename to packages/shared/widget-plugin-dropdown-filter/src/hocs/withCustomOptionsGuard.tsx
index 844527f811..2a76c657c3 100644
--- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withCustomOptionsGuard.tsx
+++ b/packages/shared/widget-plugin-dropdown-filter/src/hocs/withCustomOptionsGuard.tsx
@@ -1,7 +1,7 @@
import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
import { useState, createElement } from "react";
-import { FilterOptionsType } from "../../typings/DatagridDropdownFilterProps";
-import { StaticSelectFilterStore } from "@mendix/widget-plugin-filtering/stores/picker/StaticSelectFilterStore";
+import { FilterOptionsType } from "../typings/widget";
+import { StaticSelectFilterStore } from "../stores/StaticSelectFilterStore";
interface Props {
filterStore: StaticSelectFilterStore;
diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/BaseSelectStore.ts
similarity index 65%
rename from packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/stores/BaseSelectStore.ts
index d31e08f06e..7ae086f7fc 100644
--- a/packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/stores/BaseSelectStore.ts
@@ -1,6 +1,5 @@
+import { FilterData, InputData } from "@mendix/filter-commons/typings/settings";
import { action, makeObservable, observable } from "mobx";
-import { FilterData } from "../../typings/settings";
-import { isInputData } from "../utils/is-input-data";
export class BaseSelectStore {
protected defaultSelected: Iterable = [];
@@ -40,10 +39,33 @@ export class BaseSelectStore {
}
fromJSON(json: FilterData): void {
- if (json === null || isInputData(json)) {
+ 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-filtering/src/stores/picker/OptionsSerializer.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/OptionsSerializer.ts
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/stores/picker/OptionsSerializer.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/stores/OptionsSerializer.ts
diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/RefFilterStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/RefFilterStore.ts
similarity index 69%
rename from packages/shared/widget-plugin-filtering/src/stores/picker/RefFilterStore.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/stores/RefFilterStore.ts
index c545fbb634..1a67a3d348 100644
--- a/packages/shared/widget-plugin-filtering/src/stores/picker/RefFilterStore.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/stores/RefFilterStore.ts
@@ -1,40 +1,36 @@
-import { ListAttributeValue, ListReferenceSetValue, ListReferenceValue, ListValue, ObjectItem } from "mendix";
+import { flattenRefCond, selectedFromCond } from "@mendix/filter-commons/condition-utils";
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate";
+import { AssociationMetaData, AttributeMetaData, 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 { association, attribute, contains, 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 { OptionWithState } from "../typings/OptionWithState";
import { BaseSelectStore } from "./BaseSelectStore";
import { SearchStore } from "./SearchStore";
-type ListAttributeId = ListAttributeValue["id"];
+type ListAttributeId = AttributeMetaData["id"];
export interface RefFilterStoreProps {
- ref: ListReferenceValue | ListReferenceSetValue;
- datasource: ListValue;
- searchAttrId?: ListAttributeId;
fetchOptionsLazy?: boolean;
- caption: CaptionAccessor;
+ refEntity: AssociationMetaData;
+ refCaption: CaptionAccessor;
+ refOptions: ListValue;
+ searchAttrId?: ListAttributeId;
}
interface CaptionAccessor {
get: (obj: ObjectItem) => { value: string | undefined };
}
+type Gate = DerivedPropsGate;
+
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 gate: Gate;
+ private readonly searchAttrId?: ListAttributeId;
private readonly initCondArray: Array;
private readonly pageSize = 20;
private readonly searchSize = 100;
@@ -44,11 +40,11 @@ export class RefFilterStore extends BaseSelectStore {
lazyMode: boolean;
search: SearchStore;
- constructor(props: RefFilterStoreProps, initCond: FilterCondition | null) {
+ constructor({ gate, initCond }: { gate: Gate; initCond: FilterCondition | null }) {
super();
- this.caption = props.caption;
- this.datasource = props.datasource;
- this.listRef = props.ref;
+ const { props } = gate;
+ this.gate = gate;
+
this.lazyMode = props.fetchOptionsLazy ?? true;
this.searchAttrId = props.searchAttrId;
this.initCondArray = initCond ? flattenRefCond(initCond) : [];
@@ -59,16 +55,14 @@ export class RefFilterStore extends BaseSelectStore {
this.datasource.setLimit(0);
}
- makeObservable(this, {
- datasource: observable.ref,
- listRef: observable.ref,
- caption: observable.ref,
- searchAttrId: observable.ref,
+ makeObservable(this, {
+ datasource: computed,
+ refEntity: computed,
+ caption: computed,
options: computed,
hasMore: computed,
isLoading: computed,
condition: computed,
- updateProps: action,
fromViewState: action,
fetchReady: observable,
setFetchReady: action,
@@ -82,6 +76,18 @@ export class RefFilterStore extends BaseSelectStore {
}
}
+ private get datasource(): ListValue {
+ return this.gate.props.refOptions;
+ }
+
+ private get refEntity(): AssociationMetaData {
+ return this.gate.props.refEntity;
+ }
+
+ private get caption(): CaptionAccessor {
+ return this.gate.props.refCaption;
+ }
+
get hasMore(): boolean {
return this.datasource.hasMoreItems ?? false;
}
@@ -115,19 +121,18 @@ export class RefFilterStore extends BaseSelectStore {
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])];
+ if (obj) {
+ try {
+ return [contains(association(this.refEntity.id), literal(obj))];
+ } catch {
+ return [equals(association(this.refEntity.id), literal(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;
}
@@ -146,14 +151,14 @@ export class RefFilterStore extends BaseSelectStore {
}
setup(): () => void {
- const [disposers, dispose] = disposeFx();
+ const [add, dispose] = disposeBatch();
- disposers.push(this.search.setup());
- disposers.push(reaction(...this.searchChangeFx()));
- disposers.push(autorun(...this.computeSelectedItemsFx()));
+ add(this.search.setup());
+ add(reaction(...this.searchChangeFx()));
+ add(autorun(...this.computeSelectedItemsFx()));
if (this.lazyMode) {
- disposers.push(
+ add(
when(
() => this.fetchReady,
() => this.loadMore()
@@ -209,12 +214,6 @@ export class RefFilterStore extends BaseSelectStore {
}
}
- 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);
}
@@ -239,12 +238,3 @@ export class RefFilterStore extends BaseSelectStore {
}
}
}
-
-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/SearchStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/SearchStore.ts
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/stores/picker/SearchStore.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/stores/SearchStore.ts
diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/StaticSelectFilterStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/StaticSelectFilterStore.ts
similarity index 72%
rename from packages/shared/widget-plugin-filtering/src/stores/picker/StaticSelectFilterStore.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/stores/StaticSelectFilterStore.ts
index 97b27353b9..54607e0acd 100644
--- a/packages/shared/widget-plugin-filtering/src/stores/picker/StaticSelectFilterStore.ts
+++ b/packages/shared/widget-plugin-dropdown-filter/src/stores/StaticSelectFilterStore.ts
@@ -1,10 +1,10 @@
-import { ListAttributeValue } from "mendix";
+import { selectedFromCond } from "@mendix/filter-commons/condition-utils";
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { AttributeMetaData } 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 { OptionWithState } from "../typings/OptionWithState";
import { BaseSelectStore } from "./BaseSelectStore";
import { SearchStore } from "./SearchStore";
@@ -15,11 +15,11 @@ interface CustomOption {
export class StaticSelectFilterStore extends BaseSelectStore {
readonly storeType = "select";
- _attributes: ListAttributeValue[] = [];
+ _attributes: AttributeMetaData[] = [];
_customOptions: CustomOption[] = [];
search: SearchStore;
- constructor(attributes: ListAttributeValue[], initCond: FilterCondition | null) {
+ constructor(attributes: AttributeMetaData[], initCond: FilterCondition | null) {
super();
this.search = new SearchStore();
this._attributes = attributes;
@@ -34,7 +34,6 @@ export class StaticSelectFilterStore extends BaseSelectStore {
condition: computed,
setCustomOptions: action,
setDefaultSelected: action,
- updateProps: action,
fromViewState: action
});
@@ -47,14 +46,19 @@ export class StaticSelectFilterStore extends BaseSelectStore {
const selected = this.selected;
if (this._customOptions.length > 0) {
- return this._customOptions.map(opt => ({ ...opt, selected: selected.has(opt.value) }));
+ 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),
+ // TODO: fix when formatter is available
+ // caption: attr.formatter.format(value),
+ caption: stringValue,
value: stringValue,
selected: selected.has(stringValue)
};
@@ -93,8 +97,8 @@ export class StaticSelectFilterStore extends BaseSelectStore {
}
setup(): () => void {
- const [disposers, dispose] = disposeFx();
- disposers.push(this.search.setup());
+ const [add, dispose] = disposeBatch();
+ add(this.search.setup());
return dispose;
}
@@ -110,12 +114,8 @@ export class StaticSelectFilterStore extends BaseSelectStore {
}
}
- updateProps(attributes: ListAttributeValue[]): void {
- this._attributes = attributes;
- }
-
checkAttrs(): TypeError | null {
- const isValidAttr = (attr: ListAttributeValue): boolean => /Enum|Boolean/.test(attr.type);
+ const isValidAttr = (attr: AttributeMetaData): boolean => /Enum|Boolean/.test(attr.type);
if (this._attributes.every(isValidAttr)) {
return null;
@@ -150,33 +150,26 @@ export class StaticSelectFilterStore extends BaseSelectStore {
}
function getFilterCondition(
- listAttribute: ListAttributeValue | undefined,
+ listAttribute: AttributeMetaData | undefined,
selected: Set
): FilterCondition | undefined {
- if (!listAttribute || !listAttribute.filterable || selected.size === 0) {
+ if (!listAttribute) {
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);
+ if (selected.size < 1) {
+ return undefined;
}
- const [filterValue] = filters;
- return filterValue;
+ 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 {
+function universeValue(type: AttributeMetaData["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-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-filtering/src/typings/OptionWithState.ts b/packages/shared/widget-plugin-dropdown-filter/src/typings/OptionWithState.ts
similarity index 100%
rename from packages/shared/widget-plugin-filtering/src/typings/OptionWithState.ts
rename to packages/shared/widget-plugin-dropdown-filter/src/typings/OptionWithState.ts
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/src/typings/widget.ts b/packages/shared/widget-plugin-dropdown-filter/src/typings/widget.ts
new file mode 100644
index 0000000000..a839b1c9c3
--- /dev/null
+++ b/packages/shared/widget-plugin-dropdown-filter/src/typings/widget.ts
@@ -0,0 +1,10 @@
+import { DynamicValue } from "mendix";
+
+export interface FilterOptionsType {
+ caption: DynamicValue;
+ value: DynamicValue;
+}
+
+export type SelectedItemsStyleEnum = "text" | "boxes";
+
+export type SelectionMethodEnum = "checkbox" | "rowClick";
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
+ }
+}
diff --git a/packages/shared/widget-plugin-filtering/package.json b/packages/shared/widget-plugin-filtering/package.json
index 7a31cc4efa..a574c6493e 100644
--- a/packages/shared/widget-plugin-filtering/package.json
+++ b/packages/shared/widget-plugin-filtering/package.json
@@ -1,6 +1,6 @@
{
"name": "@mendix/widget-plugin-filtering",
- "version": "1.1.1",
+ "version": "2.0.0",
"description": "Filtering API plugin.",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
@@ -28,17 +28,19 @@
"dev": "tsc --watch",
"format": "prettier --write .",
"lint": "eslint src/ package.json",
- "prepare": "tsc",
"test": "jest"
},
"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:^",
"@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/__tests__/RefFilterStore.spec.ts b/packages/shared/widget-plugin-filtering/src/__tests__/RefFilterStore.spec.ts
deleted file mode 100644
index de11ebf61e..0000000000
--- a/packages/shared/widget-plugin-filtering/src/__tests__/RefFilterStore.spec.ts
+++ /dev/null
@@ -1,254 +0,0 @@
-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";
-
-describe("RefFilterStore", () => {
- afterEach(() => _resetGlobalState());
-
- describe("get options()", () => {
- let items: ObjectItem[];
- let mapItem: (item: ObjectItem) => string;
- let store: RefFilterStore;
- beforeEach(() => {
- const [a, b, c] = [obj("id3n"), obj("f8x3"), obj("932c")];
- items = [a, b, c];
- mapItem = cases([a, "Alice"], [b, "Bob"], [c, "Chuck"], [undefined, "No caption"]);
- store = new RefFilterStore(
- {
- ref: listReference(),
- datasource: list(items),
- caption: listExpression(mapItem)
- },
- null
- );
- });
-
- it("returns array of options", () => {
- expect(store.options).toMatchInlineSnapshot(`
- [
- {
- "caption": "Alice",
- "selected": false,
- "value": "obj_id3n",
- },
- {
- "caption": "Bob",
- "selected": false,
- "value": "obj_f8x3",
- },
- {
- "caption": "Chuck",
- "selected": false,
- "value": "obj_932c",
- },
- ]
- `);
- });
- });
-
- describe("get condition()", () => {
- it.todo("return filter condition using equals");
- });
-
- describe("toJSON()", () => {
- let store: RefFilterStore;
- beforeEach(() => {
- store = new RefFilterStore(
- {
- ref: listReference(),
- datasource: list(3),
- caption: listExpression(() => "[string]")
- },
- null
- );
- });
-
- it("returns state as JS array", () => {
- expect(store.toJSON()).toEqual([]);
- });
-
- it("save state with one item", () => {
- const guid = store.options[0].value;
- store.toggle(guid);
- expect(store.toJSON()).toEqual([guid]);
- });
-
- it("save state with multiple items", () => {
- const [guid1, guid2] = [store.options[0].value, store.options[1].value];
- store.toggle(guid1);
- store.toggle(guid2);
- const output = store.toJSON();
-
- expect(output).toHaveLength(2);
- expect(output).toContain(guid1);
- expect(output).toContain(guid2);
- });
- });
-
- describe("fromJSON()", () => {
- let store: RefFilterStore;
- beforeEach(() => {
- store = new RefFilterStore(
- {
- ref: listReference(),
- datasource: list([obj("id3n"), obj("f8x3"), obj("932c")]),
- caption: listExpression(() => "[string]")
- },
- null
- );
- });
-
- it("restore state from JS array", () => {
- const output: Array = [];
- autorun(() => output.push(store.options));
-
- // Check that every option is unselected
- expect(output[0].map(option => option.selected)).toEqual([false, false, false]);
- // Restore state
- store.fromJSON(["obj_id3n"]);
- // Check that first option is selected
- expect(output[1].map(option => option.selected)).toEqual([true, false, false]);
- });
-
- it("overrides any previous state", () => {
- const output: Array = [];
-
- // Select each option
- store.options.forEach(option => {
- store.toggle(option.value);
- });
- // Start observing
- autorun(() => output.push(store.options));
- // Check the state
- expect(output[0].map(option => option.selected)).toEqual([true, true, true]);
- store.fromJSON(["obj_932c"]);
- expect(output[1].map(option => option.selected)).toEqual([false, false, true]);
- });
-
- it("should not change state if json is null", () => {
- const state = store.toJSON();
- store.fromJSON(null);
- expect(store.toJSON()).toEqual(state);
- });
- });
-
- describe("toggle()", () => {
- let store: RefFilterStore;
- beforeEach(() => {
- store = new RefFilterStore(
- {
- ref: listReference(),
- datasource: list([obj("id3n"), obj("f8x3"), obj("932c")]),
- caption: listExpression(() => "[string]")
- },
- null
- );
- });
-
- it("compute new options", () => {
- const output: Array = [];
- const [x, y] = store.options;
-
- autorun(() => output.push(store.options));
- expect(output[0].map(opt => opt.selected)).toEqual([false, false, false]);
-
- store.toggle(x.value);
- store.toggle(y.value);
- expect(output[2].map(opt => opt.selected)).toEqual([true, true, false]);
-
- store.toggle(x.value);
- expect(output[3].map(opt => opt.selected)).toEqual([false, true, false]);
- });
- });
-
- describe("with 'fetchOptionsLazy' flag", () => {
- it("should set limit to 0 on options datasource", () => {
- const datasource = list.loading();
- const props = {
- ref: listReference(),
- datasource,
- caption: listExpression(() => "[string]"),
- fetchOptionsLazy: true
- };
-
- new RefFilterStore(props, null);
-
- expect(datasource.setLimit).toHaveBeenLastCalledWith(0);
- });
-
- it("should not change limit if flag is false", () => {
- const datasource = list.loading();
- const props = {
- ref: listReference(),
- datasource,
- caption: listExpression(() => "[string]"),
- fetchOptionsLazy: false
- };
-
- new RefFilterStore(props, null);
-
- expect(datasource.setLimit).not.toHaveBeenCalled();
- });
- });
-
- describe("when datasource changed", () => {
- it("compute new options", () => {
- const a = obj("id3n");
- const props = {
- ref: listReference(),
- datasource: list.loading(),
- caption: listExpression(cases([a, "Alice"], [undefined, "No caption"]))
- };
- const store = new RefFilterStore(props, null);
- const output: any[] = [];
- autorun(() => output.push(store.options));
-
- expect(output[0]).toEqual([]);
- store.updateProps({ ...props, datasource: list([a]) });
- expect(output[1]).toEqual([
- {
- caption: "Alice",
- selected: false,
- value: "obj_id3n"
- }
- ]);
- });
- });
-
- describe("json data filtering", () => {
- const savedJson = ["obj_xx", "obj_xiii", "obj_deleted", "obj_yy", "obj_unknown"];
- let a: ObjectItem;
- let b: ObjectItem;
- let props: Omit;
- beforeEach(() => {
- a = obj("xx");
- b = obj("yy");
- props = {
- ref: listReference(),
- caption: listExpression(() => "[string]")
- };
- });
-
- it("skip filtering if has just part of the list", () => {
- const store = new RefFilterStore({ ...props, datasource: list.loading() }, null);
- // Restore state
- store.fromJSON(savedJson);
- // Check state
- expect(store.toJSON()).toEqual(savedJson);
- // Update with partial data
- const datasource = new ListValueBuilder().withItems([a, b]).withHasMore(true).build();
- store.updateProps({ ...props, datasource });
- // Check that state still has unknown GUIDs
- expect(store.toJSON()).toEqual(savedJson);
- });
-
- it("allows to restore any state if datasource is loading", () => {
- const store = new RefFilterStore({ ...props, datasource: list.loading() }, null);
- // Restore state
- store.fromJSON(savedJson);
- // Check state
- expect(store.toJSON()).toEqual(savedJson);
- });
- });
-});
diff --git a/packages/shared/widget-plugin-filtering/src/context.ts b/packages/shared/widget-plugin-filtering/src/context.ts
index 2a44be0ed0..d9aeca4867 100644
--- a/packages/shared/widget-plugin-filtering/src/context.ts
+++ b/packages/shared/widget-plugin-filtering/src/context.ts
@@ -1,58 +1,48 @@
+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.js";
-import { Result, error, value } from "./result-meta.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 { InputFilterInterface } from "./typings/InputFilterInterface";
+import { ObservableFilterHost } from "./typings/ObservableFilterHost";
-export interface FilterAPIv2 {
- version: 2;
+export interface FilterAPI {
+ version: 3;
parentChannelName: string;
- provider: Result;
+ provider: Result;
+ filterObserver: ObservableFilterHost;
+ sharedInitFilter: Array;
}
-/** @deprecated */
-export enum FilterType {
- STRING = "string",
- NUMBER = "number",
- ENUMERATION = "enum",
- DATE = "date"
-}
-
-export type FilterStoreProvider = DirectProvider | KeyProvider | LegacyProvider;
-
-export type FilterStore = InputFilterInterface | PickerFilterStore;
+export type FilterStore = InputFilterInterface | StaticSelectFilterStore;
interface DirectProvider {
type: "direct";
store: FilterStore | null;
}
-export interface KeyProvider {
- type: "key-value";
- get: (key: string) => FilterStore | null;
+interface ProviderStub {
+ type: "stub";
+ hint: "No filter store available";
}
-/** @deprecated */
-export interface LegacyProvider {
- type: "legacy";
- get: (type: FilterType) => FilterStore | null;
-}
+export const PROVIDER_STUB = Object.freeze({ type: "stub", hint: "No filter store available" } as const);
-type Context_v2 = Context;
+type FilterAPIContext = Context;
const CONTEXT_OBJECT_PATH = "com.mendix.widgets.web.filterable.filterContext.v2" as const;
declare global {
interface Window {
- [CONTEXT_OBJECT_PATH]: Context_v2 | undefined;
+ [CONTEXT_OBJECT_PATH]: FilterAPIContext | undefined;
}
}
-export function getGlobalFilterContextObject(): Context_v2 {
- return (window[CONTEXT_OBJECT_PATH] ??= createContext(null));
+export function getGlobalFilterContextObject(): FilterAPIContext {
+ return (window[CONTEXT_OBJECT_PATH] ??= createContext(null));
}
-export function useFilterContextValue(): Result {
+export function useFilterAPI(): Result {
const context = getGlobalFilterContextObject();
const contextValue = useContext(context);
@@ -63,15 +53,19 @@ export function useFilterContextValue(): Result {
return value(contextValue);
}
-export function getFilterStore(provider: FilterStoreProvider, legacyType: FilterType, key: string): FilterStore | null {
- switch (provider.type) {
- case "direct":
- return provider.store;
- case "key-value":
- return provider.get(key);
- case "legacy":
- return provider.get(legacyType);
- default:
- return null;
- }
+/** @deprecated This hook is renamed, use `useFilterAPI` instead. */
+export const useFilterContextValue = useFilterAPI;
+
+export function createContextWithStub(options: {
+ filterObserver: ObservableFilterHost;
+ parentChannelName: string;
+ sharedInitFilter: Array;
+}): FilterAPI {
+ return {
+ version: 3,
+ parentChannelName: options.parentChannelName,
+ provider: value(PROVIDER_STUB),
+ filterObserver: options.filterObserver,
+ sharedInitFilter: options.sharedInitFilter
+ };
}
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 9c0391aa8c..f46a8abafc 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/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/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 f1293bdb27..799d47047a 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 16c5a985e4..6a94b93fe0 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
new file mode 100644
index 0000000000..41299b12e1
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/BaseStoreProvider.ts
@@ -0,0 +1,32 @@
+import { isAnd, isTag } from "@mendix/filter-commons/condition-utils";
+import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch";
+import { ISetupable } from "@mendix/widget-plugin-mobx-kit/setupable";
+import { FilterCondition } from "mendix/filters";
+import { FilterAPI } from "../context";
+import { Filter } from "../typings/ObservableFilterHost";
+
+export abstract class BaseStoreProvider implements ISetupable {
+ protected abstract _store: S;
+ protected abstract filterAPI: FilterAPI;
+ abstract readonly dataKey: string;
+
+ protected findInitFilter(conditions: Array, key: string): FilterCondition | null {
+ for (const cond of conditions) {
+ if (cond && isAnd(cond)) {
+ const [tag, initFilter] = cond.args;
+ if (isTag(tag) && tag.arg1.value === key) {
+ return initFilter;
+ }
+ }
+ }
+ return null;
+ }
+
+ setup(): () => void {
+ const [add, disposeAll] = disposeBatch();
+ this.filterAPI.filterObserver.observe(this.dataKey, this._store);
+ add(() => this.filterAPI.filterObserver.unobserve(this.dataKey));
+ add(this._store.setup?.());
+ return disposeAll;
+ }
+}
diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/DateStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/DateStoreProvider.ts
new file mode 100644
index 0000000000..65cfe580d5
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/DateStoreProvider.ts
@@ -0,0 +1,25 @@
+import { FilterAPI } from "../context";
+import { DateInputFilterStore } from "../stores/input/DateInputFilterStore";
+import { Date_InputFilterInterface } from "../typings/InputFilterInterface";
+import { BaseStoreProvider } from "./BaseStoreProvider";
+import { FilterSpec } from "./typings";
+
+export class DateStoreProvider extends BaseStoreProvider {
+ protected _store: DateInputFilterStore;
+ protected filterAPI: FilterAPI;
+ readonly dataKey: string;
+
+ constructor(filterAPI: FilterAPI, spec: FilterSpec) {
+ super();
+ this.filterAPI = filterAPI;
+ this.dataKey = spec.dataKey;
+ this._store = new DateInputFilterStore(
+ spec.attributes,
+ this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey)
+ );
+ }
+
+ get store(): Date_InputFilterInterface {
+ return this._store;
+ }
+}
diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/EnumStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/EnumStoreProvider.ts
new file mode 100644
index 0000000000..b0ace1ff31
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/EnumStoreProvider.ts
@@ -0,0 +1,24 @@
+import { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore";
+import { FilterAPI } from "../context";
+import { BaseStoreProvider } from "./BaseStoreProvider";
+import { FilterSpec } from "./typings";
+
+export class EnumStoreProvider extends BaseStoreProvider {
+ protected _store: StaticSelectFilterStore;
+ protected filterAPI: FilterAPI;
+ readonly dataKey: string;
+
+ constructor(filterAPI: FilterAPI, spec: FilterSpec) {
+ super();
+ this.filterAPI = filterAPI;
+ this.dataKey = spec.dataKey;
+ this._store = new StaticSelectFilterStore(
+ spec.attributes,
+ this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey)
+ );
+ }
+
+ get store(): StaticSelectFilterStore {
+ return this._store;
+ }
+}
diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/NumberStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/NumberStoreProvider.ts
new file mode 100644
index 0000000000..da7f77e639
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/NumberStoreProvider.ts
@@ -0,0 +1,25 @@
+import { FilterAPI } from "../context";
+import { NumberInputFilterStore } from "../stores/input/NumberInputFilterStore";
+import { Number_InputFilterInterface } from "../typings/InputFilterInterface";
+import { BaseStoreProvider } from "./BaseStoreProvider";
+import { FilterSpec } from "./typings";
+
+export class NumberStoreProvider extends BaseStoreProvider {
+ protected _store: NumberInputFilterStore;
+ protected filterAPI: FilterAPI;
+ readonly dataKey: string;
+
+ constructor(filterAPI: FilterAPI, spec: FilterSpec) {
+ super();
+ this.filterAPI = filterAPI;
+ this.dataKey = spec.dataKey;
+ this._store = new NumberInputFilterStore(
+ spec.attributes,
+ this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey)
+ );
+ }
+
+ get store(): Number_InputFilterInterface {
+ return this._store;
+ }
+}
diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/StringStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/StringStoreProvider.ts
new file mode 100644
index 0000000000..b1270ac3e3
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/StringStoreProvider.ts
@@ -0,0 +1,25 @@
+import { FilterAPI } from "../context";
+import { StringInputFilterStore } from "../stores/input/StringInputFilterStore";
+import { String_InputFilterInterface } from "../typings/InputFilterInterface";
+import { BaseStoreProvider } from "./BaseStoreProvider";
+import { FilterSpec } from "./typings";
+
+export class StringStoreProvider extends BaseStoreProvider {
+ protected _store: StringInputFilterStore;
+ protected filterAPI: FilterAPI;
+ readonly dataKey: string;
+
+ constructor(filterAPI: FilterAPI, spec: FilterSpec) {
+ super();
+ this.filterAPI = filterAPI;
+ this.dataKey = spec.dataKey;
+ this._store = new StringInputFilterStore(
+ spec.attributes,
+ this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey)
+ );
+ }
+
+ get store(): String_InputFilterInterface {
+ return this._store;
+ }
+}
diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/typings.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/typings.ts
new file mode 100644
index 0000000000..5cf80885d2
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/typings.ts
@@ -0,0 +1,8 @@
+import { AttributeMetaData, EditableValue } from "mendix";
+
+type AttributeValue_2 = EditableValue["value"];
+
+export interface FilterSpec {
+ attributes: Array>;
+ dataKey: string;
+}
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts b/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts
index d2391337f3..6805b94642 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts
@@ -1,6 +1,6 @@
import { useRef } from "react";
-import { FilterType, getFilterStore, useFilterContextValue } from "../context";
-import { APIError, EKEYMISSING, EMISSINGSTORE, EStoreTypeMisMatch } from "../errors";
+import { useFilterAPI } from "../context";
+import { APIError, EMISSINGSTORE, EStoreTypeMisMatch } from "../errors";
import { error, Result, value } from "../result-meta";
import { isDateFilter } from "../stores/input/store-utils";
import { Date_InputFilterInterface } from "../typings/InputFilterInterface";
@@ -10,8 +10,8 @@ export interface Date_FilterAPIv2 {
parentChannelName?: string;
}
-export function useDateFilterAPI(key: string): Result {
- const ctx = useFilterContextValue();
+export function useDateFilterAPI(): Result {
+ const ctx = useFilterAPI();
const dateAPI = useRef();
if (ctx.hasError) {
@@ -24,11 +24,7 @@ export function useDateFilterAPI(key: string): Result {
- const ctx = useFilterContextValue();
+export function useNumberFilterAPI(): Result {
+ const ctx = useFilterAPI();
const numAPI = useRef();
if (ctx.hasError) {
@@ -24,11 +24,7 @@ export function useNumberFilterAPI(key: string): Result,
- 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
deleted file mode 100644
index b3a675806e..0000000000
--- a/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-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;
- parentChannelName?: string;
-}
-
-interface Props {
- filterable: boolean;
-}
-
-export function useSelectFilterAPI({ filterable }: Props): Result {
- const ctx = useFilterContextValue();
- const slctAPI = useRef();
-
- if (ctx.hasError) {
- return error(ctx.error);
- }
-
- const api = ctx.value;
-
- if (api.provider.hasError) {
- return error(api.provider.error);
- }
-
- const store = getFilterStore(api.provider.value, FilterType.ENUMERATION, "");
-
- if (store === null) {
- return error(EMISSINGSTORE);
- }
-
- if (store.storeType !== "select" && store.storeType !== "refselect") {
- return error(EStoreTypeMisMatch("dropdown filter", store.arg1.type));
- }
-
- if (store.storeType === "refselect") {
- const configurationConflict = filterable && store.optionsFilterable === false;
- if (configurationConflict) {
- return error(OPTIONS_NOT_FILTERABLE);
- }
- }
-
- return value((slctAPI.current ??= { filterStore: store, parentChannelName: api.parentChannelName }));
-}
diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useSetup.ts b/packages/shared/widget-plugin-filtering/src/helpers/useSetup.ts
deleted file mode 100644
index c21157a027..0000000000
--- a/packages/shared/widget-plugin-filtering/src/helpers/useSetup.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { useEffect, useState } from "react";
-
-export interface Setupable {
- setup(): void | (() => 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 377ff90477..9e5b49dcd7 100644
--- a/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterAPI.ts
+++ b/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterAPI.ts
@@ -1,6 +1,6 @@
import { useRef } from "react";
-import { FilterType, getFilterStore, useFilterContextValue } from "../context";
-import { APIError, EKEYMISSING, EMISSINGSTORE, EStoreTypeMisMatch } from "../errors";
+import { useFilterAPI } from "../context";
+import { APIError, EMISSINGSTORE, EStoreTypeMisMatch } from "../errors";
import { error, Result, value } from "../result-meta";
import { isStringFilter } from "../stores/input/store-utils";
import { String_InputFilterInterface } from "../typings/InputFilterInterface";
@@ -10,8 +10,8 @@ export interface String_FilterAPIv2 {
parentChannelName?: string;
}
-export function useStringFilterAPI(key: string): Result {
- const ctx = useFilterContextValue();
+export function useStringFilterAPI(): Result {
+ const ctx = useFilterAPI();
const strAPI = useRef();
if (ctx.hasError) {
@@ -24,11 +24,7 @@ export function useStringFilterAPI(key: string): Result,
- 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/helpers/withFilterAPI.tsx b/packages/shared/widget-plugin-filtering/src/helpers/withFilterAPI.tsx
new file mode 100644
index 0000000000..cc28fc6f27
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/helpers/withFilterAPI.tsx
@@ -0,0 +1,14 @@
+import { FC, createElement } from "react";
+import { FilterAPI, useFilterAPI } from "../context";
+import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
+
+export function withFilterAPI(Component: FC
): FC
{
+ return function FilterAPIProvider(props) {
+ const filterAPI = useFilterAPI();
+
+ if (filterAPI.hasError) {
+ return {filterAPI.error.message} ;
+ }
+ return ;
+ };
+}
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/providers/LegacyPv.ts b/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts
deleted file mode 100644
index d9cb4f3b14..0000000000
--- a/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-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 { 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;
- [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/CustomFilterHost.ts b/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts
new file mode 100644
index 0000000000..2732aaf64a
--- /dev/null
+++ b/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts
@@ -0,0 +1,49 @@
+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 { Filter, ObservableFilterHost } from "../../typings/ObservableFilterHost";
+
+export class CustomFilterHost implements ObservableFilterHost {
+ private filters: Map void]> = new Map();
+ private settingsBuffer: FiltersSettingsMap