diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/package.json b/packages/pluggableWidgets/datagrid-dropdown-filter-web/package.json index 888026f3fc..dcf5ad9514 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/package.json +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/package.json @@ -41,6 +41,7 @@ "verify": "rui-verify-package-format" }, "dependencies": { + "@mendix/widget-plugin-dropdown-filter": "workspace:^", "@mendix/widget-plugin-external-events": "workspace:*", "@mendix/widget-plugin-filtering": "workspace:*", "classnames": "^2.3.2" diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorConfig.ts b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorConfig.ts index cd2dbe6373..fd528df8a8 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorConfig.ts +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorConfig.ts @@ -1,4 +1,4 @@ -import { hidePropertyIn, Problem, Properties } from "@mendix/pluggable-widgets-tools"; +import { hidePropertiesIn, hidePropertyIn, Problem, Properties } from "@mendix/pluggable-widgets-tools"; import { chevronDownIcon, chevronDownIconDark } from "@mendix/widget-plugin-filtering/preview/editor-preview-icons"; import { ContainerProps, @@ -13,8 +13,10 @@ export function getProperties(values: DatagridDropdownFilterPreviewProps, defaul const showSelectedItemsStyle = values.filterable && values.multiSelect; const showSelectionMethod = showSelectedItemsStyle && values.selectedItemsStyle === "boxes"; - if (values.auto) { - hidePropertyIn(defaultProperties, values, "filterOptions"); + if (values.baseType === "attr") { + defaultProperties = attrGroupProperties(values, defaultProperties); + } else { + hidePropertiesIn(defaultProperties, values, ["attr", "attrChoice", "filterOptions", "auto"]); } if (values.filterable) { @@ -33,6 +35,21 @@ export function getProperties(values: DatagridDropdownFilterPreviewProps, defaul return defaultProperties; } +function attrGroupProperties(values: DatagridDropdownFilterPreviewProps, defaultProperties: Properties): Properties { + hidePropertiesIn(defaultProperties, values, ["refEntity", "refOptions", "refCaption", "fetchOptionsLazy"]); + + if (values.attrChoice === "auto") { + hidePropertyIn(defaultProperties, {} as { linkedDs: unknown }, "linkedDs"); + hidePropertyIn(defaultProperties, values, "attr"); + } + + if (values.auto) { + hidePropertyIn(defaultProperties, values, "filterOptions"); + } + + return defaultProperties; +} + export const getPreview = (values: DatagridDropdownFilterPreviewProps, isDarkMode: boolean): StructurePreviewProps => { const palette = structurePreviewPalette[isDarkMode ? "dark" : "light"]; return { diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorPreview.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorPreview.tsx index 9d42df5e76..59596a465a 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.editorPreview.tsx @@ -1,10 +1,10 @@ import { enableStaticRendering } from "mobx-react-lite"; -enableStaticRendering(true); - import { createElement, ReactElement } from "react"; import { DatagridDropdownFilterPreviewProps } from "../typings/DatagridDropdownFilterProps"; import { parseStyle } from "@mendix/widget-plugin-platform/preview/parse-style"; -import { Select } from "@mendix/widget-plugin-filtering/controls/select/Select"; +import { Select } from "@mendix/widget-plugin-dropdown-filter/controls/select/Select"; + +enableStaticRendering(true); function Preview(props: DatagridDropdownFilterPreviewProps): ReactElement { return ( diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.tsx index 5435f96422..b116cc4667 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.tsx +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.tsx @@ -1,42 +1,17 @@ import { createElement, ReactElement } from "react"; import { withPreloader } from "@mendix/widget-plugin-platform/hoc/withPreloader"; import { DatagridDropdownFilterContainerProps } from "../typings/DatagridDropdownFilterProps"; -import { StaticFilterContainer } from "./components/StaticFilterContainer"; -import { withSelectFilterAPI, Select_FilterAPIv2 } from "./hocs/withSelectFilterAPI"; -import { RefFilterContainer } from "./components/RefFilterContainer"; +import { AttrFilter } from "./components/AttrFilter"; +import { RefFilter } from "./components/RefFilter"; -function Container(props: DatagridDropdownFilterContainerProps & Select_FilterAPIv2): React.ReactElement { - const commonProps = { - ariaLabel: props.ariaLabel?.value, - className: props.class, - tabIndex: props.tabIndex, - styles: props.style, - onChange: props.onChange, - valueAttribute: props.valueAttribute, - parentChannelName: props.parentChannelName, - name: props.name, - multiselect: props.multiSelect, - emptyCaption: props.emptyOptionCaption?.value, - defaultValue: props.defaultValue?.value, - filterable: props.filterable, - selectionMethod: props.selectionMethod, - selectedItemsStyle: props.selectedItemsStyle, - clearable: props.clearable - }; - - if (props.filterStore.storeType === "refselect") { - return ; +function Container(props: DatagridDropdownFilterContainerProps): ReactElement { + if (props.baseType === "attr") { + return ; } - return ( - - ); + return ; } -const container = withPreloader(Container, props => props.defaultValue?.status === "loading"); - -const Widget = withSelectFilterAPI(container); +const DatagridDropdownFilter = withPreloader(Container, props => props.defaultValue?.status === "loading"); -export default function DatagridDropdownFilter(props: DatagridDropdownFilterContainerProps): ReactElement { - return ; -} +export default DatagridDropdownFilter; diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.xml b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.xml index 5e32a4a329..1da95f1c1b 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.xml +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/DatagridDropdownFilter.xml @@ -7,16 +7,41 @@ https://docs.mendix.com/appstore/modules/data-grid-2#7-2-drop-down-filter - + + + Filter by + + + Attribute + Association + + + + Datasource to Filter + + + + + + Attribute config + "Auto" works only when the widget is placed in a Data grid column. + + Auto + Custom + + + + Attribute + + + + + + Automatic options Show options based on the references or the enumeration values and captions. - - Default value - Empty option caption will be shown by default or if configured default value matches none of the options - - Options @@ -34,6 +59,40 @@ + + + + + Entity + Set the entity to enable filtering over association. + + + + + + + Selectable objects + The options to show in the Drop-down filter widget. + + + Caption + + + + + + + Use lazy load + Lazy loading enables faster parent loading, but with personalization enabled, value restoration will be limited. + + + + + + Default value + Empty option caption will be shown by default or if configured default value matches none of the options + + Filterable diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/AttrFilter.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/AttrFilter.tsx new file mode 100644 index 0000000000..e917258648 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/AttrFilter.tsx @@ -0,0 +1,33 @@ +import { ReactElement, createElement } from "react"; +import { StaticFilterContainer } from "@mendix/widget-plugin-dropdown-filter/containers/StaticFilterContainer"; +import { withParentProvidedEnumStore } from "../hocs/withParentProvidedEnumStore"; +import { DatagridDropdownFilterContainerProps } from "../../typings/DatagridDropdownFilterProps"; +import { withLinkedEnumStore } from "../hocs/withLinkedEnumStore"; +import { EnumFilterAPI } from "./typings"; + +export function AttrFilter(props: DatagridDropdownFilterContainerProps): ReactElement { + if (props.auto) { + return ; + } + + return ; +} + +const AutoAttrFilter = withParentProvidedEnumStore(Connector); + +const LinkedAttrFilter = withLinkedEnumStore(Connector); + +function Connector(props: DatagridDropdownFilterContainerProps & EnumFilterAPI): ReactElement { + return ( + + ); +} diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilter.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilter.tsx new file mode 100644 index 0000000000..ed72a02cbf --- /dev/null +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilter.tsx @@ -0,0 +1,22 @@ +import { createElement, ReactElement } from "react"; +import { withLinkedRefStore } from "../hocs/withLinkedRefStore"; +import { RefFilterContainer } from "@mendix/widget-plugin-dropdown-filter/containers/RefFilterContainer"; +import { RefFilterAPI } from "./typings"; +import { DatagridDropdownFilterContainerProps } from "../../typings/DatagridDropdownFilterProps"; + +function Connector(props: DatagridDropdownFilterContainerProps & RefFilterAPI): ReactElement { + return ( + + ); +} + +export const RefFilter = withLinkedRefStore(Connector); 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 deleted file mode 100644 index 784ade5f13..0000000000 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/DataGridDropdownFilter.spec.tsx +++ /dev/null @@ -1,484 +0,0 @@ -import "@testing-library/jest-dom"; -import { FilterAPI } from "@mendix/widget-plugin-filtering/context"; -import { - HeaderFiltersStore, - 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"; -import { FilterObserver } from "@mendix/widget-plugin-filtering/typings/FilterObserver"; - -const commonProps = { - class: "filter-custom-class", - tabIndex: 0, - name: "filter-test", - advanced: false, - groupKey: "dropdown-filter", - filterable: false, - clearable: true, - selectionMethod: "checkbox" as const, - selectedItemsStyle: "text" as const -}; - -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[]) => { - const [msg] = args; - - if (typeof msg === "string" && msg.startsWith("downshift:")) { - return; - } - - consoleError(...args); -}); - -describe("Dropdown Filter", () => { - describe("with single instance", () => { - afterEach(() => { - delete (global as any)["com.mendix.widgets.web.UUID"]; - }); - - describe("with single attribute", () => { - function mockCtx(universe: string[]): void { - const spec = mockSpec({ - filterList: [ - { - filter: new ListAttributeValueBuilder() - .withUniverse(universe) - .withType("Enum") - .withFilterable(true) - .withFormatter( - value => value, - () => console.log("Parsed") - ) - .build() - } - ] - }); - const headerFilterStore = new HeaderFiltersStore(spec); - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext( - headerFilterStore.context - ); - } - beforeEach(() => { - mockCtx(["enum_value_1", "enum_value_2"]); - }); - - describe("with auto options", () => { - it("loads correct values from universe", async () => { - const filter = render( - - ); - - const trigger = filter.getByRole("combobox"); - - await fireEvent.click(trigger); - - const items = filter.getAllByRole("option"); - - items.forEach((item, index) => { - if (index === 0) { - return; - } - expect(item.textContent).toEqual(`enum_value_${index}`); - }); - }); - }); - - describe("DOM structure", () => { - it("renders correctly", () => { - const { asFragment } = render( - - ); - - expect(asFragment()).toMatchSnapshot(); - }); - }); - - describe("with defaultValue", () => { - it("initialize component with defaultValue", () => { - render( - ("enum_value_1")} - /> - ); - - expect(screen.getByRole("combobox")).toHaveAccessibleName("enum_value_1"); - }); - - it("don't sync defaultValue with state when defaultValue changes from undefined to string", async () => { - const { rerender } = render( - ("")} - /> - ); - - await waitFor(() => { - expect(screen.getByRole("combobox")).toHaveAccessibleName("Select"); - }); - - // “Real” context causes widgets to re-renders multiple times, replicate this in mocked context. - rerender( - ("")} - /> - ); - rerender( - ("enum_value_1")} - /> - ); - - await waitFor(() => { - expect(screen.getByRole("combobox")).toHaveAccessibleName("Select"); - }); - }); - - it("don't sync defaultValue with state when defaultValue changes from string to undefined", async () => { - mockCtx(["xyz", "abc"]); - const { rerender } = render( - ("xyz")} - /> - ); - - expect(screen.getByRole("combobox")).toHaveAccessibleName("xyz"); - - // “Real” context causes widgets to re-renders multiple times, replicate this in mocked context. - rerender( - ("xyz")} - /> - ); - rerender( - - ); - - await waitFor(() => { - expect(screen.getByRole("combobox")).toHaveAccessibleName("xyz"); - }); - }); - }); - - afterAll(() => { - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = undefined; - }); - }); - - describe("with multiple attributes", () => { - beforeAll(() => { - const spec = mockSpec({ - filterList: [ - { - filter: new ListAttributeValueBuilder() - .withId("attribute1") - .withUniverse(["enum_value_1", "enum_value_2"]) - .withType("Enum") - .withFilterable(true) - .withFormatter( - value => value, - () => console.log("Parsed") - ) - .build() - }, - { - filter: new ListAttributeValueBuilder() - .withId("attribute2") - .withUniverse([true, false]) - .withType("Boolean") - .withFilterable(true) - .withFormatter( - value => (value ? "Yes" : "No"), - () => console.log("Parsed") - ) - .build() - } - ] - }); - const headerFilterStore = new HeaderFiltersStore(spec); - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext( - headerFilterStore.context - ); - }); - - describe("with auto options", () => { - it("loads correct values from universes", async () => { - const filter = render( - - ); - - const trigger = filter.getByRole("combobox"); - await fireEvent.click(trigger); - - expect(filter.getAllByRole("option").map(item => item.textContent)).toStrictEqual([ - "None", - "enum_value_1", - "enum_value_2", - "Yes", - "No" - ]); - }); - }); - - afterAll(() => { - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = undefined; - }); - }); - - describe("with wrong attribute's type", () => { - beforeAll(() => { - const spec = mockSpec({ - filterList: [ - { - filter: new ListAttributeValueBuilder().withType("String").withFilterable(true).build() - } - ] - }); - const headerFilterStore = new HeaderFiltersStore(spec); - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext( - headerFilterStore.context - ); - }); - - it("renders error message", () => { - const { container } = render( - - ); - - expect(container.querySelector(".alert")?.textContent).toBe( - "Unable to get filter store. Check parent widget configuration." - ); - }); - - afterAll(() => { - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = undefined; - }); - }); - - describe("with wrong multiple attributes' types", () => { - beforeAll(() => { - const spec = mockSpec({ - filterList: [ - { - filter: new ListAttributeValueBuilder() - .withId("attribute1") - .withType("String") - .withFilterable(true) - .build() - }, - { - filter: new ListAttributeValueBuilder() - .withId("attribute2") - .withType("Decimal") - .withFilterable(true) - .build() - } - ] - }); - const headerFilterStore = new HeaderFiltersStore(spec); - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext( - headerFilterStore.context - ); - }); - - it("renders error message", () => { - const { container } = render( - - ); - - expect(container.querySelector(".alert")?.textContent).toBe( - "Unable to get filter store. Check parent widget configuration." - ); - }); - - afterAll(() => { - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = undefined; - }); - }); - - describe("with no context", () => { - beforeAll(() => { - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = undefined; - }); - - it("renders error message", () => { - const { container } = render( - - ); - - expect(container.querySelector(".alert")?.textContent).toBe( - "The filter widget must be placed inside the column or header of the Data grid 2.0 or inside header of the Gallery widget." - ); - }); - }); - - describe("with invalid values", () => { - beforeAll(() => { - const spec = mockSpec({ - filterList: [ - { - filter: new ListAttributeValueBuilder() - .withUniverse(["enum_value_1", "enum_value_2"]) - .withType("Enum") - .withFilterable(true) - .build() - } - ] - }); - const headerFilterStore = new HeaderFiltersStore(spec); - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext( - headerFilterStore.context - ); - }); - - it("renders error message", () => { - const { container } = render( - ("wrong value"), - value: dynamicValue("enum_value_3") - } - ]} - /> - ); - - expect(container.querySelector(".alert")?.textContent).toBe("Invalid option value: 'enum_value_3'"); - }); - }); - - describe("with multiple invalid values", () => { - beforeAll(() => { - const spec = mockSpec({ - filterList: [ - { - filter: new ListAttributeValueBuilder() - .withUniverse(["enum_value_1", "enum_value_2"]) - .withType("Enum") - .withFilterable(true) - .build() - }, - { - filter: new ListAttributeValueBuilder() - .withUniverse([true, false]) - .withType("Boolean") - .withFilterable(true) - .build() - } - ] - }); - const headerFilterStore = new HeaderFiltersStore(spec); - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext( - headerFilterStore.context - ); - }); - - it("renders error message", () => { - const { container } = render( - ("wrong enum value"), - value: dynamicValue("enum_value_3") - }, - { - caption: dynamicValue("wrong boolean value"), - value: dynamicValue("no") - } - ]} - /> - ); - - expect(container.querySelector(".alert")?.textContent).toBe("Invalid option value: 'enum_value_3'"); - }); - }); - }); - - describe("with multiple instances", () => { - beforeAll(() => { - const spec = mockSpec({ - filterList: [ - { - filter: new ListAttributeValueBuilder() - .withUniverse(["enum_value_1", "enum_value_2"]) - .withType("Enum") - .withFilterable(true) - .withFormatter( - value => value, - () => console.log("Parsed") - ) - .build() - } - ] - }); - const headerFilterStore = new HeaderFiltersStore(spec); - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = createContext( - headerFilterStore.context - ); - }); - - it("renders with a unique id", () => { - const { asFragment: fragment1 } = render( - - ); - const { asFragment: fragment2 } = render( - - ); - - expect(fragment1().querySelector("button")?.getAttribute("aria-controls")).not.toBe( - fragment2().querySelector("button")?.getAttribute("aria-controls") - ); - }); - - afterAll(() => { - (window as any)["com.mendix.widgets.web.filterable.filterContext.v2"] = undefined; - delete (global as any)["com.mendix.widgets.web.UUID"]; - }); - }); -}); diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/__snapshots__/DataGridDropdownFilter.spec.tsx.snap b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/__snapshots__/DataGridDropdownFilter.spec.tsx.snap deleted file mode 100644 index 28606ed0ee..0000000000 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/__tests__/__snapshots__/DataGridDropdownFilter.spec.tsx.snap +++ /dev/null @@ -1,59 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Dropdown Filter with single instance with single attribute DOM structure renders correctly 1`] = ` - -
- - -
-
-`; diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/typings.ts b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/typings.ts new file mode 100644 index 0000000000..8cc6e56ea4 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/typings.ts @@ -0,0 +1,12 @@ +import { RefFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/RefFilterStore"; +import { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore"; + +export interface EnumFilterAPI { + filterStore: StaticSelectFilterStore; + parentChannelName?: string; +} + +export interface RefFilterAPI { + filterStore: RefFilterStore; + parentChannelName?: string; +} diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedEnumStore.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedEnumStore.tsx new file mode 100644 index 0000000000..2dc91ade11 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedEnumStore.tsx @@ -0,0 +1,57 @@ +import { createElement } from "react"; +import { Alert } from "@mendix/widget-plugin-component-kit/Alert"; +import { ISetupable } from "@mendix/widget-plugin-mobx-kit/setupable"; +import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; +import { useFilterAPI } from "@mendix/widget-plugin-filtering/context"; +import { APIError } from "@mendix/widget-plugin-filtering/errors"; +import { Result, value, error } from "@mendix/widget-plugin-filtering/result-meta"; +import { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore"; +import { EnumStoreProvider } from "@mendix/widget-plugin-filtering/custom-filter-api/EnumStoreProvider"; +import { AttributeMetaData } from "mendix"; +import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup"; +import { EnumFilterAPI } from "../components/typings"; + +interface StoreProvider extends ISetupable { + store: StaticSelectFilterStore; +} + +interface RequiredProps { + attr: AttributeMetaData; + name: string; +} + +type Component

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

(Cmp: Component

): Component

{ + function ProviderHost(props: P & { provider: StoreProvider; channel: string }): React.ReactElement { + useSetup(() => props.provider); + return ; + } + + return function FilterAPIProvider(props) { + const api = useEnumStoreProvider(props); + + if (api.hasError) { + return {api.error.message}; + } + + return ; + }; +} + +function useEnumStoreProvider(props: RequiredProps): Result<{ provider: StoreProvider; channel: string }, APIError> { + const filterAPI = useFilterAPI(); + return useConst(() => { + if (filterAPI.hasError) { + return error(filterAPI.error); + } + + return value({ + provider: new EnumStoreProvider(filterAPI.value, { + attributes: [props.attr], + dataKey: props.name + }), + channel: filterAPI.value.parentChannelName + }); + }); +} diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedRefStore.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedRefStore.tsx new file mode 100644 index 0000000000..319d7e48e5 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withLinkedRefStore.tsx @@ -0,0 +1,90 @@ +import { useEffect, createElement } from "react"; +import { Alert } from "@mendix/widget-plugin-component-kit/Alert"; +import { RefFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/RefFilterStore"; +import { FilterAPI, useFilterAPI } from "@mendix/widget-plugin-filtering/context"; +import { BaseStoreProvider } from "@mendix/widget-plugin-filtering/custom-filter-api/BaseStoreProvider"; +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; +import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider"; +import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; +import { ListValue, ListAttributeValue, AssociationMetaData } from "mendix"; +import { DatagridDropdownFilterContainerProps } from "../../typings/DatagridDropdownFilterProps"; +import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup"; +import { RefFilterAPI } from "../components/typings"; + +type WidgetProps = Pick; + +export interface RequiredProps { + name: string; + refEntity: AssociationMetaData; + refOptions: ListValue; + refCaption: ListAttributeValue; + searchAttrId: ListAttributeValue["id"]; +} + +type Component

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

(Cmp: Component

): Component

{ + function StoreProvider(props: P & { filterAPI: FilterAPI }): React.ReactElement { + const gate = useGate(props); + const provider = useSetup(() => new RefStoreProvider(props.filterAPI, gate)); + return ; + } + return function FilterAPIProvider(props) { + const api = useFilterAPI(); + + if (api.hasError) { + return {api.error.message}; + } + + return ; + }; +} + +function mapProps(props: WidgetProps): RequiredProps { + if (!props.refEntity) { + throw new Error("RefFilterStoreProvider: refEntity is required"); + } + + if (!props.refOptions) { + throw new Error("RefFilterStoreProvider: refOptions is required"); + } + + if (!props.refCaption) { + throw new Error("RefFilterStoreProvider: refCaption is required"); + } + return { + name: props.name, + refEntity: props.refEntity, + refOptions: props.refOptions, + refCaption: props.refCaption, + searchAttrId: props.refCaption.id + }; +} + +function useGate(props: WidgetProps): DerivedPropsGate { + const gp = useConst(() => new GateProvider(mapProps(props))); + useEffect(() => { + gp.setProps(mapProps(props)); + }); + return gp.gate; +} + +class RefStoreProvider extends BaseStoreProvider { + protected _store: RefFilterStore; + protected filterAPI: FilterAPI; + readonly dataKey: string; + + constructor(filterAPI: FilterAPI, gate: DerivedPropsGate) { + super(); + this.filterAPI = filterAPI; + this.dataKey = gate.props.name; + this._store = new RefFilterStore({ + gate, + initCond: this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey) + }); + } + + get store(): RefFilterStore { + return this._store; + } +} diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withParentProvidedEnumStore.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withParentProvidedEnumStore.tsx new file mode 100644 index 0000000000..d6cec1c09d --- /dev/null +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withParentProvidedEnumStore.tsx @@ -0,0 +1,48 @@ +import { Alert } from "@mendix/widget-plugin-component-kit/Alert"; +import { useRef, createElement } from "react"; +import { useFilterAPI } from "@mendix/widget-plugin-filtering/context"; +import { APIError, EMISSINGSTORE, EStoreTypeMisMatch } from "@mendix/widget-plugin-filtering/errors"; +import { Result, error, value } from "@mendix/widget-plugin-filtering/result-meta"; +import { EnumFilterAPI } from "../components/typings"; + +export function withParentProvidedEnumStore

( + Component: (props: P & EnumFilterAPI) => React.ReactElement +): (props: P) => React.ReactElement { + return function FilterAPIProvider(props: P): React.ReactElement { + const api = useEnumFilterAPI(); + if (api.hasError) { + return {api.error.message}; + } + + return ( + + ); + }; +} + +function useEnumFilterAPI(): Result { + const ctx = useFilterAPI(); + const slctAPI = useRef(); + + if (ctx.hasError) { + return error(ctx.error); + } + + const api = ctx.value; + + if (api.provider.hasError) { + return error(api.provider.error); + } + + const store = api.provider.value.type === "direct" ? api.provider.value.store : null; + + if (store === null) { + return error(EMISSINGSTORE); + } + + if (store.storeType !== "select") { + return error(EStoreTypeMisMatch("dropdown filter", store.arg1.type)); + } + + return value((slctAPI.current ??= { filterStore: store, parentChannelName: api.parentChannelName })); +} diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withSelectFilterAPI.tsx b/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withSelectFilterAPI.tsx deleted file mode 100644 index f8f6a74f88..0000000000 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withSelectFilterAPI.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Alert } from "@mendix/widget-plugin-component-kit/Alert"; -import { Select_FilterAPIv2, useSelectFilterAPI } from "@mendix/widget-plugin-filtering/helpers/useSelectFilterAPI"; -import { createElement } from "react"; - -export { Select_FilterAPIv2 }; - -export function withSelectFilterAPI

( - Component: (props: P & Select_FilterAPIv2) => React.ReactElement -): (props: P) => React.ReactElement { - return function FilterAPIProvider(props: P): React.ReactElement { - const api = useSelectFilterAPI(props); - if (api.hasError) { - return {api.error.message}; - } - - return ( - - ); - }; -} diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/typings/DatagridDropdownFilterProps.d.ts b/packages/pluggableWidgets/datagrid-dropdown-filter-web/typings/DatagridDropdownFilterProps.d.ts index c1c4cd5e41..a443bd6589 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/typings/DatagridDropdownFilterProps.d.ts +++ b/packages/pluggableWidgets/datagrid-dropdown-filter-web/typings/DatagridDropdownFilterProps.d.ts @@ -4,7 +4,11 @@ * @author Mendix Widgets Framework Team */ import { CSSProperties } from "react"; -import { ActionValue, DynamicValue, EditableValue } from "mendix"; +import { ActionValue, AssociationMetaData, AttributeMetaData, DynamicValue, EditableValue, ListValue, ListAttributeValue } from "mendix"; + +export type BaseTypeEnum = "attr" | "ref"; + +export type AttrChoiceEnum = "auto" | "linked"; export interface FilterOptionsType { caption: DynamicValue; @@ -25,9 +29,16 @@ export interface DatagridDropdownFilterContainerProps { class: string; style?: CSSProperties; tabIndex?: number; + baseType: BaseTypeEnum; + attrChoice: AttrChoiceEnum; + attr: AttributeMetaData; auto: boolean; - defaultValue?: DynamicValue; filterOptions: FilterOptionsType[]; + refEntity: AssociationMetaData; + refOptions?: ListValue; + refCaption?: ListAttributeValue; + fetchOptionsLazy: boolean; + defaultValue?: DynamicValue; filterable: boolean; multiSelect: boolean; emptyOptionCaption?: DynamicValue; @@ -50,9 +61,16 @@ export interface DatagridDropdownFilterPreviewProps { readOnly: boolean; renderMode: "design" | "xray" | "structure"; translate: (text: string) => string; + baseType: BaseTypeEnum; + attrChoice: AttrChoiceEnum; + attr: string; auto: boolean; - defaultValue: string; filterOptions: FilterOptionsPreviewType[]; + refEntity: string; + refOptions: {} | { caption: string } | { type: string } | null; + refCaption: string; + fetchOptionsLazy: boolean; + defaultValue: string; filterable: boolean; multiSelect: boolean; emptyOptionCaption: string; diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts index bdf7464d68..21e05df9e6 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts @@ -74,21 +74,6 @@ export function getProperties( "hidable" ]); } - - if (!column.filterAssociation) { - hideNestedPropertiesIn(defaultProperties, values, "columns", index, [ - "filterAssociationOptions", - "filterAssociationOptionLabel", - "fetchOptionsLazy", - "filterCaptionType", - "filterAssociationOptionLabelAttr" - ]); - } - if (column.filterCaptionType === "attribute") { - hidePropertyIn(defaultProperties, values, "columns", index, "filterAssociationOptionLabel"); - } else { - hidePropertyIn(defaultProperties, values, "columns", index, "filterAssociationOptionLabelAttr"); - } }); if (values.pagination === "buttons") { hidePropertyIn(defaultProperties, values, "showNumberOfRows"); @@ -209,11 +194,6 @@ export const getPreview = ( draggable: false, dynamicText: "Dynamic text", filter: { widgetCount: 0, renderer: () => null }, - filterAssociation: "", - filterAssociationOptionLabel: "", - filterAssociationOptionLabelAttr: "", - filterAssociationOptions: {}, - filterCaptionType: "attribute", header: "Column", hidable: "no", resizable: false, @@ -227,8 +207,7 @@ export const getPreview = ( minWidth: "auto", minWidthLimit: 100, allowEventPropagation: true, - exportValue: "", - fetchOptionsLazy: true + exportValue: "" } ]; const columns = rowLayout({ diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx index 1bef235a15..ba1df3f098 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorPreview.tsx @@ -35,11 +35,7 @@ const initColumns: ColumnsPreviewType[] = [ draggable: false, dynamicText: "Dynamic Text", filter: { renderer: () =>

, widgetCount: 0 }, - filterAssociation: "", - filterAssociationOptionLabel: "", - filterAssociationOptionLabelAttr: "", - filterAssociationOptions: {}, - filterCaptionType: "expression", + header: "Column", hidable: "no", resizable: false, @@ -53,8 +49,7 @@ const initColumns: ColumnsPreviewType[] = [ minWidth: "auto", minWidthLimit: 100, allowEventPropagation: true, - exportValue: "", - fetchOptionsLazy: true + exportValue: "" } ]; diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx index 0178fe3ea8..018b3a94d4 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.tsx @@ -82,7 +82,7 @@ const Container = observer((props: Props): ReactElement => { headerTitle={props.filterSectionTitle?.value} headerContent={ props.filtersPlaceholder && ( - + {props.filtersPlaceholder} ) diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml index 3f00e78f9c..92e6bd9e90 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml @@ -121,44 +121,6 @@ - - - Entity - Set the entity to enable filtering over association with the Drop-down filter widget. - - - - - - - Selectable objects - The options to show in the Drop-down filter widget. - - - Use lazy load - Lazy loading enables faster data grid loading, but with personalization enabled, value restoration will be limited. - - - Option caption type - - - Attribute - Expression - - - - Option caption - - - - - Option caption - - - - - - Can sort diff --git a/packages/pluggableWidgets/datagrid-web/src/components/StickySentinel.tsx b/packages/pluggableWidgets/datagrid-web/src/components/StickySentinel.tsx deleted file mode 100644 index cde33fdd50..0000000000 --- a/packages/pluggableWidgets/datagrid-web/src/components/StickySentinel.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import classNames from "classnames"; -import { createElement, ReactElement, useState, useEffect, useRef } from "react"; - -/** - * StickySentinel - A small hidden element that uses "IntersectionObserver" - * to detect the "scrolled" state of the grid. By toggling the "container-stuck" class - * on this element, we can force "position: sticky" for column headers. - */ -export function StickySentinel(): ReactElement { - const sentinelRef = useRef(null); - const [ratio, setRatio] = useState(1); - - useEffect(() => { - const target = sentinelRef.current; - - if (target === null) { - return; - } - - return createObserver(target, setRatio); - }, []); - - return ( -
- ); -} - -function createObserver(target: Element, onIntersectionChange: (ratio: number) => void): () => void { - const options = { threshold: [0, 1] }; - - const observer = new IntersectionObserver(([entry]) => { - if (entry.intersectionRatio === 0 || entry.intersectionRatio === 1) { - onIntersectionChange(entry.intersectionRatio); - } - }, options); - - observer.observe(target); - - return () => observer.unobserve(target); -} diff --git a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx b/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx index 840cf55d2d..651c04806a 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/WidgetHeaderContext.tsx @@ -1,35 +1,27 @@ import { getGlobalFilterContextObject } from "@mendix/widget-plugin-filtering/context"; -import { HeaderFiltersStore } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore"; import { getGlobalSelectionContext, SelectionHelper, useCreateSelectionContextValue } from "@mendix/widget-plugin-grid/selection"; import { createElement, memo, ReactElement, ReactNode } from "react"; +import { RootGridStore } from "../helpers/state/RootGridStore"; interface WidgetHeaderContextProps { children?: ReactNode; - filtersStore: HeaderFiltersStore; selectionHelper?: SelectionHelper; + rootStore: RootGridStore; } const SelectionContext = getGlobalSelectionContext(); const FilterContext = getGlobalFilterContextObject(); -function FilterAPIProvider(props: { filtersStore: HeaderFiltersStore; children?: ReactNode }): ReactElement { - return {props.children}; -} - -function SelectionStatusProvider(props: { selectionHelper?: SelectionHelper; children?: ReactNode }): ReactElement { - const value = useCreateSelectionContextValue(props.selectionHelper); - return {props.children}; -} - function HeaderContainer(props: WidgetHeaderContextProps): ReactElement { + const selectionContext = useCreateSelectionContextValue(props.selectionHelper); return ( - - {props.children} - + + {props.children} + ); } diff --git a/packages/pluggableWidgets/datagrid-web/src/consistency-check.ts b/packages/pluggableWidgets/datagrid-web/src/consistency-check.ts index 33595d4591..c66fe31bad 100644 --- a/packages/pluggableWidgets/datagrid-web/src/consistency-check.ts +++ b/packages/pluggableWidgets/datagrid-web/src/consistency-check.ts @@ -4,13 +4,7 @@ import { ColumnsPreviewType, DatagridPreviewProps } from "../typings/DatagridPro export function check(values: DatagridPreviewProps): Problem[] { const errors: Problem[] = []; - const columnChecks = [ - checkAssociationSettings, - checkFilteringSettings, - checkDisplaySettings, - checkSortingSettings, - checkHidableSettings - ]; + const columnChecks = [checkFilteringSettings, checkDisplaySettings, checkSortingSettings, checkHidableSettings]; values.columns.forEach((column: ColumnsPreviewType, index) => { for (const check of columnChecks) { @@ -28,33 +22,33 @@ export function check(values: DatagridPreviewProps): Problem[] { const columnPropPath = (prop: string, index: number): string => `columns/${index + 1}/${prop}`; -const checkAssociationSettings = ( - values: DatagridPreviewProps, - column: ColumnsPreviewType, - index: number -): Problem | undefined => { - if (!values.columnsFilterable) { - return; - } - - if (!column.filterAssociation) { - return; - } - - if (column.filterCaptionType === "expression" && !column.filterAssociationOptionLabel) { - return { - property: columnPropPath("filterAssociationOptionLabel", index), - message: `A caption is required when using associations. Please set 'Option caption' property for column (${column.header})` - }; - } - - if (column.filterCaptionType === "attribute" && !column.filterAssociationOptionLabelAttr) { - return { - property: columnPropPath("filterAssociationOptionLabelAttr", index), - message: `A caption is required when using associations. Please set 'Option caption' property for column (${column.header})` - }; - } -}; +// const checkAssociationSettings = ( +// values: DatagridPreviewProps, +// column: ColumnsPreviewType, +// index: number +// ): Problem | undefined => { +// if (!values.columnsFilterable) { +// return; +// } + +// if (!column.filterAssociation) { +// return; +// } + +// if (column.filterCaptionType === "expression" && !column.filterAssociationOptionLabel) { +// return { +// property: columnPropPath("filterAssociationOptionLabel", index), +// message: `A caption is required when using associations. Please set 'Option caption' property for column (${column.header})` +// }; +// } + +// if (column.filterCaptionType === "attribute" && !column.filterAssociationOptionLabelAttr) { +// return { +// property: columnPropPath("filterAssociationOptionLabelAttr", index), +// message: `A caption is required when using associations. Please set 'Option caption' property for column (${column.header})` +// }; +// } +// }; const checkFilteringSettings = ( values: DatagridPreviewProps, @@ -65,7 +59,7 @@ const checkFilteringSettings = ( return; } - if (!column.attribute && !column.filterAssociation) { + if (!column.attribute) { return { property: columnPropPath("attribute", index), message: `An attribute or reference is required when filtering is enabled. Please select 'Attribute' or 'Reference' property for column (${column.header})` diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts index 9a79deb4e0..d99e7f2d06 100644 --- a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts +++ b/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceParamsController.ts @@ -1,11 +1,11 @@ -import { compactArray, fromCompactArray, isAnd } from "@mendix/widget-plugin-filtering/condition-utils"; +import { compactArray, fromCompactArray, isAnd } from "@mendix/filter-commons/condition-utils"; +import { QueryController } from "@mendix/widget-plugin-grid/query/query-controller"; import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller"; import { FilterCondition } from "mendix/filters"; import { and } from "mendix/filters/builders"; import { makeAutoObservable, reaction } from "mobx"; import { SortInstruction } from "../typings/sorting"; -import { QueryController } from "./query-controller"; interface Columns { conditions: Array; @@ -19,20 +19,17 @@ interface FiltersInput { type DatasourceParamsControllerSpec = { query: QueryController; columns: Columns; - header: FiltersInput; customFilters: FiltersInput; }; export class DatasourceParamsController implements ReactiveController { private columns: Columns; - private header: FiltersInput; private query: QueryController; private customFilters: FiltersInput; constructor(host: ReactiveControllerHost, spec: DatasourceParamsControllerSpec) { host.addController(this); this.columns = spec.columns; - this.header = spec.header; this.query = spec.query; this.customFilters = spec.customFilters; @@ -40,13 +37,9 @@ export class DatasourceParamsController implements ReactiveController { } private get derivedFilter(): FilterCondition | undefined { - const { columns, header, customFilters } = this; + const { columns, customFilters } = this; - return and( - compactArray(columns.conditions), - compactArray(header.conditions), - compactArray(customFilters.conditions) - ); + return and(compactArray(columns.conditions), compactArray(customFilters.conditions)); } private get derivedSortOrder(): SortInstruction[] | undefined { @@ -75,22 +68,18 @@ export class DatasourceParamsController implements ReactiveController { static unzipFilter( filter?: FilterCondition - ): [ - columns: Array, - header: Array, - sharedFilter: Array - ] { + ): [columns: Array, sharedFilter: Array] { if (!filter) { - return [[], [], []]; + return [[], []]; } if (!isAnd(filter)) { - return [[], [], []]; + return [[], []]; } - if (filter.args.length !== 3) { - return [[], [], []]; + if (filter.args.length !== 2) { + return [[], []]; } - const [columns, header, shared] = filter.args; - return [fromCompactArray(columns), fromCompactArray(header), fromCompactArray(shared)]; + const [columns, shared] = filter.args; + return [fromCompactArray(columns), fromCompactArray(shared)]; } } diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/PaginationController.ts b/packages/pluggableWidgets/datagrid-web/src/controllers/PaginationController.ts index 28a966e029..454c47a674 100644 --- a/packages/pluggableWidgets/datagrid-web/src/controllers/PaginationController.ts +++ b/packages/pluggableWidgets/datagrid-web/src/controllers/PaginationController.ts @@ -1,7 +1,7 @@ +import { QueryController } from "@mendix/widget-plugin-grid/query/query-controller"; import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller"; import { PaginationEnum, ShowPagingButtonsEnum } from "../../typings/DatagridProps"; -import { QueryController } from "./query-controller"; type Gate = DerivedPropsGate<{ pageSize: number; diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts index ae76ecf01b..b3660c0767 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/ColumnGroupStore.ts @@ -1,5 +1,5 @@ -import { disposeFx } from "@mendix/widget-plugin-filtering/mobx-utils"; -import { FiltersSettingsMap } from "@mendix/widget-plugin-filtering/typings/settings"; +import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings"; +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { FilterCondition } from "mendix/filters"; import { action, computed, makeObservable, observable } from "mobx"; import { DatagridContainerProps } from "../../../typings/DatagridProps"; @@ -83,9 +83,9 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore { } setup(): () => void { - const [disposers, dispose] = disposeFx(); + const [add, dispose] = disposeBatch(); for (const filter of this.columnFilters) { - disposers.push(filter.setup()); + add(filter.setup()); } return dispose; } @@ -93,7 +93,6 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore { updateProps(props: Pick): void { props.columns.forEach((columnProps, i) => { this._allColumns[i].updateProps(columnProps); - this.columnFilters[i].updateProps(columnProps); }); if (this.visibleColumns.length < 1) { @@ -145,7 +144,7 @@ export class ColumnGroupStore implements IColumnGroupStore, IColumnParentStore { get conditions(): Array { return this.columnFilters.map((store, index) => { - return this._allColumns[index].isHidden ? undefined : store.condition2; + return this._allColumns[index].isHidden ? undefined : store.condition; }); } diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts index c9a062b6ac..0eb8609bed 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/GridPersonalizationStore.ts @@ -1,6 +1,6 @@ +import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings"; import { error, Result, value } from "@mendix/widget-plugin-filtering/result-meta"; import { 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"; import { ColumnId } from "../../typings/GridColumn"; diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts index 1e03c6b941..cc7e718a08 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/RootGridStore.ts @@ -1,16 +1,16 @@ +import { createContextWithStub, FilterAPI } from "@mendix/widget-plugin-filtering/context"; import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost"; -import { HeaderFiltersStore } from "@mendix/widget-plugin-filtering/stores/generic/HeaderFiltersStore"; +import { DatasourceController } from "@mendix/widget-plugin-grid/query/DatasourceController"; +import { RefreshController } from "@mendix/widget-plugin-grid/query/RefreshController"; import { BaseControllerHost } from "@mendix/widget-plugin-mobx-kit/BaseControllerHost"; import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; -import { autorun, computed } from "mobx"; +import { autorun } from "mobx"; import { DatagridContainerProps } from "../../../typings/DatagridProps"; -import { DatasourceController } from "../../controllers/DatasourceController"; import { DatasourceParamsController } from "../../controllers/DatasourceParamsController"; import { DerivedLoaderController } from "../../controllers/DerivedLoaderController"; import { PaginationController } from "../../controllers/PaginationController"; -import { RefreshController } from "../../controllers/RefreshController"; import { ProgressStore } from "../../features/data-export/ProgressStore"; import { StaticInfo } from "../../typings/static-info"; import { ColumnGroupStore } from "./ColumnGroupStore"; @@ -25,12 +25,12 @@ type Spec = { export class RootGridStore extends BaseControllerHost { columnsStore: ColumnGroupStore; - headerFiltersStore: HeaderFiltersStore; settingsStore: GridPersonalizationStore; staticInfo: StaticInfo; exportProgressCtrl: ProgressStore; loaderCtrl: DerivedLoaderController; paginationCtrl: PaginationController; + readonly autonomousFilterAPI: FilterAPI; private gate: Gate; @@ -38,9 +38,7 @@ export class RootGridStore extends BaseControllerHost { super(); const { props } = gate; - const [columnsInitFilter, headerInitFilter, sharedInitFilter] = DatasourceParamsController.unzipFilter( - props.datasource.filter - ); + const [columnsInitFilter, sharedInitFilter] = DatasourceParamsController.unzipFilter(props.datasource.filter); this.gate = gate; this.staticInfo = { @@ -49,17 +47,15 @@ export class RootGridStore extends BaseControllerHost { }; const customFilterHost = new CustomFilterHost(); const query = new DatasourceController(this, { gate }); + this.autonomousFilterAPI = createContextWithStub({ + filterObserver: customFilterHost, + sharedInitFilter, + parentChannelName: this.staticInfo.filtersChannelName + }); 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; @@ -67,12 +63,11 @@ export class RootGridStore extends BaseControllerHost { new DatasourceParamsController(this, { query, columns, - header, customFilters: customFilterHost }); new RefreshController(this, { - query: computed(() => query.computedCopy), + query: query.derivedQuery, delay: props.refreshInterval * 1000 }); @@ -87,7 +82,6 @@ export class RootGridStore extends BaseControllerHost { const [add, disposeAll] = disposeBatch(); add(super.setup()); add(this.columnsStore.setup()); - add(this.headerFiltersStore.setup()); add(() => this.settingsStore.dispose()); add(autorun(() => this.updateProps(this.gate.props))); diff --git a/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx b/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx index cf930ae8a3..c6559ff258 100644 --- a/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/helpers/state/column/ColumnFilterStore.tsx @@ -1,23 +1,22 @@ 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 { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore"; import { InputFilterStore, attrgroupFilterStore } from "@mendix/widget-plugin-filtering/stores/input/store-utils"; -import { ensure } from "@mendix/widget-plugin-platform/utils/ensure"; +import { FilterData } from "@mendix/filter-commons/typings/settings"; +import { value } from "@mendix/widget-plugin-filtering/result-meta"; +import { FilterObserver } from "@mendix/widget-plugin-filtering/typings/FilterObserver"; +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; +import { ListAttributeListValue, ListAttributeValue } from "mendix"; import { FilterCondition } from "mendix/filters"; -import { ListAttributeValue, ListAttributeListValue } from "mendix"; -import { action, computed, makeObservable } from "mobx"; +import { computed, makeObservable } from "mobx"; import { ReactNode, createElement } from "react"; import { ColumnsType } from "../../../../typings/DatagridProps"; import { StaticInfo } from "../../../typings/static-info"; -import { FilterData } from "@mendix/widget-plugin-filtering/typings/settings"; -import { value } from "@mendix/widget-plugin-filtering/result-meta"; -import { disposeFx } from "@mendix/widget-plugin-filtering/mobx-utils"; -import { FilterObserver } from "@mendix/widget-plugin-filtering/typings/FilterObserver"; + export interface IColumnFilterStore { renderFilterWidgets(): ReactNode; } -type FilterStore = InputFilterStore | StaticSelectFilterStore | RefFilterStore; +type FilterStore = InputFilterStore | StaticSelectFilterStore; const { Provider } = getGlobalFilterContextObject(); @@ -33,61 +32,20 @@ export class ColumnFilterStore implements IColumnFilterStore { this._filterStore = this.createFilterStore(props, dsViewState); this._context = this.createContext(this._filterStore, info); - makeObservable(this, { - _updateStore: action, - condition2: computed, - updateProps: action + makeObservable(this, { + condition: computed }); } setup(): () => void { - const [disposers, dispose] = disposeFx(); + const [add, disposeAll] = disposeBatch(); if (this._filterStore && "setup" in this._filterStore) { - disposers.push(this._filterStore.setup()); - } - return dispose; - } - - updateProps(props: ColumnsType): void { - this._widget = props.filter; - this._updateStore(props); - } - - private _updateStore(props: ColumnsType): void { - const store = this._filterStore; - - if (store === null) { - return; - } - - if (store.storeType === "refselect") { - store.updateProps(this.toRefselectProps(props)); - } else if (isListAttributeValue(props.attribute)) { - store.updateProps([props.attribute]); + add(this._filterStore.setup()); } - } - - private toRefselectProps(props: ColumnsType): RefFilterStoreProps { - const searchAttrId = props.filterAssociationOptionLabelAttr?.id; - const caption = - props.filterCaptionType === "expression" - ? ensure(props.filterAssociationOptionLabel, errorMessage("filterAssociationOptionLabel")) - : ensure(props.filterAssociationOptionLabelAttr, errorMessage("filterAssociationOptionLabelAttr")); - - return { - ref: ensure(props.filterAssociation, errorMessage("filterAssociation")), - datasource: ensure(props.filterAssociationOptions, errorMessage("filterAssociationOptions")), - searchAttrId, - fetchOptionsLazy: props.fetchOptionsLazy, - caption - }; + return disposeAll; } private createFilterStore(props: ColumnsType, dsViewState: FilterCondition | null): FilterStore | null { - if (props.filterAssociation) { - return new RefFilterStore(this.toRefselectProps(props), dsViewState); - } - if (isListAttributeValue(props.attribute)) { return attrgroupFilterStore(props.attribute.type, [props.attribute], dsViewState); } @@ -112,7 +70,7 @@ export class ColumnFilterStore implements IColumnFilterStore { return {this._widget}; } - get condition2(): FilterCondition | undefined { + get condition(): FilterCondition | undefined { return this._filterStore ? this._filterStore.condition : undefined; } @@ -135,9 +93,6 @@ const isListAttributeValue = ( return !!(attribute && attribute.isList === false); }; -const errorMessage = (propName: string): string => - `Can't map ColumnsType to AssociationProperties: ${propName} is undefined`; - export interface ObserverBag { customFilterHost: 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 6c64391741..663dcb3b32 100644 --- a/packages/pluggableWidgets/datagrid-web/src/typings/personalization-settings.ts +++ b/packages/pluggableWidgets/datagrid-web/src/typings/personalization-settings.ts @@ -1,4 +1,4 @@ -import { FilterData } from "@mendix/widget-plugin-filtering/typings/settings"; +import { FilterData } from "@mendix/filter-commons/typings/settings"; import { ColumnId } from "./GridColumn"; import { SortDirection, SortRule } from "./sorting"; diff --git a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx index 3a9a345e20..9a347d20aa 100644 --- a/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/utils/test-utils.tsx @@ -29,9 +29,7 @@ export const column = (header = "Test", patch?: (col: ColumnsType) => void): Col visible: dynamicValue(true), minWidth: "auto", minWidthLimit: 100, - allowEventPropagation: true, - fetchOptionsLazy: true, - filterCaptionType: "attribute" + allowEventPropagation: true }; if (patch) { diff --git a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts index 3749768048..d3d6a22f94 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts @@ -4,7 +4,7 @@ * @author Mendix Widgets Framework Team */ import { ComponentType, CSSProperties, ReactNode } from "react"; -import { ActionValue, DynamicValue, EditableValue, ListValue, ListActionValue, ListAttributeValue, ListAttributeListValue, ListExpressionValue, ListReferenceValue, ListReferenceSetValue, ListWidgetValue, SelectionSingleValue, SelectionMultiValue } from "mendix"; +import { ActionValue, DynamicValue, EditableValue, ListValue, ListActionValue, ListAttributeValue, ListAttributeListValue, ListExpressionValue, ListWidgetValue, SelectionSingleValue, SelectionMultiValue } from "mendix"; import { Big } from "big.js"; export type ItemSelectionMethodEnum = "checkbox" | "rowClick"; @@ -15,8 +15,6 @@ export type LoadingTypeEnum = "spinner" | "skeleton"; export type ShowContentAsEnum = "attribute" | "dynamicText" | "customContent"; -export type FilterCaptionTypeEnum = "attribute" | "expression"; - export type HidableEnum = "yes" | "hidden" | "no"; export type WidthEnum = "autoFill" | "autoFit" | "manual"; @@ -35,12 +33,6 @@ export interface ColumnsType { tooltip?: ListExpressionValue; filter?: ReactNode; visible: DynamicValue; - filterAssociation?: ListReferenceValue | ListReferenceSetValue; - filterAssociationOptions?: ListValue; - fetchOptionsLazy: boolean; - filterCaptionType: FilterCaptionTypeEnum; - filterAssociationOptionLabel?: ListExpressionValue; - filterAssociationOptionLabelAttr?: ListAttributeValue; sortable: boolean; resizable: boolean; draggable: boolean; @@ -81,12 +73,6 @@ export interface ColumnsPreviewType { tooltip: string; filter: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; visible: string; - filterAssociation: string; - filterAssociationOptions: {} | { caption: string } | { type: string } | null; - fetchOptionsLazy: boolean; - filterCaptionType: FilterCaptionTypeEnum; - filterAssociationOptionLabel: string; - filterAssociationOptionLabelAttr: string; sortable: boolean; resizable: boolean; draggable: boolean; diff --git a/packages/pluggableWidgets/gallery-web/package.json b/packages/pluggableWidgets/gallery-web/package.json index 86577461ce..59300fa1e4 100644 --- a/packages/pluggableWidgets/gallery-web/package.json +++ b/packages/pluggableWidgets/gallery-web/package.json @@ -43,6 +43,7 @@ "dependencies": { "@mendix/widget-plugin-external-events": "workspace:*", "@mendix/widget-plugin-filtering": "workspace:*", + "@mendix/widget-plugin-mobx-kit": "workspace:^", "@mendix/widget-plugin-sorting": "workspace:*", "classnames": "^2.3.2", "mobx": "6.12.3", diff --git a/packages/pluggableWidgets/gallery-web/src/stores/RootG2Store.ts b/packages/pluggableWidgets/gallery-web/src/stores/RootG2Store.ts new file mode 100644 index 0000000000..cb2622b3bd --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/src/stores/RootG2Store.ts @@ -0,0 +1,120 @@ +import { compactArray, fromCompactArray, isAnd } from "@mendix/filter-commons/condition-utils"; +import { createContextWithStub, FilterAPI } from "@mendix/widget-plugin-filtering/context"; +import { CustomFilterHost } from "@mendix/widget-plugin-filtering/stores/generic/CustomFilterHost"; +import { DatasourceController } from "@mendix/widget-plugin-grid/query/DatasourceController"; +import { PaginationController } from "@mendix/widget-plugin-grid/query/PaginationController"; +import { RefreshController } from "@mendix/widget-plugin-grid/query/RefreshController"; +import { BaseControllerHost } from "@mendix/widget-plugin-mobx-kit/BaseControllerHost"; +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; +import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller"; +import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; +import { ListValue } from "mendix"; +import { FilterCondition } from "mendix/filters"; +import { makeAutoObservable, reaction } from "mobx"; +import { PaginationEnum } from "../../typings/GalleryProps"; + +interface DynamicProps { + datasource: ListValue; +} + +interface StaticProps { + pagination: PaginationEnum; + showPagingButtons: "always" | "auto"; + showTotalCount: boolean; + pageSize: number; +} + +type Gate = DerivedPropsGate; + +type GalleryStoreSpec = StaticProps & { + gate: Gate; +}; + +export class GalleryStore extends BaseControllerHost { + private readonly _id: string; + private readonly _query: DatasourceController; + readonly paging: PaginationController; + readonly filterAPI: FilterAPI; + + constructor(spec: GalleryStoreSpec) { + super(); + + this._id = `GalleryStore@${generateUUID()}`; + + this._query = new DatasourceController(this, { gate: spec.gate }); + + this.paging = new PaginationController(this, { + gate: undefined, + query: this._query, + pageSize: spec.pageSize, + pagination: spec.pagination, + showPagingButtons: spec.showPagingButtons, + showTotalCount: true + }); + + const filterObserver = new CustomFilterHost(); + + const paramCtrl = new QueryParamsController(this, this._query, filterObserver); + + this.filterAPI = createContextWithStub({ + filterObserver, + parentChannelName: this._id, + sharedInitFilter: paramCtrl.unzipFilter(spec.gate.props.datasource.filter) + }); + + new RefreshController(this, { + delay: 0, + query: this._query.derivedQuery + }); + } +} + +class QueryParamsController implements ReactiveController { + private readonly _query: DatasourceController; + private readonly _filters: CustomFilterHost; + + constructor(host: ReactiveControllerHost, query: DatasourceController, filters: CustomFilterHost) { + host.addController(this); + + this._query = query; + this._filters = filters; + + makeAutoObservable(this, { setup: false }); + } + + private get _derivedSortOrder(): ListValue["sortOrder"] { + return []; + } + + private get _derivedFilter(): FilterCondition { + return compactArray(this._filters.conditions); + } + + setup(): () => void { + const [add, disposeAll] = disposeBatch(); + add( + reaction( + () => this._derivedSortOrder, + sortOrder => this._query.setSortOrder(sortOrder), + { fireImmediately: true } + ) + ); + add( + reaction( + () => this._derivedFilter, + filter => this._query.setFilter(filter), + { fireImmediately: true } + ) + ); + + return disposeAll; + } + + unzipFilter(filter?: FilterCondition): Array { + if (!filter || !isAnd(filter)) { + return []; + } + return fromCompactArray(filter); + } +} diff --git a/packages/shared/filter-commons/.prettierrc.cjs b/packages/shared/filter-commons/.prettierrc.cjs new file mode 100644 index 0000000000..0892704ab0 --- /dev/null +++ b/packages/shared/filter-commons/.prettierrc.cjs @@ -0,0 +1 @@ +module.exports = require("@mendix/prettier-config-web-widgets"); diff --git a/packages/shared/filter-commons/__mocks__/mendix/filters/builders.js b/packages/shared/filter-commons/__mocks__/mendix/filters/builders.js new file mode 100644 index 0000000000..28fe810c06 --- /dev/null +++ b/packages/shared/filter-commons/__mocks__/mendix/filters/builders.js @@ -0,0 +1 @@ +module.exports = require("@mendix/widget-plugin-test-utils/__mocks__/mendix/filters/builders.js"); diff --git a/packages/shared/filter-commons/eslint.config.mjs b/packages/shared/filter-commons/eslint.config.mjs new file mode 100644 index 0000000000..ed68ae9e78 --- /dev/null +++ b/packages/shared/filter-commons/eslint.config.mjs @@ -0,0 +1,3 @@ +import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs"; + +export default config; diff --git a/packages/shared/filter-commons/jest.config.cjs b/packages/shared/filter-commons/jest.config.cjs new file mode 100644 index 0000000000..3f605bedaa --- /dev/null +++ b/packages/shared/filter-commons/jest.config.cjs @@ -0,0 +1,26 @@ +module.exports = { + modulePathIgnorePatterns: ["/dist/"], + transform: { + "^.+\\.(t|j)sx?$": [ + "@swc/jest", + { + jsc: { + transform: { + react: { + runtime: "automatic" + } + } + } + } + ] + }, + moduleDirectories: ["node_modules", "src"], + moduleNameMapper: { + "big.js": "big.js", + "(.+)\\.js": "$1" + }, + extensionsToTreatAsEsm: [".ts"], + testEnvironment: "jsdom", + collectCoverage: !process.env.CI, + coverageProvider: "v8" +}; diff --git a/packages/shared/filter-commons/package.json b/packages/shared/filter-commons/package.json new file mode 100644 index 0000000000..9405077fae --- /dev/null +++ b/packages/shared/filter-commons/package.json @@ -0,0 +1,48 @@ +{ + "name": "@mendix/filter-commons", + "version": "0.1.0", + "description": "Common filter utilities and types for filter widgets", + "copyright": "© Mendix Technology BV 2025. All rights reserved.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/mendix/web-widgets.git" + }, + "type": "module", + "files": [ + "dist", + "!*.map" + ], + "exports": { + "./*": "./dist/*.js" + }, + "typesVersions": { + "*": { + "*": [ + "dist/*" + ] + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "format": "prettier --write .", + "lint": "eslint src/ package.json", + "prepare": "tsc", + "test": "jest" + }, + "dependencies": { + "mendix": "^10.21.64362" + }, + "peerDependencies": { + "mobx": "6.12.3", + "mobx-react-lite": "4.0.7" + }, + "devDependencies": { + "@mendix/eslint-config-web-widgets": "workspace:*", + "@mendix/prettier-config-web-widgets": "workspace:*", + "@mendix/tsconfig-web-widgets": "workspace:*", + "@mendix/widget-plugin-test-utils": "workspace:*", + "@swc/core": "^1.7.26" + } +} diff --git a/packages/shared/widget-plugin-filtering/src/__tests__/condition-utils.spec.ts b/packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts similarity index 100% rename from packages/shared/widget-plugin-filtering/src/__tests__/condition-utils.spec.ts rename to packages/shared/filter-commons/src/__tests__/condition-utils.spec.ts diff --git a/packages/shared/widget-plugin-filtering/src/condition-utils.ts b/packages/shared/filter-commons/src/condition-utils.ts similarity index 100% rename from packages/shared/widget-plugin-filtering/src/condition-utils.ts rename to packages/shared/filter-commons/src/condition-utils.ts diff --git a/packages/shared/widget-plugin-filtering/src/typings/FilterFunctions.ts b/packages/shared/filter-commons/src/typings/FilterFunctions.ts similarity index 99% rename from packages/shared/widget-plugin-filtering/src/typings/FilterFunctions.ts rename to packages/shared/filter-commons/src/typings/FilterFunctions.ts index 7fc85213c1..13cc4f228a 100644 --- a/packages/shared/widget-plugin-filtering/src/typings/FilterFunctions.ts +++ b/packages/shared/filter-commons/src/typings/FilterFunctions.ts @@ -1,6 +1,9 @@ export type FilterFunctionNonValue = "empty" | "notEmpty"; + export type FilterFunctionGeneric = "equal" | "notEqual" | "greater" | "greaterEqual" | "smaller" | "smallerEqual"; + export type FilterFunctionBinary = "between"; // | "betweenEqRight" | "betweenEqLeft" | "betweenEqBoth"; + export type FilterFunctionString = "contains" | "startsWith" | "endsWith"; export type AllFunctions = FilterFunctionNonValue | FilterFunctionGeneric | FilterFunctionBinary | FilterFunctionString; diff --git a/packages/shared/widget-plugin-filtering/src/typings/IJSActionsControlled.ts b/packages/shared/filter-commons/src/typings/IJSActionsControlled.ts similarity index 100% rename from packages/shared/widget-plugin-filtering/src/typings/IJSActionsControlled.ts rename to packages/shared/filter-commons/src/typings/IJSActionsControlled.ts diff --git a/packages/shared/filter-commons/src/typings/mendix.ts b/packages/shared/filter-commons/src/typings/mendix.ts new file mode 100644 index 0000000000..528a614adf --- /dev/null +++ b/packages/shared/filter-commons/src/typings/mendix.ts @@ -0,0 +1,3 @@ +import { FilterCondition } from "mendix/filters"; + +export type FilterName = FilterCondition extends { name: infer Name } ? Name : never; diff --git a/packages/shared/widget-plugin-filtering/src/typings/settings.ts b/packages/shared/filter-commons/src/typings/settings.ts similarity index 100% rename from packages/shared/widget-plugin-filtering/src/typings/settings.ts rename to packages/shared/filter-commons/src/typings/settings.ts diff --git a/packages/shared/filter-commons/tsconfig.json b/packages/shared/filter-commons/tsconfig.json new file mode 100644 index 0000000000..052cc1cee7 --- /dev/null +++ b/packages/shared/filter-commons/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@mendix/tsconfig-web-widgets/esm-library-with-jsx", + "include": ["./src/**/*"], + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true + } +} diff --git a/packages/shared/widget-plugin-dropdown-filter/.prettierrc.cjs b/packages/shared/widget-plugin-dropdown-filter/.prettierrc.cjs new file mode 100644 index 0000000000..0892704ab0 --- /dev/null +++ b/packages/shared/widget-plugin-dropdown-filter/.prettierrc.cjs @@ -0,0 +1 @@ +module.exports = require("@mendix/prettier-config-web-widgets"); diff --git a/packages/shared/widget-plugin-dropdown-filter/eslint.config.mjs b/packages/shared/widget-plugin-dropdown-filter/eslint.config.mjs new file mode 100644 index 0000000000..ed68ae9e78 --- /dev/null +++ b/packages/shared/widget-plugin-dropdown-filter/eslint.config.mjs @@ -0,0 +1,3 @@ +import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs"; + +export default config; diff --git a/packages/shared/widget-plugin-dropdown-filter/jest.config.cjs b/packages/shared/widget-plugin-dropdown-filter/jest.config.cjs new file mode 100644 index 0000000000..3f605bedaa --- /dev/null +++ b/packages/shared/widget-plugin-dropdown-filter/jest.config.cjs @@ -0,0 +1,26 @@ +module.exports = { + modulePathIgnorePatterns: ["/dist/"], + transform: { + "^.+\\.(t|j)sx?$": [ + "@swc/jest", + { + jsc: { + transform: { + react: { + runtime: "automatic" + } + } + } + } + ] + }, + moduleDirectories: ["node_modules", "src"], + moduleNameMapper: { + "big.js": "big.js", + "(.+)\\.js": "$1" + }, + extensionsToTreatAsEsm: [".ts"], + testEnvironment: "jsdom", + collectCoverage: !process.env.CI, + coverageProvider: "v8" +}; diff --git a/packages/shared/widget-plugin-dropdown-filter/package.json b/packages/shared/widget-plugin-dropdown-filter/package.json new file mode 100644 index 0000000000..41ffc4217b --- /dev/null +++ b/packages/shared/widget-plugin-dropdown-filter/package.json @@ -0,0 +1,41 @@ +{ + "name": "@mendix/widget-plugin-dropdown-filter", + "version": "1.0.0", + "description": "Drop-down filter widget plugin", + "copyright": "© Mendix Technology BV 2025. All rights reserved.", + "license": "Apache-2.0", + "type": "module", + "files": [ + "dist", + "!*.map" + ], + "exports": { + "./*": "./dist/*.js" + }, + "typesVersions": { + "*": { + "*": [ + "dist/*" + ] + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "format": "prettier --write .", + "lint": "eslint src/ package.json", + "test": "jest" + }, + "dependencies": { + "@mendix/widget-plugin-mobx-kit": "workspace:^", + "downshift": "^9.0.9", + "mendix": "^10.21.64362", + "mobx": "6.12.3", + "mobx-react-lite": "4.0.7" + }, + "devDependencies": { + "@mendix/eslint-config-web-widgets": "workspace:*", + "@mendix/prettier-config-web-widgets": "workspace:*", + "@mendix/tsconfig-web-widgets": "workspace:*" + } +} diff --git a/packages/shared/widget-plugin-dropdown-filter/src/__tests__/RefFilterStore.spec.ts b/packages/shared/widget-plugin-dropdown-filter/src/__tests__/RefFilterStore.spec.ts new file mode 100644 index 0000000000..c7b800b61f --- /dev/null +++ b/packages/shared/widget-plugin-dropdown-filter/src/__tests__/RefFilterStore.spec.ts @@ -0,0 +1,253 @@ +// describe("RefFilterStore", () => { +// afterEach(() => _resetGlobalState()); + +// describe("get options()", () => { +// let items: ObjectItem[]; +// let mapItem: (item: ObjectItem) => string; +// let store: RefFilterStore; +// beforeEach(() => { +// const [a, b, c] = [obj("id3n"), obj("f8x3"), obj("932c")]; +// items = [a, b, c]; +// mapItem = cases([a, "Alice"], [b, "Bob"], [c, "Chuck"], [undefined, "No caption"]); +// store = new RefFilterStore( +// { +// ref: listReference(), +// datasource: list(items), +// caption: listExpression(mapItem) +// }, +// null +// ); +// }); + +// it("returns array of options", () => { +// expect(store.options).toMatchInlineSnapshot(` +// [ +// { +// "caption": "Alice", +// "selected": false, +// "value": "obj_id3n", +// }, +// { +// "caption": "Bob", +// "selected": false, +// "value": "obj_f8x3", +// }, +// { +// "caption": "Chuck", +// "selected": false, +// "value": "obj_932c", +// }, +// ] +// `); +// }); +// }); + +// describe("get condition()", () => { +// it.todo("return filter condition using equals"); +// }); + +// describe("toJSON()", () => { +// let store: RefFilterStore; +// beforeEach(() => { +// store = new RefFilterStore( +// { +// ref: listReference(), +// datasource: list(3), +// caption: listExpression(() => "[string]") +// }, +// null +// ); +// }); + +// it("returns state as JS array", () => { +// expect(store.toJSON()).toEqual([]); +// }); + +// it("save state with one item", () => { +// const guid = store.options[0].value; +// store.toggle(guid); +// expect(store.toJSON()).toEqual([guid]); +// }); + +// it("save state with multiple items", () => { +// const [guid1, guid2] = [store.options[0].value, store.options[1].value]; +// store.toggle(guid1); +// store.toggle(guid2); +// const output = store.toJSON(); + +// expect(output).toHaveLength(2); +// expect(output).toContain(guid1); +// expect(output).toContain(guid2); +// }); +// }); + +// describe("fromJSON()", () => { +// let store: RefFilterStore; +// beforeEach(() => { +// store = new RefFilterStore( +// { +// ref: listReference(), +// datasource: list([obj("id3n"), obj("f8x3"), obj("932c")]), +// caption: listExpression(() => "[string]") +// }, +// null +// ); +// }); + +// it("restore state from JS array", () => { +// const output: Array = []; +// autorun(() => output.push(store.options)); + +// // Check that every option is unselected +// expect(output[0].map(option => option.selected)).toEqual([false, false, false]); +// // Restore state +// store.fromJSON(["obj_id3n"]); +// // Check that first option is selected +// expect(output[1].map(option => option.selected)).toEqual([true, false, false]); +// }); + +// it("overrides any previous state", () => { +// const output: Array = []; + +// // Select each option +// store.options.forEach(option => { +// store.toggle(option.value); +// }); +// // Start observing +// autorun(() => output.push(store.options)); +// // Check the state +// expect(output[0].map(option => option.selected)).toEqual([true, true, true]); +// store.fromJSON(["obj_932c"]); +// expect(output[1].map(option => option.selected)).toEqual([false, false, true]); +// }); + +// it("should not change state if json is null", () => { +// const state = store.toJSON(); +// store.fromJSON(null); +// expect(store.toJSON()).toEqual(state); +// }); +// }); + +// describe("toggle()", () => { +// let store: RefFilterStore; +// beforeEach(() => { +// store = new RefFilterStore( +// { +// ref: listReference(), +// datasource: list([obj("id3n"), obj("f8x3"), obj("932c")]), +// caption: listExpression(() => "[string]") +// }, +// null +// ); +// }); + +// it("compute new options", () => { +// const output: Array = []; +// const [x, y] = store.options; + +// autorun(() => output.push(store.options)); +// expect(output[0].map(opt => opt.selected)).toEqual([false, false, false]); + +// store.toggle(x.value); +// store.toggle(y.value); +// expect(output[2].map(opt => opt.selected)).toEqual([true, true, false]); + +// store.toggle(x.value); +// expect(output[3].map(opt => opt.selected)).toEqual([false, true, false]); +// }); +// }); + +// describe("with 'fetchOptionsLazy' flag", () => { +// it("should set limit to 0 on options datasource", () => { +// const datasource = list.loading(); +// const props = { +// ref: listReference(), +// datasource, +// caption: listExpression(() => "[string]"), +// fetchOptionsLazy: true +// }; + +// new RefFilterStore(props, null); + +// expect(datasource.setLimit).toHaveBeenLastCalledWith(0); +// }); + +// it("should not change limit if flag is false", () => { +// const datasource = list.loading(); +// const props = { +// ref: listReference(), +// datasource, +// caption: listExpression(() => "[string]"), +// fetchOptionsLazy: false +// }; + +// new RefFilterStore(props, null); + +// expect(datasource.setLimit).not.toHaveBeenCalled(); +// }); +// }); + +// describe("when datasource changed", () => { +// it("compute new options", () => { +// const a = obj("id3n"); +// const props = { +// ref: listReference(), +// datasource: list.loading(), +// caption: listExpression(cases([a, "Alice"], [undefined, "No caption"])) +// }; +// const store = new RefFilterStore(props, null); +// const output: any[] = []; +// autorun(() => output.push(store.options)); + +// expect(output[0]).toEqual([]); +// store.updateProps({ ...props, datasource: list([a]) }); +// expect(output[1]).toEqual([ +// { +// caption: "Alice", +// selected: false, +// value: "obj_id3n" +// } +// ]); +// }); +// }); + +// describe("json data filtering", () => { +// const savedJson = ["obj_xx", "obj_xiii", "obj_deleted", "obj_yy", "obj_unknown"]; +// let a: ObjectItem; +// let b: ObjectItem; +// let props: Omit; +// beforeEach(() => { +// a = obj("xx"); +// b = obj("yy"); +// props = { +// ref: listReference(), +// caption: listExpression(() => "[string]") +// }; +// }); + +// it("skip filtering if has just part of the list", () => { +// const store = new RefFilterStore({ ...props, datasource: list.loading() }, null); +// // Restore state +// store.fromJSON(savedJson); +// // Check state +// expect(store.toJSON()).toEqual(savedJson); +// // Update with partial data +// const datasource = new ListValueBuilder().withItems([a, b]).withHasMore(true).build(); +// store.updateProps({ ...props, datasource }); +// // Check that state still has unknown GUIDs +// expect(store.toJSON()).toEqual(savedJson); +// }); + +// it("allows to restore any state if datasource is loading", () => { +// const store = new RefFilterStore({ ...props, datasource: list.loading() }, null); +// // Restore state +// store.fromJSON(savedJson); +// // Check state +// expect(store.toJSON()).toEqual(savedJson); +// }); +// }); +// }); + +describe("RefFilterStore", () => { + it.todo("should be implemented"); +}); diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilterContainer.tsx b/packages/shared/widget-plugin-dropdown-filter/src/containers/RefFilterContainer.tsx similarity index 70% rename from packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilterContainer.tsx rename to packages/shared/widget-plugin-dropdown-filter/src/containers/RefFilterContainer.tsx index 5b332e833e..fe4a658567 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/RefFilterContainer.tsx +++ b/packages/shared/widget-plugin-dropdown-filter/src/containers/RefFilterContainer.tsx @@ -1,18 +1,22 @@ -import { RefSelectController } from "@mendix/widget-plugin-filtering/controllers/picker/RefSelectController"; -import { RefComboboxController } from "@mendix/widget-plugin-filtering/controllers/picker/RefComboboxController"; -import { RefTagPickerController } from "@mendix/widget-plugin-filtering/controllers/picker/RefTagPickerController"; -import { Select } from "@mendix/widget-plugin-filtering/controls/select/Select"; -import { Combobox } from "@mendix/widget-plugin-filtering/controls/combobox/Combobox"; -import { TagPicker } from "@mendix/widget-plugin-filtering/controls/tag-picker/TagPicker"; -import { usePickerJSActions } from "@mendix/widget-plugin-filtering/helpers/usePickerJSActions"; -import { RefFilterStore } from "@mendix/widget-plugin-filtering/stores/picker/RefFilterStore"; +import { RefSelectController } from "../controllers/RefSelectController"; +import { RefComboboxController } from "../controllers/RefComboboxController"; +import { RefTagPickerController } from "../controllers/RefTagPickerController"; +import { Select } from "../controls/select/Select"; +import { Combobox } from "../controls/combobox/Combobox"; +import { TagPicker } from "../controls/tag-picker/TagPicker"; +import { usePickerJSActions } from "../helpers/usePickerJSActions"; +import { RefFilterStore } from "../stores/RefFilterStore"; import { ActionValue, EditableValue } from "mendix"; import { observer } from "mobx-react-lite"; -import { createElement, CSSProperties } from "react"; -import { useSetupUpdate } from "@mendix/widget-plugin-filtering/helpers/useSetupUpdate"; -import { useFrontendType } from "../hooks/useFrontendType"; -import { SelectedItemsStyleEnum, SelectionMethodEnum } from "../../typings/DatagridDropdownFilterProps"; +import { createElement, CSSProperties, useEffect } from "react"; + +import { useFrontendType } from "../helpers/useFrontendType"; import { useOnScrollBottom } from "@mendix/widget-plugin-hooks/useOnScrollBottom"; +import { SelectedItemsStyleEnum, SelectionMethodEnum } from "../typings/widget"; +import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup"; +import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; +import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider"; +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; export interface RefFilterContainerProps { ariaLabel?: string; @@ -48,7 +52,8 @@ function Container(props: RefFilterContainerProps): React.ReactElement { } const SelectWidget = observer(function SelectWidget(props: RefFilterContainerProps): React.ReactElement { - const ctrl1 = useSetupUpdate(() => new RefSelectController(props), props); + const gate = useGate(props); + const ctrl1 = useSetup(() => new RefSelectController({ gate })); const handleMenuScroll = useOnScrollBottom(ctrl1.handleMenuScrollEnd, { triggerZoneHeight: 100 }); usePickerJSActions(ctrl1, props); @@ -71,7 +76,8 @@ const SelectWidget = observer(function SelectWidget(props: RefFilterContainerPro }); const ComboboxWidget = observer(function ComboboxWidget(props: RefFilterContainerProps): React.ReactElement { - const ctrl2 = useSetupUpdate(() => new RefComboboxController(props), props); + const gate = useGate(props); + const ctrl2 = useSetup(() => new RefComboboxController({ gate })); const handleMenuScroll = useOnScrollBottom(ctrl2.handleMenuScrollEnd, { triggerZoneHeight: 100 }); usePickerJSActions(ctrl2, props); @@ -93,7 +99,8 @@ const ComboboxWidget = observer(function ComboboxWidget(props: RefFilterContaine }); const TagPickerWidget = observer(function TagPickerWidget(props: RefFilterContainerProps): React.ReactElement { - const ctrl3 = useSetupUpdate(() => new RefTagPickerController(props), props); + const gate = useGate(props); + const ctrl3 = useSetup(() => new RefTagPickerController({ gate })); const handleMenuScroll = useOnScrollBottom(ctrl3.handleMenuScrollEnd, { triggerZoneHeight: 100 }); usePickerJSActions(ctrl3, props); @@ -119,3 +126,9 @@ const TagPickerWidget = observer(function TagPickerWidget(props: RefFilterContai }); export const RefFilterContainer = Container; + +function useGate(props: RefFilterContainerProps): DerivedPropsGate { + const gp = useConst(() => new GateProvider(props)); + useEffect(() => gp.setProps(props)); + return gp.gate; +} diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/StaticFilterContainer.tsx b/packages/shared/widget-plugin-dropdown-filter/src/containers/StaticFilterContainer.tsx similarity index 66% rename from packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/StaticFilterContainer.tsx rename to packages/shared/widget-plugin-dropdown-filter/src/containers/StaticFilterContainer.tsx index cd0e25c46f..f06728949d 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/components/StaticFilterContainer.tsx +++ b/packages/shared/widget-plugin-dropdown-filter/src/containers/StaticFilterContainer.tsx @@ -1,22 +1,21 @@ -import { StaticSelectController } from "@mendix/widget-plugin-filtering/controllers/picker/StaticSelectController"; -import { StaticComboboxController } from "@mendix/widget-plugin-filtering/controllers/picker/StaticComboboxController"; -import { Select } from "@mendix/widget-plugin-filtering/controls/select/Select"; -import { Combobox } from "@mendix/widget-plugin-filtering/controls/combobox/Combobox"; +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; +import { useConst } from "@mendix/widget-plugin-mobx-kit/react/useConst"; +import { GateProvider } from "@mendix/widget-plugin-mobx-kit/GateProvider"; import { ActionValue, EditableValue } from "mendix"; import { observer } from "mobx-react-lite"; -import { createElement, CSSProperties } from "react"; -import { - FilterOptionsType, - SelectedItemsStyleEnum, - SelectionMethodEnum -} from "../../typings/DatagridDropdownFilterProps"; +import { createElement, CSSProperties, useEffect } from "react"; +import { StaticComboboxController } from "../controllers/StaticComboboxController"; +import { StaticSelectController } from "../controllers/StaticSelectController"; +import { StaticTagPickerController } from "../controllers/StaticTagPickerController"; +import { Combobox } from "../controls/combobox/Combobox"; +import { Select } from "../controls/select/Select"; +import { TagPicker } from "../controls/tag-picker/TagPicker"; +import { useFrontendType } from "../helpers/useFrontendType"; +import { usePickerJSActions } from "../helpers/usePickerJSActions"; import { withCustomOptionsGuard } from "../hocs/withCustomOptionsGuard"; -import { StaticSelectFilterStore } from "@mendix/widget-plugin-filtering/stores/picker/StaticSelectFilterStore"; -import { StaticTagPickerController } from "@mendix/widget-plugin-filtering/controllers/picker/StaticTagPickerController"; -import { TagPicker } from "@mendix/widget-plugin-filtering/controls/tag-picker/TagPicker"; -import { useSetupUpdate } from "@mendix/widget-plugin-filtering/helpers/useSetupUpdate"; -import { usePickerJSActions } from "@mendix/widget-plugin-filtering/helpers/usePickerJSActions"; -import { useFrontendType } from "../hooks/useFrontendType"; +import { StaticSelectFilterStore } from "../stores/StaticSelectFilterStore"; +import { FilterOptionsType, SelectedItemsStyleEnum, SelectionMethodEnum } from "../typings/widget"; +import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup"; export interface StaticFilterContainerProps { ariaLabel?: string; @@ -53,7 +52,8 @@ function Container(props: StaticFilterContainerProps): React.ReactElement { } const SelectWidget = observer(function SelectWidget(props: StaticFilterContainerProps): React.ReactElement { - const ctrl1 = useSetupUpdate(() => new StaticSelectController(props), props); + const gate = useGate(props); + const ctrl1 = useSetup(() => new StaticSelectController({ gate })); usePickerJSActions(ctrl1, props); @@ -73,7 +73,8 @@ const SelectWidget = observer(function SelectWidget(props: StaticFilterContainer }); const ComboboxWidget = observer(function ComboboxWidget(props: StaticFilterContainerProps): React.ReactElement { - const ctrl2 = useSetupUpdate(() => new StaticComboboxController(props), props); + const gate = useGate(props); + const ctrl2 = useSetup(() => new StaticComboboxController({ gate })); usePickerJSActions(ctrl2, props); @@ -93,7 +94,8 @@ const ComboboxWidget = observer(function ComboboxWidget(props: StaticFilterConta }); const TagPickerWidget = observer(function TagPickerWidget(props: StaticFilterContainerProps): React.ReactElement { - const ctrl3 = useSetupUpdate(() => new StaticTagPickerController(props), props); + const gate = useGate(props); + const ctrl3 = useSetup(() => new StaticTagPickerController({ gate })); usePickerJSActions(ctrl3, props); @@ -116,3 +118,9 @@ const TagPickerWidget = observer(function TagPickerWidget(props: StaticFilterCon }); export const StaticFilterContainer = withCustomOptionsGuard(Container); + +function useGate(props: StaticFilterContainerProps): DerivedPropsGate { + const gp = useConst(() => new GateProvider(props)); + useEffect(() => gp.setProps(props)); + return gp.gate; +} diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerBaseController.ts similarity index 73% rename from packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerBaseController.ts index a762277879..a834478824 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/PickerBaseController.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerBaseController.ts @@ -1,9 +1,10 @@ +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { ActionValue, EditableValue } from "mendix"; -import { OptionsSerializer } from "../../stores/picker/OptionsSerializer"; -import { IJSActionsControlled, ResetHandler, SetValueHandler } from "../../typings/IJSActionsControlled"; -import { OptionWithState } from "../../typings/OptionWithState"; -import { PickerChangeHelper } from "../generic/PickerChangeHelper"; -import { PickerJSActionsHelper } from "../generic/PickerJSActionsHelper"; +import { OptionsSerializer } from "../stores/OptionsSerializer"; +import { IJSActionsControlled, ResetHandler, SetValueHandler } from "../typings/IJSActionsControlled"; +import { OptionWithState } from "../typings/OptionWithState"; +import { PickerChangeHelper } from "./PickerChangeHelper"; +import { PickerJSActionsHelper } from "./PickerJSActionsHelper"; interface FilterStore { reset: () => void; @@ -13,6 +14,17 @@ interface FilterStore { options: OptionWithState[]; } +export interface PickerBaseControllerProps { + defaultValue?: string; + filterStore: S; + multiselect: boolean; + onChange?: ActionValue; + valueAttribute?: EditableValue; + emptyCaption?: string; +} + +type Gate = DerivedPropsGate>; + export class PickerBaseController implements IJSActionsControlled { protected actionHelper: PickerJSActionsHelper; protected changeHelper: PickerChangeHelper; @@ -21,9 +33,10 @@ export class PickerBaseController implements IJSActionsCo filterStore: S; multiselect: boolean; - constructor(props: PickerBaseControllerProps) { + constructor({ gate, multiselect }: { gate: Gate; multiselect: boolean }) { + const props = gate.props; this.filterStore = props.filterStore; - this.multiselect = props.multiselect; + this.multiselect = multiselect; this.serializer = new OptionsSerializer({ store: this.filterStore }); this.defaultValue = this.parseDefaultValue(props.defaultValue); this.actionHelper = new PickerJSActionsHelper({ @@ -31,7 +44,7 @@ export class PickerBaseController implements IJSActionsCo parse: value => this.serializer.fromStorableValue(value) ?? [], multiselect: props.multiselect }); - this.changeHelper = new PickerChangeHelper(props, () => this.serializer.value); + this.changeHelper = new PickerChangeHelper(gate, () => this.serializer.value); } parseDefaultValue = (value: string | undefined): Iterable | undefined => { @@ -51,12 +64,3 @@ export class PickerBaseController implements IJSActionsCo this.actionHelper.handleResetValue(...args); }; } - -export interface PickerBaseControllerProps { - defaultValue?: string; - filterStore: S; - multiselect: boolean; - onChange?: ActionValue; - valueAttribute?: EditableValue; - emptyCaption?: string; -} diff --git a/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerChangeHelper.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts similarity index 54% rename from packages/shared/widget-plugin-filtering/src/controllers/generic/PickerChangeHelper.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts index f4a51110f3..d74bfdce29 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerChangeHelper.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerChangeHelper.ts @@ -1,3 +1,4 @@ +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; import { ActionValue, EditableValue } from "mendix"; import { IReactionDisposer, reaction } from "mobx"; @@ -8,28 +9,21 @@ interface Props { } export class PickerChangeHelper { - private onChange?: ActionValue; - private valueAttribute?: EditableValue; + private readonly gate: DerivedPropsGate; private valueFn: () => string | undefined; - constructor(props: Props, valueFn: () => string | undefined) { - this.onChange = props.onChange; - this.valueAttribute = props.valueAttribute; + constructor(gate: DerivedPropsGate, valueFn: () => string | undefined) { + this.gate = gate; this.valueFn = valueFn; } setup(): IReactionDisposer { const effect = (value: string | undefined): void => { - this.valueAttribute?.setValue(value); - - executeAction(this.onChange); + const { valueAttribute, onChange } = this.gate.props; + valueAttribute?.setValue(value); + executeAction(onChange); }; return reaction(this.valueFn, effect); } - - updateProps(props: Props): void { - this.onChange = props.onChange; - this.valueAttribute = props.valueAttribute; - } } diff --git a/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerJSActionsHelper.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerJSActionsHelper.ts similarity index 96% rename from packages/shared/widget-plugin-filtering/src/controllers/generic/PickerJSActionsHelper.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerJSActionsHelper.ts index 3cd56a159e..c71b741cb7 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/generic/PickerJSActionsHelper.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/PickerJSActionsHelper.ts @@ -1,4 +1,4 @@ -import { IJSActionsControlled, ResetHandler, SetValueHandler } from "../../typings/IJSActionsControlled"; +import { IJSActionsControlled, ResetHandler, SetValueHandler } from "../typings/IJSActionsControlled"; interface FilterStore { reset: () => void; diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefBaseController.ts similarity index 52% rename from packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/RefBaseController.ts index c355a2e530..eabd9ad9df 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefBaseController.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefBaseController.ts @@ -1,31 +1,24 @@ +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { ActionValue, EditableValue } from "mendix"; -import { action, makeObservable } from "mobx"; -import { disposeFx } from "../../mobx-utils"; -import { RefFilterStore } from "../../stores/picker/RefFilterStore"; +import { RefFilterStore } from "../stores/RefFilterStore"; import { PickerBaseController } from "./PickerBaseController"; export class RefBaseController extends PickerBaseController { - constructor(props: RefBaseControllerProps) { - super(props); - makeObservable(this, { - updateProps: action - }); + constructor({ gate, multiselect }: { gate: DerivedPropsGate; multiselect: boolean }) { + super({ gate, multiselect }); } setup(): () => void { - const [disposers, dispose] = disposeFx(); + const [add, disposeAll] = disposeBatch(); - disposers.push(this.changeHelper.setup()); + add(this.changeHelper.setup()); if (this.defaultValue) { this.filterStore.setDefaultSelected(this.defaultValue); } - return dispose; - } - - updateProps(props: RefBaseControllerProps): void { - this.changeHelper.updateProps(props); + return disposeAll; } } diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefComboboxController.ts similarity index 64% rename from packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/RefComboboxController.ts index d211132988..57b956400f 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefComboboxController.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefComboboxController.ts @@ -1,10 +1,11 @@ +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { ComboboxControllerMixin } from "./mixins/ComboboxControllerMixin"; import { RefBaseController, RefBaseControllerProps } from "./RefBaseController"; export class RefComboboxController extends ComboboxControllerMixin(RefBaseController) { - constructor(props: RefBaseControllerProps) { - super({ ...props, multiselect: false }); - this.inputPlaceholder = props.placeholder ?? "Search"; + constructor({ gate }: { gate: DerivedPropsGate }) { + super({ gate, multiselect: false }); + this.inputPlaceholder = gate.props.placeholder ?? "Search"; } handleFocus = (event: React.FocusEvent): void => { diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefSelectController.ts similarity index 54% rename from packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/RefSelectController.ts index 6d5c46280c..4b178af430 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefSelectController.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefSelectController.ts @@ -1,11 +1,12 @@ +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { RefBaseController, RefBaseControllerProps } from "./RefBaseController"; import { SelectControllerMixin } from "./mixins/SelectControllerMixin"; export class RefSelectController extends SelectControllerMixin(RefBaseController) { - constructor(props: RefBaseControllerProps) { - super(props); - this.emptyOption.caption = props.emptyCaption || "None"; - this.placeholder = props.placeholder || "Search"; + constructor({ gate }: { gate: DerivedPropsGate }) { + super({ gate, multiselect: gate.props.multiselect }); + this.emptyOption.caption = gate.props.emptyCaption || "None"; + this.placeholder = gate.props.placeholder || "Search"; } handleFocus = (): void => { diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefTagPickerController.ts similarity index 64% rename from packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/RefTagPickerController.ts index 0595766f8d..e238ebd173 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/RefTagPickerController.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/RefTagPickerController.ts @@ -1,3 +1,4 @@ +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { RefBaseController, RefBaseControllerProps } from "./RefBaseController"; import { TagPickerControllerMixin } from "./mixins/TagPickerControllerMixin"; @@ -13,12 +14,12 @@ export class RefTagPickerController extends TagPickerControllerMixin(RefBaseCont selectionMethod: SelectionMethodEnum; selectedStyle: SelectedItemsStyleEnum; - constructor(props: Props) { - super(props); - this.inputPlaceholder = props.placeholder ?? "Search"; - this.filterSelectedOptions = props.selectionMethod === "rowClick"; - this.selectedStyle = props.selectedItemsStyle; - this.selectionMethod = this.selectedStyle === "boxes" ? props.selectionMethod : "checkbox"; + constructor({ gate }: { gate: DerivedPropsGate }) { + super({ gate, multiselect: gate.props.multiselect }); + this.inputPlaceholder = gate.props.placeholder ?? "Search"; + this.filterSelectedOptions = gate.props.selectionMethod === "rowClick"; + this.selectedStyle = gate.props.selectedItemsStyle; + this.selectionMethod = this.selectedStyle === "boxes" ? gate.props.selectionMethod : "checkbox"; } handleFocus = (): void => { diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticBaseController.ts similarity index 53% rename from packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticBaseController.ts index b9506dcd99..0a3bc88b69 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticBaseController.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticBaseController.ts @@ -1,31 +1,34 @@ +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { ActionValue, DynamicValue, EditableValue } from "mendix"; -import { action, autorun, makeObservable, observable } from "mobx"; -import { disposeFx } from "../../mobx-utils"; -import { StaticSelectFilterStore } from "../../stores/picker/StaticSelectFilterStore"; +import { autorun, computed, makeObservable } from "mobx"; +import { StaticSelectFilterStore } from "../stores/StaticSelectFilterStore"; import { PickerBaseController } from "./PickerBaseController"; export class StaticBaseController extends PickerBaseController { - filterOptions: Array>>; + private readonly gate: DerivedPropsGate; - constructor(props: StaticBaseControllerProps) { - super(props); - this.filterOptions = props.filterOptions; - makeObservable(this, { - updateProps: action, - filterOptions: observable.struct + constructor({ gate, multiselect }: { gate: DerivedPropsGate; multiselect: boolean }) { + super({ gate, multiselect }); + this.gate = gate; + makeObservable(this, { + filterOptions: computed.struct }); } + private get filterOptions(): Array> { + return this.gate.props.filterOptions.map(this.toStoreOption); + } + setup(): () => void { - const [disposers, dispose] = disposeFx(); + const [addDisposer, dispose] = disposeBatch(); - disposers.push(this.changeHelper.setup()); + addDisposer(this.changeHelper.setup()); - disposers.push( + addDisposer( autorun(() => { if (this.filterOptions.length > 0) { - const options = this.filterOptions.map(this.toStoreOption); - this.filterStore.setCustomOptions(options); + this.filterStore.setCustomOptions(this.filterOptions); } }) ); @@ -37,11 +40,6 @@ export class StaticBaseController extends PickerBaseController>): CustomOption => ({ caption: `${opt.caption?.value}`, value: `${opt.value?.value}` diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticComboboxController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticComboboxController.ts new file mode 100644 index 0000000000..af85ad482d --- /dev/null +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticComboboxController.ts @@ -0,0 +1,10 @@ +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; +import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController"; +import { ComboboxControllerMixin } from "./mixins/ComboboxControllerMixin"; + +export class StaticComboboxController extends ComboboxControllerMixin(StaticBaseController) { + constructor({ gate }: { gate: DerivedPropsGate }) { + super({ gate, multiselect: false }); + this.inputPlaceholder = gate.props.placeholder ?? "Search"; + } +} diff --git a/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticSelectController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticSelectController.ts new file mode 100644 index 0000000000..48fe7a9909 --- /dev/null +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticSelectController.ts @@ -0,0 +1,11 @@ +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; +import { SelectControllerMixin } from "./mixins/SelectControllerMixin"; +import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController"; + +export class StaticSelectController extends SelectControllerMixin(StaticBaseController) { + constructor({ gate }: { gate: DerivedPropsGate }) { + super({ gate, multiselect: gate.props.multiselect }); + this.emptyOption.caption = gate.props.emptyCaption || "None"; + this.placeholder = gate.props.emptyCaption || "Select"; + } +} diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticTagPickerController.ts similarity index 59% rename from packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticTagPickerController.ts index 11bc90fd30..7863520b51 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticTagPickerController.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/StaticTagPickerController.ts @@ -1,3 +1,4 @@ +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController"; import { TagPickerControllerMixin } from "./mixins/TagPickerControllerMixin"; @@ -13,11 +14,11 @@ export class StaticTagPickerController extends TagPickerControllerMixin(StaticBa selectionMethod: SelectionMethodEnum; selectedStyle: SelectedItemsStyleEnum; - constructor(props: Props) { - super(props); - this.inputPlaceholder = props.placeholder ?? "Search"; - this.filterSelectedOptions = props.selectionMethod === "rowClick"; - this.selectedStyle = props.selectedItemsStyle; - this.selectionMethod = this.selectedStyle === "boxes" ? props.selectionMethod : "checkbox"; + constructor({ gate }: { gate: DerivedPropsGate }) { + super({ gate, multiselect: gate.props.multiselect }); + this.inputPlaceholder = gate.props.placeholder ?? "Search"; + this.filterSelectedOptions = gate.props.selectionMethod === "rowClick"; + this.selectedStyle = gate.props.selectedItemsStyle; + this.selectionMethod = this.selectedStyle === "boxes" ? gate.props.selectionMethod : "checkbox"; } } diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/ComboboxControllerMixin.ts similarity index 92% rename from packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/ComboboxControllerMixin.ts index 0364d9331f..12282fa36a 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/ComboboxControllerMixin.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/ComboboxControllerMixin.ts @@ -1,9 +1,9 @@ +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { useCombobox, UseComboboxProps } from "downshift"; import { action, autorun, computed, makeObservable, observable, reaction } from "mobx"; -import { disposeFx } from "../../../mobx-utils"; -import { SearchStore } from "../../../stores/picker/SearchStore"; -import { OptionWithState } from "../../../typings/OptionWithState"; -import { GConstructor } from "../../../typings/type-utils"; +import { SearchStore } from "../../stores/SearchStore"; +import { OptionWithState } from "../../typings/OptionWithState"; +import { GConstructor } from "../../typings/type-utils"; export interface FilterStore { clear: () => void; @@ -45,13 +45,13 @@ export function ComboboxControllerMixin(Base: TBas } setup(): () => void { - const [disposers, dispose] = disposeFx(); - disposers.push(autorun(...this.searchSyncFx())); + const [add, dispose] = disposeBatch(); + add(autorun(...this.searchSyncFx())); // Set input when store state changes - disposers.push(reaction(...this.storeSyncFx())); + add(reaction(...this.storeSyncFx())); - disposers.push(super.setup()); + add(super.setup()); this.setInputValue(this.inputInitValue); return dispose; } diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/SelectControllerMixin.ts similarity index 96% rename from packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/SelectControllerMixin.ts index c765116ed8..929bb1d743 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/SelectControllerMixin.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/SelectControllerMixin.ts @@ -1,7 +1,7 @@ import { useSelect, UseSelectProps } from "downshift"; import { action, computed, makeObservable } from "mobx"; -import { OptionWithState } from "../../../typings/OptionWithState"; -import { GConstructor } from "../../../typings/type-utils"; +import { OptionWithState } from "../../typings/OptionWithState"; +import { GConstructor } from "../../typings/type-utils"; export interface FilterStore { toggle: (value: string) => void; diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/TagPickerControllerMixin.ts similarity index 94% rename from packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts rename to packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/TagPickerControllerMixin.ts index 42ed406a37..be215ca8dc 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/mixins/TagPickerControllerMixin.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/controllers/mixins/TagPickerControllerMixin.ts @@ -1,9 +1,9 @@ +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { useCombobox, UseComboboxProps, useMultipleSelection, UseMultipleSelectionProps } from "downshift"; import { action, autorun, computed, makeObservable, observable } from "mobx"; -import { disposeFx } from "../../../mobx-utils"; -import { SearchStore } from "../../../stores/picker/SearchStore"; -import { OptionWithState } from "../../../typings/OptionWithState"; -import { GConstructor } from "../../../typings/type-utils"; +import { SearchStore } from "../../stores/SearchStore"; +import { OptionWithState } from "../../typings/OptionWithState"; +import { GConstructor } from "../../typings/type-utils"; export interface FilterStore { toggle: (value: string) => void; @@ -47,9 +47,9 @@ export function TagPickerControllerMixin(Base: TBa } setup(): () => void { - const [disposers, dispose] = disposeFx(); - disposers.push(autorun(...this.searchSyncFx())); - disposers.push(super.setup()); + const [add, dispose] = disposeBatch(); + add(autorun(...this.searchSyncFx())); + add(super.setup()); return dispose; } diff --git a/packages/shared/widget-plugin-filtering/src/controls/base/ClearButton.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/base/ClearButton.tsx similarity index 100% rename from packages/shared/widget-plugin-filtering/src/controls/base/ClearButton.tsx rename to packages/shared/widget-plugin-dropdown-filter/src/controls/base/ClearButton.tsx diff --git a/packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/base/OptionsWrapper.tsx similarity index 100% rename from packages/shared/widget-plugin-filtering/src/controls/base/OptionsWrapper.tsx rename to packages/shared/widget-plugin-dropdown-filter/src/controls/base/OptionsWrapper.tsx diff --git a/packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/combobox/Combobox.tsx similarity index 100% rename from packages/shared/widget-plugin-filtering/src/controls/combobox/Combobox.tsx rename to packages/shared/widget-plugin-dropdown-filter/src/controls/combobox/Combobox.tsx diff --git a/packages/shared/widget-plugin-filtering/src/controls/hooks/useFloatingMenu.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/hooks/useFloatingMenu.tsx similarity index 100% rename from packages/shared/widget-plugin-filtering/src/controls/hooks/useFloatingMenu.tsx rename to packages/shared/widget-plugin-dropdown-filter/src/controls/hooks/useFloatingMenu.tsx diff --git a/packages/shared/widget-plugin-filtering/src/controls/picker-primitives.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/picker-primitives.tsx similarity index 100% rename from packages/shared/widget-plugin-filtering/src/controls/picker-primitives.tsx rename to packages/shared/widget-plugin-dropdown-filter/src/controls/picker-primitives.tsx diff --git a/packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/select/Select.tsx similarity index 100% rename from packages/shared/widget-plugin-filtering/src/controls/select/Select.tsx rename to packages/shared/widget-plugin-dropdown-filter/src/controls/select/Select.tsx diff --git a/packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx b/packages/shared/widget-plugin-dropdown-filter/src/controls/tag-picker/TagPicker.tsx similarity index 100% rename from packages/shared/widget-plugin-filtering/src/controls/tag-picker/TagPicker.tsx rename to packages/shared/widget-plugin-dropdown-filter/src/controls/tag-picker/TagPicker.tsx diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hooks/useFrontendType.ts b/packages/shared/widget-plugin-dropdown-filter/src/helpers/useFrontendType.ts similarity index 100% rename from packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hooks/useFrontendType.ts rename to packages/shared/widget-plugin-dropdown-filter/src/helpers/useFrontendType.ts diff --git a/packages/shared/widget-plugin-filtering/src/helpers/usePickerJSActions.ts b/packages/shared/widget-plugin-dropdown-filter/src/helpers/usePickerJSActions.ts similarity index 85% rename from packages/shared/widget-plugin-filtering/src/helpers/usePickerJSActions.ts rename to packages/shared/widget-plugin-dropdown-filter/src/helpers/usePickerJSActions.ts index ee7a00dc1a..6c76d13a76 100644 --- a/packages/shared/widget-plugin-filtering/src/helpers/usePickerJSActions.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/helpers/usePickerJSActions.ts @@ -1,5 +1,5 @@ +import { IJSActionsControlled } from "@mendix/filter-commons/typings/IJSActionsControlled"; import { useOnResetValueEvent, useOnSetValueEvent } from "@mendix/widget-plugin-external-events/hooks"; -import { IJSActionsControlled } from "../typings/IJSActionsControlled"; export function usePickerJSActions( controller: IJSActionsControlled, diff --git a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withCustomOptionsGuard.tsx b/packages/shared/widget-plugin-dropdown-filter/src/hocs/withCustomOptionsGuard.tsx similarity index 82% rename from packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withCustomOptionsGuard.tsx rename to packages/shared/widget-plugin-dropdown-filter/src/hocs/withCustomOptionsGuard.tsx index 844527f811..2a76c657c3 100644 --- a/packages/pluggableWidgets/datagrid-dropdown-filter-web/src/hocs/withCustomOptionsGuard.tsx +++ b/packages/shared/widget-plugin-dropdown-filter/src/hocs/withCustomOptionsGuard.tsx @@ -1,7 +1,7 @@ import { Alert } from "@mendix/widget-plugin-component-kit/Alert"; import { useState, createElement } from "react"; -import { FilterOptionsType } from "../../typings/DatagridDropdownFilterProps"; -import { StaticSelectFilterStore } from "@mendix/widget-plugin-filtering/stores/picker/StaticSelectFilterStore"; +import { FilterOptionsType } from "../typings/widget"; +import { StaticSelectFilterStore } from "../stores/StaticSelectFilterStore"; interface Props { filterStore: StaticSelectFilterStore; diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/BaseSelectStore.ts similarity index 65% rename from packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts rename to packages/shared/widget-plugin-dropdown-filter/src/stores/BaseSelectStore.ts index ca1b324fd0..7ae086f7fc 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/picker/BaseSelectStore.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/stores/BaseSelectStore.ts @@ -1,6 +1,5 @@ +import { FilterData, InputData } from "@mendix/filter-commons/typings/settings"; import { action, makeObservable, observable } from "mobx"; -import { FilterData } from "../../typings/settings"; -import { isInputData } from "../utils/is-input-data"; export class BaseSelectStore { protected defaultSelected: Iterable = []; @@ -40,10 +39,33 @@ export class BaseSelectStore { } fromJSON(json: FilterData): void { - if (json == null || isInputData(json)) { + if (json === undefined || json === null || isInputData(json)) { return; } this.setSelected(json); this.blockSetDefaults = true; } } + +const fnNames = new Set([ + "empty", + "notEmpty", + "equal", + "notEqual", + "greater", + "greaterEqual", + "smaller", + "smallerEqual", + "between", + "contains", + "startsWith", + "endsWith" +]); + +export function isInputData(data: unknown): data is InputData { + if (Array.isArray(data)) { + const [name] = data; + return fnNames.has(name); + } + return false; +} diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/OptionsSerializer.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/OptionsSerializer.ts similarity index 100% rename from packages/shared/widget-plugin-filtering/src/stores/picker/OptionsSerializer.ts rename to packages/shared/widget-plugin-dropdown-filter/src/stores/OptionsSerializer.ts diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/RefFilterStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/RefFilterStore.ts similarity index 69% rename from packages/shared/widget-plugin-filtering/src/stores/picker/RefFilterStore.ts rename to packages/shared/widget-plugin-dropdown-filter/src/stores/RefFilterStore.ts index c545fbb634..f6de498c0d 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/picker/RefFilterStore.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/stores/RefFilterStore.ts @@ -1,40 +1,36 @@ -import { ListAttributeValue, ListReferenceSetValue, ListReferenceValue, ListValue, ObjectItem } from "mendix"; +import { flattenRefCond, selectedFromCond } from "@mendix/filter-commons/condition-utils"; +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; +import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; +import { AssociationMetaData, AttributeMetaData, ListValue, ObjectItem } from "mendix"; import { ContainsCondition, EqualsCondition, FilterCondition, LiteralExpression } from "mendix/filters"; -import { association, attribute, contains, empty, equals, literal, or } from "mendix/filters/builders"; +import { association, attribute, contains, literal, or } from "mendix/filters/builders"; import { action, autorun, computed, makeObservable, observable, reaction, runInAction, when } from "mobx"; -import { flattenRefCond, selectedFromCond } from "../../condition-utils"; -import { disposeFx } from "../../mobx-utils"; -import { OptionWithState } from "../../typings/OptionWithState"; +import { OptionWithState } from "../typings/OptionWithState"; import { BaseSelectStore } from "./BaseSelectStore"; import { SearchStore } from "./SearchStore"; -type ListAttributeId = ListAttributeValue["id"]; +type ListAttributeId = AttributeMetaData["id"]; export interface RefFilterStoreProps { - ref: ListReferenceValue | ListReferenceSetValue; - datasource: ListValue; - searchAttrId?: ListAttributeId; fetchOptionsLazy?: boolean; - caption: CaptionAccessor; + refEntity: AssociationMetaData; + refCaption: CaptionAccessor; + refOptions: ListValue; + searchAttrId?: ListAttributeId; } interface CaptionAccessor { get: (obj: ObjectItem) => { value: string | undefined }; } +type Gate = DerivedPropsGate; + export class RefFilterStore extends BaseSelectStore { readonly storeType = "refselect"; readonly optionsFilterable: boolean; - private datasource: ListValue; - private listRef: ListReferenceValue | ListReferenceSetValue; - private caption: CaptionAccessor; - private searchAttrId?: ListAttributeId; - /** - * As Ref filter fetch options lazily, - * we just keep condition and - * return it if options not loaded yet. - */ + private readonly gate: Gate; + private readonly searchAttrId?: ListAttributeId; private readonly initCondArray: Array; private readonly pageSize = 20; private readonly searchSize = 100; @@ -44,11 +40,11 @@ export class RefFilterStore extends BaseSelectStore { lazyMode: boolean; search: SearchStore; - constructor(props: RefFilterStoreProps, initCond: FilterCondition | null) { + constructor({ gate, initCond }: { gate: Gate; initCond: FilterCondition | null }) { super(); - this.caption = props.caption; - this.datasource = props.datasource; - this.listRef = props.ref; + const { props } = gate; + this.gate = gate; + this.lazyMode = props.fetchOptionsLazy ?? true; this.searchAttrId = props.searchAttrId; this.initCondArray = initCond ? flattenRefCond(initCond) : []; @@ -59,16 +55,14 @@ export class RefFilterStore extends BaseSelectStore { this.datasource.setLimit(0); } - makeObservable(this, { - datasource: observable.ref, - listRef: observable.ref, - caption: observable.ref, - searchAttrId: observable.ref, + makeObservable(this, { + datasource: computed, + refEntity: computed, + caption: computed, options: computed, hasMore: computed, isLoading: computed, condition: computed, - updateProps: action, fromViewState: action, fetchReady: observable, setFetchReady: action, @@ -82,6 +76,18 @@ export class RefFilterStore extends BaseSelectStore { } } + private get datasource(): ListValue { + return this.gate.props.refOptions; + } + + private get refEntity(): AssociationMetaData { + return this.gate.props.refEntity; + } + + private get caption(): CaptionAccessor { + return this.gate.props.refCaption; + } + get hasMore(): boolean { return this.datasource.hasMoreItems ?? false; } @@ -115,19 +121,14 @@ export class RefFilterStore extends BaseSelectStore { const exp = (guid: string): FilterCondition[] => { const obj = this.selectedItems.find(o => o.id === guid); - if (obj && this.listRef.type === "Reference") { - return [refEquals(this.listRef, obj)]; - } else if (obj && this.listRef.type === "ReferenceSet") { - return [refContains(this.listRef, [obj])]; + if (obj) { + return [contains(association(this.refEntity.id), literal(obj))]; } const viewExp = this.initCondArray.find(e => { if (e.arg2.type !== "literal") { return false; } - if (e.arg2.valueType === "Reference") { - return e.arg2.value === guid; - } if (e.arg2.valueType === "ReferenceSet") { return e.arg2.value.at(0) === guid; } @@ -146,14 +147,14 @@ export class RefFilterStore extends BaseSelectStore { } setup(): () => void { - const [disposers, dispose] = disposeFx(); + const [add, dispose] = disposeBatch(); - disposers.push(this.search.setup()); - disposers.push(reaction(...this.searchChangeFx())); - disposers.push(autorun(...this.computeSelectedItemsFx())); + add(this.search.setup()); + add(reaction(...this.searchChangeFx())); + add(autorun(...this.computeSelectedItemsFx())); if (this.lazyMode) { - disposers.push( + add( when( () => this.fetchReady, () => this.loadMore() @@ -209,12 +210,6 @@ export class RefFilterStore extends BaseSelectStore { } } - updateProps(props: RefFilterStoreProps): void { - this.listRef = props.ref; - this.datasource = props.datasource; - this.caption = props.caption; - } - loadMore(): void { this.datasource.setLimit(this.datasource.limit + this.pageSize); } @@ -239,12 +234,3 @@ export class RefFilterStore extends BaseSelectStore { } } } - -export function refEquals(associationValue: ListReferenceValue, value: ObjectItem): EqualsCondition { - return equals(association(associationValue.id), literal(value)); -} - -export function refContains(associationValue: ListReferenceSetValue, value: ObjectItem[]): ContainsCondition { - const v = value.length ? literal(value.slice()) : empty(); - return contains(association(associationValue.id), v); -} diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/SearchStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/SearchStore.ts similarity index 100% rename from packages/shared/widget-plugin-filtering/src/stores/picker/SearchStore.ts rename to packages/shared/widget-plugin-dropdown-filter/src/stores/SearchStore.ts diff --git a/packages/shared/widget-plugin-filtering/src/stores/picker/StaticSelectFilterStore.ts b/packages/shared/widget-plugin-dropdown-filter/src/stores/StaticSelectFilterStore.ts similarity index 72% rename from packages/shared/widget-plugin-filtering/src/stores/picker/StaticSelectFilterStore.ts rename to packages/shared/widget-plugin-dropdown-filter/src/stores/StaticSelectFilterStore.ts index 97b27353b9..54607e0acd 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/picker/StaticSelectFilterStore.ts +++ b/packages/shared/widget-plugin-dropdown-filter/src/stores/StaticSelectFilterStore.ts @@ -1,10 +1,10 @@ -import { ListAttributeValue } from "mendix"; +import { selectedFromCond } from "@mendix/filter-commons/condition-utils"; +import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; +import { AttributeMetaData } from "mendix"; import { FilterCondition, LiteralExpression } from "mendix/filters"; import { attribute, equals, literal, or } from "mendix/filters/builders"; import { action, computed, makeObservable, observable } from "mobx"; -import { selectedFromCond } from "../../condition-utils"; -import { disposeFx } from "../../mobx-utils"; -import { OptionWithState } from "../../typings/OptionWithState"; +import { OptionWithState } from "../typings/OptionWithState"; import { BaseSelectStore } from "./BaseSelectStore"; import { SearchStore } from "./SearchStore"; @@ -15,11 +15,11 @@ interface CustomOption { export class StaticSelectFilterStore extends BaseSelectStore { readonly storeType = "select"; - _attributes: ListAttributeValue[] = []; + _attributes: AttributeMetaData[] = []; _customOptions: CustomOption[] = []; search: SearchStore; - constructor(attributes: ListAttributeValue[], initCond: FilterCondition | null) { + constructor(attributes: AttributeMetaData[], initCond: FilterCondition | null) { super(); this.search = new SearchStore(); this._attributes = attributes; @@ -34,7 +34,6 @@ export class StaticSelectFilterStore extends BaseSelectStore { condition: computed, setCustomOptions: action, setDefaultSelected: action, - updateProps: action, fromViewState: action }); @@ -47,14 +46,19 @@ export class StaticSelectFilterStore extends BaseSelectStore { const selected = this.selected; if (this._customOptions.length > 0) { - return this._customOptions.map(opt => ({ ...opt, selected: selected.has(opt.value) })); + return this._customOptions.map(opt => ({ + ...opt, + selected: selected.has(opt.value) + })); } const options = this._attributes.flatMap(attr => Array.from(attr.universe ?? [], value => { const stringValue = `${value}`; return { - caption: attr.formatter.format(value), + // TODO: fix when formatter is available + // caption: attr.formatter.format(value), + caption: stringValue, value: stringValue, selected: selected.has(stringValue) }; @@ -93,8 +97,8 @@ export class StaticSelectFilterStore extends BaseSelectStore { } setup(): () => void { - const [disposers, dispose] = disposeFx(); - disposers.push(this.search.setup()); + const [add, dispose] = disposeBatch(); + add(this.search.setup()); return dispose; } @@ -110,12 +114,8 @@ export class StaticSelectFilterStore extends BaseSelectStore { } } - updateProps(attributes: ListAttributeValue[]): void { - this._attributes = attributes; - } - checkAttrs(): TypeError | null { - const isValidAttr = (attr: ListAttributeValue): boolean => /Enum|Boolean/.test(attr.type); + const isValidAttr = (attr: AttributeMetaData): boolean => /Enum|Boolean/.test(attr.type); if (this._attributes.every(isValidAttr)) { return null; @@ -150,33 +150,26 @@ export class StaticSelectFilterStore extends BaseSelectStore { } function getFilterCondition( - listAttribute: ListAttributeValue | undefined, + listAttribute: AttributeMetaData | undefined, selected: Set ): FilterCondition | undefined { - if (!listAttribute || !listAttribute.filterable || selected.size === 0) { + if (!listAttribute) { return undefined; } - - const { id, type } = listAttribute; - const filterAttribute = attribute(id); - - const filters = [...selected] - .filter(value => listAttribute.universe?.includes(universeValue(listAttribute.type, value))) - .map(value => equals(filterAttribute, literal(universeValue(type, value)))); - - if (filters.length > 1) { - return or(...filters); + if (selected.size < 1) { + return undefined; } - const [filterValue] = filters; - return filterValue; + const attrExp = attribute(listAttribute.id); + const conditions = Array.from(selected, value => + equals(attrExp, literal(universeValue(listAttribute.type, value))) + ); + + return conditions.length > 1 ? or(...conditions) : conditions[0]; } -function universeValue(type: ListAttributeValue["type"], value: string): boolean | string { +function universeValue(type: AttributeMetaData["type"], value: string): boolean | string { if (type === "Boolean") { - if (value !== "true" && value !== "false") { - return value; - } return value === "true"; } return value; diff --git a/packages/shared/widget-plugin-dropdown-filter/src/typings/IJSActionsControlled.ts b/packages/shared/widget-plugin-dropdown-filter/src/typings/IJSActionsControlled.ts new file mode 100644 index 0000000000..50117d47ba --- /dev/null +++ b/packages/shared/widget-plugin-dropdown-filter/src/typings/IJSActionsControlled.ts @@ -0,0 +1,11 @@ +export interface IJSActionsControlled { + handleResetValue: ResetHandler; + handleSetValue: SetValueHandler; +} + +export type ResetHandler = (useDefaultValue: boolean) => void; + +export type SetValueHandler = ( + useDefaultValue: boolean, + params: { operators: any; stringValue: string; numberValue: Big.Big; dateTimeValue: Date; dateTimeValue2: Date } +) => void; diff --git a/packages/shared/widget-plugin-filtering/src/typings/OptionWithState.ts b/packages/shared/widget-plugin-dropdown-filter/src/typings/OptionWithState.ts similarity index 100% rename from packages/shared/widget-plugin-filtering/src/typings/OptionWithState.ts rename to packages/shared/widget-plugin-dropdown-filter/src/typings/OptionWithState.ts diff --git a/packages/shared/widget-plugin-dropdown-filter/src/typings/type-utils.ts b/packages/shared/widget-plugin-dropdown-filter/src/typings/type-utils.ts new file mode 100644 index 0000000000..443afb18a9 --- /dev/null +++ b/packages/shared/widget-plugin-dropdown-filter/src/typings/type-utils.ts @@ -0,0 +1 @@ +export type GConstructor = new (...args: any[]) => T; diff --git a/packages/shared/widget-plugin-dropdown-filter/src/typings/widget.ts b/packages/shared/widget-plugin-dropdown-filter/src/typings/widget.ts new file mode 100644 index 0000000000..a839b1c9c3 --- /dev/null +++ b/packages/shared/widget-plugin-dropdown-filter/src/typings/widget.ts @@ -0,0 +1,10 @@ +import { DynamicValue } from "mendix"; + +export interface FilterOptionsType { + caption: DynamicValue; + value: DynamicValue; +} + +export type SelectedItemsStyleEnum = "text" | "boxes"; + +export type SelectionMethodEnum = "checkbox" | "rowClick"; diff --git a/packages/shared/widget-plugin-dropdown-filter/tsconfig.json b/packages/shared/widget-plugin-dropdown-filter/tsconfig.json new file mode 100644 index 0000000000..052cc1cee7 --- /dev/null +++ b/packages/shared/widget-plugin-dropdown-filter/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@mendix/tsconfig-web-widgets/esm-library-with-jsx", + "include": ["./src/**/*"], + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true + } +} diff --git a/packages/shared/widget-plugin-filtering/package.json b/packages/shared/widget-plugin-filtering/package.json index e92778dc25..8f077978c2 100644 --- a/packages/shared/widget-plugin-filtering/package.json +++ b/packages/shared/widget-plugin-filtering/package.json @@ -28,18 +28,19 @@ "dev": "tsc --watch", "format": "prettier --write .", "lint": "eslint src/ package.json", - "prepare": "tsc", "test": "jest" }, "dependencies": { "@floating-ui/react": "^0.26.27", "@floating-ui/react-dom": "^2.1.2", + "@mendix/filter-commons": "workspace:*", + "@mendix/widget-plugin-dropdown-filter": "workspace:*", "@mendix/widget-plugin-external-events": "workspace:*", "@mendix/widget-plugin-hooks": "workspace:*", "@mendix/widget-plugin-mobx-kit": "workspace:^", "@mendix/widget-plugin-platform": "workspace:*", "downshift": "^9.0.8", - "mendix": "^10.16.49747", + "mendix": "^10.21.64362", "mobx": "6.12.3", "mobx-react-lite": "4.0.7" }, diff --git a/packages/shared/widget-plugin-filtering/src/__tests__/DateInputFilterStore.spec.ts b/packages/shared/widget-plugin-filtering/src/__tests__/DateInputFilterStore.spec.ts index 25ba577ab6..b54f27e74a 100644 --- a/packages/shared/widget-plugin-filtering/src/__tests__/DateInputFilterStore.spec.ts +++ b/packages/shared/widget-plugin-filtering/src/__tests__/DateInputFilterStore.spec.ts @@ -1,5 +1,5 @@ jest.mock("mendix/filters/builders"); -import { attrId, listAttr } from "@mendix/widget-plugin-test-utils"; +import { attrId, listAttribute } from "@mendix/widget-plugin-test-utils"; import { ListAttributeValue } from "mendix"; import { and, @@ -28,7 +28,7 @@ describe("DateInputFilterStore", () => { let store: DateInputFilterStore; beforeEach(() => { - attr = listAttr(() => new Date()); + attr = listAttribute(() => new Date()); attr.id = attrId("attr_unset"); store = new DateInputFilterStore([attr], null); }); @@ -125,7 +125,7 @@ describe("DateInputFilterStore", () => { }); it("uses 'or' when have multiple attributes", () => { - const [attr1, attr2] = [listAttr(() => new Date()), listAttr(() => new Date())]; + const [attr1, attr2] = [listAttribute(() => new Date()), listAttribute(() => new Date())]; store = new DateInputFilterStore([attr1, attr2], null); const date1 = new Date("2024-09-17T01:01:01.000Z"); store.filterFunction = "equal"; @@ -146,7 +146,7 @@ describe("DateInputFilterStore", () => { let store: DateInputFilterStore; beforeEach(() => { - attr = listAttr(() => new Date()); + attr = listAttribute(() => new Date()); store = new DateInputFilterStore([attr], null); expect(store.filterFunction).toBe("equal"); }); diff --git a/packages/shared/widget-plugin-filtering/src/__tests__/RefFilterStore.spec.ts b/packages/shared/widget-plugin-filtering/src/__tests__/RefFilterStore.spec.ts deleted file mode 100644 index de11ebf61e..0000000000 --- a/packages/shared/widget-plugin-filtering/src/__tests__/RefFilterStore.spec.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { cases, list, listExpression, listReference, ListValueBuilder, obj } from "@mendix/widget-plugin-test-utils"; -import { ObjectItem } from "mendix"; -import { _resetGlobalState, autorun } from "mobx"; -import { RefFilterStore, RefFilterStoreProps } from "../stores/picker/RefFilterStore"; - -describe("RefFilterStore", () => { - afterEach(() => _resetGlobalState()); - - describe("get options()", () => { - let items: ObjectItem[]; - let mapItem: (item: ObjectItem) => string; - let store: RefFilterStore; - beforeEach(() => { - const [a, b, c] = [obj("id3n"), obj("f8x3"), obj("932c")]; - items = [a, b, c]; - mapItem = cases([a, "Alice"], [b, "Bob"], [c, "Chuck"], [undefined, "No caption"]); - store = new RefFilterStore( - { - ref: listReference(), - datasource: list(items), - caption: listExpression(mapItem) - }, - null - ); - }); - - it("returns array of options", () => { - expect(store.options).toMatchInlineSnapshot(` - [ - { - "caption": "Alice", - "selected": false, - "value": "obj_id3n", - }, - { - "caption": "Bob", - "selected": false, - "value": "obj_f8x3", - }, - { - "caption": "Chuck", - "selected": false, - "value": "obj_932c", - }, - ] - `); - }); - }); - - describe("get condition()", () => { - it.todo("return filter condition using equals"); - }); - - describe("toJSON()", () => { - let store: RefFilterStore; - beforeEach(() => { - store = new RefFilterStore( - { - ref: listReference(), - datasource: list(3), - caption: listExpression(() => "[string]") - }, - null - ); - }); - - it("returns state as JS array", () => { - expect(store.toJSON()).toEqual([]); - }); - - it("save state with one item", () => { - const guid = store.options[0].value; - store.toggle(guid); - expect(store.toJSON()).toEqual([guid]); - }); - - it("save state with multiple items", () => { - const [guid1, guid2] = [store.options[0].value, store.options[1].value]; - store.toggle(guid1); - store.toggle(guid2); - const output = store.toJSON(); - - expect(output).toHaveLength(2); - expect(output).toContain(guid1); - expect(output).toContain(guid2); - }); - }); - - describe("fromJSON()", () => { - let store: RefFilterStore; - beforeEach(() => { - store = new RefFilterStore( - { - ref: listReference(), - datasource: list([obj("id3n"), obj("f8x3"), obj("932c")]), - caption: listExpression(() => "[string]") - }, - null - ); - }); - - it("restore state from JS array", () => { - const output: Array = []; - autorun(() => output.push(store.options)); - - // Check that every option is unselected - expect(output[0].map(option => option.selected)).toEqual([false, false, false]); - // Restore state - store.fromJSON(["obj_id3n"]); - // Check that first option is selected - expect(output[1].map(option => option.selected)).toEqual([true, false, false]); - }); - - it("overrides any previous state", () => { - const output: Array = []; - - // Select each option - store.options.forEach(option => { - store.toggle(option.value); - }); - // Start observing - autorun(() => output.push(store.options)); - // Check the state - expect(output[0].map(option => option.selected)).toEqual([true, true, true]); - store.fromJSON(["obj_932c"]); - expect(output[1].map(option => option.selected)).toEqual([false, false, true]); - }); - - it("should not change state if json is null", () => { - const state = store.toJSON(); - store.fromJSON(null); - expect(store.toJSON()).toEqual(state); - }); - }); - - describe("toggle()", () => { - let store: RefFilterStore; - beforeEach(() => { - store = new RefFilterStore( - { - ref: listReference(), - datasource: list([obj("id3n"), obj("f8x3"), obj("932c")]), - caption: listExpression(() => "[string]") - }, - null - ); - }); - - it("compute new options", () => { - const output: Array = []; - const [x, y] = store.options; - - autorun(() => output.push(store.options)); - expect(output[0].map(opt => opt.selected)).toEqual([false, false, false]); - - store.toggle(x.value); - store.toggle(y.value); - expect(output[2].map(opt => opt.selected)).toEqual([true, true, false]); - - store.toggle(x.value); - expect(output[3].map(opt => opt.selected)).toEqual([false, true, false]); - }); - }); - - describe("with 'fetchOptionsLazy' flag", () => { - it("should set limit to 0 on options datasource", () => { - const datasource = list.loading(); - const props = { - ref: listReference(), - datasource, - caption: listExpression(() => "[string]"), - fetchOptionsLazy: true - }; - - new RefFilterStore(props, null); - - expect(datasource.setLimit).toHaveBeenLastCalledWith(0); - }); - - it("should not change limit if flag is false", () => { - const datasource = list.loading(); - const props = { - ref: listReference(), - datasource, - caption: listExpression(() => "[string]"), - fetchOptionsLazy: false - }; - - new RefFilterStore(props, null); - - expect(datasource.setLimit).not.toHaveBeenCalled(); - }); - }); - - describe("when datasource changed", () => { - it("compute new options", () => { - const a = obj("id3n"); - const props = { - ref: listReference(), - datasource: list.loading(), - caption: listExpression(cases([a, "Alice"], [undefined, "No caption"])) - }; - const store = new RefFilterStore(props, null); - const output: any[] = []; - autorun(() => output.push(store.options)); - - expect(output[0]).toEqual([]); - store.updateProps({ ...props, datasource: list([a]) }); - expect(output[1]).toEqual([ - { - caption: "Alice", - selected: false, - value: "obj_id3n" - } - ]); - }); - }); - - describe("json data filtering", () => { - const savedJson = ["obj_xx", "obj_xiii", "obj_deleted", "obj_yy", "obj_unknown"]; - let a: ObjectItem; - let b: ObjectItem; - let props: Omit; - beforeEach(() => { - a = obj("xx"); - b = obj("yy"); - props = { - ref: listReference(), - caption: listExpression(() => "[string]") - }; - }); - - it("skip filtering if has just part of the list", () => { - const store = new RefFilterStore({ ...props, datasource: list.loading() }, null); - // Restore state - store.fromJSON(savedJson); - // Check state - expect(store.toJSON()).toEqual(savedJson); - // Update with partial data - const datasource = new ListValueBuilder().withItems([a, b]).withHasMore(true).build(); - store.updateProps({ ...props, datasource }); - // Check that state still has unknown GUIDs - expect(store.toJSON()).toEqual(savedJson); - }); - - it("allows to restore any state if datasource is loading", () => { - const store = new RefFilterStore({ ...props, datasource: list.loading() }, null); - // Restore state - store.fromJSON(savedJson); - // Check state - expect(store.toJSON()).toEqual(savedJson); - }); - }); -}); diff --git a/packages/shared/widget-plugin-filtering/src/context.ts b/packages/shared/widget-plugin-filtering/src/context.ts index e34c9f5375..ca8578cdc7 100644 --- a/packages/shared/widget-plugin-filtering/src/context.ts +++ b/packages/shared/widget-plugin-filtering/src/context.ts @@ -1,42 +1,33 @@ -import { FilterCondition } from "mendix/filters/index.js"; +import { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore"; +import { FilterCondition } from "mendix/filters"; import { Context, createContext, useContext } from "react"; -import { APIError, ENOCONTEXT } from "./errors.js"; -import { Result, error, value } from "./result-meta.js"; -import { FilterObserver } from "./typings/FilterObserver.js"; -import { InputFilterInterface } from "./typings/InputFilterInterface.js"; -import { PickerFilterStore } from "./typings/PickerFilterStore.js"; +import { APIError, ENOCONTEXT } from "./errors"; +import { Result, error, value } from "./result-meta"; +import { FilterObserver } from "./typings/FilterObserver"; +import { InputFilterInterface } from "./typings/InputFilterInterface"; export interface FilterAPI { version: 3; parentChannelName: string; - provider: Result; + provider: Result; filterObserver: FilterObserver; sharedInitFilter: Array; } -/** @deprecated */ -export enum FilterType { - STRING = "string", - NUMBER = "number", - ENUMERATION = "enum", - DATE = "date" -} - -export type FilterStoreProvider = DirectProvider | LegacyProvider; - -export type FilterStore = InputFilterInterface | PickerFilterStore; +export type FilterStore = InputFilterInterface | StaticSelectFilterStore; interface DirectProvider { type: "direct"; store: FilterStore | null; } -/** @deprecated */ -export interface LegacyProvider { - type: "legacy"; - get: (type: FilterType) => FilterStore | null; +interface ProviderStub { + type: "stub"; + hint: "No filter store available"; } +export const PROVIDER_STUB = Object.freeze({ type: "stub", hint: "No filter store available" } as const); + type FilterAPIContext = Context; const CONTEXT_OBJECT_PATH = "com.mendix.widgets.web.filterable.filterContext.v2" as const; @@ -65,13 +56,16 @@ export function useFilterAPI(): Result { /** @deprecated This hook is renamed, use `useFilterAPI` instead. */ export const useFilterContextValue = useFilterAPI; -export function getFilterStore(provider: FilterStoreProvider, legacyType: FilterType): FilterStore | null { - switch (provider.type) { - case "direct": - return provider.store; - case "legacy": - return provider.get(legacyType); - default: - return null; - } +export function createContextWithStub(options: { + filterObserver: FilterObserver; + parentChannelName: string; + sharedInitFilter: Array; +}): FilterAPI { + return { + version: 3, + parentChannelName: options.parentChannelName, + provider: value(PROVIDER_STUB), + filterObserver: options.filterObserver, + sharedInitFilter: options.sharedInitFilter + }; } diff --git a/packages/shared/widget-plugin-filtering/src/controllers/input/NumberInputController.ts b/packages/shared/widget-plugin-filtering/src/controllers/input/NumberInputController.ts index 3edbd1e4f9..685846dacc 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/input/NumberInputController.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/input/NumberInputController.ts @@ -1,8 +1,12 @@ +import { + FilterFunctionBinary, + FilterFunctionGeneric, + FilterFunctionNonValue +} from "@mendix/filter-commons/typings/FilterFunctions"; import { debounce } from "@mendix/widget-plugin-platform/utils/debounce"; import { action, autorun, computed, makeObservable, reaction, runInAction } from "mobx"; import { createRef } from "react"; import { InputStore } from "../../stores/input/InputStore"; -import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue } from "../../typings/FilterFunctions"; import { FilterV, Number_InputFilterInterface } from "../../typings/InputFilterInterface"; export type Params = { diff --git a/packages/shared/widget-plugin-filtering/src/controllers/input/StringInputController.ts b/packages/shared/widget-plugin-filtering/src/controllers/input/StringInputController.ts index 91aef3be74..6f58b6730b 100644 --- a/packages/shared/widget-plugin-filtering/src/controllers/input/StringInputController.ts +++ b/packages/shared/widget-plugin-filtering/src/controllers/input/StringInputController.ts @@ -1,13 +1,13 @@ -import { debounce } from "@mendix/widget-plugin-platform/utils/debounce"; -import { action, autorun, computed, makeObservable, reaction, runInAction } from "mobx"; -import { createRef } from "react"; -import { InputStore } from "../../stores/input/InputStore"; import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue, FilterFunctionString -} from "../../typings/FilterFunctions"; +} from "@mendix/filter-commons/typings/FilterFunctions"; +import { debounce } from "@mendix/widget-plugin-platform/utils/debounce"; +import { action, autorun, computed, makeObservable, reaction, runInAction } from "mobx"; +import { createRef } from "react"; +import { InputStore } from "../../stores/input/InputStore"; import { FilterV, String_InputFilterInterface } from "../../typings/InputFilterInterface"; export type Params = { diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticComboboxController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticComboboxController.ts deleted file mode 100644 index c6410a631c..0000000000 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticComboboxController.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController"; -import { ComboboxControllerMixin } from "./mixins/ComboboxControllerMixin"; - -export class StaticComboboxController extends ComboboxControllerMixin(StaticBaseController) { - constructor(props: StaticBaseControllerProps) { - super({ ...props, multiselect: false }); - this.inputPlaceholder = props.placeholder ?? "Search"; - } -} diff --git a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticSelectController.ts b/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticSelectController.ts deleted file mode 100644 index 8174fbb975..0000000000 --- a/packages/shared/widget-plugin-filtering/src/controllers/picker/StaticSelectController.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { StaticBaseController, StaticBaseControllerProps } from "./StaticBaseController"; -import { SelectControllerMixin } from "./mixins/SelectControllerMixin"; - -export class StaticSelectController extends SelectControllerMixin(StaticBaseController) { - constructor(props: StaticBaseControllerProps) { - super(props); - this.emptyOption.caption = props.emptyCaption || "None"; - this.placeholder = props.emptyCaption || "Select"; - } -} diff --git a/packages/shared/widget-plugin-filtering/src/controls/filter-selector/useSelect.tsx b/packages/shared/widget-plugin-filtering/src/controls/filter-selector/useSelect.tsx index c589f08c33..c8e901ea87 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/filter-selector/useSelect.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/filter-selector/useSelect.tsx @@ -36,7 +36,7 @@ export function useSelect(props: useSelectProps): ViewProps { items: props.options, selectedItem, itemToString, - onSelectedItemChange: ({ selectedItem }) => props.onSelect(selectedItem.value) + onSelectedItemChange: ({ selectedItem }) => props.onSelect(selectedItem?.value ?? null) }); const { refs, floatingStyles } = useFloating({ diff --git a/packages/shared/widget-plugin-filtering/src/controls/input/InputWithFilters.tsx b/packages/shared/widget-plugin-filtering/src/controls/input/InputWithFilters.tsx index dd779c096f..5c203b23bf 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/input/InputWithFilters.tsx +++ b/packages/shared/widget-plugin-filtering/src/controls/input/InputWithFilters.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; import classNames from "classnames"; import { FilterSelector } from "../filter-selector/FilterSelector"; import { InputComponentProps } from "./typings"; -import { AllFunctions } from "../../typings/FilterFunctions"; +import { AllFunctions } from "@mendix/filter-commons/typings/FilterFunctions"; export function InputWithFiltersComponent(props: InputComponentProps): React.ReactElement { const { diff --git a/packages/shared/widget-plugin-filtering/src/controls/input/typings.ts b/packages/shared/widget-plugin-filtering/src/controls/input/typings.ts index becf0608be..be875f3474 100644 --- a/packages/shared/widget-plugin-filtering/src/controls/input/typings.ts +++ b/packages/shared/widget-plugin-filtering/src/controls/input/typings.ts @@ -1,5 +1,5 @@ +import { AllFunctions } from "@mendix/filter-commons/typings/FilterFunctions"; import { InputStore } from "../../stores/input/InputStore"; -import { AllFunctions } from "../../typings/FilterFunctions"; import { InputFilterInterface } from "../../typings/InputFilterInterface"; export interface BaseProps { diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/BaseStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/BaseStoreProvider.ts index 883290b619..7cd0dfc276 100644 --- a/packages/shared/widget-plugin-filtering/src/custom-filter-api/BaseStoreProvider.ts +++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/BaseStoreProvider.ts @@ -1,7 +1,7 @@ +import { isAnd, isTag } from "@mendix/filter-commons/condition-utils"; import { disposeBatch } from "@mendix/widget-plugin-mobx-kit/disposeBatch"; import { ISetupable } from "@mendix/widget-plugin-mobx-kit/setupable"; import { FilterCondition } from "mendix/filters"; -import { isAnd, isTag } from "../condition-utils"; import { FilterAPI } from "../context"; import { Filter } from "../typings/FilterObserver"; diff --git a/packages/shared/widget-plugin-filtering/src/custom-filter-api/EnumStoreProvider.ts b/packages/shared/widget-plugin-filtering/src/custom-filter-api/EnumStoreProvider.ts new file mode 100644 index 0000000000..b0ace1ff31 --- /dev/null +++ b/packages/shared/widget-plugin-filtering/src/custom-filter-api/EnumStoreProvider.ts @@ -0,0 +1,24 @@ +import { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore"; +import { FilterAPI } from "../context"; +import { BaseStoreProvider } from "./BaseStoreProvider"; +import { FilterSpec } from "./typings"; + +export class EnumStoreProvider extends BaseStoreProvider { + protected _store: StaticSelectFilterStore; + protected filterAPI: FilterAPI; + readonly dataKey: string; + + constructor(filterAPI: FilterAPI, spec: FilterSpec) { + super(); + this.filterAPI = filterAPI; + this.dataKey = spec.dataKey; + this._store = new StaticSelectFilterStore( + spec.attributes, + this.findInitFilter(filterAPI.sharedInitFilter, this.dataKey) + ); + } + + get store(): StaticSelectFilterStore { + return this._store; + } +} diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts b/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts index d6cdab88bf..6805b94642 100644 --- a/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts +++ b/packages/shared/widget-plugin-filtering/src/helpers/useDateFilterAPI.ts @@ -1,5 +1,5 @@ import { useRef } from "react"; -import { FilterType, getFilterStore, useFilterContextValue } from "../context"; +import { useFilterAPI } from "../context"; import { APIError, EMISSINGSTORE, EStoreTypeMisMatch } from "../errors"; import { error, Result, value } from "../result-meta"; import { isDateFilter } from "../stores/input/store-utils"; @@ -11,7 +11,7 @@ export interface Date_FilterAPIv2 { } export function useDateFilterAPI(): Result { - const ctx = useFilterContextValue(); + const ctx = useFilterAPI(); const dateAPI = useRef(); if (ctx.hasError) { @@ -24,7 +24,7 @@ export function useDateFilterAPI(): Result { return error(api.provider.error); } - const store = getFilterStore(api.provider.value, FilterType.DATE); + const store = api.provider.value.type === "direct" ? api.provider.value.store : null; if (store === null) { return error(EMISSINGSTORE); diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useDateSync.ts b/packages/shared/widget-plugin-filtering/src/helpers/useDateSync.ts index cc09e597a3..b1363eb67a 100644 --- a/packages/shared/widget-plugin-filtering/src/helpers/useDateSync.ts +++ b/packages/shared/widget-plugin-filtering/src/helpers/useDateSync.ts @@ -1,7 +1,11 @@ +import { + FilterFunctionBinary, + FilterFunctionGeneric, + FilterFunctionNonValue +} from "@mendix/filter-commons/typings/FilterFunctions"; import { ActionValue, EditableValue } from "mendix"; import { reaction } from "mobx"; import { useEffect, useRef } from "react"; -import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue } from "../typings/FilterFunctions"; import { InputFilterInterface } from "../typings/InputFilterInterface"; interface Props { diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterAPI.ts b/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterAPI.ts index c1790cc83c..ca908bbcb9 100644 --- a/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterAPI.ts +++ b/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterAPI.ts @@ -1,5 +1,5 @@ import { useRef } from "react"; -import { FilterType, getFilterStore, useFilterContextValue } from "../context"; +import { useFilterAPI } from "../context"; import { APIError, EMISSINGSTORE, EStoreTypeMisMatch } from "../errors"; import { error, Result, value } from "../result-meta"; import { isNumberFilter } from "../stores/input/store-utils"; @@ -11,7 +11,7 @@ export interface Number_FilterAPIv2 { } export function useNumberFilterAPI(): Result { - const ctx = useFilterContextValue(); + const ctx = useFilterAPI(); const numAPI = useRef(); if (ctx.hasError) { @@ -24,7 +24,7 @@ export function useNumberFilterAPI(): Result { return error(api.provider.error); } - const store = getFilterStore(api.provider.value, FilterType.NUMBER); + const store = api.provider.value.type === "direct" ? api.provider.value.store : null; if (store === null) { return error(EMISSINGSTORE); diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts b/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts index 9100b1fd16..b40927efb9 100644 --- a/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts +++ b/packages/shared/widget-plugin-filtering/src/helpers/useNumberFilterController.ts @@ -1,17 +1,7 @@ -import { useEffect, useState } from "react"; +import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup"; import { NumberFilterController, Params } from "../controllers/input/NumberInputController"; -import { ArgumentInterface } from "../typings/ArgumentInterface"; -import { AllFunctions } from "../typings/FilterFunctions"; -import { FilterFn, InputFilterBaseInterface } from "../typings/InputFilterInterface"; - -export function useNumberFilterController< - F extends InputFilterBaseInterface, - A extends ArgumentInterface, - Fn extends AllFunctions = FilterFn ->(params: Params): NumberFilterController { - const [ctrl] = useState(() => new NumberFilterController(params)); - - useEffect(() => ctrl.setup(), [ctrl]); +export function useNumberFilterController(params: Params): NumberFilterController { + const ctrl = useSetup(() => new NumberFilterController(params)); return ctrl; } diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts b/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts deleted file mode 100644 index 24c7d9fa57..0000000000 --- a/packages/shared/widget-plugin-filtering/src/helpers/useSelectFilterAPI.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useRef } from "react"; -import { FilterType, getFilterStore, useFilterContextValue } from "../context"; -import { APIError, EMISSINGSTORE, EStoreTypeMisMatch, OPTIONS_NOT_FILTERABLE } from "../errors"; -import { Result, error, value } from "../result-meta"; -import { PickerFilterStore } from "../typings/PickerFilterStore"; - -export interface Select_FilterAPIv2 { - filterStore: PickerFilterStore; - parentChannelName?: string; -} - -interface Props { - filterable: boolean; -} - -export function useSelectFilterAPI({ filterable }: Props): Result { - const ctx = useFilterContextValue(); - const slctAPI = useRef(); - - if (ctx.hasError) { - return error(ctx.error); - } - - const api = ctx.value; - - if (api.provider.hasError) { - return error(api.provider.error); - } - - const store = getFilterStore(api.provider.value, FilterType.ENUMERATION); - - if (store === null) { - return error(EMISSINGSTORE); - } - - if (store.storeType !== "select" && store.storeType !== "refselect") { - return error(EStoreTypeMisMatch("dropdown filter", store.arg1.type)); - } - - if (store.storeType === "refselect") { - const configurationConflict = filterable && store.optionsFilterable === false; - if (configurationConflict) { - return error(OPTIONS_NOT_FILTERABLE); - } - } - - return value((slctAPI.current ??= { filterStore: store, parentChannelName: api.parentChannelName })); -} diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useSetup.ts b/packages/shared/widget-plugin-filtering/src/helpers/useSetup.ts deleted file mode 100644 index c21157a027..0000000000 --- a/packages/shared/widget-plugin-filtering/src/helpers/useSetup.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useEffect, useState } from "react"; - -export interface Setupable { - setup(): void | (() => void); -} - -export function useSetup(props: () => T): T { - const [obj] = useState(props); - useEffect(() => obj.setup(), [obj]); - return obj; -} diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useSetupUpdate.ts b/packages/shared/widget-plugin-filtering/src/helpers/useSetupUpdate.ts deleted file mode 100644 index 1c22ec422d..0000000000 --- a/packages/shared/widget-plugin-filtering/src/helpers/useSetupUpdate.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useEffect } from "react"; -import { Setupable, useSetup } from "./useSetup"; - -interface IUpdateModel

extends Setupable { - updateProps: (props: P) => void; -} - -export function useSetupUpdate, P>(setup: () => T, props: P): T { - const obj = useSetup(setup); - - // NOTE: Don't use dependencies. - // Model should be updated on every render. - useEffect(() => obj.updateProps(props)); - return obj; -} diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterAPI.ts b/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterAPI.ts index f3c11fbcda..9e5b49dcd7 100644 --- a/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterAPI.ts +++ b/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterAPI.ts @@ -1,5 +1,5 @@ import { useRef } from "react"; -import { FilterType, getFilterStore, useFilterContextValue } from "../context"; +import { useFilterAPI } from "../context"; import { APIError, EMISSINGSTORE, EStoreTypeMisMatch } from "../errors"; import { error, Result, value } from "../result-meta"; import { isStringFilter } from "../stores/input/store-utils"; @@ -11,7 +11,7 @@ export interface String_FilterAPIv2 { } export function useStringFilterAPI(): Result { - const ctx = useFilterContextValue(); + const ctx = useFilterAPI(); const strAPI = useRef(); if (ctx.hasError) { @@ -24,7 +24,7 @@ export function useStringFilterAPI(): Result { return error(api.provider.error); } - const store = getFilterStore(api.provider.value, FilterType.STRING); + const store = api.provider.value.type === "direct" ? api.provider.value.store : null; if (store === null) { return error(EMISSINGSTORE); diff --git a/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts b/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts index 35c26ad8c4..0316703559 100644 --- a/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts +++ b/packages/shared/widget-plugin-filtering/src/helpers/useStringFilterController.ts @@ -1,17 +1,8 @@ -import { useEffect, useState } from "react"; +import { useSetup } from "@mendix/widget-plugin-mobx-kit/react/useSetup"; import { Params, StringFilterController } from "../controllers/input/StringInputController"; -import { ArgumentInterface } from "../typings/ArgumentInterface"; -import { AllFunctions } from "../typings/FilterFunctions"; -import { FilterFn, InputFilterBaseInterface } from "../typings/InputFilterInterface"; -export function useStringFilterController< - F extends InputFilterBaseInterface, - A extends ArgumentInterface, - Fn extends AllFunctions = FilterFn ->(params: Params): StringFilterController { - const [ctrl] = useState(() => new StringFilterController(params)); - - useEffect(() => ctrl.setup(), [ctrl]); +export function useStringFilterController(params: Params): StringFilterController { + const ctrl = useSetup(() => new StringFilterController(params)); return ctrl; } diff --git a/packages/shared/widget-plugin-filtering/src/mobx-utils.ts b/packages/shared/widget-plugin-filtering/src/mobx-utils.ts deleted file mode 100644 index 0b5ab0649a..0000000000 --- a/packages/shared/widget-plugin-filtering/src/mobx-utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function disposeFx(): [disposers: Array<() => void>, dispose: () => void] { - const disposers: Array<() => void> = []; - return [ - disposers, - () => { - disposers.forEach(dispose => dispose()); - disposers.length = 0; - } - ]; -} diff --git a/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts b/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts deleted file mode 100644 index d9cb4f3b14..0000000000 --- a/packages/shared/widget-plugin-filtering/src/providers/LegacyPv.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { ListAttributeValue } from "mendix"; -import { FilterCondition } from "mendix/filters"; -import { action, comparer, computed, makeObservable, observable, reaction } from "mobx"; -import { FilterType as Ft, LegacyProvider } from "../context"; -import { DateInputFilterStore } from "../stores/input/DateInputFilterStore"; -import { NumberInputFilterStore } from "../stores/input/NumberInputFilterStore"; -import { StringInputFilterStore } from "../stores/input/StringInputFilterStore"; -import { StaticSelectFilterStore } from "../stores/picker/StaticSelectFilterStore"; -import { InputFilterInterface } from "../typings/InputFilterInterface"; -import { PickerFilterStore } from "../typings/PickerFilterStore"; -import { FiltersSettingsMap } from "../typings/settings"; - -type FilterMap = { - [Ft.STRING]: StringInputFilterStore | null; - [Ft.NUMBER]: NumberInputFilterStore | null; - [Ft.DATE]: DateInputFilterStore | null; - [Ft.ENUMERATION]: StaticSelectFilterStore | null; -}; - -type FilterList = [ - StringInputFilterStore | null, - NumberInputFilterStore | null, - DateInputFilterStore | null, - StaticSelectFilterStore | null -]; - -export class LegacyPv implements LegacyProvider { - readonly type = "legacy"; - private _attrs: ListAttributeValue[]; - dispose?: () => void; - filterMap: FilterMap; - filterList: FilterList; - - constructor(attrs: ListAttributeValue[], dsViewState: Array | null) { - this._attrs = attrs; - const map = (this.filterMap = createMap(attrs, dsViewState)); - this.filterList = [map[Ft.STRING], map[Ft.NUMBER], map[Ft.DATE], map[Ft.ENUMERATION]]; - makeObservable(this, { - _attrs: observable.ref, - conditions: computed, - settings: computed, - updateProps: action - }); - } - - get conditions(): Array { - return this.filterList.map(store => (store ? store.condition : undefined)); - } - - get settings(): FiltersSettingsMap { - return new Map(); - } - - set settings(_: unknown) {} - - get = (type: Ft): InputFilterInterface | PickerFilterStore | null => { - return this.filterMap[type]; - }; - - updateProps(attrs: ListAttributeValue[]): void { - this._attrs = attrs; - } - - updateFilters(attrs: ListAttributeValue[]): void { - const [str, num, dte, enm] = groupByType(attrs); - const [s1, s2, s3, s4] = this.filterList; - s1?.updateProps(str); - s2?.updateProps(num); - s3?.updateProps(dte); - s4?.updateProps(enm); - } - - setup(): () => void { - const disposers: Array<() => void> = []; - this.filterList.forEach(store => { - if (store && "setup" in store) { - disposers.push(store.setup()); - } - }); - disposers.push( - reaction( - () => this._attrs, - attrs => this.updateFilters(attrs), - { equals: comparer.shallow } - ) - ); - - return (this.dispose = () => disposers.forEach(dispose => dispose())); - } -} - -function createMap(attrs: ListAttributeValue[], dsViewState: Array | null): FilterMap { - const [ini1 = null, ini2 = null, ini3 = null, ini4 = null] = dsViewState ?? []; - const [str, num, dte, enm] = groupByType(attrs); - - const r: FilterMap = { - [Ft.STRING]: str.length > 0 ? new StringInputFilterStore(str, ini1) : null, - [Ft.NUMBER]: num.length > 0 ? new NumberInputFilterStore(num, ini2) : null, - [Ft.DATE]: dte.length > 0 ? new DateInputFilterStore(dte, ini3) : null, - [Ft.ENUMERATION]: enm.length > 0 ? new StaticSelectFilterStore(enm, ini4) : null - }; - - return r; -} - -function groupByType( - attrs: ListAttributeValue[] -): [ - Array>, - Array>, - Array>, - Array> -] { - return [ - attrs.filter(isStringAttr), - attrs.filter(isNumberAttr), - attrs.filter(isDateAttr), - attrs.filter((a): a is ListAttributeValue => isBoolAttr(a) || isEnumAttr(a)) - ]; -} - -function isDateAttr(attr: ListAttributeValue): attr is ListAttributeValue { - return attr.type === "DateTime"; -} - -function isBoolAttr(attr: ListAttributeValue): attr is ListAttributeValue { - return attr.type === "Boolean"; -} - -function isEnumAttr(attr: ListAttributeValue): attr is ListAttributeValue { - return attr.type === "Enum"; -} - -function isNumberAttr(attr: ListAttributeValue): attr is ListAttributeValue { - return attr.type === "Long" || attr.type === "Decimal" || attr.type === "Integer" || attr.type === "AutoNumber"; -} -function isStringAttr(attr: ListAttributeValue): attr is ListAttributeValue { - return attr.type === "HashString" || attr.type === "String"; -} diff --git a/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts b/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts index b798603b73..a999e31901 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/generic/CustomFilterHost.ts @@ -1,9 +1,9 @@ +import { tag } from "@mendix/filter-commons/condition-utils"; +import { FiltersSettingsMap } from "@mendix/filter-commons/typings/settings"; import { FilterCondition } from "mendix/filters"; import { and } from "mendix/filters/builders"; import { autorun, makeAutoObservable } from "mobx"; -import { tag } from "../../condition-utils"; import { Filter, FilterObserver } from "../../typings/FilterObserver"; -import { FiltersSettingsMap } from "../../typings/settings"; export class CustomFilterHost implements FilterObserver { private filters: Map = new Map(); diff --git a/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts b/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts deleted file mode 100644 index f627172731..0000000000 --- a/packages/shared/widget-plugin-filtering/src/stores/generic/HeaderFiltersStore.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ListAttributeValue } from "mendix"; -import { FilterCondition } from "mendix/filters"; -import { computed, makeObservable } from "mobx"; -import { FilterAPI } from "../../context"; -import { APIError } from "../../errors"; -import { LegacyPv } from "../../providers/LegacyPv"; -import { Result, value } from "../../result-meta"; -import { FilterObserver } from "../../typings/FilterObserver"; -import { FiltersSettingsMap } from "../../typings/settings"; - -export interface FilterListType { - filter: ListAttributeValue; -} - -export interface HeaderFiltersStoreSpec { - filterList: FilterListType[]; - filterChannelName: string; - headerInitFilter: Array; - sharedInitFilter: Array; - customFilterHost: FilterObserver; -} - -export class HeaderFiltersStore { - private provider: Result; - context: FilterAPI; - - constructor({ - filterList, - filterChannelName, - headerInitFilter, - sharedInitFilter, - customFilterHost: filterObserver - }: HeaderFiltersStoreSpec) { - this.provider = this.createProvider(filterList, headerInitFilter); - this.context = { - version: 3, - parentChannelName: filterChannelName, - provider: this.provider, - sharedInitFilter, - filterObserver - }; - makeObservable(this, { - conditions: computed, - settings: computed - }); - } - - get conditions(): Array { - return this.provider.hasError ? [] : this.provider.value.conditions; - } - - get settings(): FiltersSettingsMap { - return this.provider.hasError ? new Map() : this.provider.value.settings; - } - - set settings(value: FiltersSettingsMap | undefined) { - if (this.provider.hasError) { - return; - } - - this.provider.value.settings = value; - } - - createProvider( - filterList: FilterListType[], - initFilter: Array - ): Result { - return value( - new LegacyPv( - filterList.map(f => f.filter), - initFilter - ) - ); - } - - setup(): (() => void) | void { - if (this.provider.hasError) { - return; - } - - return this.provider.value.setup(); - } -} diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts index 08c21e5cfc..f599e940cf 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/input/BaseInputFilterStore.ts @@ -1,3 +1,5 @@ +import { AllFunctions } from "@mendix/filter-commons/typings/FilterFunctions"; +import { FilterData, InputData } from "@mendix/filter-commons/typings/settings"; import { Big } from "big.js"; import { AttributeMetaData } from "mendix"; import { FilterCondition } from "mendix/filters"; @@ -17,8 +19,6 @@ import { startsWith } from "mendix/filters/builders"; import { action, computed, makeObservable, observable } from "mobx"; -import { AllFunctions } from "../../typings/FilterFunctions"; -import { FilterData, InputData } from "../../typings/settings"; import { Argument } from "./Argument"; type StateTuple = [Fn] | [Fn, V] | [Fn, V, V]; diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts index 7a8807a803..b7fbcfcbf5 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/input/DateInputFilterStore.ts @@ -1,3 +1,18 @@ +import { + betweenToState, + isAnd, + isEmptyExp, + isNotEmptyExp, + isOr, + singularToState +} from "@mendix/filter-commons/condition-utils"; +import { + FilterFunctionBinary, + FilterFunctionGeneric, + FilterFunctionNonValue +} from "@mendix/filter-commons/typings/FilterFunctions"; +import { FilterName } from "@mendix/filter-commons/typings/mendix"; +import { FilterData, InputData } from "@mendix/filter-commons/typings/settings"; import { AttributeMetaData, DateTimeFormatter, ListAttributeValue, SimpleFormatter } from "mendix"; import { AndCondition, FilterCondition, LiteralExpression } from "mendix/filters"; import { @@ -15,11 +30,7 @@ import { or } from "mendix/filters/builders"; import { action, comparer, IReactionDisposer, makeObservable, observable, reaction } from "mobx"; -import { betweenToState, isAnd, isEmptyExp, isNotEmptyExp, isOr, singularToState } from "../../condition-utils"; -import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue } from "../../typings/FilterFunctions"; import { Date_InputFilterInterface } from "../../typings/InputFilterInterface"; -import { FilterFunction } from "../../typings/mendix"; -import { FilterData, InputData } from "../../typings/settings"; import { DateArgument } from "./Argument"; import { BaseInputFilterStore } from "./BaseInputFilterStore"; @@ -249,7 +260,7 @@ export class DateInputFilterStore } } - private mapFn = (name: FilterFunction | "between" | "empty" | "notEmpty"): DateFns => { + private mapFn = (name: FilterName | "between" | "empty" | "notEmpty"): DateFns => { switch (name) { case "day:=": return "equal"; diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts index 18a302d02d..7985c66ba9 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/input/NumberInputFilterStore.ts @@ -1,11 +1,15 @@ +import { inputStateFromCond } from "@mendix/filter-commons/condition-utils"; +import { + FilterFunctionBinary, + FilterFunctionGeneric, + FilterFunctionNonValue +} from "@mendix/filter-commons/typings/FilterFunctions"; +import { FilterData, InputData } from "@mendix/filter-commons/typings/settings"; import { Big } from "big.js"; import { AttributeMetaData, ListAttributeValue, SimpleFormatter } from "mendix"; import { FilterCondition } from "mendix/filters"; import { action, comparer, makeObservable } from "mobx"; -import { inputStateFromCond } from "../../condition-utils"; -import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue } from "../../typings/FilterFunctions"; import { Number_InputFilterInterface } from "../../typings/InputFilterInterface"; -import { FilterData, InputData } from "../../typings/settings"; import { NumberArgument } from "./Argument"; import { BaseInputFilterStore } from "./BaseInputFilterStore"; import { baseNames } from "./fn-mappers"; diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts b/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts index 31c720798b..3bae4046c5 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/input/StringInputFilterStore.ts @@ -1,15 +1,15 @@ -import { AttributeMetaData, ListAttributeValue, SimpleFormatter } from "mendix"; -import { FilterCondition } from "mendix/filters"; -import { action, comparer, makeObservable } from "mobx"; -import { inputStateFromCond } from "../../condition-utils"; +import { inputStateFromCond } from "@mendix/filter-commons/condition-utils"; import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue, FilterFunctionString -} from "../../typings/FilterFunctions"; +} from "@mendix/filter-commons/typings/FilterFunctions"; +import { FilterData, InputData } from "@mendix/filter-commons/typings/settings"; +import { AttributeMetaData, ListAttributeValue, SimpleFormatter } from "mendix"; +import { FilterCondition } from "mendix/filters"; +import { action, comparer, makeObservable } from "mobx"; import { String_InputFilterInterface } from "../../typings/InputFilterInterface"; -import { FilterData, InputData } from "../../typings/settings"; import { StringArgument } from "./Argument"; import { BaseInputFilterStore } from "./BaseInputFilterStore"; import { baseNames } from "./fn-mappers"; diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/fn-mappers.ts b/packages/shared/widget-plugin-filtering/src/stores/input/fn-mappers.ts index 71894c6d81..8ad3c58361 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/input/fn-mappers.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/input/fn-mappers.ts @@ -1,8 +1,12 @@ -import { FilterFunctionBinary, FilterFunctionGeneric, FilterFunctionNonValue } from "../../typings/FilterFunctions"; -import { FilterFunction } from "../../typings/mendix"; +import { + FilterFunctionBinary, + FilterFunctionGeneric, + FilterFunctionNonValue +} from "@mendix/filter-commons/typings/FilterFunctions"; +import { FilterName } from "@mendix/filter-commons/typings/mendix"; export function baseNames( - fn: FilterFunction | "between" | "empty" | "notEmpty" + fn: FilterName | "between" | "empty" | "notEmpty" ): FilterFunctionGeneric | FilterFunctionNonValue | FilterFunctionBinary { switch (fn) { case "=": diff --git a/packages/shared/widget-plugin-filtering/src/stores/input/store-utils.ts b/packages/shared/widget-plugin-filtering/src/stores/input/store-utils.ts index 63f382dea3..542391c3eb 100644 --- a/packages/shared/widget-plugin-filtering/src/stores/input/store-utils.ts +++ b/packages/shared/widget-plugin-filtering/src/stores/input/store-utils.ts @@ -1,3 +1,4 @@ +import { StaticSelectFilterStore } from "@mendix/widget-plugin-dropdown-filter/stores/StaticSelectFilterStore"; import { ListAttributeValue } from "mendix"; import { FilterCondition } from "mendix/filters"; import { @@ -9,7 +10,6 @@ import { import { DateInputFilterStore } from "../input/DateInputFilterStore"; import { NumberInputFilterStore } from "../input/NumberInputFilterStore"; import { StringInputFilterStore } from "../input/StringInputFilterStore"; -import { StaticSelectFilterStore } from "../picker/StaticSelectFilterStore"; export type InputFilterStore = StringInputFilterStore | NumberInputFilterStore | DateInputFilterStore; diff --git a/packages/shared/widget-plugin-filtering/src/stores/utils/is-input-data.ts b/packages/shared/widget-plugin-filtering/src/stores/utils/is-input-data.ts deleted file mode 100644 index ce484a9507..0000000000 --- a/packages/shared/widget-plugin-filtering/src/stores/utils/is-input-data.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { InputData } from "../../typings/settings"; - -const fnNames = new Set([ - "empty", - "notEmpty", - "equal", - "notEqual", - "greater", - "greaterEqual", - "smaller", - "smallerEqual", - "between", - "contains", - "startsWith", - "endsWith" -]); - -export function isInputData(data: unknown): data is InputData { - if (Array.isArray(data)) { - const [name] = data; - return fnNames.has(name); - } - return false; -} diff --git a/packages/shared/widget-plugin-filtering/src/typings/FilterObserver.ts b/packages/shared/widget-plugin-filtering/src/typings/FilterObserver.ts index c572ed5d38..f9a9a672ff 100644 --- a/packages/shared/widget-plugin-filtering/src/typings/FilterObserver.ts +++ b/packages/shared/widget-plugin-filtering/src/typings/FilterObserver.ts @@ -1,5 +1,5 @@ +import { FilterData, FiltersSettingsMap } from "@mendix/filter-commons/typings/settings"; import { FilterCondition } from "mendix/filters"; -import { FilterData, FiltersSettingsMap } from "./settings"; export interface Filter { toJSON(): FilterData; diff --git a/packages/shared/widget-plugin-filtering/src/typings/InputFilterInterface.ts b/packages/shared/widget-plugin-filtering/src/typings/InputFilterInterface.ts index c58a879fea..8063c3ae3e 100644 --- a/packages/shared/widget-plugin-filtering/src/typings/InputFilterInterface.ts +++ b/packages/shared/widget-plugin-filtering/src/typings/InputFilterInterface.ts @@ -4,7 +4,7 @@ import { FilterFunctionGeneric, FilterFunctionNonValue, FilterFunctionString -} from "./FilterFunctions"; +} from "@mendix/filter-commons/typings/FilterFunctions"; import { ArgumentInterface, DateArgumentInterface, diff --git a/packages/shared/widget-plugin-filtering/src/typings/OptionListFilterInterface.ts b/packages/shared/widget-plugin-filtering/src/typings/OptionListFilterInterface.ts deleted file mode 100644 index 22f50c16a3..0000000000 --- a/packages/shared/widget-plugin-filtering/src/typings/OptionListFilterInterface.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { OptionWithState } from "./OptionWithState"; - -export interface CustomOption { - caption: string; - value: string; -} - -export interface OptionListFilterInterface { - type: "refselect" | "select"; - storeType: "optionlist" | "refselect" | "select"; - options: OptionWithState[]; - selected: string[]; - isLoading: boolean; - hasMore: boolean; - canSearch: boolean; - - replace(value: string[]): void; - toggle(value: string): void; - loadMore(): void; - setSearch(term: string | undefined): void; - isValidValue(value: string): boolean; - reset(): void; - clear(): void; - UNSAFE_setDefaults(value?: string[]): void; - setup?(): () => void | void; - setCustomOptions(options: CustomOption[]): void; -} diff --git a/packages/shared/widget-plugin-filtering/src/typings/PickerFilterStore.ts b/packages/shared/widget-plugin-filtering/src/typings/PickerFilterStore.ts deleted file mode 100644 index d64f165603..0000000000 --- a/packages/shared/widget-plugin-filtering/src/typings/PickerFilterStore.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { RefFilterStore } from "../stores/picker/RefFilterStore"; -import { StaticSelectFilterStore } from "../stores/picker/StaticSelectFilterStore"; - -export type PickerFilterStore = RefFilterStore | StaticSelectFilterStore; diff --git a/packages/shared/widget-plugin-filtering/src/typings/mendix.ts b/packages/shared/widget-plugin-filtering/src/typings/mendix.ts deleted file mode 100644 index 2544d74ee0..0000000000 --- a/packages/shared/widget-plugin-filtering/src/typings/mendix.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { FilterCondition } from "mendix/filters"; -import { FnName } from "./type-utils"; - -export type FilterFunction = FnName; diff --git a/packages/shared/widget-plugin-filtering/src/typings/type-utils.ts b/packages/shared/widget-plugin-filtering/src/typings/type-utils.ts deleted file mode 100644 index a61d66ebf5..0000000000 --- a/packages/shared/widget-plugin-filtering/src/typings/type-utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type FnName = T extends { name: infer Name } ? Name : never; - -export type Dispose = () => void; - -export type GConstructor = new (...args: any[]) => T; diff --git a/packages/shared/widget-plugin-grid/package.json b/packages/shared/widget-plugin-grid/package.json index acd2a13cd7..a73239ef24 100644 --- a/packages/shared/widget-plugin-grid/package.json +++ b/packages/shared/widget-plugin-grid/package.json @@ -33,6 +33,9 @@ "prepare": "tsc", "test": "jest" }, + "dependencies": { + "@mendix/widget-plugin-mobx-kit": "workspace:^" + }, "devDependencies": { "@mendix/eslint-config-web-widgets": "workspace:*", "@mendix/prettier-config-web-widgets": "workspace:*", diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceController.ts b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts similarity index 90% rename from packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceController.ts rename to packages/shared/widget-plugin-grid/src/query/DatasourceController.ts index 803618e756..fbee1be307 100644 --- a/packages/pluggableWidgets/datagrid-web/src/controllers/DatasourceController.ts +++ b/packages/shared/widget-plugin-grid/src/query/DatasourceController.ts @@ -1,10 +1,11 @@ import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/props-gate"; import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller"; import { ListValue, ValueStatus } from "mendix"; -import { action, autorun, makeAutoObservable } from "mobx"; +import { action, autorun, computed, IComputedValue, makeAutoObservable } from "mobx"; import { QueryController } from "./query-controller"; type Gate = DerivedPropsGate<{ datasource: ListValue }>; + type DatasourceControllerSpec = { gate: Gate }; export class DatasourceController implements ReactiveController, QueryController { @@ -58,7 +59,7 @@ export class DatasourceController implements ReactiveController, QueryController return this.datasource.status === "loading"; } - private get datasource(): ListValue { + get datasource(): ListValue { return this.gate.props.datasource; } @@ -90,12 +91,13 @@ export class DatasourceController implements ReactiveController, QueryController } /** - * Returns a new copy of the controller. + * Returns computed value that holds controller copy. * Recomputes the copy every time the datasource changes. */ - get computedCopy(): DatasourceController { - const [copy] = [this.datasource].map(() => Object.create(this)); - return copy; + get derivedQuery(): IComputedValue { + const data = (): DatasourceController => [this.datasource].map(() => Object.create(this))[0]; + + return computed(data); } setup(): () => void { diff --git a/packages/shared/widget-plugin-grid/src/query/PaginationController.ts b/packages/shared/widget-plugin-grid/src/query/PaginationController.ts new file mode 100644 index 0000000000..012e069e1f --- /dev/null +++ b/packages/shared/widget-plugin-grid/src/query/PaginationController.ts @@ -0,0 +1,92 @@ +import { ReactiveController, ReactiveControllerHost } from "@mendix/widget-plugin-mobx-kit/reactive-controller"; +import { QueryController } from "./query-controller"; + + +type PaginationEnum = "buttons" | "virtualScrolling" | "loadMore"; + +type ShowPagingButtonsEnum = "always" | "auto"; + +type PaginationKind = `${PaginationEnum}.${ShowPagingButtonsEnum}`; + +interface StaticProps { + pageSize: number; + pagination: PaginationEnum; + showPagingButtons: ShowPagingButtonsEnum; + showTotalCount: boolean; +} + +type Gate = undefined; + +type PaginationControllerSpec = StaticProps &{ + gate: Gate; + query: QueryController; +}; + + +export class PaginationController implements ReactiveController { + private readonly _pageSize: number; + private readonly _query: QueryController; + readonly pagination: PaginationEnum; + readonly paginationKind: PaginationKind; + readonly showPagingButtons: ShowPagingButtonsEnum; + readonly showTotalCount: boolean; + + constructor(host: ReactiveControllerHost, spec: PaginationControllerSpec) { + host.addController(this); + this._pageSize = spec.pageSize; + this._query = spec.query; + this.pagination = spec.pagination; + this.showPagingButtons = spec.showPagingButtons; + this.showTotalCount = spec.showTotalCount; + this.paginationKind = `${this.pagination}.${this.showPagingButtons}`; + this._setInitParams(); + } + + get isLimitBased(): boolean { + return this.pagination === "virtualScrolling" || this.pagination === "loadMore"; + } + + get pageSize(): number { + return this._pageSize; + } + + get currentPage(): number { + const { + _query: { limit, offset }, + pageSize + } = this; + return this.isLimitBased ? limit / pageSize : offset / pageSize; + } + + get showPagination(): boolean { + switch (this.paginationKind) { + case "buttons.always": + return true; + case "buttons.auto": { + const { totalCount = -1 } = this._query; + return totalCount > this._query.limit; + } + default: + return this.showTotalCount; + } + } + + private _setInitParams(): void { + if (this.pagination === "buttons" || this.showTotalCount) { + this._query.requestTotalCount(true); + } + + this._query.setPageSize(this.pageSize); + } + + setup(): void {} + + setPage = (computePage: (prevPage: number) => number): void => { + const newPage = computePage(this.currentPage); + if (this.isLimitBased) { + this._query.setLimit(newPage * this.pageSize); + } else { + this._query.setOffset(newPage * this.pageSize); + } + }; +} diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/RefreshController.ts b/packages/shared/widget-plugin-grid/src/query/RefreshController.ts similarity index 100% rename from packages/pluggableWidgets/datagrid-web/src/controllers/RefreshController.ts rename to packages/shared/widget-plugin-grid/src/query/RefreshController.ts diff --git a/packages/pluggableWidgets/datagrid-web/src/controllers/query-controller.ts b/packages/shared/widget-plugin-grid/src/query/query-controller.ts similarity index 100% rename from packages/pluggableWidgets/datagrid-web/src/controllers/query-controller.ts rename to packages/shared/widget-plugin-grid/src/query/query-controller.ts diff --git a/packages/shared/widget-plugin-test-utils/package.json b/packages/shared/widget-plugin-test-utils/package.json index 889942b8ce..8b2c5d0021 100644 --- a/packages/shared/widget-plugin-test-utils/package.json +++ b/packages/shared/widget-plugin-test-utils/package.json @@ -30,7 +30,7 @@ "test": "jest" }, "dependencies": { - "mendix": "^10.16.49747" + "mendix": "^10.21.64362" }, "devDependencies": { "@mendix/eslint-config-web-widgets": "workspace:*",