From 5f1bbec88f9cfdc2070c316561c337a7040cd66b Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Mon, 28 Aug 2023 18:13:04 +0200 Subject: [PATCH 001/122] wip: add advanced search drawer --- packages/app-aco/package.json | 2 + .../AdvancedSearch/AdvancedSearch.tsx | 35 +++++ .../AdvancedSearch/FilterManager.ts | 51 ++++++ .../src/components/AdvancedSearch/Form.tsx | 145 ++++++++++++++++++ .../src/components/AdvancedSearch/index.tsx | 1 + .../src/components/AdvancedSearch/styled.tsx | 12 ++ .../src/components/AdvancedSearch/types.tsx | 8 + .../ContentEntries/Filters/Filters.tsx | 37 ++++- yarn.lock | 2 + 9 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/FilterManager.ts create mode 100644 packages/app-aco/src/components/AdvancedSearch/Form.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/index.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/styled.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/types.tsx diff --git a/packages/app-aco/package.json b/packages/app-aco/package.json index 054c506f6f7..ed8750e320e 100644 --- a/packages/app-aco/package.json +++ b/packages/app-aco/package.json @@ -31,6 +31,8 @@ "graphql": "^15.7.2", "graphql-tag": "^2.12.6", "lodash": "^4.2.0", + "mobx": "^6.9.0", + "mobx-react-lite": "^3.4.3", "react": "17.0.2", "react-dnd": "^16.0.1", "react-dom": "17.0.2", diff --git a/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx new file mode 100644 index 00000000000..cbcdeb0a435 --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx @@ -0,0 +1,35 @@ +import React, { useState } from "react"; +import { ReactComponent as CloseIcon } from "@material-design-icons/svg/round/close.svg"; + +import { DrawerContent, DrawerHeader } from "@webiny/ui/Drawer"; +import { ButtonPrimary, IconButton } from "@webiny/ui/Button"; +import { Typography } from "@webiny/ui/Typography"; + +import { Drawer } from "~/components/AdvancedSearch/styled"; + +import { Field } from "./types"; +import Form from "~/components/AdvancedSearch/Form"; + +interface AdvancedSearchProps { + fields: Field[]; +} + +export const AdvancedSearch: React.VFC = ({ fields }) => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(false)} dir="rtl"> + + Search details + } onClick={() => setOpen(false)} /> + + +
+ + + + setOpen(!open)}>Create new search filter + + ); +}; diff --git a/packages/app-aco/src/components/AdvancedSearch/FilterManager.ts b/packages/app-aco/src/components/AdvancedSearch/FilterManager.ts new file mode 100644 index 00000000000..31b2d0b9ed6 --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/FilterManager.ts @@ -0,0 +1,51 @@ +import { makeAutoObservable } from "mobx"; + +interface Filter { + id: string; + field?: string; + condition?: string; + value?: string; + operation?: "AND" | "OR"; +} + +export class FilterManager { + private _filters: Map = new Map(); + + constructor(initialFilters?: Filter[]) { + makeAutoObservable(this); + if (initialFilters) { + for (const filter of initialFilters) { + this._filters.set(filter.id, filter); + } + } + } + + addFilter(filter: Filter) { + this._filters.set(filter.id, filter); + } + + updateFilter(updatedFilter: Filter) { + if (this._filters.has(updatedFilter.id)) { + this._filters.set(updatedFilter.id, updatedFilter); + } else { + console.error(`Filter with ID '${updatedFilter.id}' does not exist.`); + } + } + + removeFilter(filterId: string) { + if (this._filters.has(filterId)) { + this._filters.delete(filterId); + } else { + console.error(`Filter with ID '${filterId}' does not exist.`); + } + } + + getFilters() { + console.log("demo", Array.from(this._filters.values())); + return Array.from(this._filters.values()); + } + + removeAllFilters() { + this._filters.clear(); + } +} diff --git a/packages/app-aco/src/components/AdvancedSearch/Form.tsx b/packages/app-aco/src/components/AdvancedSearch/Form.tsx new file mode 100644 index 00000000000..013bded1c91 --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/Form.tsx @@ -0,0 +1,145 @@ +import React, { useEffect, useMemo } from "react"; +import { ReactComponent as TagIcon } from "@material-design-icons/svg/round/tag.svg"; +import { ReactComponent as DeleteIcon } from "@material-design-icons/svg/round/delete.svg"; +import { Cell, Grid } from "@webiny/ui/Grid"; +import { Form as DefaultForm, FormOnSubmit, useBind } from "@webiny/form"; +import { validation } from "@webiny/validation"; +import { Select } from "@webiny/ui/Select"; +import { Input } from "@webiny/ui/Input"; +import { observer } from "mobx-react-lite"; + +import { FilterManager } from "./FilterManager"; + +import { Field } from "./types"; +import { IconButton, ButtonPrimary } from "@webiny/ui/Button"; +import { generateId } from "@webiny/utils"; + +interface FormProps { + fields: Field[]; +} + +interface FormRowProps { + id: string; + fields: Field[]; + onChange: (id: string, data: any) => void; + onRemove: () => void; +} + +const FormRow: React.VFC = ({ id, fields, onRemove, onChange }) => { + const fieldOptions = useMemo( + () => + fields.map(field => ({ + label: field.label, + value: field.id + })), + [fields] + ); + + const conditionOptions = useMemo( + () => [ + { + label: "is equal to", + value: " " + }, + { + label: "contains", + value: "_contains" + }, + { + label: "starts with", + value: "_startsWith" + }, + { + label: "is not equal to", + value: "_not" + }, + { + label: "doesn't contain", + value: "_not_contains" + }, + { + label: "doesn't start with", + value: "_not_startsWith" + } + ], + [] + ); + + return ( + onChange(id, data)}> + {({ form, Bind, submit }) => ( + + + + + + )} + + + + {form.data.condition && ( + + + + )} + + + + } onClick={onRemove} /> + + + )} + + ); +}; + +const Form: React.VFC = ({ fields }) => { + const filterManager = new FilterManager([{ id: `filter-${generateId()}` }]); + + useEffect(() => { + return () => { + filterManager.removeAllFilters(); + }; + }, []); + + useEffect(() => { + console.log("filterManager.filters", filterManager.getFilters()); + }, [filterManager.getFilters().length]); + + const onChange = (id: string, data: any) => { + filterManager.updateFilter({ + id, + ...data + }); + }; + + return ( + <> + {filterManager.getFilters().map(filter => ( + filterManager.removeFilter(filter.id)} + onChange={onChange} + /> + ))} + { + filterManager.addFilter({ id: `filter-${generateId()}` }); + }} + > + Add a field + + + ); +}; + +export default observer(Form); diff --git a/packages/app-aco/src/components/AdvancedSearch/index.tsx b/packages/app-aco/src/components/AdvancedSearch/index.tsx new file mode 100644 index 00000000000..11a0a0d7e9c --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/index.tsx @@ -0,0 +1 @@ +export * from "./AdvancedSearch"; diff --git a/packages/app-aco/src/components/AdvancedSearch/styled.tsx b/packages/app-aco/src/components/AdvancedSearch/styled.tsx new file mode 100644 index 00000000000..1c325919b6e --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/styled.tsx @@ -0,0 +1,12 @@ +import styled from "@emotion/styled"; +import { Drawer as RmwcDrawer } from "@webiny/ui/Drawer"; + +export const Drawer = styled(RmwcDrawer)` + width: 60vw; +`; + +export const HideEmptyCells = styled.div` + .mdc-layout-grid__cell:empty { + display: none; + } +`; diff --git a/packages/app-aco/src/components/AdvancedSearch/types.tsx b/packages/app-aco/src/components/AdvancedSearch/types.tsx new file mode 100644 index 00000000000..4dc50275288 --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/types.tsx @@ -0,0 +1,8 @@ +export interface Field { + id: string; + type: string; + label: string; + settings: { + modelIds: string[]; + }; +} diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/Filters/Filters.tsx b/packages/app-headless-cms/src/admin/components/ContentEntries/Filters/Filters.tsx index e7d457bbb38..4b4dc23eff4 100644 --- a/packages/app-headless-cms/src/admin/components/ContentEntries/Filters/Filters.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/Filters/Filters.tsx @@ -2,10 +2,32 @@ import React from "react"; import { Filters as BaseFilters, FiltersOnSubmit } from "@webiny/app-admin"; import { useContentEntryListConfig } from "~/admin/config/contentEntries"; import { useContentEntriesList } from "~/admin/views/contentEntries/hooks"; +import { AdvancedSearch } from "@webiny/app-aco"; +import { useModel } from "~/admin/hooks"; +import { CmsModelField } from "@webiny/app-headless-cms-common/types"; + +interface Fields { + id: string; + type: string; + label: string; + settings: any; +} + +const parseModelFields = (modelFields: CmsModelField[]): Fields[] => { + return modelFields.map(modelField => { + return { + id: modelField.fieldId, + type: modelField.type, + label: modelField.label, + settings: modelField.settings + }; + }); +}; export const Filters = () => { const { browser } = useContentEntryListConfig(); const list = useContentEntriesList(); + const { model } = useModel(); const applyFilters: FiltersOnSubmit = data => { if (!Object.keys(data).length) { @@ -21,11 +43,14 @@ export const Filters = () => { }; return ( - + <> + + + ); }; diff --git a/yarn.lock b/yarn.lock index 6c2ebdd5995..072f8566b5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14210,6 +14210,8 @@ __metadata: graphql: ^15.7.2 graphql-tag: ^2.12.6 lodash: ^4.2.0 + mobx: ^6.9.0 + mobx-react-lite: ^3.4.3 react: 17.0.2 react-dnd: ^16.0.1 react-dom: 17.0.2 From 3c7d1e913ccbcb8df82040a7bd9c0be1601b1973 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Tue, 29 Aug 2023 19:29:54 +0200 Subject: [PATCH 002/122] wip: improve filter manager --- .../AdvancedSearch/AdvancedSearch.tsx | 22 +- .../src/components/AdvancedSearch/Button.tsx | 10 + .../src/components/AdvancedSearch/Drawer.tsx | 35 +++ .../AdvancedSearch/FilterManager.ts | 112 ++++++-- .../src/components/AdvancedSearch/Form.tsx | 120 ++------- .../src/components/AdvancedSearch/Row.tsx | 247 ++++++++++++++++++ .../src/components/AdvancedSearch/styled.tsx | 2 +- .../src/components/AdvancedSearch/types.tsx | 14 +- packages/app-aco/src/components/index.tsx | 1 + packages/app-aco/src/types.ts | 2 + .../ContentEntries/Filters/Filters.tsx | 33 +-- 11 files changed, 435 insertions(+), 163 deletions(-) create mode 100644 packages/app-aco/src/components/AdvancedSearch/Button.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/Drawer.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/Row.tsx diff --git a/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx index cbcdeb0a435..97d8ef31311 100644 --- a/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx @@ -1,14 +1,9 @@ import React, { useState } from "react"; -import { ReactComponent as CloseIcon } from "@material-design-icons/svg/round/close.svg"; -import { DrawerContent, DrawerHeader } from "@webiny/ui/Drawer"; -import { ButtonPrimary, IconButton } from "@webiny/ui/Button"; -import { Typography } from "@webiny/ui/Typography"; - -import { Drawer } from "~/components/AdvancedSearch/styled"; +import { Button } from "~/components/AdvancedSearch/Button"; +import { Drawer } from "~/components/AdvancedSearch/Drawer"; import { Field } from "./types"; -import Form from "~/components/AdvancedSearch/Form"; interface AdvancedSearchProps { fields: Field[]; @@ -19,17 +14,8 @@ export const AdvancedSearch: React.VFC = ({ fields }) => { return ( <> - setOpen(false)} dir="rtl"> - - Search details - } onClick={() => setOpen(false)} /> - - - - - - - setOpen(!open)}>Create new search filter + )} @@ -32,28 +39,42 @@ const FormViewWithBind: React.FC = ({ onSubmit }) => { ); }; -const Input = () => { - const { value, onChange, validation } = useBind({ name: "name" }); +const Input = (bindProps: BindComponentProps & { "data-testid"?: string }) => { + const { value, onChange, validate, validation, disabled } = useBind(bindProps); return (
- - onChange(e.target.value)} /> + + { + e.persist(); + validate(); + }} + onChange={e => onChange(e.target.value)} + /> + {disabled ?
: null} {validation.isValid === false ? ( -
{validation.message}
+
{validation.message}
+ ) : null} + {validation.isValid === true ? ( +
Valid!
) : null}
); }; -const FormViewWithHooks: React.FC = ({ onSubmit, invalidFields, data }) => { +const FormViewWithHooks: React.FC = ({ children = null, data, onSubmit, ...props }) => { const [formData] = useState(data || { name: "empty name" }); return ( - + onSubmit && onSubmit(data)} {...props}> {({ form }) => (
- + + {children}
)} @@ -95,7 +116,7 @@ describe("Form", () => { const { rerender } = render(); // anchor - expect(screen.queryByTestId("validation")).toBeNull(); + expect(screen.queryByTestId("validation-error")).toBeNull(); // pivot rerender( @@ -104,4 +125,151 @@ describe("Form", () => { expect(screen.queryByText("Not a valid field!")).toBeTruthy(); }); + + test("should validate data on form submit", async () => { + jest.useFakeTimers(); + const onSubmit = jest.fn(); + const onInvalid = jest.fn(); + + const formElement = ( + + + + ); + + const { rerender } = render(formElement); + + const submitBtn = screen.getByRole("button", { name: /submit/i }); + + // anchor + expect(screen.queryByTestId("validation-error")).toBeNull(); + expect(screen.queryByTestId("validation-success")).toBeNull(); + + // pivot 1 - submit a form without any user input + { + userEvent.click(submitBtn); + jest.runAllTimers(); + await waitFor(() => onInvalid.mock.calls.length > 0); + rerender(formElement); + const errorElement = screen.queryByTestId("validation-error"); + expect(onSubmit).toHaveBeenCalledTimes(0); + expect(onInvalid).toHaveBeenCalledTimes(1); + expect(errorElement).toBeTruthy(); + expect(errorElement!.innerHTML).toBe("Value is required."); + } + + // pivot 2 - enter a partially valid value + { + onInvalid.mockReset(); + const priceInput = screen.getByTestId("price"); + userEvent.type(priceInput, "100"); + userEvent.click(submitBtn); + jest.runAllTimers(); + await waitFor(() => onInvalid.mock.calls.length > 0); + rerender(formElement); + const errorElement = screen.queryByTestId("validation-error"); + expect(onSubmit).toHaveBeenCalledTimes(0); + expect(onInvalid).toHaveBeenCalledTimes(1); + expect(errorElement).toBeTruthy(); + expect(errorElement!.innerHTML).toBe("Value needs to be greater than 100."); + } + + // pivot 3 - enter a valid value + { + onInvalid.mockReset(); + const priceInput = screen.getByTestId("price"); + userEvent.type(priceInput, "200"); + userEvent.click(submitBtn); + jest.runAllTimers(); + await waitFor(() => onInvalid.mock.calls.length > 0); + rerender(formElement); + const successElement = screen.queryByTestId("validation-success"); + const errorElement = screen.queryByTestId("validation-error"); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onInvalid).toHaveBeenCalledTimes(0); + expect(successElement).toBeTruthy(); + expect(errorElement).toBeNull(); + expect(successElement!.innerHTML).toBe("Valid!"); + } + }); + + test("should validate data change immediately, without form submission", async () => { + jest.useFakeTimers(); + const onInvalid = jest.fn(); + + const formElement = ( + + + + ); + + render(formElement); + + // anchor + expect(screen.queryByTestId("validation-error")).toBeNull(); + expect(screen.queryByTestId("validation-success")).toBeNull(); + + // pivot - enter an invalid value + { + const priceInput = screen.getByTestId("price"); + fireEvent.blur(priceInput); + jest.runAllTimers(); + await waitFor(() => false); + const errorElement = screen.queryByTestId("validation-error"); + expect(errorElement).toBeTruthy(); + expect(errorElement!.innerHTML).toBe("Value is required."); + } + }); + + test("should submit form when Enter is pressed (if `submitOnEnter` prop is set)", async () => { + await act(async () => { + jest.useFakeTimers(); + + const onSubmit = jest.fn(); + + const hitEnter = () => { + const inputElement = screen.getByTestId("name"); + fireEvent.keyDown(inputElement, { key: "Enter", code: "Enter", charCode: 13 }); + }; + + const { rerender } = render(); + hitEnter(); + + await waitFor(() => onSubmit.mock.calls.length === 0); + + rerender(); + hitEnter(); + + jest.runAllTimers(); + + await waitFor(() => onSubmit.mock.calls.length > 0); + expect(onSubmit).toHaveBeenLastCalledWith({ name: "empty name" }); + }); + }); + + test("should execute the `onChange` callback whenever data is changed", async () => { + const onChange = jest.fn(); + + const formElement = ( + + + + ); + render(formElement); + + const priceInput = screen.getByTestId("price"); + userEvent.type(priceInput, "100"); + expect(onChange).toHaveBeenCalledTimes(3); + userEvent.type(priceInput, "200"); + expect(onChange).toHaveBeenCalledTimes(6); + + const [formData] = onChange.mock.calls[onChange.mock.calls.length - 1]; + expect(formData).toEqual({ name: "empty name", price: "100200" }); + }); + + test("should disable all fields if Form is disabled", async () => { + const { queryByTestId } = render(); + + expect(queryByTestId("is-disabled")).toBeTruthy(); + }); }); diff --git a/packages/form/src/Form.tsx b/packages/form/src/Form.tsx index 111dc71946e..8f204896bb3 100644 --- a/packages/form/src/Form.tsx +++ b/packages/form/src/Form.tsx @@ -69,6 +69,14 @@ function FormInner( props: FormProps, ref: React.ForwardedRef ) { + const isMounted = useRef(true); + + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + const [state, setState] = useState>({ data: props.data as T, originalData: (props.data || {}) as T, @@ -341,32 +349,36 @@ function FormInner( const { validators } = inputs.current[name]; const hasValidators = Object.keys(validators).length > 0; - setState(state => ({ - ...state, - validation: { - ...state.validation, - [name]: { - ...state.validation[name], - isValidating: true + if (isMounted.current) { + setState(state => ({ + ...state, + validation: { + ...state.validation, + [name]: { + ...state.validation[name], + isValidating: true + } } - } - })); + })); + } return Promise.resolve(executeValidators(value, validators)) .then(validationResults => { const isValid = hasValidators ? (value === null ? null : true) : null; - setState(state => ({ - ...state, - validation: { - ...state.validation, - [name]: { - isValid, - message: null, - results: validationResults + if (isMounted.current) { + setState(state => ({ + ...state, + validation: { + ...state.validation, + [name]: { + isValid, + message: null, + results: validationResults + } } - } - })); + })); + } return validationResults; }) From bce6abc04952bfb39992c57d6a5e38e8774fef0f Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Fri, 22 Sep 2023 16:25:40 +0200 Subject: [PATCH 024/122] feat: various UI --- packages/app-aco/package.json | 1 - .../AdvancedSearch/AdvancedSearch.tsx | 2 +- .../src/components/AdvancedSearch/Button.tsx | 10 -- .../AdvancedSearch/Button/Button.styled.tsx | 8 + .../AdvancedSearch/Button/Button.tsx | 15 ++ .../AdvancedSearch/Button/index.tsx | 1 + .../AdvancedSearch/Drawer/Drawer.styled.tsx | 42 ++++++ .../AdvancedSearch/{ => Drawer}/Drawer.tsx | 3 +- .../AdvancedSearch/{ => Drawer}/Footer.tsx | 7 +- .../AdvancedSearch/{ => Drawer}/Header.tsx | 2 +- .../AdvancedSearch/Drawer/index.tsx | 1 + .../QueryBuilder/components/Filter.tsx | 79 +++++----- .../components/Querybuilder.styled.tsx | 48 ------ .../components/controls/RemoveFilter.tsx | 2 +- .../components/controls/RemoveGroup.tsx | 2 +- .../QueryBuilder/domain/Field.ts | 2 +- .../src/components/AdvancedSearch/styled.tsx | 98 ------------- packages/app-aco/tsconfig.build.json | 1 - packages/app-aco/tsconfig.json | 3 - .../src/components/Filters/Filters.styles.tsx | 11 +- .../src/components/Filters/Filters.tsx | 29 ++-- .../ContentEntries/Filters/Filters.tsx | 137 +++++++++--------- yarn.lock | 1 - 23 files changed, 216 insertions(+), 289 deletions(-) delete mode 100644 packages/app-aco/src/components/AdvancedSearch/Button.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/Button/Button.styled.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/Button/Button.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/Button/index.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/Drawer/Drawer.styled.tsx rename packages/app-aco/src/components/AdvancedSearch/{ => Drawer}/Drawer.tsx (96%) rename packages/app-aco/src/components/AdvancedSearch/{ => Drawer}/Footer.tsx (90%) rename packages/app-aco/src/components/AdvancedSearch/{ => Drawer}/Header.tsx (91%) create mode 100644 packages/app-aco/src/components/AdvancedSearch/Drawer/index.tsx delete mode 100644 packages/app-aco/src/components/AdvancedSearch/styled.tsx diff --git a/packages/app-aco/package.json b/packages/app-aco/package.json index 940ccee6525..e80fafde875 100644 --- a/packages/app-aco/package.json +++ b/packages/app-aco/package.json @@ -49,7 +49,6 @@ "@babel/preset-typescript": "^7.22.5", "@babel/runtime": "^7.22.6", "@types/react": "17.0.39", - "@webiny/api-headless-cms": "0.0.0", "@webiny/cli": "0.0.0", "@webiny/project-utils": "0.0.0", "apollo-client": "^2.6.10", diff --git a/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx index 7abe2f1ce50..ad8b5b61569 100644 --- a/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx @@ -10,7 +10,7 @@ interface AdvancedSearchProps { onSubmit: (data: any) => void; } -export const AdvancedSearch: React.VFC = ({ fields, onSubmit }) => { +export const AdvancedSearch = ({ fields, onSubmit }: AdvancedSearchProps) => { const [open, setOpen] = useState(false); const onDrawerSubmit = useCallback( diff --git a/packages/app-aco/src/components/AdvancedSearch/Button.tsx b/packages/app-aco/src/components/AdvancedSearch/Button.tsx deleted file mode 100644 index c36b769cdbf..00000000000 --- a/packages/app-aco/src/components/AdvancedSearch/Button.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import { ButtonPrimary } from "@webiny/ui/Button"; - -export interface ButtonProps { - onClick: () => void; -} - -export const Button: React.VFC = ({ onClick }) => { - return Create new search filter; -}; diff --git a/packages/app-aco/src/components/AdvancedSearch/Button/Button.styled.tsx b/packages/app-aco/src/components/AdvancedSearch/Button/Button.styled.tsx new file mode 100644 index 00000000000..35739189f80 --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/Button/Button.styled.tsx @@ -0,0 +1,8 @@ +import styled from "@emotion/styled"; +import { ReactComponent as SettingsIcon } from "@material-design-icons/svg/round/tune.svg"; + +export const Icon = styled(SettingsIcon)` + fill: var(--mdc-theme-primary); + width: 18px; + margin-left: 8px; +`; diff --git a/packages/app-aco/src/components/AdvancedSearch/Button/Button.tsx b/packages/app-aco/src/components/AdvancedSearch/Button/Button.tsx new file mode 100644 index 00000000000..7162dee78f9 --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/Button/Button.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { ButtonDefault } from "@webiny/ui/Button"; +import { Icon } from "./Button.styled"; + +export interface ButtonProps { + onClick: () => void; +} + +export const Button = ({ onClick }: ButtonProps) => { + return ( + + Advanced search filter + + ); +}; diff --git a/packages/app-aco/src/components/AdvancedSearch/Button/index.tsx b/packages/app-aco/src/components/AdvancedSearch/Button/index.tsx new file mode 100644 index 00000000000..e22c29adcf9 --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/Button/index.tsx @@ -0,0 +1 @@ +export * from "./Button"; diff --git a/packages/app-aco/src/components/AdvancedSearch/Drawer/Drawer.styled.tsx b/packages/app-aco/src/components/AdvancedSearch/Drawer/Drawer.styled.tsx new file mode 100644 index 00000000000..797201de70b --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/Drawer/Drawer.styled.tsx @@ -0,0 +1,42 @@ +import styled from "@emotion/styled"; + +import { IconButton } from "@webiny/ui/Button"; +import { Drawer as RmwcDrawer } from "@webiny/ui/Drawer"; + +export const CloseButton = styled(IconButton)` + position: absolute; + top: 15px; +`; + +export const DrawerContainer = styled(RmwcDrawer)` + width: 1000px; + /* Fix for the dir=rtl when a form is inside a drawer placed on the right side */ + .mdc-floating-label { + transform-origin: left top !important; + left: 16px !important; + right: auto !important; + } + + .mdc-select__dropdown-icon { + left: auto !important; + right: 8px !important; + } + + .mdc-select__selected-text { + padding-left: 16px !important; + padding-right: 52px !important; + } + + .mdc-switch__native-control { + left: initial !important; + right: 0 !important; + } + + .mdc-switch__thumb-underlay { + left: -18px; + } + + .mdc-switch--checked .mdc-switch__thumb-underlay { + transform: translateX(20px); + } +`; diff --git a/packages/app-aco/src/components/AdvancedSearch/Drawer.tsx b/packages/app-aco/src/components/AdvancedSearch/Drawer/Drawer.tsx similarity index 96% rename from packages/app-aco/src/components/AdvancedSearch/Drawer.tsx rename to packages/app-aco/src/components/AdvancedSearch/Drawer/Drawer.tsx index 9668e2377da..36a580ff46f 100644 --- a/packages/app-aco/src/components/AdvancedSearch/Drawer.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/Drawer/Drawer.tsx @@ -7,11 +7,12 @@ import { observer } from "mobx-react-lite"; import { useHotkeys } from "react-hotkeyz"; import { Footer } from "./Footer"; import { Header } from "./Header"; -import { DrawerContainer } from "./styled"; import { QueryBuilder } from "~/components/AdvancedSearch/QueryBuilder/QueryBuilder"; import { FieldRaw } from "~/components/AdvancedSearch/QueryBuilder/domain"; +import { DrawerContainer } from "./Drawer.styled"; + interface DrawerProps { open: boolean; onClose: () => void; diff --git a/packages/app-aco/src/components/AdvancedSearch/Footer.tsx b/packages/app-aco/src/components/AdvancedSearch/Drawer/Footer.tsx similarity index 90% rename from packages/app-aco/src/components/AdvancedSearch/Footer.tsx rename to packages/app-aco/src/components/AdvancedSearch/Drawer/Footer.tsx index b32ec7eb3e5..9eb41dca725 100644 --- a/packages/app-aco/src/components/AdvancedSearch/Footer.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/Drawer/Footer.tsx @@ -1,8 +1,9 @@ import React from "react"; import styled from "@emotion/styled"; -import { SimpleFormFooter } from "@webiny/app-admin/components/SimpleForm"; -import { ButtonDefault, ButtonPrimary } from "@webiny/ui/Button"; import { FormAPI } from "@webiny/form"; +import { ButtonDefault, ButtonPrimary } from "@webiny/ui/Button"; + +import { SimpleFormFooter } from "@webiny/app-admin/components/SimpleForm"; const SimpleFormFooterStyled = styled(SimpleFormFooter)` justify-content: flex-end; @@ -13,7 +14,7 @@ interface FooterProps { formRef: React.RefObject; } -export const Footer: React.VFC = ({ formRef, onClose }) => { +export const Footer = ({ formRef, onClose }: FooterProps) => { return ( Cancel diff --git a/packages/app-aco/src/components/AdvancedSearch/Header.tsx b/packages/app-aco/src/components/AdvancedSearch/Drawer/Header.tsx similarity index 91% rename from packages/app-aco/src/components/AdvancedSearch/Header.tsx rename to packages/app-aco/src/components/AdvancedSearch/Drawer/Header.tsx index 3b33898b113..42d2655729c 100644 --- a/packages/app-aco/src/components/AdvancedSearch/Header.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/Drawer/Header.tsx @@ -2,7 +2,7 @@ import React from "react"; import { ReactComponent as CloseIcon } from "@material-design-icons/svg/outlined/close.svg"; import { SimpleFormHeader } from "@webiny/app-admin/components/SimpleForm"; -import { CloseButton } from "./styled"; +import { CloseButton } from "./Drawer.styled"; interface HeaderProps { onClose: () => void; diff --git a/packages/app-aco/src/components/AdvancedSearch/Drawer/index.tsx b/packages/app-aco/src/components/AdvancedSearch/Drawer/index.tsx new file mode 100644 index 00000000000..2ce290b60cb --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/Drawer/index.tsx @@ -0,0 +1 @@ +export * from "./Drawer"; diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/Filter.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/Filter.tsx index f1947e6ff64..533136e3b1c 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/Filter.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/Filter.tsx @@ -23,51 +23,58 @@ export const Filter = ({ name, onDelete, onEmpty, fields, filter }: FilterProps) - - {({ value, onChange, validation }) => ( - field.value === filter.field) - ?.conditions || [] - } + label={"Field"} + options={fields.map(field => ({ + label: field.label, + value: field.value + }))} value={value} - onChange={onChange} + onChange={data => { + // We need to empty previously entered data into other fields + onEmpty(); + // Setting the right data into `field` + onChange(data); + }} validation={validation} /> )} - )} + + + + + {filter.field && ( + + {({ value, onChange, validation }) => ( + { value }))} onChange={handleTimeZoneChange} - required={true} /> ); diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/DateWithoutTimezone.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/DateWithoutTimezone.tsx index 94a0e7426ec..a666bacdc07 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/DateWithoutTimezone.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/DateWithoutTimezone.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { useBind } from "@webiny/form"; import { Input } from "@webiny/ui/Input"; @@ -8,31 +8,22 @@ interface DateWithoutTimezoneProps { } export const DateWithoutTimezone = ({ name }: DateWithoutTimezoneProps) => { - const { onChange, validate } = useBind({ + const [dateTime, setDateTime] = useState(""); + + const { onChange } = useBind({ name }); - const onBlur = (ev: React.SyntheticEvent) => { - if (validate) { - // Since we are accessing event in an async operation, we need to persist it. - // See https://reactjs.org/docs/events.html#event-pooling. - ev.persist(); - validate(); - } - }; - const handleOnChange = (value: string) => { - const date = new Date(value); - onChange(date.toISOString()); + const dateWithTimeZone = value + "Z"; + const date = new Date(dateWithTimeZone); + const dateTimeToISOString = date.toISOString(); + + setDateTime(dateTimeToISOString.slice(0, -5)); + onChange(dateTimeToISOString); }; return ( - + ); }; diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Field.ts b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Field.ts index 49e17b8af19..34c8fde3466 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Field.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Field.ts @@ -12,7 +12,7 @@ export enum TypeDTO { DATE = "date", TIME = "time", DATETIME_WITH_TIMEZONE = "dateTimeWithTimezone", - DATETIME_WITHOUT_TIMEZONE = "dateTimeWithOutTimezone", + DATETIME_WITHOUT_TIMEZONE = "dateTimeWithoutTimezone", MULTIPLE_VALUES = "multipleValues" } From 274139bc48f876b8329986cf61043aeda5185eb1 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Tue, 26 Sep 2023 16:26:51 +0200 Subject: [PATCH 028/122] fix: remove Observer from Drawer component --- .../app-aco/src/components/AdvancedSearch/Drawer/Drawer.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/app-aco/src/components/AdvancedSearch/Drawer/Drawer.tsx b/packages/app-aco/src/components/AdvancedSearch/Drawer/Drawer.tsx index 36a580ff46f..b27069ea861 100644 --- a/packages/app-aco/src/components/AdvancedSearch/Drawer/Drawer.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/Drawer/Drawer.tsx @@ -2,7 +2,6 @@ import React, { useRef } from "react"; import { FormAPI } from "@webiny/form"; import { DrawerContent } from "@webiny/ui/Drawer"; -import { observer } from "mobx-react-lite"; // @ts-ignore import { useHotkeys } from "react-hotkeyz"; import { Footer } from "./Footer"; @@ -20,7 +19,7 @@ interface DrawerProps { fields: FieldRaw[]; } -export const Drawer = observer(({ open, onClose, fields, onSubmit }: DrawerProps) => { +export const Drawer = ({ open, onClose, fields, onSubmit }: DrawerProps) => { useHotkeys({ zIndex: 55, disabled: !open, @@ -44,4 +43,4 @@ export const Drawer = observer(({ open, onClose, fields, onSubmit }: DrawerProps ); -}); +}; From 851b7c77d7798694c3d05fdcccbad7bcdc4ba080 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Tue, 26 Sep 2023 16:35:28 +0200 Subject: [PATCH 029/122] fix: set presenter without useEffect --- .../AdvancedSearch/QueryBuilder/QueryBuilder.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/QueryBuilder.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/QueryBuilder.tsx index 4672cede8d7..47d72e37dfa 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/QueryBuilder.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/QueryBuilder.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { QueryBuilder as QueryBuilderComponent } from "./components/QueryBuilder"; import { QueryBuilderPresenter } from "./adapters/QueryBuilderPresenter"; @@ -12,17 +12,7 @@ interface QueryBuilderProps { } export const QueryBuilder = ({ fields, onForm, onSubmit }: QueryBuilderProps) => { - const [presenter, setPresenter] = useState(); - - useEffect(() => { - if (fields) { - setPresenter(new QueryBuilderPresenter(fields)); - } - }, []); - - if (!presenter) { - return null; - } + const [presenter] = useState(new QueryBuilderPresenter(fields)); return ; }; From 9ec6a2e9c18b5f472dd97348ae3985d50a64097b Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Tue, 26 Sep 2023 17:36:35 +0200 Subject: [PATCH 030/122] fix: remove QueryBuilderViewModel class --- .../adapters/QueryBuilderPresenter.ts | 25 ++++++++++++------- .../adapters/QueryBuilderViewModel.ts | 14 ----------- .../QueryBuilder/adapters/index.ts | 1 - 3 files changed, 16 insertions(+), 24 deletions(-) delete mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderViewModel.ts diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts index 392f0243ccc..f4eaa70891c 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts @@ -1,16 +1,16 @@ import { makeAutoObservable } from "mobx"; import { - QueryObject, - QueryObjectMapper, Field, + FieldDTO, FieldMapper, FieldRaw, Operation, - QueryObjectDTO + QueryObject, + QueryObjectDTO, + QueryObjectMapper } from "../domain"; -import { QueryBuilderViewModel } from "./QueryBuilderViewModel"; -interface IQueryBuilderPresenter { +export interface IQueryBuilderPresenter { getViewModel: () => QueryBuilderViewModel; addGroup: () => void; deleteGroup: (groupIndex: number) => void; @@ -21,16 +21,23 @@ interface IQueryBuilderPresenter { onSubmit: (queryObject: QueryObjectDTO, onSuccess?: () => void, onError?: () => void) => void; } +export interface QueryBuilderViewModel { + queryObject: QueryObjectDTO; + fields: FieldDTO[]; + invalidFields: Record; +} + export class QueryBuilderPresenter implements IQueryBuilderPresenter { private readonly viewModel: QueryBuilderViewModel; private formWasSubmitted = false; constructor(fields: FieldRaw[]) { + this.viewModel = { + queryObject: QueryObjectMapper.toDTO(QueryObject.createEmpty()), + fields: FieldMapper.toDTO(fields.map(field => Field.createFromRaw(field))), + invalidFields: {} + }; makeAutoObservable(this); - this.viewModel = new QueryBuilderViewModel( - QueryObjectMapper.toDTO(QueryObject.createEmpty()), - FieldMapper.toDTO(fields.map(field => Field.createFromRaw(field))) - ); } getViewModel() { diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderViewModel.ts b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderViewModel.ts deleted file mode 100644 index 51c562d1ba9..00000000000 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderViewModel.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { makeAutoObservable } from "mobx"; -import { FieldDTO, QueryObjectDTO } from "../domain"; - -export class QueryBuilderViewModel { - public queryObject: QueryObjectDTO; - public fields: FieldDTO[]; - public invalidFields: Record = {}; - - constructor(queryObject: QueryObjectDTO, fields: FieldDTO[]) { - this.queryObject = queryObject; - this.fields = fields; - makeAutoObservable(this); - } -} diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/index.ts b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/index.ts index 2fa0b26a991..c9fe8a36d75 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/index.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/index.ts @@ -1,2 +1 @@ export * from "./QueryBuilderPresenter"; -export * from "./QueryBuilderViewModel"; From 8339036dc2a0cc473b283752833bcb837024f389 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Tue, 26 Sep 2023 17:40:31 +0200 Subject: [PATCH 031/122] chore: rename TypeDTO into FieldType --- .../QueryBuilderPresenter.test.ts | 102 +++++++++--------- .../QueryBuilder/components/InputField.tsx | 10 +- .../QueryBuilder/components/fields/Input.tsx | 4 +- .../QueryBuilder/domain/Field.ts | 31 +++--- .../QueryBuilder/domain/FieldMapper.ts | 4 +- 5 files changed, 76 insertions(+), 75 deletions(-) diff --git a/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts b/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts index 9977acf4b0e..1e70190eb64 100644 --- a/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts +++ b/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts @@ -5,8 +5,8 @@ import { import { FieldDTO, FieldRaw, - Operation, - TypeDTO + FieldType, + Operation } from "~/components/AdvancedSearch/QueryBuilder/domain"; describe("QueryBuilderPresenter", () => { @@ -241,14 +241,14 @@ describe("FieldDTO definition", () => { const fields: [FieldRaw, FieldDTO][] = [ [ { - id: `${TypeDTO.TEXT}-field`, - label: `${TypeDTO.TEXT} field`, - type: TypeDTO.TEXT + id: `${FieldType.TEXT}-field`, + label: `${FieldType.TEXT} field`, + type: FieldType.TEXT }, { - label: `${TypeDTO.TEXT} field`, - value: `${TypeDTO.TEXT}-field`, + label: `${FieldType.TEXT} field`, + value: `${FieldType.TEXT}-field`, conditions: [ { label: "is equal to", value: " " }, { label: "contains", value: "_contains" }, @@ -258,7 +258,7 @@ describe("FieldDTO definition", () => { { label: "doesn't start with", value: "_not_startsWith" } ], predefined: [], - type: TypeDTO.TEXT + type: FieldType.TEXT } ], [ @@ -275,38 +275,38 @@ describe("FieldDTO definition", () => { { label: "doesn't contain", value: "_not_contains" } ], predefined: [], - type: TypeDTO.TEXT + type: FieldType.TEXT } ], [ { - id: `${TypeDTO.BOOLEAN}-field`, - label: `${TypeDTO.BOOLEAN} field`, - type: TypeDTO.BOOLEAN + id: `${FieldType.BOOLEAN}-field`, + label: `${FieldType.BOOLEAN} field`, + type: FieldType.BOOLEAN }, { - label: `${TypeDTO.BOOLEAN} field`, - value: `${TypeDTO.BOOLEAN}-field`, + label: `${FieldType.BOOLEAN} field`, + value: `${FieldType.BOOLEAN}-field`, conditions: [ { label: "is", value: " " }, { label: "is not", value: "_not" } ], predefined: [], - type: TypeDTO.BOOLEAN + type: FieldType.BOOLEAN } ], [ { - id: `${TypeDTO.DATE}-field`, - label: `${TypeDTO.DATE} field`, + id: `${FieldType.DATE}-field`, + label: `${FieldType.DATE} field`, type: "datetime", settings: { - type: TypeDTO.DATE + type: FieldType.DATE } }, { - label: `${TypeDTO.DATE} field`, - value: `${TypeDTO.DATE}-field`, + label: `${FieldType.DATE} field`, + value: `${FieldType.DATE}-field`, conditions: [ { label: "is equal to", value: " " }, { label: "is not equal to", value: "_not" }, @@ -316,21 +316,21 @@ describe("FieldDTO definition", () => { { label: "is after or equal to", value: "_gte" } ], predefined: [], - type: TypeDTO.DATE + type: FieldType.DATE } ], [ { - id: `${TypeDTO.TIME}-field`, - label: `${TypeDTO.TIME} field`, + id: `${FieldType.TIME}-field`, + label: `${FieldType.TIME} field`, type: "datetime", settings: { - type: TypeDTO.TIME + type: FieldType.TIME } }, { - label: `${TypeDTO.TIME} field`, - value: `${TypeDTO.TIME}-field`, + label: `${FieldType.TIME} field`, + value: `${FieldType.TIME}-field`, conditions: [ { label: "is equal to", value: " " }, { label: "is not equal to", value: "_not" }, @@ -340,21 +340,21 @@ describe("FieldDTO definition", () => { { label: "is after or equal to", value: "_gte" } ], predefined: [], - type: TypeDTO.TIME + type: FieldType.TIME } ], [ { - id: `${TypeDTO.DATETIME_WITH_TIMEZONE}-field`, - label: `${TypeDTO.DATETIME_WITH_TIMEZONE} field`, + id: `${FieldType.DATETIME_WITH_TIMEZONE}-field`, + label: `${FieldType.DATETIME_WITH_TIMEZONE} field`, type: "datetime", settings: { - type: TypeDTO.DATETIME_WITH_TIMEZONE + type: FieldType.DATETIME_WITH_TIMEZONE } }, { - label: `${TypeDTO.DATETIME_WITH_TIMEZONE} field`, - value: `${TypeDTO.DATETIME_WITH_TIMEZONE}-field`, + label: `${FieldType.DATETIME_WITH_TIMEZONE} field`, + value: `${FieldType.DATETIME_WITH_TIMEZONE}-field`, conditions: [ { label: "is equal to", value: " " }, { label: "is not equal to", value: "_not" }, @@ -364,21 +364,21 @@ describe("FieldDTO definition", () => { { label: "is after or equal to", value: "_gte" } ], predefined: [], - type: TypeDTO.DATETIME_WITH_TIMEZONE + type: FieldType.DATETIME_WITH_TIMEZONE } ], [ { - id: `${TypeDTO.DATETIME_WITHOUT_TIMEZONE}-field`, - label: `${TypeDTO.DATETIME_WITHOUT_TIMEZONE} field`, + id: `${FieldType.DATETIME_WITHOUT_TIMEZONE}-field`, + label: `${FieldType.DATETIME_WITHOUT_TIMEZONE} field`, type: "datetime", settings: { - type: TypeDTO.DATETIME_WITHOUT_TIMEZONE + type: FieldType.DATETIME_WITHOUT_TIMEZONE } }, { - label: `${TypeDTO.DATETIME_WITHOUT_TIMEZONE} field`, - value: `${TypeDTO.DATETIME_WITHOUT_TIMEZONE}-field`, + label: `${FieldType.DATETIME_WITHOUT_TIMEZONE} field`, + value: `${FieldType.DATETIME_WITHOUT_TIMEZONE}-field`, conditions: [ { label: "is equal to", value: " " }, { label: "is not equal to", value: "_not" }, @@ -388,13 +388,13 @@ describe("FieldDTO definition", () => { { label: "is after or equal to", value: "_gte" } ], predefined: [], - type: TypeDTO.DATETIME_WITHOUT_TIMEZONE + type: FieldType.DATETIME_WITHOUT_TIMEZONE } ], [ { - id: `${TypeDTO.MULTIPLE_VALUES}-field`, - label: `${TypeDTO.MULTIPLE_VALUES} field`, + id: `${FieldType.MULTIPLE_VALUES}-field`, + label: `${FieldType.MULTIPLE_VALUES} field`, type: "text", multipleValues: true, predefinedValues: { @@ -408,8 +408,8 @@ describe("FieldDTO definition", () => { } }, { - label: `${TypeDTO.MULTIPLE_VALUES} field`, - value: `${TypeDTO.MULTIPLE_VALUES}-field`, + label: `${FieldType.MULTIPLE_VALUES} field`, + value: `${FieldType.MULTIPLE_VALUES}-field`, conditions: [ { label: "contains", value: "_in" }, { label: "doesn't contain", value: "_not_in" } @@ -420,18 +420,18 @@ describe("FieldDTO definition", () => { label: "value 1" } ], - type: TypeDTO.MULTIPLE_VALUES + type: FieldType.MULTIPLE_VALUES } ], [ { - id: `${TypeDTO.NUMBER}-field`, - label: `${TypeDTO.NUMBER} field`, - type: TypeDTO.NUMBER + id: `${FieldType.NUMBER}-field`, + label: `${FieldType.NUMBER} field`, + type: FieldType.NUMBER }, { - label: `${TypeDTO.NUMBER} field`, - value: `${TypeDTO.NUMBER}-field`, + label: `${FieldType.NUMBER} field`, + value: `${FieldType.NUMBER}-field`, conditions: [ { label: "is equal to", value: " " }, { label: "is not equal to", value: "_not" }, @@ -441,7 +441,7 @@ describe("FieldDTO definition", () => { { label: "is greater or equal to", value: "_gte" } ], predefined: [], - type: TypeDTO.NUMBER + type: FieldType.NUMBER } ], [ @@ -455,7 +455,7 @@ describe("FieldDTO definition", () => { value: "any-field", conditions: [], predefined: [], - type: TypeDTO.TEXT + type: FieldType.TEXT } ] ]; diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/InputField.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/InputField.tsx index 32e193e4816..4d479286304 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/InputField.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/InputField.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Boolean, DateWithoutTimezone, DateWithTimezone, Input, MultipleValues } from "./fields"; -import { FieldDTO, TypeDTO } from "~/components/AdvancedSearch/QueryBuilder/domain"; +import { FieldDTO, FieldType } from "~/components/AdvancedSearch/QueryBuilder/domain"; interface InputFieldProps { field?: FieldDTO; @@ -15,13 +15,13 @@ export const InputField: React.VFC = ({ field, name }) => { } switch (field.type) { - case TypeDTO.BOOLEAN: + case FieldType.BOOLEAN: return ; - case TypeDTO.DATETIME_WITH_TIMEZONE: + case FieldType.DATETIME_WITH_TIMEZONE: return ; - case TypeDTO.DATETIME_WITHOUT_TIMEZONE: + case FieldType.DATETIME_WITHOUT_TIMEZONE: return ; - case TypeDTO.MULTIPLE_VALUES: + case FieldType.MULTIPLE_VALUES: return ; default: return ; diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/Input.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/Input.tsx index f3ace4e9223..6768782c5da 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/Input.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/Input.tsx @@ -3,10 +3,10 @@ import React from "react"; import { Bind } from "@webiny/form"; import { Input as BaseInput } from "@webiny/ui/Input"; -import { TypeDTO } from "~/components/AdvancedSearch/QueryBuilder/domain"; +import { FieldType } from "~/components/AdvancedSearch/QueryBuilder/domain"; interface InputProps { - type: TypeDTO; + type: FieldType; name: string; } diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Field.ts b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Field.ts index 34c8fde3466..3ba0c292e2d 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Field.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Field.ts @@ -5,7 +5,7 @@ export type FieldRaw = Pick< "id" | "type" | "label" | "multipleValues" | "predefinedValues" | "settings" >; -export enum TypeDTO { +export enum FieldType { TEXT = "text", NUMBER = "number", BOOLEAN = "boolean", @@ -30,7 +30,7 @@ export interface FieldDTO { label: string; value: string; conditions: ConditionDTO[]; - type: TypeDTO; + type: FieldType; predefined: PredefinedDTO[]; } @@ -160,38 +160,39 @@ export class Predefined { } export class Type { - public readonly value: TypeDTO; + public readonly value: FieldType; static createFromField(rawData: FieldRaw) { - if (rawData.settings?.type === TypeDTO.DATETIME_WITH_TIMEZONE) { - return new Type(TypeDTO.DATETIME_WITH_TIMEZONE); + if (rawData.settings?.type === FieldType.DATETIME_WITH_TIMEZONE) { + return new Type(FieldType.DATETIME_WITH_TIMEZONE); } - if (rawData.settings?.type === TypeDTO.DATETIME_WITHOUT_TIMEZONE) { - return new Type(TypeDTO.DATETIME_WITHOUT_TIMEZONE); + if (rawData.settings?.type === FieldType.DATETIME_WITHOUT_TIMEZONE) { + return new Type(FieldType.DATETIME_WITHOUT_TIMEZONE); } if (rawData?.multipleValues && rawData.predefinedValues?.enabled) { - return new Type(TypeDTO.MULTIPLE_VALUES); + return new Type(FieldType.MULTIPLE_VALUES); } if (rawData.type === "datetime") { - const value = rawData.settings?.type === TypeDTO.TIME ? TypeDTO.TIME : TypeDTO.DATE; + const value = + rawData.settings?.type === FieldType.TIME ? FieldType.TIME : FieldType.DATE; return new Type(value); } - if (rawData.type === TypeDTO.BOOLEAN) { - return new Type(TypeDTO.BOOLEAN); + if (rawData.type === FieldType.BOOLEAN) { + return new Type(FieldType.BOOLEAN); } - if (rawData.type === TypeDTO.NUMBER) { - return new Type(TypeDTO.NUMBER); + if (rawData.type === FieldType.NUMBER) { + return new Type(FieldType.NUMBER); } - return new Type(TypeDTO.TEXT); + return new Type(FieldType.TEXT); } - private constructor(value: TypeDTO) { + private constructor(value: FieldType) { this.value = value; } } diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/FieldMapper.ts b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/FieldMapper.ts index c5f8a72220f..7a46c4e78df 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/FieldMapper.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/FieldMapper.ts @@ -6,7 +6,7 @@ import { Predefined, PredefinedDTO, Type, - TypeDTO + FieldType } from "./Field"; export class FieldMapper { @@ -42,7 +42,7 @@ export class PredefinedMapper { } export class TypeMapper { - static toTDO(type: Type): TypeDTO { + static toTDO(type: Type): FieldType { return type.value; } } From dd9cc4c43e598b554d6d570bc9648526d4414544 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Tue, 26 Sep 2023 21:51:39 +0200 Subject: [PATCH 032/122] feat: move field definition to FieldsMapper --- .../ContentEntries/Filters/FieldsMapper.ts | 67 ++++++++++++++++++ .../ContentEntries/Filters/Filters.tsx | 69 +++---------------- 2 files changed, 78 insertions(+), 58 deletions(-) create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntries/Filters/FieldsMapper.ts diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/Filters/FieldsMapper.ts b/packages/app-headless-cms/src/admin/components/ContentEntries/Filters/FieldsMapper.ts new file mode 100644 index 00000000000..7ac55f184c3 --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/Filters/FieldsMapper.ts @@ -0,0 +1,67 @@ +import sortBy from "lodash/sortBy"; +import { CmsModel } from "@webiny/app-headless-cms-common/types"; +import { FieldRaw } from "@webiny/app-aco/components/AdvancedSearch/QueryBuilder/domain"; + +export class FieldsMapper { + private static excluded: FieldRaw["type"][] = [ + "ref", + "rich-text", + "file", + "object", + "dynamicZone" + ]; + private static defaultFields: FieldRaw[] = [ + { + id: "status", + type: "text", + label: "Status", + multipleValues: true, + predefinedValues: { + enabled: true, + values: [ + { + label: "Draft", + value: "draft" + }, + { + label: "Published", + value: "published" + }, + { + label: "Unpublished", + value: "unpublished" + } + ] + } + }, + { + id: "createdOn", + type: "datetime", + label: "Created on", + settings: { type: "dateTimeWithoutTimezone" } + }, + { + id: "savedOn", + type: "datetime", + label: "Modified on", + settings: { type: "dateTimeWithoutTimezone" } + } + ]; + + static toRaw(model: CmsModel): FieldRaw[] { + const modelFields = model.fields + .filter(modelField => !this.excluded.includes(modelField.type)) + .map(modelField => { + return { + id: modelField.fieldId, + type: modelField.type, + label: modelField.label, + multipleValues: modelField.multipleValues || false, + predefinedValues: modelField?.predefinedValues || undefined, + settings: modelField.settings + }; + }); + + return sortBy([...this.defaultFields, ...modelFields], ["label"]); + } +} diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/Filters/Filters.tsx b/packages/app-headless-cms/src/admin/components/ContentEntries/Filters/Filters.tsx index 3967b96264a..c12a689e3d9 100644 --- a/packages/app-headless-cms/src/admin/components/ContentEntries/Filters/Filters.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/Filters/Filters.tsx @@ -1,9 +1,10 @@ -import React, { useCallback } from "react"; +import React, { useEffect, useState } from "react"; import { Filters as BaseFilters, FiltersOnSubmit } from "@webiny/app-admin"; import { useContentEntryListConfig } from "~/admin/config/contentEntries"; import { useContentEntriesList } from "~/admin/views/contentEntries/hooks"; import { AdvancedSearch, GraphQLInputMapper } from "@webiny/app-aco"; import { useModel } from "~/admin/hooks"; +import { FieldsMapper } from "./FieldsMapper"; import { FieldRaw, QueryObjectDTO @@ -13,63 +14,11 @@ export const Filters = () => { const { browser } = useContentEntryListConfig(); const list = useContentEntriesList(); const { model } = useModel(); + const [fields, setFields] = useState(); - const getFields = useCallback((): FieldRaw[] => { - const excludedFieldTypes = ["ref", "rich-text", "file", "object", "dynamicZone"]; - - const defaultFields = [ - { - id: "status", - type: "text", - label: "Status", - multipleValues: true, - predefinedValues: { - enabled: true, - values: [ - { - label: "Draft", - value: "draft" - }, - { - label: "Published", - value: "published" - }, - { - label: "Unpublished", - value: "unpublished" - } - ] - } - }, - { - id: "createdOn", - type: "datetime", - label: "Created on", - settings: { type: "dateTimeWithoutTimezone" } - }, - { - id: "savedOn", - type: "datetime", - label: "Modified on", - settings: { type: "dateTimeWithoutTimezone" } - } - ]; - - const fields = model.fields - .filter(modelField => !excludedFieldTypes.includes(modelField.type)) - .map(modelField => { - return { - id: modelField.fieldId, - type: modelField.type, - label: modelField.label, - multipleValues: modelField.multipleValues || false, - predefinedValues: modelField?.predefinedValues || undefined, - settings: modelField.settings - }; - }); - - return [...defaultFields, ...fields]; - }, [model.fields]); + useEffect(() => { + setFields(FieldsMapper.toRaw(model)); + }, [model]); const applyFilters: FiltersOnSubmit = data => { if (!Object.keys(data).length) { @@ -92,6 +41,10 @@ export const Filters = () => { list.setFilters(GraphQLInputMapper.toGraphQL(data)); }; + if (!fields) { + return null; + } + return ( { data={{}} onChange={applyFilters} > - + ); }; From c410c5ad72e5d5e022544e84fd5256353b3d39eb Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Wed, 27 Sep 2023 09:06:03 +0200 Subject: [PATCH 033/122] refactor: filter model definition --- packages/api-aco/__tests__/filter.so.test.ts | 2 +- packages/api-aco/src/filter/filter.model.ts | 62 ++++++++------------ 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/packages/api-aco/__tests__/filter.so.test.ts b/packages/api-aco/__tests__/filter.so.test.ts index b7ba3e416ca..65008436d75 100644 --- a/packages/api-aco/__tests__/filter.so.test.ts +++ b/packages/api-aco/__tests__/filter.so.test.ts @@ -150,7 +150,7 @@ describe("`filter` CRUD", () => { }); }); - it.skip("should not allow creating a `filter` with no `operation` provided", async () => { + it("should not allow creating a `filter` with no `operation` provided", async () => { const [response] = await aco.createFilter({ data: { ...filterMocks.filterA, diff --git a/packages/api-aco/src/filter/filter.model.ts b/packages/api-aco/src/filter/filter.model.ts index 9cbb2207809..4095684fe2b 100644 --- a/packages/api-aco/src/filter/filter.model.ts +++ b/packages/api-aco/src/filter/filter.model.ts @@ -1,5 +1,6 @@ import { createModelField } from "~/utils/createModelField"; import { CmsPrivateModelFull } from "@webiny/api-headless-cms"; +import { CmsModelField } from "@webiny/api-headless-cms/types"; export type FilterModelDefinition = Omit; @@ -16,7 +17,7 @@ const name = () => ] }); -const mainOperation = () => +const operation = () => createModelField({ label: "Operation", fieldId: "operation", @@ -43,71 +44,52 @@ const mainOperation = () => ] }); -const groups = () => +const groups = (fields: CmsModelField[]): CmsModelField => createModelField({ label: "Groups", fieldId: "groups", type: "object", settings: { - fields: [groupOperation(), filters()], - layout: [["operation"], ["filters"]] + fields, + layout: fields.map(field => [field.storageId]) }, multipleValues: true, + predefinedValues: { + values: [], + enabled: false + }, listValidation: [ { name: "minLength", + message: "Value is too short.", settings: { value: "1" - }, - message: "Value is too short." - } - ] - }); - -const groupOperation = () => - createModelField({ - label: "Operation", - fieldId: "operation", - type: "text", - predefinedValues: { - enabled: true, - values: [ - { - label: "AND", - value: "AND" - }, - { - label: "OR", - value: "OR" } - ] - }, - multipleValues: false, - validation: [ - { - name: "required", - message: "Value is required." } ] }); -const filters = () => +const filters = (fields: CmsModelField[]): CmsModelField => createModelField({ label: "Filters", fieldId: "filters", type: "object", settings: { - fields: [filterField(), filterCondition(), filterValue()], - layout: [["field"], ["condition"], ["value"]] + fields, + layout: fields.map(field => [field.storageId]) }, multipleValues: true, + predefinedValues: { + values: [], + enabled: false + }, listValidation: [ { name: "minLength", + message: "Value is too short.", settings: { value: "1" - }, - message: "Value is too short." + } } ] }); @@ -159,7 +141,11 @@ export const createFilterModelDefinition = (): FilterModelDefinition => { modelId: FILTER_MODEL_ID, titleFieldId: "name", layout: [["name"], ["operation"], ["groups"]], - fields: [name(), mainOperation(), groups()], + fields: [ + name(), + operation(), + groups([operation(), filters([filterField(), filterCondition(), filterValue()])]) + ], description: "ACO - Filter content model", isPrivate: true }; From c9280e195020070225e7b553bd5a10de455dbf25 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Wed, 27 Sep 2023 09:58:30 +0200 Subject: [PATCH 034/122] feat: add filter gql query and mutation --- packages/app-aco/src/graphql/filters.gql.ts | 84 +++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 packages/app-aco/src/graphql/filters.gql.ts diff --git a/packages/app-aco/src/graphql/filters.gql.ts b/packages/app-aco/src/graphql/filters.gql.ts new file mode 100644 index 00000000000..e0abb09a08a --- /dev/null +++ b/packages/app-aco/src/graphql/filters.gql.ts @@ -0,0 +1,84 @@ +import gql from "graphql-tag"; + +const ERROR_FIELD = /* GraphQL */ ` + { + code + data + message + } +`; + +const DATA_FIELD = /* GraphQL */ ` + { + id + name + operation + groups { + operation + filters { + field + condition + value + } + } + createdBy { + id + displayName + } + } +`; + +export const CREATE_FILTER = gql` + mutation CreateFilter($data: FilterCreateInput!) { + aco { + createFilter(data: $data) { + data ${DATA_FIELD} + error ${ERROR_FIELD} + } + } + } +`; + +export const LIST_FILTERS = gql` + query ListFilters($where: FiltersListWhereInput!, $limit: Int!) { + aco { + listFilters(where: $where, limit: $limit) { + data ${DATA_FIELD} + error ${ERROR_FIELD} + } + } + } +`; + +export const GET_FILTER = gql` + query GetFilter($id: ID!) { + aco { + getFilter(id: $id) { + data ${DATA_FIELD} + error ${ERROR_FIELD} + } + } + } +`; + +export const UPDATE_FILTER = gql` + mutation UpdateFilter($id: ID!, $data: FilterUpdateInput!) { + aco { + updateFilter(id: $id, data: $data) { + data ${DATA_FIELD} + error ${ERROR_FIELD} + } + } + } +`; + +export const DELETE_FILTER = gql` + mutation DeleteFilter($id: ID!) { + aco { + deleteFilter(id: $id) { + data + error ${ERROR_FIELD} + } + } + } +`; From c4ed22fe4ff1072ddaff2c7753d99024528100fc Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Wed, 27 Sep 2023 10:36:19 +0200 Subject: [PATCH 035/122] fix: viewModel assignment --- .../adapters/QueryBuilderPresenter.ts | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts index f4eaa70891c..ab81202e80a 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts @@ -28,37 +28,40 @@ export interface QueryBuilderViewModel { } export class QueryBuilderPresenter implements IQueryBuilderPresenter { - private readonly viewModel: QueryBuilderViewModel; + private queryObject: QueryBuilderViewModel["queryObject"]; + private readonly fields: QueryBuilderViewModel["fields"]; + private invalidFields: QueryBuilderViewModel["invalidFields"] = {}; private formWasSubmitted = false; constructor(fields: FieldRaw[]) { - this.viewModel = { - queryObject: QueryObjectMapper.toDTO(QueryObject.createEmpty()), - fields: FieldMapper.toDTO(fields.map(field => Field.createFromRaw(field))), - invalidFields: {} - }; + this.queryObject = QueryObjectMapper.toDTO(QueryObject.createEmpty()); + this.fields = FieldMapper.toDTO(fields.map(field => Field.createFromRaw(field))); makeAutoObservable(this); } - getViewModel() { - return this.viewModel; + getViewModel(): QueryBuilderViewModel { + return { + queryObject: this.queryObject, + fields: this.fields, + invalidFields: this.invalidFields + }; } addGroup() { - this.viewModel.queryObject.groups.push({ + this.queryObject.groups.push({ operation: Operation.AND, filters: [{ field: "", value: "", condition: "" }] }); } deleteGroup(groupIndex: number) { - this.viewModel.queryObject.groups = this.viewModel.queryObject.groups.filter( + this.queryObject.groups = this.queryObject.groups.filter( (_, index) => index !== groupIndex ); // Make sure we always have at least 1 group! - if (this.viewModel.queryObject.groups.length === 0) { - this.viewModel.queryObject.groups.push({ + if (this.queryObject.groups.length === 0) { + this.queryObject.groups.push({ operation: Operation.AND, filters: [{ field: "", value: "", condition: "" }] }); @@ -66,7 +69,7 @@ export class QueryBuilderPresenter implements IQueryBuilderPresenter { } addNewFilterToGroup(groupIndex: number) { - this.viewModel.queryObject.groups[groupIndex].filters.push({ + this.queryObject.groups[groupIndex].filters.push({ field: "", value: "", condition: "" @@ -74,14 +77,14 @@ export class QueryBuilderPresenter implements IQueryBuilderPresenter { } deleteFilterFromGroup(groupIndex: number, filterIndex: number) { - const filters = this.viewModel.queryObject.groups[groupIndex].filters; - this.viewModel.queryObject.groups[groupIndex].filters = filters.filter( + const filters = this.queryObject.groups[groupIndex].filters; + this.queryObject.groups[groupIndex].filters = filters.filter( (_, index) => index !== filterIndex ); // Make sure we always have at least 1 filter! - if (this.viewModel.queryObject.groups[groupIndex].filters.length === 0) { - this.viewModel.queryObject.groups[groupIndex].filters.push({ + if (this.queryObject.groups[groupIndex].filters.length === 0) { + this.queryObject.groups[groupIndex].filters.push({ field: "", value: "", condition: "" @@ -90,19 +93,19 @@ export class QueryBuilderPresenter implements IQueryBuilderPresenter { } emptyFilterIntoGroup(groupIndex: number, filterIndex: number) { - this.viewModel.queryObject.groups[groupIndex].filters = [ - ...this.viewModel.queryObject.groups[groupIndex].filters.slice(0, filterIndex), + this.queryObject.groups[groupIndex].filters = [ + ...this.queryObject.groups[groupIndex].filters.slice(0, filterIndex), { field: "", value: "", condition: "" }, - ...this.viewModel.queryObject.groups[groupIndex].filters.slice(filterIndex + 1) + ...this.queryObject.groups[groupIndex].filters.slice(filterIndex + 1) ]; } setQueryObject(queryObject: QueryObjectDTO) { - this.viewModel.queryObject = queryObject; + this.queryObject = queryObject; if (this.formWasSubmitted) { this.validateQueryObject(queryObject); } @@ -122,14 +125,14 @@ export class QueryBuilderPresenter implements IQueryBuilderPresenter { const validation = QueryObject.validate(data); if (!validation.success) { - this.viewModel.invalidFields = validation.error.issues.reduce((acc, issue) => { + this.invalidFields = validation.error.issues.reduce((acc, issue) => { return { ...acc, [issue.path.join(".")]: issue.message }; }, {}); } else { - this.viewModel.invalidFields = {}; + this.invalidFields = {}; } return validation; From c183ffa18bf4a0619b6f796f1000be18b19a6a63 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Wed, 27 Sep 2023 14:59:45 +0200 Subject: [PATCH 036/122] test: fix presenter.getViewModel() --- .../QueryBuilderPresenter.test.ts | 84 ++++++++++--------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts b/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts index 1e70190eb64..96f98d38f8c 100644 --- a/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts +++ b/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts @@ -125,44 +125,52 @@ describe("QueryBuilderPresenter", () => { }); it("should be able to set the queryObject", () => { - // should be able to set the `queryObject` operation - presenter.setQueryObject({ - ...viewModel.queryObject, - operation: Operation.OR - }); - - expect(viewModel.queryObject.operation).toEqual(Operation.OR); - - // should be able to set the `group` operation - presenter.setQueryObject({ - ...viewModel.queryObject, - groups: [ - { - ...viewModel.queryObject.groups[0], - operation: Operation.OR - } - ] - }); + { + // should be able to set the `queryObject` operation + presenter.setQueryObject({ + ...viewModel.queryObject, + operation: Operation.OR + }); + + expect(presenter.getViewModel().queryObject.operation).toEqual(Operation.OR); + } + + { + // should be able to set the `group` operation + presenter.setQueryObject({ + ...viewModel.queryObject, + groups: [ + { + ...viewModel.queryObject.groups[0], + operation: Operation.OR + } + ] + }); - expect(viewModel.queryObject.groups[0].operation).toEqual(Operation.OR); + expect(presenter.getViewModel().queryObject.groups[0].operation).toEqual(Operation.OR); + } - // should be able to change the filter definition - presenter.setQueryObject({ - ...viewModel.queryObject, - groups: [ - { - ...viewModel.queryObject.groups[0], - filters: [ - { - ...testFilter, - field: "any-field" - } - ] - } - ] - }); + { + // should be able to change the filter definition + presenter.setQueryObject({ + ...viewModel.queryObject, + groups: [ + { + ...viewModel.queryObject.groups[0], + filters: [ + { + ...testFilter, + field: "any-field" + } + ] + } + ] + }); - expect(viewModel.queryObject.groups[0].filters[0].field).toEqual("any-field"); + expect(presenter.getViewModel().queryObject.groups[0].filters[0].field).toEqual( + "any-field" + ); + } }); it("should perform validation and call provided callbacks `onSubmit`", () => { @@ -185,10 +193,10 @@ describe("QueryBuilderPresenter", () => { ] }); - presenter.onSubmit(viewModel.queryObject, onSuccess, onError); + presenter.onSubmit(presenter.getViewModel().queryObject, onSuccess, onError); expect(onError).toBeCalledTimes(1); - expect(Object.keys(viewModel.invalidFields).length).toBe(1); + expect(Object.keys(presenter.getViewModel().invalidFields).length).toBe(1); presenter.setQueryObject({ ...viewModel.queryObject, @@ -206,7 +214,7 @@ describe("QueryBuilderPresenter", () => { ] }); - presenter.onSubmit(viewModel.queryObject, onSuccess, onError); + presenter.onSubmit(presenter.getViewModel().queryObject, onSuccess, onError); expect(onSuccess).toBeCalledTimes(1); expect(viewModel.invalidFields).toEqual({}); From 45fcb3805989a7c9391194fc5b181b2a8528eba7 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Thu, 28 Sep 2023 11:25:52 +0200 Subject: [PATCH 037/122] feat: add model field --- .../api-aco/__tests__/filter.hooks.test.ts | 138 ++++++++++++++++++ packages/api-aco/__tests__/filter.so.test.ts | 39 +++-- .../api-aco/__tests__/graphql/filter.gql.ts | 1 + .../api-aco/__tests__/mocks/filter.mock.ts | 24 +++ .../api-aco/__tests__/mocks/lifecycle.mock.ts | 25 ++++ packages/api-aco/src/filter/filter.gql.ts | 4 + packages/api-aco/src/filter/filter.model.ts | 16 +- packages/api-aco/src/filter/filter.types.ts | 7 +- 8 files changed, 239 insertions(+), 15 deletions(-) create mode 100644 packages/api-aco/__tests__/filter.hooks.test.ts diff --git a/packages/api-aco/__tests__/filter.hooks.test.ts b/packages/api-aco/__tests__/filter.hooks.test.ts new file mode 100644 index 00000000000..7932afeab9d --- /dev/null +++ b/packages/api-aco/__tests__/filter.hooks.test.ts @@ -0,0 +1,138 @@ +import { useGraphQlHandler } from "./utils/useGraphQlHandler"; + +import { assignFilterLifecycleEvents, tracker } from "./mocks/lifecycle.mock"; +import { Operation } from "~/filter/filter.types"; + +const name = "Filter Lifecycle Events"; +const model = "demo-lifecycle-events"; +const operation = Operation.AND; +const groups = [ + { + operation: Operation.OR, + filters: [ + { + field: "any-field", + condition: "any-condition", + value: "any-value" + } + ] + } +]; + +describe("Filter Lifecycle Events", () => { + const { aco } = useGraphQlHandler({ + plugins: [assignFilterLifecycleEvents()] + }); + + beforeEach(async () => { + tracker.reset(); + }); + + it("should trigger create lifecycle events", async () => { + const [response] = await aco.createFilter({ + data: { + name, + model, + operation, + groups + } + }); + + expect(response).toMatchObject({ + data: { + aco: { + createFilter: { + data: { + name, + model, + operation, + groups + }, + error: null + } + } + } + }); + + expect(tracker.isExecutedOnce("filter:beforeCreate")).toEqual(true); + expect(tracker.isExecutedOnce("filter:afterCreate")).toEqual(true); + expect(tracker.isExecutedOnce("filter:beforeUpdate")).toEqual(false); + expect(tracker.isExecutedOnce("filter:afterUpdate")).toEqual(false); + expect(tracker.isExecutedOnce("filter:beforeDelete")).toEqual(false); + expect(tracker.isExecutedOnce("filter:afterDelete")).toEqual(false); + }); + + it("should trigger update lifecycle events", async () => { + const [createResponse] = await aco.createFilter({ + data: { + name, + model, + operation, + groups + } + }); + + tracker.reset(); + + const [updateResponse] = await aco.updateFilter({ + id: createResponse.data.aco.createFilter.data.id, + data: { + name: `${name} updated` + } + }); + + expect(updateResponse).toMatchObject({ + data: { + aco: { + updateFilter: { + data: { + name: `${name} updated` + }, + error: null + } + } + } + }); + + expect(tracker.isExecutedOnce("filter:beforeCreate")).toEqual(false); + expect(tracker.isExecutedOnce("filter:afterCreate")).toEqual(false); + expect(tracker.isExecutedOnce("filter:beforeUpdate")).toEqual(true); + expect(tracker.isExecutedOnce("filter:afterUpdate")).toEqual(true); + expect(tracker.isExecutedOnce("filter:beforeDelete")).toEqual(false); + expect(tracker.isExecutedOnce("filter:afterDelete")).toEqual(false); + }); + + it("should trigger delete lifecycle events", async () => { + const [createResponse] = await aco.createFilter({ + data: { + name, + model, + operation, + groups + } + }); + + tracker.reset(); + + const [deleteResponse] = await aco.deleteFilter({ + id: createResponse.data.aco.createFilter.data.id + }); + expect(deleteResponse).toMatchObject({ + data: { + aco: { + deleteFilter: { + data: true, + error: null + } + } + } + }); + + expect(tracker.isExecutedOnce("filter:beforeCreate")).toEqual(false); + expect(tracker.isExecutedOnce("filter:afterCreate")).toEqual(false); + expect(tracker.isExecutedOnce("filter:beforeUpdate")).toEqual(false); + expect(tracker.isExecutedOnce("filter:afterUpdate")).toEqual(false); + expect(tracker.isExecutedOnce("filter:beforeDelete")).toEqual(true); + expect(tracker.isExecutedOnce("filter:afterDelete")).toEqual(true); + }); +}); diff --git a/packages/api-aco/__tests__/filter.so.test.ts b/packages/api-aco/__tests__/filter.so.test.ts index 65008436d75..c592955110f 100644 --- a/packages/api-aco/__tests__/filter.so.test.ts +++ b/packages/api-aco/__tests__/filter.so.test.ts @@ -24,9 +24,19 @@ describe("`filter` CRUD", () => { ...filterMocks.filterB }); - // Let's check whether both of the filters exist. - const [listResponse] = await aco.listFilters({ where: { createdBy: userMock.id } }); - expect(listResponse.data.aco.listFilters).toEqual( + const [responseC] = await aco.createFilter({ data: filterMocks.filterC }); + const filterC = responseC.data.aco.createFilter.data; + expect(filterC).toEqual({ + id: filterC.id, + createdBy: userMock, + ...filterMocks.filterC + }); + + // Let's check whether both of the filter exists, listing them by `model`. + const [listResponse1] = await aco.listFilters({ + where: { model: "demo-1", createdBy: userMock.id } + }); + expect(listResponse1.data.aco.listFilters).toEqual( expect.objectContaining({ data: expect.arrayContaining([ expect.objectContaining(filterMocks.filterA), @@ -36,8 +46,18 @@ describe("`filter` CRUD", () => { }) ); + const [listResponse2] = await aco.listFilters({ + where: { model: "demo-2", createdBy: userMock.id } + }); + expect(listResponse2.data.aco.listFilters).toEqual( + expect.objectContaining({ + data: expect.arrayContaining([expect.objectContaining(filterMocks.filterC)]), + error: null + }) + ); + const [nonExistingUserResponse] = await aco.listFilters({ - where: { createdBy: "any-id" } + where: { model: "demo-2", createdBy: "any-id" } }); expect(nonExistingUserResponse.data.aco.listFilters).toEqual( @@ -159,14 +179,7 @@ describe("`filter` CRUD", () => { }); expect(response).toEqual({ - data: { - aco: { - createFilter: expect.objectContaining({ - data: null, - errors: expect.any(Array) - }) - } - } + errors: expect.any(Array) }); }); @@ -200,7 +213,7 @@ describe("`filter` CRUD", () => { }); }); - it("should not allow creating a `filter` with empty `groups.filters` provided", async () => { + it.skip("should not allow creating a `filter` with empty `groups.filters` provided", async () => { const [response] = await aco.createFilter({ data: { ...filterMocks.filterA, diff --git a/packages/api-aco/__tests__/graphql/filter.gql.ts b/packages/api-aco/__tests__/graphql/filter.gql.ts index b801b3c6cba..f209a4a7587 100644 --- a/packages/api-aco/__tests__/graphql/filter.gql.ts +++ b/packages/api-aco/__tests__/graphql/filter.gql.ts @@ -2,6 +2,7 @@ const DATA_FIELD = /* GraphQL */ ` { id name + model operation groups { operation diff --git a/packages/api-aco/__tests__/mocks/filter.mock.ts b/packages/api-aco/__tests__/mocks/filter.mock.ts index 330ad1b001d..bd3f352c4fe 100644 --- a/packages/api-aco/__tests__/mocks/filter.mock.ts +++ b/packages/api-aco/__tests__/mocks/filter.mock.ts @@ -3,6 +3,7 @@ import { Operation } from "~/filter/filter.types"; export const filterMocks = { filterA: { name: "Filter A", + model: "demo-1", operation: Operation.AND, groups: [ { @@ -24,6 +25,7 @@ export const filterMocks = { }, filterB: { name: "Filter B", + model: "demo-1", operation: Operation.OR, groups: [ { @@ -42,5 +44,27 @@ export const filterMocks = { ] } ] + }, + filterC: { + name: "Filter C", + model: "demo-2", + operation: Operation.AND, + groups: [ + { + operation: Operation.OR, + filters: [ + { + field: "field-1", + condition: " ", + value: "value 1" + }, + { + field: "field-2", + condition: "_not", + value: "value 2" + } + ] + } + ] } }; diff --git a/packages/api-aco/__tests__/mocks/lifecycle.mock.ts b/packages/api-aco/__tests__/mocks/lifecycle.mock.ts index 42d77008db5..8b8ef344862 100644 --- a/packages/api-aco/__tests__/mocks/lifecycle.mock.ts +++ b/packages/api-aco/__tests__/mocks/lifecycle.mock.ts @@ -60,3 +60,28 @@ export const assignRecordLifecycleEvents = () => { }); }); }; + +export const assignFilterLifecycleEvents = () => { + return new ContextPlugin(async context => { + context.aco.filter.onFilterBeforeCreate.subscribe(async params => { + tracker.track("filter:beforeCreate", params); + }); + context.aco.filter.onFilterAfterCreate.subscribe(async params => { + tracker.track("filter:afterCreate", params); + }); + + context.aco.filter.onFilterBeforeUpdate.subscribe(async params => { + tracker.track("filter:beforeUpdate", params); + }); + context.aco.filter.onFilterAfterUpdate.subscribe(async params => { + tracker.track("filter:afterUpdate", params); + }); + + context.aco.filter.onFilterBeforeDelete.subscribe(async params => { + tracker.track("filter:beforeDelete", params); + }); + context.aco.filter.onFilterAfterDelete.subscribe(async params => { + tracker.track("filter:afterDelete", params); + }); + }); +}; diff --git a/packages/api-aco/src/filter/filter.gql.ts b/packages/api-aco/src/filter/filter.gql.ts index ec13380fe6f..93f68d37de9 100644 --- a/packages/api-aco/src/filter/filter.gql.ts +++ b/packages/api-aco/src/filter/filter.gql.ts @@ -38,6 +38,7 @@ export const filterSchema = new GraphQLSchemaPlugin({ type Filter { id: ID! name: String! + model: String! operation: OperationEnum! groups: [GroupType]! savedOn: DateTime @@ -47,17 +48,20 @@ export const filterSchema = new GraphQLSchemaPlugin({ input FilterCreateInput { name: String! + model: String! operation: OperationEnum! groups: [GroupInput]! } input FilterUpdateInput { name: String + model: String operation: OperationEnum groups: [GroupInput] } input FiltersListWhereInput { + model: String createdBy: ID } diff --git a/packages/api-aco/src/filter/filter.model.ts b/packages/api-aco/src/filter/filter.model.ts index 4095684fe2b..275b51da6bf 100644 --- a/packages/api-aco/src/filter/filter.model.ts +++ b/packages/api-aco/src/filter/filter.model.ts @@ -17,6 +17,19 @@ const name = () => ] }); +const model = () => + createModelField({ + label: "Model", + fieldId: "model", + type: "text", + validation: [ + { + name: "required", + message: "Value is required." + } + ] + }); + const operation = () => createModelField({ label: "Operation", @@ -140,9 +153,10 @@ export const createFilterModelDefinition = (): FilterModelDefinition => { name: "ACO - Filter", modelId: FILTER_MODEL_ID, titleFieldId: "name", - layout: [["name"], ["operation"], ["groups"]], + layout: [["name"], ["model"], ["operation"], ["groups"]], fields: [ name(), + model(), operation(), groups([operation(), filters([filterField(), filterCondition(), filterValue()])]) ], diff --git a/packages/api-aco/src/filter/filter.types.ts b/packages/api-aco/src/filter/filter.types.ts index 472f447e026..0138d272ca8 100644 --- a/packages/api-aco/src/filter/filter.types.ts +++ b/packages/api-aco/src/filter/filter.types.ts @@ -19,11 +19,13 @@ export interface Group { export interface Filter extends AcoBaseFields { name: string; + model: string; operation: Operation; groups: Group[]; } export interface ListFiltersWhere { + model: string; createdBy: User["id"]; } @@ -34,10 +36,13 @@ export interface ListFiltersParams { after?: string | null; } -export type CreateFilterParams = Pick; +export type CreateFilterParams = Pick; export interface UpdateFilterParams { name?: string; + model?: string; + operation?: Operation; + groups?: Group[]; } export interface DeleteFilterParams { From 6f7052cb8d4835e266b2b2796d2a7b1fbbdfd666 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Thu, 28 Sep 2023 21:25:09 +0200 Subject: [PATCH 038/122] feat: add new JSON type --- .../api-aco/__tests__/filter.hooks.test.ts | 14 +- packages/api-aco/__tests__/filter.so.test.ts | 178 +++++++++++++++--- .../api-aco/__tests__/graphql/filter.gql.ts | 12 +- .../api-aco/__tests__/mocks/filter.mock.ts | 21 ++- packages/api-aco/package.json | 3 +- packages/api-aco/src/createAcoFields.ts | 28 +++ packages/api-aco/src/filter/filter.gql.ts | 40 +--- packages/api-aco/src/filter/filter.model.ts | 99 ++-------- packages/api-aco/src/filter/filter.so.ts | 9 +- packages/api-aco/src/filter/filter.types.ts | 20 +- .../api-aco/src/filter/filter.validation.ts | 51 +++++ packages/api-aco/src/index.ts | 3 +- yarn.lock | 1 + 13 files changed, 304 insertions(+), 175 deletions(-) create mode 100644 packages/api-aco/src/createAcoFields.ts create mode 100644 packages/api-aco/src/filter/filter.validation.ts diff --git a/packages/api-aco/__tests__/filter.hooks.test.ts b/packages/api-aco/__tests__/filter.hooks.test.ts index 7932afeab9d..3848fe8ae6b 100644 --- a/packages/api-aco/__tests__/filter.hooks.test.ts +++ b/packages/api-aco/__tests__/filter.hooks.test.ts @@ -4,10 +4,10 @@ import { assignFilterLifecycleEvents, tracker } from "./mocks/lifecycle.mock"; import { Operation } from "~/filter/filter.types"; const name = "Filter Lifecycle Events"; -const model = "demo-lifecycle-events"; +const modelId = "demo-lifecycle-events"; const operation = Operation.AND; const groups = [ - { + JSON.stringify({ operation: Operation.OR, filters: [ { @@ -16,7 +16,7 @@ const groups = [ value: "any-value" } ] - } + }) ]; describe("Filter Lifecycle Events", () => { @@ -32,7 +32,7 @@ describe("Filter Lifecycle Events", () => { const [response] = await aco.createFilter({ data: { name, - model, + modelId, operation, groups } @@ -44,7 +44,7 @@ describe("Filter Lifecycle Events", () => { createFilter: { data: { name, - model, + modelId, operation, groups }, @@ -66,7 +66,7 @@ describe("Filter Lifecycle Events", () => { const [createResponse] = await aco.createFilter({ data: { name, - model, + modelId, operation, groups } @@ -106,7 +106,7 @@ describe("Filter Lifecycle Events", () => { const [createResponse] = await aco.createFilter({ data: { name, - model, + modelId, operation, groups } diff --git a/packages/api-aco/__tests__/filter.so.test.ts b/packages/api-aco/__tests__/filter.so.test.ts index c592955110f..82a23eaa95a 100644 --- a/packages/api-aco/__tests__/filter.so.test.ts +++ b/packages/api-aco/__tests__/filter.so.test.ts @@ -32,10 +32,11 @@ describe("`filter` CRUD", () => { ...filterMocks.filterC }); - // Let's check whether both of the filter exists, listing them by `model`. + // Let's check whether both of the filter exists, listing them by `modelId`. const [listResponse1] = await aco.listFilters({ - where: { model: "demo-1", createdBy: userMock.id } + where: { modelId: "demo-1" } }); + expect(listResponse1.data.aco.listFilters).toEqual( expect.objectContaining({ data: expect.arrayContaining([ @@ -47,8 +48,9 @@ describe("`filter` CRUD", () => { ); const [listResponse2] = await aco.listFilters({ - where: { model: "demo-2", createdBy: userMock.id } + where: { modelId: "demo-2" } }); + expect(listResponse2.data.aco.listFilters).toEqual( expect.objectContaining({ data: expect.arrayContaining([expect.objectContaining(filterMocks.filterC)]), @@ -56,17 +58,6 @@ describe("`filter` CRUD", () => { }) ); - const [nonExistingUserResponse] = await aco.listFilters({ - where: { model: "demo-2", createdBy: "any-id" } - }); - - expect(nonExistingUserResponse.data.aco.listFilters).toEqual( - expect.objectContaining({ - data: [], - error: null - }) - ); - // Let's update the "filter-b". const update = { name: "Filter B - UPDATED", @@ -170,6 +161,36 @@ describe("`filter` CRUD", () => { }); }); + it("should not allow creating a `filter` with no `modelId` provided", async () => { + const [response] = await aco.createFilter({ + data: { + ...filterMocks.filterA, + modelId: "" + } + }); + + expect(response).toEqual({ + data: { + aco: { + createFilter: { + data: null, + error: { + code: "VALIDATION_FAILED", + message: "Validation failed.", + data: [ + { + error: "Value is required.", + fieldId: "modelId", + storageId: "text@modelId" + } + ] + } + } + } + } + }); + }); + it("should not allow creating a `filter` with no `operation` provided", async () => { const [response] = await aco.createFilter({ data: { @@ -201,9 +222,48 @@ describe("`filter` CRUD", () => { message: "Validation failed.", data: [ { - error: "Value is too short.", - fieldId: "groups", - storageId: "object@groups" + error: "Array must contain at least 1 element(s)", + path: "" + } + ] + } + } + } + } + }); + }); + + it("should not allow creating a `filter` with empty `groups.operation` provided", async () => { + const [response] = await aco.createFilter({ + data: { + ...filterMocks.filterA, + groups: [ + JSON.stringify({ + operation: "", + filters: [ + { + field: "any", + condition: "any", + value: "any" + } + ] + }) + ] + } + }); + + expect(response).toEqual({ + data: { + aco: { + createFilter: { + data: null, + error: { + code: "VALIDATION_FAILED", + message: "Validation failed.", + data: [ + { + error: "Invalid enum value. Expected 'AND' | 'OR', received ''", + path: "0.operation" } ] } @@ -213,15 +273,55 @@ describe("`filter` CRUD", () => { }); }); - it.skip("should not allow creating a `filter` with empty `groups.filters` provided", async () => { + it("should not allow creating a `filter` with empty `groups.filters` provided", async () => { const [response] = await aco.createFilter({ data: { ...filterMocks.filterA, groups: [ - { + JSON.stringify({ operation: Operation.AND, filters: [] + }) + ] + } + }); + + expect(response).toEqual({ + data: { + aco: { + createFilter: { + data: null, + error: { + code: "VALIDATION_FAILED", + message: "Validation failed.", + data: [ + { + error: "Array must contain at least 1 element(s)", + path: "0.filters" + } + ] + } } + } + } + }); + }); + + it("should not allow creating a `filter` with wrong `groups.filters` provided", async () => { + const [response] = await aco.createFilter({ + data: { + ...filterMocks.filterA, + groups: [ + JSON.stringify({ + operation: Operation.AND, + filters: [ + { + field: "", + condition: "", + value: "" + } + ] + }) ] } }); @@ -236,9 +336,16 @@ describe("`filter` CRUD", () => { message: "Validation failed.", data: [ { - error: "Value is too short.", - fieldId: "groups", - storageId: "object@groups" + error: "Field is required.", + path: "0.filters.0.field" + }, + { + error: "Condition is required.", + path: "0.filters.0.condition" + }, + { + error: "Value is required.", + path: "0.filters.0.value" } ] } @@ -267,6 +374,31 @@ describe("`filter` CRUD", () => { }); }); + it("should not list filters created by other users", async () => { + const { aco: otherAco } = useGraphQlHandler({ + identity: { + id: "abcdefgh", + type: "admin", + displayName: "Smith Smith" + } + }); + const { aco } = useGraphQlHandler(); + + // Let's create some filters. + await aco.createFilter({ data: filterMocks.filterA }); + + const [listResponse] = await otherAco.listFilters({ + where: { modelId: "demo-1" } + }); + + expect(listResponse.data.aco.listFilters).toEqual( + expect.objectContaining({ + data: [], + error: null + }) + ); + }); + it("should enforce security rules", async () => { const { aco: anonymousAco } = useGraphQlHandler({ identity: null }); const { aco } = useGraphQlHandler(); @@ -299,7 +431,7 @@ describe("`filter` CRUD", () => { // List with anonymous identity { const [listResponse] = await anonymousAco.listFilters({ - where: { createdBy: userMock.id } + where: { modelId: "demo-1" } }); expect(listResponse.data.aco.listFilters).toEqual( expect.objectContaining(notAuthorizedResponse) diff --git a/packages/api-aco/__tests__/graphql/filter.gql.ts b/packages/api-aco/__tests__/graphql/filter.gql.ts index f209a4a7587..32fde71e0c4 100644 --- a/packages/api-aco/__tests__/graphql/filter.gql.ts +++ b/packages/api-aco/__tests__/graphql/filter.gql.ts @@ -2,16 +2,10 @@ const DATA_FIELD = /* GraphQL */ ` { id name - model + description + modelId operation - groups { - operation - filters { - field - condition - value - } - } + groups createdBy { id displayName diff --git a/packages/api-aco/__tests__/mocks/filter.mock.ts b/packages/api-aco/__tests__/mocks/filter.mock.ts index bd3f352c4fe..1887776c97f 100644 --- a/packages/api-aco/__tests__/mocks/filter.mock.ts +++ b/packages/api-aco/__tests__/mocks/filter.mock.ts @@ -3,10 +3,11 @@ import { Operation } from "~/filter/filter.types"; export const filterMocks = { filterA: { name: "Filter A", - model: "demo-1", + description: "Filter description A", + modelId: "demo-1", operation: Operation.AND, groups: [ - { + JSON.stringify({ operation: Operation.AND, filters: [ { @@ -20,15 +21,16 @@ export const filterMocks = { value: "value 2" } ] - } + }) ] }, filterB: { name: "Filter B", - model: "demo-1", + description: "Filter description B", + modelId: "demo-1", operation: Operation.OR, groups: [ - { + JSON.stringify({ operation: Operation.OR, filters: [ { @@ -42,15 +44,16 @@ export const filterMocks = { value: "value 2" } ] - } + }) ] }, filterC: { name: "Filter C", - model: "demo-2", + description: "Filter description C", + modelId: "demo-2", operation: Operation.AND, groups: [ - { + JSON.stringify({ operation: Operation.OR, filters: [ { @@ -64,7 +67,7 @@ export const filterMocks = { value: "value 2" } ] - } + }) ] } }; diff --git a/packages/api-aco/package.json b/packages/api-aco/package.json index fc1d99e5697..fcaed7c296e 100644 --- a/packages/api-aco/package.json +++ b/packages/api-aco/package.json @@ -33,7 +33,8 @@ "@webiny/handler-graphql": "0.0.0", "@webiny/pubsub": "0.0.0", "@webiny/utils": "0.0.0", - "lodash": "^4.4.2" + "lodash": "^4.4.2", + "zod": "^3.21.4" }, "devDependencies": { "@babel/cli": "^7.22.6", diff --git a/packages/api-aco/src/createAcoFields.ts b/packages/api-aco/src/createAcoFields.ts new file mode 100644 index 00000000000..59e2eb84c95 --- /dev/null +++ b/packages/api-aco/src/createAcoFields.ts @@ -0,0 +1,28 @@ +import { CmsModelFieldToGraphQLPlugin } from "@webiny/api-headless-cms/types"; + +// Creating an internal JSON field, we are using it inside the `record` type +const jsonField: CmsModelFieldToGraphQLPlugin = { + name: "cms-model-field-to-graphql-json", + type: "cms-model-field-to-graphql", + fieldType: "wby-aco-json", + isSortable: true, + isSearchable: true, + read: { + createTypeField({ field }) { + return `${field.fieldId}: JSON`; + }, + createGetFilters({ field }) { + return `${field.fieldId}: JSON`; + } + }, + manage: { + createTypeField({ field }) { + return `${field.fieldId}: JSON`; + }, + createInputField({ field }) { + return field.fieldId + ": JSON"; + } + } +}; + +export const createAcoFields = (): CmsModelFieldToGraphQLPlugin[] => [jsonField]; diff --git a/packages/api-aco/src/filter/filter.gql.ts b/packages/api-aco/src/filter/filter.gql.ts index 93f68d37de9..52c68d106e1 100644 --- a/packages/api-aco/src/filter/filter.gql.ts +++ b/packages/api-aco/src/filter/filter.gql.ts @@ -13,34 +13,13 @@ export const filterSchema = new GraphQLSchemaPlugin({ OR } - type GroupFilterType { - field: String! - condition: String! - value: String! - } - - input GroupFilterInput { - field: String! - condition: String! - value: String! - } - - type GroupType { - operation: OperationEnum! - filters: [GroupFilterType]! - } - - input GroupInput { - operation: OperationEnum! - filters: [GroupFilterInput]! - } - type Filter { id: ID! name: String! - model: String! + description: String + modelId: String! operation: OperationEnum! - groups: [GroupType]! + groups: [JSON]! savedOn: DateTime createdOn: DateTime createdBy: AcoUser @@ -48,21 +27,22 @@ export const filterSchema = new GraphQLSchemaPlugin({ input FilterCreateInput { name: String! - model: String! + description: String + modelId: String! operation: OperationEnum! - groups: [GroupInput]! + groups: [JSON]! } input FilterUpdateInput { name: String - model: String + description: String + modelId: String operation: OperationEnum - groups: [GroupInput] + groups: [JSON] } input FiltersListWhereInput { - model: String - createdBy: ID + modelId: String } type FilterResponse { diff --git a/packages/api-aco/src/filter/filter.model.ts b/packages/api-aco/src/filter/filter.model.ts index 275b51da6bf..b1d2e26bf46 100644 --- a/packages/api-aco/src/filter/filter.model.ts +++ b/packages/api-aco/src/filter/filter.model.ts @@ -1,6 +1,5 @@ import { createModelField } from "~/utils/createModelField"; import { CmsPrivateModelFull } from "@webiny/api-headless-cms"; -import { CmsModelField } from "@webiny/api-headless-cms/types"; export type FilterModelDefinition = Omit; @@ -17,10 +16,17 @@ const name = () => ] }); -const model = () => +const description = () => createModelField({ - label: "Model", - fieldId: "model", + label: "Description", + fieldId: "description", + type: "text" + }); + +const modelId = () => + createModelField({ + label: "Model Id", + fieldId: "modelId", type: "text", validation: [ { @@ -57,45 +63,12 @@ const operation = () => ] }); -const groups = (fields: CmsModelField[]): CmsModelField => +const groups = () => createModelField({ label: "Groups", fieldId: "groups", - type: "object", - settings: { - fields, - layout: fields.map(field => [field.storageId]) - }, - multipleValues: true, - predefinedValues: { - values: [], - enabled: false - }, - listValidation: [ - { - name: "minLength", - message: "Value is too short.", - settings: { - value: "1" - } - } - ] - }); - -const filters = (fields: CmsModelField[]): CmsModelField => - createModelField({ - label: "Filters", - fieldId: "filters", - type: "object", - settings: { - fields, - layout: fields.map(field => [field.storageId]) - }, + type: "wby-aco-json", multipleValues: true, - predefinedValues: { - values: [], - enabled: false - }, listValidation: [ { name: "minLength", @@ -107,45 +80,6 @@ const filters = (fields: CmsModelField[]): CmsModelField => ] }); -const filterField = () => - createModelField({ - label: "Field", - fieldId: "field", - type: "text", - validation: [ - { - name: "required", - message: "Value is required." - } - ] - }); - -const filterCondition = () => - createModelField({ - label: "Condition", - fieldId: "condition", - type: "text", - validation: [ - { - name: "required", - message: "Value is required." - } - ] - }); - -const filterValue = () => - createModelField({ - label: "Value", - fieldId: "value", - type: "text", - validation: [ - { - name: "required", - message: "Value is required." - } - ] - }); - export const FILTER_MODEL_ID = "acoFilter"; export const createFilterModelDefinition = (): FilterModelDefinition => { @@ -153,13 +87,8 @@ export const createFilterModelDefinition = (): FilterModelDefinition => { name: "ACO - Filter", modelId: FILTER_MODEL_ID, titleFieldId: "name", - layout: [["name"], ["model"], ["operation"], ["groups"]], - fields: [ - name(), - model(), - operation(), - groups([operation(), filters([filterField(), filterCondition(), filterValue()])]) - ], + layout: [["name"], ["description"], ["modelId"], ["operation"], ["groups"]], + fields: [name(), description(), modelId(), operation(), groups()], description: "ACO - Filter content model", isPrivate: true }; diff --git a/packages/api-aco/src/filter/filter.so.ts b/packages/api-aco/src/filter/filter.so.ts index e62583d9b95..07a2a4efa6f 100644 --- a/packages/api-aco/src/filter/filter.so.ts +++ b/packages/api-aco/src/filter/filter.so.ts @@ -1,6 +1,7 @@ import WebinyError from "@webiny/error"; import { FILTER_MODEL_ID } from "./filter.model"; +import { validateFilterGroupsInput } from "./filter.validation"; import { baseFields, CreateAcoStorageOperationsParams } from "~/createAcoStorageOperations"; import { createListSort } from "~/utils/createListSort"; import { createOperationsWrapper } from "~/utils/createOperationsWrapper"; @@ -11,7 +12,7 @@ import { AcoFilterStorageOperations } from "./filter.types"; export const createFilterOperations = ( params: CreateAcoStorageOperationsParams ): AcoFilterStorageOperations => { - const { cms } = params; + const { cms, security } = params; const { withModel } = createOperationsWrapper({ ...params, @@ -37,12 +38,13 @@ export const createFilterOperations = ( listFilters(params) { return withModel(async model => { const { sort, where } = params; + const createdBy = security.getIdentity().id; const [entries, meta] = await cms.listLatestEntries(model, { ...params, sort: createListSort(sort), where: { - ...(where || {}) + ...({ ...where, createdBy } || {}) } }); @@ -51,6 +53,7 @@ export const createFilterOperations = ( }, createFilter({ data }) { return withModel(async model => { + validateFilterGroupsInput(data.groups); const entry = await cms.createEntry(model, data); return getFilterFieldValues(entry, baseFields); }); @@ -64,6 +67,8 @@ export const createFilterOperations = ( ...data }; + validateFilterGroupsInput(input.groups); + const entry = await cms.updateEntry(model, id, input); return getFilterFieldValues(entry, baseFields); }); diff --git a/packages/api-aco/src/filter/filter.types.ts b/packages/api-aco/src/filter/filter.types.ts index 0138d272ca8..67e7587cccf 100644 --- a/packages/api-aco/src/filter/filter.types.ts +++ b/packages/api-aco/src/filter/filter.types.ts @@ -1,4 +1,4 @@ -import { AcoBaseFields, ListMeta, ListSort, User } from "~/types"; +import { AcoBaseFields, ListMeta, ListSort } from "~/types"; import { Topic } from "@webiny/pubsub/types"; export enum Operation { @@ -19,14 +19,14 @@ export interface Group { export interface Filter extends AcoBaseFields { name: string; - model: string; + description?: string; + modelId: string; operation: Operation; - groups: Group[]; + groups: string[]; } export interface ListFiltersWhere { - model: string; - createdBy: User["id"]; + modelId: string; } export interface ListFiltersParams { @@ -36,13 +36,17 @@ export interface ListFiltersParams { after?: string | null; } -export type CreateFilterParams = Pick; +export type CreateFilterParams = Pick< + Filter, + "name" | "description" | "modelId" | "operation" | "groups" +>; export interface UpdateFilterParams { name?: string; - model?: string; + description?: string; + modelId?: string; operation?: Operation; - groups?: Group[]; + groups?: string[]; } export interface DeleteFilterParams { diff --git a/packages/api-aco/src/filter/filter.validation.ts b/packages/api-aco/src/filter/filter.validation.ts new file mode 100644 index 00000000000..04ec890034f --- /dev/null +++ b/packages/api-aco/src/filter/filter.validation.ts @@ -0,0 +1,51 @@ +import zod from "zod"; +import WebinyError from "@webiny/error"; +import { Filter, Group, Operation } from "./filter.types"; + +interface ValidationErrorData { + error: string; + path: string; +} + +const operationValidator = zod.enum([Operation.AND, Operation.OR]); + +const filterValidationSchema = zod.object({ + field: zod.string().trim().nonempty("Field is required."), + condition: zod.string().nonempty("Condition is required."), + value: zod.union([ + zod.boolean(), + zod.number({ + required_error: "Value is required.", + invalid_type_error: "Value must be a number." + }), + zod.string().trim().nonempty("Value is required."), + zod + .array(zod.union([zod.boolean(), zod.number(), zod.string()])) + .nonempty("Value is too short.") + ]) +}); + +const groupValidationSchema = zod.object({ + operation: operationValidator, + filters: zod.array(filterValidationSchema).nonempty() +}); + +const validationSchema = zod.array(groupValidationSchema).nonempty(); + +export const validateFilterGroupsInput = (groups: Filter["groups"]) => { + const parsedGroups: Group[] = groups.map(group => JSON.parse(group)); + const result = validationSchema.safeParse(parsedGroups); + + if (!result.success) { + const data = result.error.issues.reduce((acc, issue) => { + acc.push({ + error: issue.message || "", + path: issue.path.join(".") || "" + }); + + return acc; + }, [] as ValidationErrorData[]); + + throw new WebinyError("Validation failed.", "VALIDATION_FAILED", data); + } +}; diff --git a/packages/api-aco/src/index.ts b/packages/api-aco/src/index.ts index f946f78a10e..1149fd1f2da 100644 --- a/packages/api-aco/src/index.ts +++ b/packages/api-aco/src/index.ts @@ -1,4 +1,5 @@ import { createAcoContext } from "~/createAcoContext"; +import { createAcoFields } from "~/createAcoFields"; import { createAcoGraphQL } from "~/createAcoGraphQL"; import { createFields } from "~/fields"; @@ -9,5 +10,5 @@ export * from "./apps"; export * from "./plugins"; export const createAco = () => { - return [...createFields(), createAcoContext(), ...createAcoGraphQL()]; + return [...createFields(), createAcoContext(), createAcoFields(), ...createAcoGraphQL()]; }; diff --git a/yarn.lock b/yarn.lock index ba2b2da33d8..3f5e557f5a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12890,6 +12890,7 @@ __metadata: rimraf: ^3.0.2 ttypescript: ^1.5.13 typescript: ^4.7.4 + zod: ^3.21.4 languageName: unknown linkType: soft From 32fb9347297f7675c0b45b0dc73c2e9dd87681f0 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Fri, 29 Sep 2023 11:10:37 +0200 Subject: [PATCH 039/122] wip: add QueryManager component --- .../QueryBuilderPresenter.test.ts | 8 +- .../AdvancedSearch/AdvancedSearch.tsx | 53 ++++- .../AdvancedSearch/Drawer/Drawer.tsx | 8 +- .../QueryBuilder/QueryBuilder.tsx | 20 +- .../adapters/QueryBuilderPresenter.ts | 14 +- .../QueryBuilder/components/QueryBuilder.tsx | 1 + .../QueryBuilder/domain/QueryObject.ts | 35 +++- .../QueryBuilder/domain/QueryObjectMapper.ts | 2 + .../QueryManager/QueryManager.tsx | 38 ++++ .../adapters/QueryManagerPresenter.ts | 89 ++++++++ .../QueryManager/adapters/index.ts | 1 + .../components/QueryManager.styled.tsx | 14 ++ .../QueryManager/components/QueryManager.tsx | 110 ++++++++++ .../QueryManager/components/index.tsx | 1 + .../QueryManager/domain/QueryObject.ts | 70 +++++++ .../QueryManager/domain/QueryObjectMapper.ts | 41 ++++ .../QueryManager/domain/index.ts | 2 + .../QueryManager/gateways/FiltersGraphQL.ts | 190 ++++++++++++++++++ .../QueryManager/gateways/index.ts | 1 + packages/app-aco/src/graphql/filters.gql.ts | 84 -------- packages/app-aco/src/types.ts | 57 ++++++ .../ContentEntries/Filters/Filters.tsx | 6 +- 22 files changed, 737 insertions(+), 108 deletions(-) create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryManager/QueryManager.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryManager/adapters/QueryManagerPresenter.ts create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryManager/adapters/index.ts create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryManager/components/QueryManager.styled.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryManager/components/QueryManager.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryManager/components/index.tsx create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/QueryObject.ts create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/QueryObjectMapper.ts create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/index.ts create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryManager/gateways/FiltersGraphQL.ts create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryManager/gateways/index.ts delete mode 100644 packages/app-aco/src/graphql/filters.gql.ts diff --git a/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts b/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts index 96f98d38f8c..add8ea3807f 100644 --- a/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts +++ b/packages/app-aco/__tests__/components/AdvancedSearch/QueryBuilder/QueryBuilderPresenter.test.ts @@ -10,6 +10,7 @@ import { } from "~/components/AdvancedSearch/QueryBuilder/domain"; describe("QueryBuilderPresenter", () => { + const modelId = "model-id"; const defaultFilter = { field: "", value: "", condition: "" }; const defaultGroup = { @@ -44,7 +45,7 @@ describe("QueryBuilderPresenter", () => { } ]; - presenter = new QueryBuilderPresenter(defaultFields); + presenter = new QueryBuilderPresenter(modelId, defaultFields); viewModel = presenter.getViewModel(); }); @@ -64,6 +65,8 @@ describe("QueryBuilderPresenter", () => { expect(viewModel.queryObject).toEqual({ id: expect.any(String), name: "Untitled", + description: "", + modelId: modelId, operation: "AND", groups: [testFolder] }); @@ -246,6 +249,7 @@ describe("QueryBuilderPresenter", () => { }); describe("FieldDTO definition", () => { + const modelId = "modelId-id"; const fields: [FieldRaw, FieldDTO][] = [ [ { @@ -470,7 +474,7 @@ describe("FieldDTO definition", () => { fields.forEach(([fieldRaw, fieldDTO]) => { it(`should transform "Raw ${fieldRaw.label}" -> "DTO ${fieldDTO.label}" `, () => { - const presenter = new QueryBuilderPresenter([fieldRaw]); + const presenter = new QueryBuilderPresenter(modelId, [fieldRaw]); const viewModel = presenter.getViewModel(); expect(viewModel.fields).toEqual([fieldDTO]); diff --git a/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx index ad8b5b61569..85d5bf4dbe3 100644 --- a/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx @@ -3,33 +3,68 @@ import React, { useCallback, useState } from "react"; import { Button } from "./Button"; import { Drawer } from "./Drawer"; -import { FieldRaw } from "./QueryBuilder/domain"; +import { FieldRaw, QueryObjectDTO } from "./QueryBuilder/domain"; +import { QueryManager } from "~/components/AdvancedSearch/QueryManager/QueryManager"; interface AdvancedSearchProps { fields: FieldRaw[]; + modelId: string; onSubmit: (data: any) => void; } -export const AdvancedSearch = ({ fields, onSubmit }: AdvancedSearchProps) => { - const [open, setOpen] = useState(false); +export const AdvancedSearch = ({ fields, modelId, onSubmit }: AdvancedSearchProps) => { + const [openManager, setOpenManager] = useState(false); + const [openBuilder, setOpenBuilder] = useState(false); + const [queryObject, setQueryObject] = useState(); - const onDrawerSubmit = useCallback( + const onQueryBuilderSubmit = useCallback( data => { // Close the drawer on submission - setOpen(false); + setOpenBuilder(false); onSubmit && onSubmit(data); }, [onSubmit] ); + const onQueryManagerSelect = useCallback( + data => { + // Close the dialog on submission + setOpenManager(false); + onSubmit && onSubmit(data); + }, + [onSubmit] + ); + + const onQueryManagerEdit = (data?: QueryObjectDTO) => { + setQueryObject(data); + setOpenManager(false); + setOpenBuilder(true); + }; + + const onQueryManagerCreate = () => { + setQueryObject(undefined); + setOpenManager(false); + setOpenBuilder(true); + }; + return ( <> - diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/Input.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/Input.tsx index 6768782c5da..27b7d56589d 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/Input.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/Input.tsx @@ -3,7 +3,7 @@ import React from "react"; import { Bind } from "@webiny/form"; import { Input as BaseInput } from "@webiny/ui/Input"; -import { FieldType } from "~/components/AdvancedSearch/QueryBuilder/domain"; +import { FieldType } from "~/components/AdvancedSearch/QueryObject"; interface InputProps { type: FieldType; diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/MultipleValues.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/MultipleValues.tsx index 690e0387e1d..cd0cf30296b 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/MultipleValues.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/fields/MultipleValues.tsx @@ -3,7 +3,7 @@ import React from "react"; import { Bind } from "@webiny/form"; import { Checkbox, CheckboxGroup } from "@webiny/ui/Checkbox"; -import { Predefined } from "~/components/AdvancedSearch/QueryBuilder/domain"; +import { Predefined } from "~/components/AdvancedSearch/QueryObject"; interface MultipleValuesProps { predefined: Predefined[]; diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/QueryObjectMapper.ts b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/QueryObjectMapper.ts deleted file mode 100644 index 33541494614..00000000000 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/QueryObjectMapper.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { QueryObject, QueryObjectDTO } from "./QueryObject"; - -export class QueryObjectMapper { - static toDTO(configuration: QueryObject | QueryObjectDTO): QueryObjectDTO { - return { - id: configuration.id, - name: configuration.name, - description: configuration.description, - modelId: configuration.modelId, - operation: configuration.operation, - groups: configuration.groups.map(group => ({ - operation: group.operation, - filters: group.filters.map(filter => ({ - field: filter.field || "", - value: filter.value || "", - condition: filter.condition || "" - })) - })) - }; - } -} diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryManager/QueryManager.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryManager/QueryManager.tsx index 8937b327ceb..f4598d92ddb 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryManager/QueryManager.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryManager/QueryManager.tsx @@ -2,28 +2,27 @@ import React, { useState } from "react"; import { QueryManager as QueryManagerComponent } from "./components/QueryManager"; import { QueryManagerPresenter } from "./adapters/QueryManagerPresenter"; -import { useApolloClient } from "@apollo/react-hooks"; -import { QueryObjectDTO } from "~/components/AdvancedSearch/QueryBuilder/domain"; +import { QueryObjectDTO } from "~/components/AdvancedSearch/QueryObject"; +import { QueryObjectRepository } from "~/components/AdvancedSearch/QueryObject/QueryObjectRepository"; interface QueryBuilderProps { - modelId: string; + repository: QueryObjectRepository; open: boolean; onClose: () => void; - onEdit: (data?: QueryObjectDTO) => void; + onEdit: (callback?: () => void) => void; onSelect: (data: QueryObjectDTO) => void; - onCreate: () => void; + onCreate: (callback?: () => void) => void; } export const QueryManager = ({ - modelId, + repository, open, onClose, onEdit, onSelect, onCreate }: QueryBuilderProps) => { - const client = useApolloClient(); - const [presenter] = useState(new QueryManagerPresenter(client, modelId)); + const [presenter] = useState(new QueryManagerPresenter(repository)); return ( QueryManagerViewModel; listFilters: () => void; - createFilter: (filter: Omit) => void; - updateFilter: (filter: Omit) => void; deleteFilter: (id: string) => void; } @@ -17,73 +16,33 @@ export interface QueryManagerViewModel { } export class QueryManagerPresenter implements IQueryManagerPresenter { - private gateway: FiltersGraphQL; - private filters: QueryObjectDTO[] = []; - private readonly modelId: string; - selected: undefined | QueryObjectDTO; + private repository: QueryObjectRepository; - constructor(client: ApolloClient, modelId: string) { - this.gateway = new FiltersGraphQL(client); - this.modelId = modelId; - this.selected = undefined; + constructor(repository: QueryObjectRepository) { + this.repository = repository; makeAutoObservable(this); } async listFilters() { - const rawFilters = await this.gateway.list(this.modelId); - - runInAction(() => { - this.filters = rawFilters.map(filter => QueryObjectMapper.toDTO(filter)); - }); - } - - async createFilter(filter: any) { - const rawFilter = QueryObjectMapper.toRaw(filter); - const response = await this.gateway.create(rawFilter); - const filterDTO = QueryObjectMapper.toDTO(response); - - if (response) { - this.filters = [...this.filters, filterDTO]; - } - } - - async updateFilter(filter: any) { - const rawFilter = QueryObjectMapper.toRaw(filter); - const response = await this.gateway.update(rawFilter); - - if (response) { - const filterIndex = this.filters.findIndex(f => f.id === filter.id); - - if (filterIndex > -1) { - const filterDTO = QueryObjectMapper.toDTO(response); - this.filters = [ - ...this.filters.slice(0, filterIndex), - { - ...this.filters[filterIndex], - ...filterDTO - }, - ...this.filters.slice(filterIndex + 1) - ]; - } - } + await this.repository.listFilters(); } async deleteFilter(id: string) { - const response = await this.gateway.delete(id); + await this.repository.deleteFilter(id); + } - if (response) { - this.filters = this.filters.filter(filter => filter.id !== id); - } + selectFilter(id?: string) { + this.repository.setSelected(id); } - setSelected(id?: string) { - this.selected = id ? this.filters.find(filter => filter.id === id) : undefined; + setMode(mode: Mode) { + this.repository.mode = mode; } getViewModel() { return { - filters: this.filters, - selected: this.selected + filters: this.repository.filters, + selected: this.repository.selected }; } } diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryManager/components/QueryManager.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryManager/components/QueryManager.tsx index f2dfa02dbda..efeacb1d889 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryManager/components/QueryManager.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryManager/components/QueryManager.tsx @@ -16,16 +16,16 @@ import { import { DialogContainer, ListActions } from "./QueryManager.styled"; import { QueryManagerPresenter } from "~/components/AdvancedSearch/QueryManager/adapters"; -import { QueryObjectDTO } from "~/components/AdvancedSearch/QueryBuilder/domain"; import { Tooltip } from "@webiny/ui/Tooltip"; import { Menu, MenuItem } from "@webiny/ui/Menu"; +import { Mode, QueryObjectDTO } from "~/components/AdvancedSearch/QueryObject"; interface QueryManagerProps { presenter: QueryManagerPresenter; open: boolean; onClose: () => void; - onCreate: () => void; - onEdit: (filter?: QueryObjectDTO) => void; + onCreate: (callback?: () => void) => void; + onEdit: (callback?: () => void) => void; onSelect: (data: QueryObjectDTO) => void; } @@ -68,7 +68,14 @@ export const QueryManager = observer(({ presenter, ...props }: QueryManagerProps /> } > - props.onEdit(filter)}> + + props.onEdit(() => { + presenter.setMode(Mode.UPDATE); + presenter.selectFilter(filter.id); + }) + } + > Edit { - presenter.setSelected(); + presenter.selectFilter(); props.onClose(); }} > @@ -96,8 +103,10 @@ export const QueryManager = observer(({ presenter, ...props }: QueryManagerProps { - presenter.setSelected(); - props.onCreate(); + props.onCreate(() => { + presenter.setMode(Mode.CREATE); + presenter.selectFilter(); + }); }} > {"Create new"} diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/QueryObject.ts b/packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/QueryObject.ts deleted file mode 100644 index f66b0ffffb9..00000000000 --- a/packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/QueryObject.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Operation } from "../../QueryBuilder/domain"; - -export interface QueryObjectRaw { - id: string; - name: string; - description?: string; - modelId: string; - operation: Operation; - groups: string[]; -} - -export class QueryObject { - public readonly operations = Operation; - public readonly id; - public name; - public description; - public modelId: string; - public operation: Operation; - public groups: Group[]; - - static createFromRaw(data: QueryObjectRaw) { - const groups = data.groups.map(group => JSON.parse(group)); - return new QueryObject( - data.id, - data.name, - data.modelId, - data.operation, - groups, - data.description - ); - } - - private constructor( - id: string, - name: string, - modelId: string, - operation: Operation, - groups: Group[], - description?: string - ) { - this.id = id; - this.name = name; - this.modelId = modelId; - this.description = description ?? ""; - this.operation = operation; - this.groups = groups; - } -} - -export class Group { - public readonly operation: Operation; - public readonly filters: Filter[]; - - constructor(operation: Operation, filters: Filter[]) { - this.operation = operation; - this.filters = filters; - } -} - -export class Filter { - public readonly field?: string; - public readonly condition?: string; - public readonly value?: string; - - constructor(field?: string, condition?: string, value?: string) { - this.field = field; - this.condition = condition; - this.value = value; - } -} diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/index.ts b/packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/index.ts deleted file mode 100644 index 15b388e9e77..00000000000 --- a/packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./QueryObject"; -export * from "./QueryObjectMapper"; diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryObject/QueryObjectRepository.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/QueryObjectRepository.ts new file mode 100644 index 00000000000..bf2b164f621 --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/QueryObject/QueryObjectRepository.ts @@ -0,0 +1,78 @@ +import { ApolloClient } from "apollo-client"; +import { makeAutoObservable, runInAction } from "mobx"; + +import { Mode, QueryObjectMapper, QueryObjectDTO } from "./domains"; +import { FiltersGraphQL } from "./gateways"; + +export class QueryObjectRepository { + private gateway: FiltersGraphQL; + private static instance: QueryObjectRepository; + modelId: string; + filters: QueryObjectDTO[] = []; + selected: QueryObjectDTO | undefined = undefined; + mode: Mode = Mode.CREATE; + + constructor(client: ApolloClient, modelId: string) { + this.gateway = new FiltersGraphQL(client); + this.modelId = modelId; + makeAutoObservable(this); + } + + static getInstance(client: ApolloClient, modelId: string) { + if (!QueryObjectRepository.instance) { + QueryObjectRepository.instance = new QueryObjectRepository(client, modelId); + } + return QueryObjectRepository.instance; + } + + async listFilters() { + const rawFilters = await this.gateway.list(this.modelId); + + runInAction(() => { + this.filters = rawFilters.map(filter => QueryObjectMapper.toDTO(filter)); + }); + } + + async createFilter(filter: QueryObjectDTO) { + const { id: _, ...rawFilter } = QueryObjectMapper.toRaw(filter); + const response = await this.gateway.create(rawFilter); + const filterDTO = QueryObjectMapper.toDTO(response); + + if (response) { + this.filters = [...this.filters, filterDTO]; + } + } + + async updateFilter(filter: QueryObjectDTO) { + const rawFilter = QueryObjectMapper.toRaw(filter); + const response = await this.gateway.update(rawFilter); + + if (response) { + const filterIndex = this.filters.findIndex(f => f.id === filter.id); + + if (filterIndex > -1) { + const filterDTO = QueryObjectMapper.toDTO(response); + this.filters = [ + ...this.filters.slice(0, filterIndex), + { + ...this.filters[filterIndex], + ...filterDTO + }, + ...this.filters.slice(filterIndex + 1) + ]; + } + } + } + + async deleteFilter(id: string) { + const response = await this.gateway.delete(id); + + if (response) { + this.filters = this.filters.filter(filter => filter.id !== id); + } + } + + setSelected(id?: string) { + this.selected = id ? this.filters.find(filter => filter.id === id) : undefined; + } +} diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Field.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/Field.ts similarity index 100% rename from packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Field.ts rename to packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/Field.ts diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/FieldMapper.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/FieldMapper.ts similarity index 100% rename from packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/FieldMapper.ts rename to packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/FieldMapper.ts diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/Mode.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/Mode.ts new file mode 100644 index 00000000000..2622c24843f --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/Mode.ts @@ -0,0 +1,4 @@ +export enum Mode { + CREATE = "CREATE", + UPDATE = "UPDATE" +} diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Operation.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/Operation.ts similarity index 100% rename from packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/Operation.ts rename to packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/Operation.ts diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/QueryObject.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/QueryObject.ts similarity index 80% rename from packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/QueryObject.ts rename to packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/QueryObject.ts index d516419c4f3..456eca5a280 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/QueryObject.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/QueryObject.ts @@ -2,6 +2,14 @@ import zod from "zod"; import { generateId } from "@webiny/utils"; import { Operation } from "./Operation"; +export interface QueryObjectRaw { + id: string; + name: string; + description?: string; + modelId: string; + operation: Operation; + groups: string[]; +} export interface FilterDTO { field: string; condition: string; @@ -67,15 +75,16 @@ export class QueryObject { return new QueryObject(modelId, Operation.AND, [new Group(Operation.AND, [new Filter()])]); } - static create(data: QueryObjectDTO) { - return new QueryObject( - data.modelId, - data.operation, - data.groups, - data.id, - data.name, - data.description - ); + static create(modelId: string, existing?: QueryObjectDTO) { + // Extract the properties from 'data' or use defaults + const operation = existing?.operation || Operation.AND; + const groups = existing?.groups || [new Group(Operation.AND, [new Filter()])]; + const id = existing?.id; + const name = existing?.name; + const description = existing?.description; + + // Create a new instance of QueryObject with the extracted properties + return new QueryObject(modelId, operation, groups, id, name, description); } static validate(data: QueryObjectDTO) { diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/QueryObjectMapper.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/QueryObjectMapper.ts similarity index 91% rename from packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/QueryObjectMapper.ts rename to packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/QueryObjectMapper.ts index 49e3db024c4..2b535d35b62 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryManager/domain/QueryObjectMapper.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/QueryObjectMapper.ts @@ -1,5 +1,5 @@ -import { QueryObjectDTO } from "../../QueryBuilder/domain"; -import { Group, QueryObject, QueryObjectRaw } from "./QueryObject"; +import { QueryObject, QueryObjectDTO } from "./QueryObject"; +import { Group, QueryObjectRaw } from "../domains"; export class QueryObjectMapper { static toDTO(configuration: QueryObject | QueryObjectRaw): QueryObjectDTO { diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/index.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/index.ts similarity index 86% rename from packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/index.ts rename to packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/index.ts index a0b4193a000..1805648a1b4 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/domain/index.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryObject/domains/index.ts @@ -1,5 +1,6 @@ export * from "./Field"; export * from "./FieldMapper"; +export * from "./Mode"; export * from "./Operation"; export * from "./QueryObject"; export * from "./QueryObjectMapper"; diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryManager/gateways/FiltersGraphQL.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/gateways/FiltersGraphQL.ts similarity index 99% rename from packages/app-aco/src/components/AdvancedSearch/QueryManager/gateways/FiltersGraphQL.ts rename to packages/app-aco/src/components/AdvancedSearch/QueryObject/gateways/FiltersGraphQL.ts index 939c77e105c..5429c99afbe 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryManager/gateways/FiltersGraphQL.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryObject/gateways/FiltersGraphQL.ts @@ -11,7 +11,7 @@ import { UpdateFilterResponse, UpdateFilterVariables } from "~/types"; -import { QueryObjectRaw } from "~/components/AdvancedSearch/QueryManager/domain"; +import { QueryObjectRaw } from "~/components/AdvancedSearch/QueryObject"; const ERROR_FIELD = /* GraphQL */ ` { diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryManager/gateways/index.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/gateways/index.ts similarity index 100% rename from packages/app-aco/src/components/AdvancedSearch/QueryManager/gateways/index.ts rename to packages/app-aco/src/components/AdvancedSearch/QueryObject/gateways/index.ts diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryObject/index.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/index.ts new file mode 100644 index 00000000000..b69db6f57ac --- /dev/null +++ b/packages/app-aco/src/components/AdvancedSearch/QueryObject/index.ts @@ -0,0 +1,3 @@ +export * from "./domains"; +export * from "./gateways"; +export * from "./QueryObjectRepository"; diff --git a/packages/app-aco/src/types.ts b/packages/app-aco/src/types.ts index 189881b8151..756ad457980 100644 --- a/packages/app-aco/src/types.ts +++ b/packages/app-aco/src/types.ts @@ -3,7 +3,7 @@ import { CmsModelField, CmsModelFieldSettings } from "@webiny/app-headless-cms-common/types"; -import { QueryObjectRaw } from "~/components/AdvancedSearch/QueryManager/domain"; +import { QueryObjectRaw } from "~/components/AdvancedSearch/QueryObject"; export * from "~/graphql/records/types"; From c3deb65f24da57026290fdaab4011f4074df1106 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Mon, 2 Oct 2023 15:51:01 +0200 Subject: [PATCH 041/122] wip: add runInAction --- .../QueryObject/QueryObjectRepository.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryObject/QueryObjectRepository.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/QueryObjectRepository.ts index bf2b164f621..9b0314e09fd 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryObject/QueryObjectRepository.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryObject/QueryObjectRepository.ts @@ -39,7 +39,9 @@ export class QueryObjectRepository { const filterDTO = QueryObjectMapper.toDTO(response); if (response) { - this.filters = [...this.filters, filterDTO]; + runInAction(() => { + this.filters = [filterDTO, ...this.filters]; + }); } } @@ -52,14 +54,17 @@ export class QueryObjectRepository { if (filterIndex > -1) { const filterDTO = QueryObjectMapper.toDTO(response); - this.filters = [ - ...this.filters.slice(0, filterIndex), - { - ...this.filters[filterIndex], - ...filterDTO - }, - ...this.filters.slice(filterIndex + 1) - ]; + + runInAction(() => { + this.filters = [ + ...this.filters.slice(0, filterIndex), + { + ...this.filters[filterIndex], + ...filterDTO + }, + ...this.filters.slice(filterIndex + 1) + ]; + }); } } } @@ -68,7 +73,9 @@ export class QueryObjectRepository { const response = await this.gateway.delete(id); if (response) { - this.filters = this.filters.filter(filter => filter.id !== id); + runInAction(() => { + this.filters = this.filters.filter(filter => filter.id !== id); + }); } } From c25e6214a9366eab8f3687c86f01dfca2a0c6307 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Mon, 2 Oct 2023 18:24:15 +0200 Subject: [PATCH 042/122] wip: add todo --- .../AdvancedSearch/AdvancedSearch.tsx | 3 ++ .../QueryBuilder/QueryBuilder.tsx | 2 ++ .../adapters/QueryBuilderPresenter.ts | 30 ++++++++++++++----- .../QueryBuilder/components/QueryBuilder.tsx | 4 +-- .../adapters/QueryManagerPresenter.ts | 2 ++ .../QueryObject/QueryObjectRepository.ts | 2 +- 6 files changed, 32 insertions(+), 11 deletions(-) diff --git a/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx index 562eeb674f2..b05a49966f7 100644 --- a/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx @@ -14,6 +14,8 @@ interface AdvancedSearchProps { } export const AdvancedSearch = ({ fields, modelId, onSubmit }: AdvancedSearchProps) => { + // TODO: create presenter for AdvancedSearch to handle these piece of state + // TODO: create repository to handle selected filter const client = useApolloClient(); const [repository] = useState(QueryObjectRepository.getInstance(client, modelId)); @@ -62,6 +64,7 @@ export const AdvancedSearch = ({ fields, modelId, onSubmit }: AdvancedSearchProp open={openManager} /> setOpenBuilder(false)} diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/QueryBuilder.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/QueryBuilder.tsx index ff169c380e4..f2a2956b382 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/QueryBuilder.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/QueryBuilder.tsx @@ -13,10 +13,12 @@ interface QueryBuilderProps { } export const QueryBuilder = ({ repository, fields, onForm, onSubmit }: QueryBuilderProps) => { + // TODO: Receive a queryObject -> const [presenter] = useState( new QueryBuilderPresenter(repository, fields) ); + // TODO: pass the selected id useEffect(() => { presenter.create(); }, [repository?.selected]); diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts index 8efbf3a1120..56ad14ebfdf 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/adapters/QueryBuilderPresenter.ts @@ -51,14 +51,6 @@ export class QueryBuilderPresenter implements IQueryBuilderPresenter { ); } - async persistViewModel() { - if (this.repository.mode === Mode.UPDATE) { - return await this.repository.updateFilter(this.queryObject); - } - - return await this.repository.createFilter(this.queryObject); - } - getViewModel(): QueryBuilderViewModel { return { queryObject: this.queryObject, @@ -141,6 +133,28 @@ export class QueryBuilderPresenter implements IQueryBuilderPresenter { } } + async onSave(queryObject: QueryObjectDTO, onSuccess?: () => void, onError?: () => void) { + this.formWasSubmitted = true; + const result = this.validateQueryObject(queryObject); + + console.log(""); + + if (result.success) { + await this.persistViewModel(queryObject); + onSuccess && onSuccess(); + } else { + onError && onError(); + } + } + + private async persistViewModel(queryObject: QueryObjectDTO) { + if (this.repository.mode === Mode.UPDATE) { + return await this.repository.updateFilter(queryObject); + } + + return await this.repository.createFilter(queryObject); + } + private validateQueryObject(data: QueryObjectDTO) { const validation = QueryObject.validate(data); diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/QueryBuilder.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/QueryBuilder.tsx index 9044414743a..5543eb0eddc 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/QueryBuilder.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilder/components/QueryBuilder.tsx @@ -43,12 +43,12 @@ export const QueryBuilder = observer(({ presenter, onForm, onSubmit }: QueryBuil onSubmit={onFormSubmit} invalidFields={viewModel.invalidFields} > - {() => ( + {({ data }) => ( {() => ( - diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryManager/adapters/QueryManagerPresenter.ts b/packages/app-aco/src/components/AdvancedSearch/QueryManager/adapters/QueryManagerPresenter.ts index 9322671d1d7..2ee0024845b 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryManager/adapters/QueryManagerPresenter.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryManager/adapters/QueryManagerPresenter.ts @@ -23,6 +23,7 @@ export class QueryManagerPresenter implements IQueryManagerPresenter { makeAutoObservable(this); } + // TODO: rename with load async listFilters() { await this.repository.listFilters(); } @@ -31,6 +32,7 @@ export class QueryManagerPresenter implements IQueryManagerPresenter { await this.repository.deleteFilter(id); } + // TODO: add getFilter selectFilter(id?: string) { this.repository.setSelected(id); } diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryObject/QueryObjectRepository.ts b/packages/app-aco/src/components/AdvancedSearch/QueryObject/QueryObjectRepository.ts index 9b0314e09fd..5f9ba8e4d63 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryObject/QueryObjectRepository.ts +++ b/packages/app-aco/src/components/AdvancedSearch/QueryObject/QueryObjectRepository.ts @@ -13,7 +13,7 @@ export class QueryObjectRepository { mode: Mode = Mode.CREATE; constructor(client: ApolloClient, modelId: string) { - this.gateway = new FiltersGraphQL(client); + this.gateway = new FiltersGraphQL(client); // TODO: inject the gateway this.modelId = modelId; makeAutoObservable(this); } From 89bc626cb552a9baccaed1f1fe548b8187fdd051 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Tue, 3 Oct 2023 09:06:55 +0200 Subject: [PATCH 043/122] refactor: set gateway as repository dependency --- .../components/AdvancedSearch/AdvancedSearch.tsx | 11 +++++++++-- .../QueryObject/QueryObjectRepository.ts | 13 ++++++------- .../QueryObject/gateways/BaseGateway.ts | 12 ++++++++++++ .../{FiltersGraphQL.ts => FiltersGraphQLGateway.ts} | 4 ++-- .../AdvancedSearch/QueryObject/gateways/index.ts | 3 ++- 5 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 packages/app-aco/src/components/AdvancedSearch/QueryObject/gateways/BaseGateway.ts rename packages/app-aco/src/components/AdvancedSearch/QueryObject/gateways/{FiltersGraphQL.ts => FiltersGraphQLGateway.ts} (96%) diff --git a/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx index b05a49966f7..b317c30c39d 100644 --- a/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/AdvancedSearch.tsx @@ -2,7 +2,11 @@ import React, { useCallback, useState } from "react"; import { useApolloClient } from "@apollo/react-hooks"; import { QueryManager } from "~/components/AdvancedSearch/QueryManager/QueryManager"; -import { FieldRaw, QueryObjectRepository } from "~/components/AdvancedSearch/QueryObject"; +import { + FieldRaw, + FiltersGraphQLGateway, + QueryObjectRepository +} from "~/components/AdvancedSearch/QueryObject"; import { Button } from "./Button"; import { Drawer } from "./Drawer"; @@ -17,7 +21,9 @@ export const AdvancedSearch = ({ fields, modelId, onSubmit }: AdvancedSearchProp // TODO: create presenter for AdvancedSearch to handle these piece of state // TODO: create repository to handle selected filter const client = useApolloClient(); - const [repository] = useState(QueryObjectRepository.getInstance(client, modelId)); + const [repository] = useState( + QueryObjectRepository.getInstance(new FiltersGraphQLGateway(client), modelId) + ); const [openManager, setOpenManager] = useState(false); const [openBuilder, setOpenBuilder] = useState(false); @@ -56,6 +62,7 @@ export const AdvancedSearch = ({ fields, modelId, onSubmit }: AdvancedSearchProp <> - - - - - - - - {viewModel.queryObject.groups.map((group, groupIndex) => ( - - - - - - - - - - - presenter.deleteGroup(groupIndex) - } - /> - - - - {group.filters.map((filter, filterIndex) => ( - { - presenter.emptyFilterIntoGroup( - groupIndex, - filterIndex - ); - }} - onDelete={() => { - presenter.deleteFilterFromGroup( - groupIndex, - filterIndex - ); - }} - /> - ))} - - - - - presenter.addNewFilterToGroup( - groupIndex - ) - } - /> - - - - - ))} - - - - presenter.addGroup()} /> - - - - - - )} - - )} - - ); -}); diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilder/components/index.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilder/components/index.tsx index 21c2deec4a7..55bee4f4d46 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilder/components/index.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilder/components/index.tsx @@ -1 +1,5 @@ -export * from "./QueryBuilder"; +export * from "./controls"; +export * from "./fields"; +export * from "./Filter"; +export * from "./InputField"; +export * from "./OperationSelector"; diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.styled.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.styled.tsx index 797201de70b..98ce817158a 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.styled.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.styled.tsx @@ -1,5 +1,6 @@ import styled from "@emotion/styled"; +import { SimpleFormFooter as BaseSimpleFormFooter } from "@webiny/app-admin/components/SimpleForm"; import { IconButton } from "@webiny/ui/Button"; import { Drawer as RmwcDrawer } from "@webiny/ui/Drawer"; @@ -40,3 +41,7 @@ export const DrawerContainer = styled(RmwcDrawer)` transform: translateX(20px); } `; + +export const SimpleFormFooter = styled(BaseSimpleFormFooter)` + justify-content: flex-end; +`; diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.tsx index 738542e0a6d..e4a1c4bc9a4 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { FormAPI } from "@webiny/form"; import { DrawerContent } from "@webiny/ui/Drawer"; @@ -15,13 +15,15 @@ import { } from "~/components/AdvancedSearch/QueryObject"; import { DrawerContainer } from "./QueryBuilderDrawer.styled"; +import { QueryBuilderPresenter } from "~/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilder/adapters"; interface DrawerProps { - queryObject: QueryObjectDTO | undefined; + queryObject: QueryObjectDTO | null; repository: QueryObjectRepository; open: boolean; onClose: () => void; - onSubmit: (data: any) => void; + onPersist: (data: QueryObjectDTO) => void; + onSubmit: (data: QueryObjectDTO) => void; fields: FieldRaw[]; } @@ -31,8 +33,17 @@ export const QueryBuilderDrawer = ({ open, onClose, fields, - onSubmit + onSubmit, + onPersist }: DrawerProps) => { + const [presenter] = useState( + new QueryBuilderPresenter(repository, fields) + ); + + useEffect(() => { + presenter.load(queryObject); + }, [queryObject]); + useHotkeys({ zIndex: 55, disabled: !open, @@ -48,13 +59,16 @@ export const QueryBuilderDrawer = ({
(ref.current = form)} onSubmit={onSubmit} + presenter={presenter} + /> +