diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.editorConfig.ts b/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.editorConfig.ts index 134c00a117..27bcfee192 100644 --- a/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.editorConfig.ts +++ b/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.editorConfig.ts @@ -1,3 +1,4 @@ +import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools"; import { betweenIcon, betweenIconDark, @@ -23,22 +24,23 @@ import { import { ContainerProps, ImageProps, + structurePreviewPalette, StructurePreviewProps, - text, - structurePreviewPalette + text } from "@mendix/widget-plugin-platform/preview/structure-preview-api"; -import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools"; import { DatagridDateFilterPreviewProps, DefaultFilterEnum } from "../typings/DatagridDateFilterProps"; -export function getProperties( - values: DatagridDateFilterPreviewProps, - defaultProperties: Properties, - platform: "web" | "desktop" -): Properties { +export function getProperties(values: DatagridDateFilterPreviewProps, defaultProperties: Properties): Properties { if (!values.adjustable) { hidePropertyIn(defaultProperties, values, "screenReaderButtonCaption"); } + + if (values.attrChoice === "auto") { + hidePropertyIn(defaultProperties, values, "attributes"); + hidePropertyIn(defaultProperties, {} as { linkedDs: unknown }, "linkedDs"); + } + if (values.defaultFilter !== "between") { hidePropertiesIn(defaultProperties, values, [ "defaultStartDate", @@ -49,13 +51,7 @@ export function getProperties( } else { hidePropertiesIn(defaultProperties, values, ["defaultValue", "valueAttribute"]); } - if (platform === "web") { - if (!values.advanced) { - hidePropertiesIn(defaultProperties, values, ["onChange", "valueAttribute"]); - } - } else { - hidePropertyIn(defaultProperties, values, "advanced"); - } + return defaultProperties; } diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.tsx b/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.tsx index d3e977451d..3b5ec9cc5d 100644 --- a/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.tsx +++ b/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.tsx @@ -4,10 +4,18 @@ import { DatagridDateFilterContainerProps } from "../typings/DatagridDateFilterP import { Container } from "./components/DateFilterContainer"; import { withDateFilterAPI } from "./hocs/withDateFilterAPI"; import { isLoadingDefaultValues } from "./utils/widget-utils"; +import { withDateLinkedAttributes } from "./hocs/withDateLinkedAttributes"; const container = withPreloader(Container, isLoadingDefaultValues); -const Widget = withDateFilterAPI(container); +const FilterAuto = withDateFilterAPI(container); +const FilterLinked = withDateLinkedAttributes(container); export default function DatagridDateFilter(props: DatagridDateFilterContainerProps): ReactElement | null { - return ; + const isAuto = props.attrChoice === "auto"; + + if (isAuto) { + return ; + } + + return ; } diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.xml b/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.xml index 39ed04bf46..bc9800a53f 100644 --- a/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.xml +++ b/packages/pluggableWidgets/datagrid-date-filter-web/src/DatagridDateFilter.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-date-filter-web/src/components/__tests__/DatagridDateFilter.spec.tsx b/packages/pluggableWidgets/datagrid-date-filter-web/src/components/__tests__/DatagridDateFilter.spec.tsx index 6cfbed5d16..3deebdf5fe 100644 --- a/packages/pluggableWidgets/datagrid-date-filter-web/src/components/__tests__/DatagridDateFilter.spec.tsx +++ b/packages/pluggableWidgets/datagrid-date-filter-web/src/components/__tests__/DatagridDateFilter.spec.tsx @@ -1,8 +1,8 @@ import "@testing-library/jest-dom"; -import { FilterAPIv2 } from "@mendix/widget-plugin-filtering/context"; +import { FilterAPI } from "@mendix/widget-plugin-filtering/context"; import { HeaderFiltersStore, - HeaderFiltersStoreProps + HeaderFiltersStoreSpec } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore"; import { actionValue, @@ -15,11 +15,7 @@ import { createContext, createElement } from "react"; import DatagridDateFilter from "../../DatagridDateFilter"; import { DatagridDateFilterContainerProps } from "../../../typings/DatagridDateFilterProps"; import { MXGlobalObject, MXSessionConfig } from "../../../typings/global"; - -interface StaticInfo { - name: string; - filtersChannelName: string; -} +import { FilterObserver } from "@mendix/widget-plugin-filtering/typings/FilterObserver"; function createMXObjectMock( code: string, @@ -54,13 +50,17 @@ const commonProps: DatagridDateFilterContainerProps = { advanced: false }; -const headerFilterStoreInfo: StaticInfo = { - name: commonProps.name, - filtersChannelName: "" -}; - const mxObject = createMXObjectMock("en_US", "en-US"); +const mockSpec = (spec: Partial): HeaderFiltersStoreSpec => ({ + filterList: [], + filterChannelName: "datagrid/1", + headerInitFilter: [], + sharedInitFilter: [], + customFilterHost: {} as FilterObserver, + ...spec +}); + describe("Date Filter", () => { describe("with single instance", () => { afterEach(() => { @@ -69,13 +69,13 @@ describe("Date Filter", () => { describe("with single attribute", () => { beforeEach(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder().withType("DateTime").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 ); (window as any).mx = mxObject; @@ -144,7 +144,7 @@ describe("Date Filter", () => { describe("with double attributes", () => { beforeAll(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder() @@ -161,9 +161,9 @@ describe("Date 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 ); (window as any).mx = mxObject; @@ -184,13 +184,13 @@ describe("Date Filter", () => { describe("with wrong attribute's type", () => { beforeAll(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder().withType("Decimal").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 ); (window as any).mx = mxObject; @@ -211,7 +211,7 @@ describe("Date Filter", () => { describe("with wrong multiple attributes' types", () => { beforeAll(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder() @@ -228,9 +228,9 @@ describe("Date 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 ); (window as any).mx = mxObject; @@ -267,13 +267,13 @@ describe("Date Filter", () => { describe("with multiple instances", () => { beforeAll(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder().withType("DateTime").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 ); (window as any).mx = mxObject; @@ -296,13 +296,13 @@ describe("Date Filter", () => { describe("with session config", () => { beforeEach(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder().withType("DateTime").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 ); }); diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/src/hocs/withDateFilterAPI.tsx b/packages/pluggableWidgets/datagrid-date-filter-web/src/hocs/withDateFilterAPI.tsx index 38b85272f1..38f9c77860 100644 --- a/packages/pluggableWidgets/datagrid-date-filter-web/src/hocs/withDateFilterAPI.tsx +++ b/packages/pluggableWidgets/datagrid-date-filter-web/src/hocs/withDateFilterAPI.tsx @@ -6,7 +6,7 @@ export function withDateFilterAPI

( Component: (props: P & Date_FilterAPIv2) => React.ReactElement ): (props: P) => React.ReactElement { return function FilterAPIProvider(props) { - const api = useDateFilterAPI(""); + const api = useDateFilterAPI(); if (api.hasError) { return {api.error.message}; diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/src/hocs/withDateLinkedAttributes.tsx b/packages/pluggableWidgets/datagrid-date-filter-web/src/hocs/withDateLinkedAttributes.tsx new file mode 100644 index 0000000000..abd3175921 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-date-filter-web/src/hocs/withDateLinkedAttributes.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 { Date_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 { DateStoreProvider } from "@mendix/widget-plugin-filtering/custom-filter-api/DateStoreProvider"; +import { ISetupable } from "@mendix/widget-plugin-mobx-kit/setupable"; + +interface RequiredProps { + attributes: Array<{ + attribute: AttributeMetaData; + }>; + name: string; +} + +interface StoreProvider extends ISetupable { + store: Date_InputFilterInterface; +} + +type Component

= (props: P) => React.ReactElement; + +export function withDateLinkedAttributes

( + 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: Date_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 DateStoreProvider(filterAPI.value, { + attributes: props.attributes.map(obj => obj.attribute), + dataKey: props.name + }), + channel: filterAPI.value.parentChannelName + }); + }); +} diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/typings/DatagridDateFilterProps.d.ts b/packages/pluggableWidgets/datagrid-date-filter-web/typings/DatagridDateFilterProps.d.ts index 2855ea37b7..970ae14f7d 100644 --- a/packages/pluggableWidgets/datagrid-date-filter-web/typings/DatagridDateFilterProps.d.ts +++ b/packages/pluggableWidgets/datagrid-date-filter-web/typings/DatagridDateFilterProps.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 = "between" | "greater" | "greaterEqual" | "equal" | "notEqual" | "smaller" | "smallerEqual" | "empty" | "notEmpty"; +export interface AttributesPreviewType { + attribute: string; +} + export interface DatagridDateFilterContainerProps { name: string; class: string; style?: CSSProperties; tabIndex?: number; - advanced: boolean; + attrChoice: AttrChoiceEnum; + attributes: AttributesType[]; defaultValue?: DynamicValue; defaultStartDate?: DynamicValue; defaultEndDate?: DynamicValue; @@ -40,7 +51,8 @@ export interface DatagridDateFilterPreviewProps { readOnly: boolean; renderMode: "design" | "xray" | "structure"; translate: (text: string) => string; - advanced: boolean; + attrChoice: AttrChoiceEnum; + attributes: AttributesPreviewType[]; defaultValue: string; defaultStartDate: string; defaultEndDate: string; diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/DataGridDropdownFilter.spec.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/DataGridDropdownFilter.spec.tsx index 2306dd4a38..784ade5f13 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/DataGridDropdownFilter.spec.tsx +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/DataGridDropdownFilter.spec.tsx @@ -1,18 +1,14 @@ import "@testing-library/jest-dom"; -import { FilterAPIv2 } from "@mendix/widget-plugin-filtering/context"; +import { FilterAPI } from "@mendix/widget-plugin-filtering/context"; import { HeaderFiltersStore, - HeaderFiltersStoreProps + HeaderFiltersStoreSpec } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore"; import { dynamicValue, ListAttributeValueBuilder } from "@mendix/widget-plugin-test-utils"; import { createContext, createElement } from "react"; import DatagridDropdownFilter from "../../DatagridDropdownFilter"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; - -interface StaticInfo { - name: string; - filtersChannelName: string; -} +import { FilterObserver } from "@mendix/widget-plugin-filtering/typings/FilterObserver"; const commonProps = { class: "filter-custom-class", @@ -26,10 +22,14 @@ const commonProps = { selectedItemsStyle: "text" as const }; -const headerFilterStoreInfo: StaticInfo = { - name: commonProps.name, - filtersChannelName: "" -}; +const mockSpec = (spec: Partial): HeaderFiltersStoreSpec => ({ + filterList: [], + filterChannelName: "datagrid/1", + headerInitFilter: [], + sharedInitFilter: [], + customFilterHost: {} as FilterObserver, + ...spec +}); const consoleError = global.console.error; jest.spyOn(global.console, "error").mockImplementation((...args: any[]) => { @@ -50,7 +50,7 @@ describe("Dropdown Filter", () => { describe("with single attribute", () => { function mockCtx(universe: string[]): void { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder() @@ -64,9 +64,9 @@ describe("Dropdown 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 ); } @@ -207,7 +207,7 @@ describe("Dropdown Filter", () => { describe("with multiple attributes", () => { beforeAll(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder() @@ -234,9 +234,9 @@ describe("Dropdown 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 ); }); @@ -267,15 +267,15 @@ describe("Dropdown Filter", () => { describe("with wrong attribute's type", () => { beforeAll(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder().withType("String").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 ); }); @@ -297,7 +297,7 @@ describe("Dropdown Filter", () => { describe("with wrong multiple attributes' types", () => { beforeAll(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder() @@ -314,9 +314,9 @@ describe("Dropdown 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 ); }); @@ -354,7 +354,7 @@ describe("Dropdown Filter", () => { describe("with invalid values", () => { beforeAll(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder() @@ -364,9 +364,9 @@ describe("Dropdown 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 ); }); @@ -392,7 +392,7 @@ describe("Dropdown Filter", () => { describe("with multiple invalid values", () => { beforeAll(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder() @@ -409,9 +409,9 @@ describe("Dropdown 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 ); }); @@ -442,7 +442,7 @@ describe("Dropdown Filter", () => { describe("with multiple instances", () => { beforeAll(() => { - const props: HeaderFiltersStoreProps = { + const spec = mockSpec({ filterList: [ { filter: new ListAttributeValueBuilder() @@ -456,9 +456,9 @@ describe("Dropdown 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/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 c97e9d1525..92767d5ff2 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/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 0d60f0e07c..64a2ac5f76 100644 --- a/packages/pluggableWidgets/datagrid-text-filter-web/package.json +++ b/packages/pluggableWidgets/datagrid-text-filter-web/package.json @@ -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/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/src/controllers/StateSyncController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts similarity index 65% rename from packages/pluggableWidgets/datagrid-web/src/controllers/StateSyncController.ts rename to packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts index 397c73e3c6..9a79deb4e0 100644 --- a/packages/pluggableWidgets/datagrid-web/src/controllers/StateSyncController.ts +++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts @@ -12,34 +12,41 @@ interface Columns { sortInstructions: SortInstruction[] | undefined; } -interface Header { +interface FiltersInput { conditions: Array; } -type StateSyncControllerSpec = { +type DatasourceParamsControllerSpec = { query: QueryController; columns: Columns; - header: Header; + header: FiltersInput; + customFilters: FiltersInput; }; -export class StateSyncController implements ReactiveController { +export class DatasourceParamsController implements ReactiveController { private columns: Columns; - private header: Header; + private header: FiltersInput; 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, header, customFilters } = this; - return and(compactArray(columns.conditions), compactArray(header.conditions)); + return and( + compactArray(columns.conditions), + compactArray(header.conditions), + compactArray(customFilters.conditions) + ); } private get derivedSortOrder(): SortInstruction[] | undefined { @@ -68,17 +75,22 @@ export class StateSyncController implements ReactiveController { static unzipFilter( filter?: FilterCondition - ): [columns: Array, header: Array] { + ): [ + columns: Array, + header: Array, + sharedFilter: Array + ] { if (!filter) { - return [[], []]; + return [[], [], []]; } if (!isAnd(filter)) { - return [[], []]; + return [[], [], []]; } - if (filter.args.length !== 2) { - return [[], []]; + if (filter.args.length !== 3) { + return [[], [], []]; } - const [columns, header] = filter.args; - return [fromCompactArray(columns), fromCompactArray(header)]; + + const [columns, header, shared] = filter.args; + return [fromCompactArray(columns), fromCompactArray(header), fromCompactArray(shared)]; } } diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts index efc8dd67ad..ae76ecf01b 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts @@ -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( diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts index 21c8024aca..c9a062b6ac 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts @@ -1,5 +1,5 @@ import { error, Result, value } from "@mendix/widget-plugin-filtering/result-meta"; -import { HeaderFiltersStore } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore"; +import { FilterObserver } from "@mendix/widget-plugin-filtering/typings/FilterObserver"; import { FiltersSettingsMap } from "@mendix/widget-plugin-filtering/typings/settings"; import { action, comparer, computed, IReactionDisposer, makeObservable, reaction } from "mobx"; import { DatagridContainerProps } from "../../../typings/DatagridProps"; @@ -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: FilterObserver ) { 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..1e03c6b941 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts @@ -1,3 +1,4 @@ +import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost"; import { HeaderFiltersStore } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore"; import { BaseControllerHost } from "@mendix/widget-plugin-mobx-kit/BaseControllerHost"; import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; @@ -6,10 +7,10 @@ import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate- import { autorun, computed } 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"; @@ -37,24 +38,37 @@ export class RootGridStore extends BaseControllerHost { super(); const { props } = gate; - const [columnsViewState, headerViewState] = StateSyncController.unzipFilter(props.datasource.filter); + const [columnsInitFilter, headerInitFilter, 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); + const columns = (this.columnsStore = new ColumnGroupStore(props, this.staticInfo, columnsInitFilter, { + customFilterHost, + sharedInitFilter + })); + const header = (this.headerFiltersStore = new HeaderFiltersStore({ + filterList: props.filterList, + filterChannelName: this.staticInfo.filtersChannelName, + headerInitFilter, + sharedInitFilter, + customFilterHost + })); + this.settingsStore = new GridPersonalizationStore(props, this.columnsStore, customFilterHost); this.paginationCtrl = new PaginationController(this, { gate, query }); this.exportProgressCtrl = exportCtrl; - new StateSyncController(this, { + new DatasourceParamsController(this, { query, columns, - header + header, + customFilters: customFilterHost }); new RefreshController(this, { @@ -73,7 +87,7 @@ export class RootGridStore extends BaseControllerHost { const [add, disposeAll] = disposeBatch(); add(super.setup()); add(this.columnsStore.setup()); - add(this.headerFiltersStore.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..cf930ae8a3 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx @@ -1,4 +1,4 @@ -import { FilterAPIv2, getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context"; +import { FilterAPI, 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 { InputFilterStore, attrgroupFilterStore } from "@mendix/widget-plugin-filtering/stores/input/store-utils"; @@ -12,6 +12,7 @@ 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"; +import { FilterObserver } from "@mendix/widget-plugin-filtering/typings/FilterObserver"; export interface IColumnFilterStore { renderFilterWidgets(): ReactNode; } @@ -23,9 +24,11 @@ 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); @@ -92,14 +95,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 }; } @@ -132,3 +137,8 @@ const isListAttributeValue = ( const errorMessage = (propName: string): string => `Can't map ColumnsType to AssociationProperties: ${propName} is undefined`; + +export interface ObserverBag { + customFilterHost: FilterObserver; + sharedInitFilter: Array; +} diff --git a/packages/pluggableWidgets/datagrid-web/src/typings/personalization-settings.ts b/packages/pluggableWidgets/datagrid-web/src/typings/personalization-settings.ts index f51a4f9b4e..6c64391741 100644 --- a/packages/pluggableWidgets/datagrid-web/src/typings/personalization-settings.ts +++ b/packages/pluggableWidgets/datagrid-web/src/typings/personalization-settings.ts @@ -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/dropdown-sort-web/src/components/__test__/DropdownSort.spec.tsx b/packages/pluggableWidgets/dropdown-sort-web/src/components/__test__/DropdownSort.spec.tsx index 099b39208f..bd7ead1a62 100644 --- a/packages/pluggableWidgets/dropdown-sort-web/src/components/__test__/DropdownSort.spec.tsx +++ b/packages/pluggableWidgets/dropdown-sort-web/src/components/__test__/DropdownSort.spec.tsx @@ -1,7 +1,7 @@ -import { FilterAPIv2 } from "@mendix/widget-plugin-filtering/context"; +import { FilterAPI } from "@mendix/widget-plugin-filtering/context"; import { HeaderFiltersStore, - HeaderFiltersStoreProps + HeaderFiltersStoreSpec } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore"; import { SortAPI } from "@mendix/widget-plugin-sorting/context"; import { SortAPIProvider, SortListType } from "@mendix/widget-plugin-sorting/providers/SortAPIProvider"; @@ -12,6 +12,7 @@ import { ListValue } from "mendix"; import { createContext, createElement } from "react"; import { DropdownSortContainerProps } from "../../../typings/DropdownSortProps"; import { DropdownSort } from "../../DropdownSort"; +import { FilterObserver } from "@mendix/widget-plugin-filtering/typings/FilterObserver"; const commonProps: DropdownSortContainerProps = { class: "filter-custom-class", @@ -19,22 +20,16 @@ const commonProps: DropdownSortContainerProps = { name: "filter-test" }; -interface StaticInfo { - name: string; - filtersChannelName: string; -} - -const headerFilterStoreInfo: StaticInfo = { - name: commonProps.name, - filtersChannelName: "" +const spec: HeaderFiltersStoreSpec = { + filterList: [], + sharedInitFilter: [], + headerInitFilter: [], + filterChannelName: "datagrid", + customFilterHost: {} as FilterObserver }; -// CONTEXT -const props: HeaderFiltersStoreProps = { - filterList: [] -}; -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/gallery-web/src/stores/RootGalleryStore.ts b/packages/pluggableWidgets/gallery-web/src/stores/RootGalleryStore.ts index 0f9bf1ebf6..24aa253be7 100644 --- a/packages/pluggableWidgets/gallery-web/src/stores/RootGalleryStore.ts +++ b/packages/pluggableWidgets/gallery-web/src/stores/RootGalleryStore.ts @@ -1,4 +1,5 @@ import { compactArray, fromCompactArray } from "@mendix/widget-plugin-filtering/condition-utils"; +import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost"; 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"; @@ -25,8 +26,13 @@ export class RootGalleryStore { filtersChannelName: `datagrid/${generateUUID()}` }; - const headerViewState = this.getDsViewState(props); - this.headerFiltersStore = new HeaderFiltersStore(props, this.staticInfo, headerViewState); + this.headerFiltersStore = new HeaderFiltersStore({ + filterList: props.filterList, + filterChannelName: this.staticInfo.filtersChannelName, + headerInitFilter: this.getDsViewState(props), + sharedInitFilter: [], + customFilterHost: new CustomFilterHost() + }); this.sortProvider = new SortAPIProvider(props); } diff --git a/packages/shared/widget-plugin-filtering/package.json b/packages/shared/widget-plugin-filtering/package.json index 7a31cc4efa..e92778dc25 100644 --- a/packages/shared/widget-plugin-filtering/package.json +++ b/packages/shared/widget-plugin-filtering/package.json @@ -36,6 +36,7 @@ "@floating-ui/react-dom": "^2.1.2", "@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", diff --git a/packages/shared/widget-plugin-filtering/src/__tests__/condition-utils.spec.ts b/packages/shared/widget-plugin-filtering/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/widget-plugin-filtering/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/widget-plugin-filtering/src/condition-utils.ts index 7f14ecbdc4..976da416b5 100644 --- a/packages/shared/widget-plugin-filtering/src/condition-utils.ts +++ b/packages/shared/widget-plugin-filtering/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/context.ts b/packages/shared/widget-plugin-filtering/src/context.ts index 2a44be0ed0..e34c9f5375 100644 --- a/packages/shared/widget-plugin-filtering/src/context.ts +++ b/packages/shared/widget-plugin-filtering/src/context.ts @@ -1,13 +1,17 @@ +import { FilterCondition } from "mendix/filters/index.js"; import { Context, createContext, useContext } from "react"; import { APIError, ENOCONTEXT } from "./errors.js"; import { Result, error, value } from "./result-meta.js"; +import { FilterObserver } from "./typings/FilterObserver.js"; import { InputFilterInterface } from "./typings/InputFilterInterface.js"; import { PickerFilterStore } from "./typings/PickerFilterStore.js"; -export interface FilterAPIv2 { - version: 2; +export interface FilterAPI { + version: 3; parentChannelName: string; provider: Result; + filterObserver: FilterObserver; + sharedInitFilter: Array; } /** @deprecated */ @@ -18,7 +22,7 @@ export enum FilterType { DATE = "date" } -export type FilterStoreProvider = DirectProvider | KeyProvider | LegacyProvider; +export type FilterStoreProvider = DirectProvider | LegacyProvider; export type FilterStore = InputFilterInterface | PickerFilterStore; @@ -27,32 +31,27 @@ interface DirectProvider { store: FilterStore | null; } -export interface KeyProvider { - type: "key-value"; - get: (key: string) => FilterStore | null; -} - /** @deprecated */ export interface LegacyProvider { type: "legacy"; get: (type: FilterType) => FilterStore | null; } -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,12 +62,13 @@ export function useFilterContextValue(): Result { return value(contextValue); } -export function getFilterStore(provider: FilterStoreProvider, legacyType: FilterType, key: string): FilterStore | null { +/** @deprecated This hook is renamed, use `useFilterAPI` instead. */ +export const useFilterContextValue = useFilterAPI; + +export function getFilterStore(provider: FilterStoreProvider, legacyType: FilterType): FilterStore | null { switch (provider.type) { case "direct": return provider.store; - case "key-value": - return provider.get(key); case "legacy": return provider.get(legacyType); default: 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..883290b619 --- /dev/null +++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/BaseStoreProvider.ts @@ -0,0 +1,32 @@ +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; +import { ISetupable } from "@mendix/widget-plugin-mobx-kit/setupable"; +import { FilterCondition } from "mendix/filters"; +import { isAnd, isTag } from "../condition-utils"; +import { FilterAPI } from "../context"; +import { Filter } from "../typings/FilterObserver"; + +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/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..d6cdab88bf 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 { 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,7 +10,7 @@ export interface Date_FilterAPIv2 { parentChannelName?: string; } -export function useDateFilterAPI(key: string): Result { +export function useDateFilterAPI(): Result { const ctx = useFilterContextValue(); const dateAPI = useRef(); @@ -24,11 +24,7 @@ export function useDateFilterAPI(key: string): Result { +export function useNumberFilterAPI(): Result { const ctx = useFilterContextValue(); const numAPI = useRef(); @@ -24,11 +24,7 @@ export function useNumberFilterAPI(key: string): Result { +export function useStringFilterAPI(): Result { const ctx = useFilterContextValue(); const strAPI = useRef(); @@ -24,11 +24,7 @@ export function useStringFilterAPI(key: string): Result = new Map(); + private settingsBuffer: FiltersSettingsMap = new Map(); + private disposeMap: Map void> = new Map(); + + constructor() { + makeAutoObservable(this); + } + + get settings(): FiltersSettingsMap { + return new Map([...this.filters].map(([key, filter]) => [key, filter.toJSON()])); + } + + set settings(data: FiltersSettingsMap) { + this.settingsBuffer = data; + } + + get conditions(): Array { + return [...this.filters].map(([key, { condition }]) => { + return condition ? and(tag(key), condition) : undefined; + }); + } + + observe(key: string, filter: Filter): void { + const dispose = autorun(() => { + if (this.settingsBuffer.has(key)) { + filter.fromJSON(this.settingsBuffer.get(key)); + } + }); + this.disposeMap.set(key, dispose); + this.filters.set(key, filter); + } + + unobserve(key: string): void { + this.disposeMap.get(key)?.(); + this.disposeMap.delete(key); + this.filters.delete(key); + } +} diff --git a/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts b/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts index 3836716e43..f627172731 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts @@ -1,40 +1,43 @@ import { ListAttributeValue } from "mendix"; import { FilterCondition } from "mendix/filters"; import { computed, makeObservable } from "mobx"; -import { FilterAPIv2 } from "../../context"; +import { FilterAPI } from "../../context"; import { APIError } from "../../errors"; import { LegacyPv } from "../../providers/LegacyPv"; import { Result, value } from "../../result-meta"; +import { FilterObserver } from "../../typings/FilterObserver"; import { FiltersSettingsMap } from "../../typings/settings"; export interface FilterListType { filter: ListAttributeValue; } -export interface HeaderFiltersStoreProps { +export interface HeaderFiltersStoreSpec { filterList: FilterListType[]; - parentChannelName?: string; -} - -export interface StaticInfo { - name: string; - filtersChannelName: string; + filterChannelName: string; + headerInitFilter: Array; + sharedInitFilter: Array; + customFilterHost: FilterObserver; } export class HeaderFiltersStore { private provider: Result; - context: FilterAPIv2; + context: FilterAPI; - constructor( - props: HeaderFiltersStoreProps, - info: StaticInfo, - dsViewState: Array | null - ) { - this.provider = this.createProvider(props, dsViewState); + constructor({ + filterList, + filterChannelName, + headerInitFilter, + sharedInitFilter, + customFilterHost: filterObserver + }: HeaderFiltersStoreSpec) { + this.provider = this.createProvider(filterList, headerInitFilter); this.context = { - version: 2, - parentChannelName: info.filtersChannelName ?? "", - provider: this.provider + version: 3, + parentChannelName: filterChannelName, + provider: this.provider, + sharedInitFilter, + filterObserver }; makeObservable(this, { conditions: computed, @@ -59,13 +62,13 @@ export class HeaderFiltersStore { } createProvider( - props: HeaderFiltersStoreProps, - dsViewState: Array | null + filterList: FilterListType[], + initFilter: Array ): Result { return value( new LegacyPv( - props.filterList.map(f => f.filter), - dsViewState + filterList.map(f => f.filter), + initFilter ) ); } @@ -77,6 +80,4 @@ export class HeaderFiltersStore { return this.provider.value.setup(); } - - updateProps(): void {} } diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts index 8dc62bfa6c..49b3eaa5e6 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts @@ -1,5 +1,5 @@ import { Big } from "big.js"; -import { ListAttributeValue } from "mendix"; +import { AttributeMetaData } from "mendix"; import { FilterCondition } from "mendix/filters"; import { and, @@ -23,8 +23,9 @@ import { Argument } from "./Argument"; type StateTuple = [Fn] | [Fn, V] | [Fn, V, V]; type Val = A["value"]; + export class BaseInputFilterStore { - protected _attributes: ListAttributeValue[] = []; + protected _attributes: AttributeMetaData[] = []; private _filterFunction: Fn; private _isFilterFunctionAdjustable: boolean = true; arg1: A; @@ -32,7 +33,7 @@ export class BaseInputFilterStore { isInitialized = false; defaultState: StateTuple>; - constructor(arg1: A, arg2: A, initFn: Fn, attributes: ListAttributeValue[]) { + constructor(arg1: A, arg2: A, initFn: Fn, attributes: AttributeMetaData[]) { this._attributes = attributes; this.defaultState = [initFn]; this._filterFunction = initFn; @@ -116,7 +117,7 @@ export class BaseInputFilterStore { } function getFilterCondition( - listAttribute: ListAttributeValue, + listAttribute: AttributeMetaData, value: T | undefined, valueR: T | undefined, operation: AllFunctions diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts index d04bf14a31..7a8807a803 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts @@ -1,4 +1,4 @@ -import { DateTimeFormatter, ListAttributeValue } from "mendix"; +import { AttributeMetaData, DateTimeFormatter, ListAttributeValue, SimpleFormatter } from "mendix"; import { AndCondition, FilterCondition, LiteralExpression } from "mendix/filters"; import { and, @@ -26,6 +26,7 @@ import { BaseInputFilterStore } from "./BaseInputFilterStore"; type DateFns = FilterFunctionGeneric | FilterFunctionNonValue | FilterFunctionBinary; type StateTuple = [DateFns, Date | undefined, Date | undefined]; type InitState = [DateFns, Date | undefined, Date | undefined] | [DateFns, Date | undefined]; +type AttrMeta = AttributeMetaData & { formatter?: SimpleFormatter }; export class DateInputFilterStore extends BaseInputFilterStore @@ -36,8 +37,8 @@ export class DateInputFilterStore private readonly rangeMarkerTag = "__RANGE_MARKER__"; private computedState: StateTuple; - constructor(attributes: Array>, initCond: FilterCondition | null) { - const { formatter } = attributes[0]; + constructor(attributes: Array>, initCond: FilterCondition | null) { + const formatter = getFormatter(attributes[0]); super(new DateArgument(formatter), new DateArgument(formatter), "equal", attributes); // NOTE: some fields already become observable in `super`. makeObservable(this, { @@ -76,7 +77,7 @@ export class DateInputFilterStore updateProps(attributes: ListAttributeValue[]): void { if (!comparer.shallow(this._attributes, attributes)) { this._attributes = attributes; - const formatter = attributes.at(0)?.formatter; + const formatter = getFormatter(attributes[0] as AttributeMetaData); this.arg1.updateProps(formatter as DateTimeFormatter); this.arg2.updateProps(formatter as DateTimeFormatter); } @@ -103,7 +104,7 @@ export class DateInputFilterStore } private getCondition( - attr: ListAttributeValue, + attr: AttributeMetaData, filterFn: DateFns, v1: Date | undefined, v2: Date | undefined @@ -121,7 +122,7 @@ export class DateInputFilterStore } private getAttrCondition( - attr: ListAttributeValue, + attr: AttributeMetaData, filterFn: Exclude, date: Date | undefined ): [FilterCondition] | [] { @@ -153,7 +154,7 @@ export class DateInputFilterStore } } - private getRangeCondition(attr: ListAttributeValue, [start, end]: [Date, Date]): [FilterCondition] | [] { + private getRangeCondition(attr: AttributeMetaData, [start, end]: [Date, Date]): [FilterCondition] | [] { const attrExp = attribute(attr.id); return [ @@ -296,3 +297,23 @@ function subDay(date: Date): Date { newDate.setUTCDate(newDate.getUTCDate() - 1); return newDate; } + +function getFormatter(attr: AttrMeta): SimpleFormatter { + if (attr.formatter) { + return attr.formatter; + } + + return { + format: v => v?.toString() ?? "", + parse: v => { + const date = Date.parse(v); + if (isNaN(date)) { + return { valid: false }; + } + return { + valid: true, + value: new Date(date) + }; + } + }; +} diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts index a8fbbe08c5..18a302d02d 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts @@ -1,5 +1,5 @@ import { Big } from "big.js"; -import { ListAttributeValue } from "mendix"; +import { AttributeMetaData, ListAttributeValue, SimpleFormatter } from "mendix"; import { FilterCondition } from "mendix/filters"; import { action, comparer, makeObservable } from "mobx"; import { inputStateFromCond } from "../../condition-utils"; @@ -11,7 +11,8 @@ import { BaseInputFilterStore } from "./BaseInputFilterStore"; import { baseNames } from "./fn-mappers"; type NumFns = FilterFunctionGeneric | FilterFunctionNonValue | FilterFunctionBinary; -type Formatter = ListAttributeValue["formatter"]; +type Formatter = SimpleFormatter; +type AttrMeta = AttributeMetaData & { formatter?: SimpleFormatter }; export class NumberInputFilterStore extends BaseInputFilterStore @@ -20,9 +21,8 @@ export class NumberInputFilterStore readonly storeType = "input"; readonly type = "number"; - constructor(attributes: Array>, initCond: FilterCondition | null) { - let { formatter } = attributes[0]; - formatter = formatterFix(formatter); + constructor(attributes: AttrMeta[], initCond: FilterCondition | null) { + const formatter = formatterFix(attributes[0].formatter); super(new NumberArgument(formatter), new NumberArgument(formatter), "equal", attributes); makeObservable(this, { updateProps: action, @@ -79,11 +79,12 @@ export class NumberInputFilterStore } } -function formatterFix(formatter: Formatter): Formatter { +function formatterFix(formatter: Formatter | undefined): Formatter { // Check formatter.parse to see if it is a valid formatter. - if (formatter.parse("none")?.valid === false) { + if (formatter && formatter.parse("none")?.valid === false) { return formatter; } + // Create a new formatter that will handle the autonumber values. return { format: (value: Big) => { diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts index 45ab2a30d9..31c720798b 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts @@ -1,4 +1,4 @@ -import { ListAttributeValue } from "mendix"; +import { AttributeMetaData, ListAttributeValue, SimpleFormatter } from "mendix"; import { FilterCondition } from "mendix/filters"; import { action, comparer, makeObservable } from "mobx"; import { inputStateFromCond } from "../../condition-utils"; @@ -15,6 +15,8 @@ import { BaseInputFilterStore } from "./BaseInputFilterStore"; import { baseNames } from "./fn-mappers"; type StrFns = FilterFunctionString | FilterFunctionGeneric | FilterFunctionNonValue | FilterFunctionBinary; +type AttrMeta = AttributeMetaData & { formatter?: SimpleFormatter }; + export class StringInputFilterStore extends BaseInputFilterStore implements String_InputFilterInterface @@ -22,8 +24,8 @@ export class StringInputFilterStore readonly storeType = "input"; readonly type = "string"; - constructor(attributes: Array>, initCond: FilterCondition | null) { - const { formatter } = attributes[0]; + constructor(attributes: AttrMeta[], initCond: FilterCondition | null) { + const formatter = getFormatter(attributes[0]); super(new StringArgument(formatter), new StringArgument(formatter), "equal", attributes); makeObservable(this, { updateProps: action, @@ -91,3 +93,13 @@ export class StringInputFilterStore this.isInitialized = true; } } + +function getFormatter(attr: { formatter?: SimpleFormatter }): SimpleFormatter { + return ( + attr.formatter ?? + ({ + format: v => v ?? "", + parse: v => ({ valid: true, value: v ?? "" }) + } as SimpleFormatter) + ); +} diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts b/packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts index d31e08f06e..ca1b324fd0 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts @@ -40,7 +40,7 @@ export class BaseSelectStore { } fromJSON(json: FilterData): void { - if (json === null || isInputData(json)) { + if (json == null || isInputData(json)) { return; } this.setSelected(json); diff --git a/packages/shared/widget-plugin-filtering/src/typings/FilterObserver.ts b/packages/shared/widget-plugin-filtering/src/typings/FilterObserver.ts new file mode 100644 index 0000000000..c572ed5d38 --- /dev/null +++ b/packages/shared/widget-plugin-filtering/src/typings/FilterObserver.ts @@ -0,0 +1,17 @@ +import { FilterCondition } from "mendix/filters"; +import { FilterData, FiltersSettingsMap } from "./settings"; + +export interface Filter { + toJSON(): FilterData; + fromJSON(data: FilterData): void; + condition: FilterCondition | undefined; + setup?: () => void | void; +} + +export interface FilterObserver { + get settings(): FiltersSettingsMap; + set settings(settings: FiltersSettingsMap); + conditions: Array; + observe(key: string, filter: Filter): void; + unobserve(key: string): void; +} diff --git a/packages/shared/widget-plugin-filtering/src/typings/settings.ts b/packages/shared/widget-plugin-filtering/src/typings/settings.ts index 7f4f3b6614..1b6cb7a373 100644 --- a/packages/shared/widget-plugin-filtering/src/typings/settings.ts +++ b/packages/shared/widget-plugin-filtering/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/widget-plugin-filtering/src/useDefaultValue.ts b/packages/shared/widget-plugin-filtering/src/useDefaultValue.ts deleted file mode 100644 index d61d99ac45..0000000000 --- a/packages/shared/widget-plugin-filtering/src/useDefaultValue.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { DynamicValue } from "mendix"; -import { useRef } from "react"; - -export function useDefaultValue(defaultValueProp?: DynamicValue): T | undefined | null { - const defaultValueRef = useRef(null); - - if (defaultValueProp?.status !== "loading" && defaultValueRef.current === null) { - defaultValueRef.current = defaultValueProp?.value; - } - - return defaultValueRef.current; -} diff --git a/packages/shared/widget-plugin-mobx-kit/src/disposeBatch.ts b/packages/shared/widget-plugin-mobx-kit/src/disposeBatch.ts index b3d899c210..7b0caf4fe4 100644 --- a/packages/shared/widget-plugin-mobx-kit/src/disposeBatch.ts +++ b/packages/shared/widget-plugin-mobx-kit/src/disposeBatch.ts @@ -1,8 +1,12 @@ -export function disposeBatch(): [add: (fn: () => void) => void, disposeAll: () => void] { +type MaybeFn = (() => void) | void; + +export function disposeBatch(): [add: (fn: MaybeFn) => void, disposeAll: () => void] { const disposers = new Set<() => void>(); - const add = (fn: () => void): void => { - disposers.add(fn); + const add = (fn: MaybeFn): void => { + if (fn) { + disposers.add(fn); + } }; const disposeAll = (): void => { diff --git a/packages/shared/widget-plugin-mobx-kit/src/setupable.ts b/packages/shared/widget-plugin-mobx-kit/src/setupable.ts new file mode 100644 index 0000000000..cb29e7ccb9 --- /dev/null +++ b/packages/shared/widget-plugin-mobx-kit/src/setupable.ts @@ -0,0 +1,3 @@ +export interface ISetupable { + setup(): void | (() => void); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37c6311295..4403b81b35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1198,6 +1198,9 @@ importers: '@mendix/widget-plugin-hooks': specifier: workspace:* version: link:../../shared/widget-plugin-hooks + '@mendix/widget-plugin-mobx-kit': + specifier: workspace:^ + version: link:../../shared/widget-plugin-mobx-kit '@mendix/widget-plugin-platform': specifier: workspace:* version: link:../../shared/widget-plugin-platform @@ -2535,6 +2538,9 @@ importers: '@mendix/widget-plugin-hooks': specifier: workspace:* version: link:../widget-plugin-hooks + '@mendix/widget-plugin-mobx-kit': + specifier: workspace:^ + version: link:../widget-plugin-mobx-kit '@mendix/widget-plugin-platform': specifier: workspace:* version: link:../widget-plugin-platform