From 857d73c338eba23fa07202bb34a0c26adc4718b7 Mon Sep 17 00:00:00 2001 From: Ayobami Akingbade <akingbadefred@gmail.com> Date: Sat, 11 Nov 2023 20:10:09 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20test(table-filters):=20add=20mor?= =?UTF-8?q?e=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/_/api-handlers/config.ts | 1 + src/__tests__/_/forCodeCoverage.spec.ts | 8 +- src/__tests__/admin/index.spec.tsx | 2 +- src/__tests__/admin/settings/menu.spec.tsx | 6 +- src/__tests__/admin/settings/theme.spec.tsx | 2 +- src/bin/index.ts | 2 +- src/frontend/_layouts/app/types.ts | 6 - .../components/EmptyWrapper/Stories.tsx | 5 - .../components/EmptyWrapper/index.tsx | 7 +- .../components/Form/FormSelect/Simple.tsx | 3 + .../components/Form/FormSelect/index.tsx | 3 + .../components/SortList/SortList.spec.tsx | 2 +- .../components/Table/filters/Boolean.tsx | 1 + .../components/Table/filters/IdField.tsx | 2 +- .../components/Table/filters/Number.tsx | 6 +- .../components/Table/filters/Status.tsx | 1 + .../components/Table/filters/Text.tsx | 2 +- .../Table/filters/_FilterOperator.tsx | 3 +- .../Table/filters/__tests__/index.spec.tsx | 531 ++++++++++++++++++ .../components/Table/filters/index.tsx | 31 +- .../components/Table/filters/types.ts | 2 + .../design-system/layouts/BaseLeftSideNav.tsx | 53 -- .../design-system/layouts/Navigation.tsx | 112 ---- .../design-system/layouts/sidebar.store.ts | 31 - src/frontend/design-system/layouts/types.ts | 34 -- .../hooks/auth/useAuthenticateUser.ts | 5 +- src/frontend/hooks/auth/useGuestCheck.ts | 3 +- src/frontend/hooks/auth/user.store.ts | 15 +- src/frontend/hooks/data/data.store.ts | 10 +- src/frontend/hooks/index.ts | 4 +- src/frontend/lib/data/types.ts | 6 + src/frontend/lib/data/useMutate/types.ts | 3 +- .../useApiMutateOptimisticOptions.ts | 3 - src/frontend/lib/form/types.ts | 2 + src/frontend/lib/routing/useRouteParam.ts | 12 + .../Dashboard/Widget/_render/Table/index.tsx | 19 +- src/frontend/views/data/Create/index.tsx | 14 +- .../views/data/Details/DetailsView.tsx | 7 +- src/frontend/views/data/Details/_Layout.tsx | 9 +- src/frontend/views/data/_BaseEntityForm.tsx | 8 +- .../views/data/useEntityViewStateMachine.ts | 15 +- .../views/settings/Versions/index.tsx | 2 +- 42 files changed, 673 insertions(+), 320 deletions(-) delete mode 100644 src/frontend/_layouts/app/types.ts create mode 100644 src/frontend/design-system/components/Table/filters/__tests__/index.spec.tsx delete mode 100644 src/frontend/design-system/layouts/BaseLeftSideNav.tsx delete mode 100644 src/frontend/design-system/layouts/Navigation.tsx delete mode 100644 src/frontend/design-system/layouts/sidebar.store.ts delete mode 100644 src/frontend/design-system/layouts/types.ts diff --git a/src/__tests__/_/api-handlers/config.ts b/src/__tests__/_/api-handlers/config.ts index ced3932c2..8feac6bcf 100644 --- a/src/__tests__/_/api-handlers/config.ts +++ b/src/__tests__/_/api-handlers/config.ts @@ -76,6 +76,7 @@ const CONFIG_VALUES = { primaryDark: `#111111`, }, disabled_entities: ["disabled-entity-1", "disabled-entity-2"], + menu_entities_order: [], disabled_menu_entities: ["entity-3"], }; diff --git a/src/__tests__/_/forCodeCoverage.spec.ts b/src/__tests__/_/forCodeCoverage.spec.ts index 2746b9bed..0e0f42933 100644 --- a/src/__tests__/_/forCodeCoverage.spec.ts +++ b/src/__tests__/_/forCodeCoverage.spec.ts @@ -36,9 +36,11 @@ import { FOR_CODE_COV as $36 } from "shared/types/storage"; import { FOR_CODE_COV as $37 } from "frontend/views/Dashboard/Widget/_manage/types"; import { FOR_CODE_COV as $38 } from "shared/types/dashboard/types"; import { FOR_CODE_COV as $39 } from "shared/types/dashboard/base"; -import { FOR_CODE_COV as $40 } from "frontend/design-system/layouts/types"; +import { FOR_CODE_COV as $40 } from "frontend/lib/data/useMutate/types"; import { FOR_CODE_COV as $41 } from "frontend/design-system/components/Form/_types"; import { FOR_CODE_COV as $42 } from "backend/menu/types"; +import { FOR_CODE_COV as $43 } from "frontend/lib/form/types"; +import { FOR_CODE_COV as $44 } from "frontend/design-system/components/Table/filters/types"; import { noop } from "shared/lib/noop"; @@ -83,7 +85,9 @@ noop( $39, $40, $41, - $42 + $42, + $43, + $44 ); describe("Code coverage ignores plain types file", () => { diff --git a/src/__tests__/admin/index.spec.tsx b/src/__tests__/admin/index.spec.tsx index 1e8fcaf71..7b5b60dae 100644 --- a/src/__tests__/admin/index.spec.tsx +++ b/src/__tests__/admin/index.spec.tsx @@ -69,7 +69,7 @@ describe("pages/admin", () => { expect(await getTableRows(widget)).toMatchInlineSnapshot(` [ - "nameage", + "NameAge", "John6", "Jane5", ] diff --git a/src/__tests__/admin/settings/menu.spec.tsx b/src/__tests__/admin/settings/menu.spec.tsx index 43b1638c5..0f619f3a2 100644 --- a/src/__tests__/admin/settings/menu.spec.tsx +++ b/src/__tests__/admin/settings/menu.spec.tsx @@ -8,7 +8,7 @@ import { setupApiHandlers } from "__tests__/_/setupApihandlers"; setupApiHandlers(); -describe.skip("pages/admin/settings/menu", () => { +describe("pages/admin/settings/menu", () => { beforeAll(() => { const useRouter = jest.spyOn(require("next/router"), "useRouter"); useRouter.mockImplementation(() => ({ @@ -68,12 +68,12 @@ describe.skip("pages/admin/settings/menu", () => { ); await userEvent.click( - screen.getByRole("button", { name: "Save Menu Entities Settings" }) + screen.getByRole("button", { name: "Save Menu Settings" }) ); expect( await screen.findByRole("status", {}, { timeout: 20000 }) - ).toHaveTextContent("Menu Entities Settings Saved Successfully"); + ).toHaveTextContent("Menu Settings Saved Successfully"); }); it("should display updated entities state", async () => { diff --git a/src/__tests__/admin/settings/theme.spec.tsx b/src/__tests__/admin/settings/theme.spec.tsx index f9df9c600..8b25dcbde 100644 --- a/src/__tests__/admin/settings/theme.spec.tsx +++ b/src/__tests__/admin/settings/theme.spec.tsx @@ -124,7 +124,7 @@ describe("pages/admin/settings/theme", () => { ); }); - it("should display not update the other scheme color", async () => { + it("should not display the other scheme color", async () => { render( <ApplicationRoot> <ThemeSettings /> diff --git a/src/bin/index.ts b/src/bin/index.ts index a51cb97af..582e87e21 100644 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import * as randomstring from "randomstring"; import { checkNodeVersion } from "./checkNodeVersion"; -// TODO test this compiles well + (async () => { const path = require("path"); const fs = require("fs-extra"); diff --git a/src/frontend/_layouts/app/types.ts b/src/frontend/_layouts/app/types.ts deleted file mode 100644 index 1d0f6bd8f..000000000 --- a/src/frontend/_layouts/app/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ISelectionView } from "frontend/design-system/layouts/types"; - -export interface IAppMenuItems extends ISelectionView { - isPermissionAllowed?: boolean; - order: number; -} diff --git a/src/frontend/design-system/components/EmptyWrapper/Stories.tsx b/src/frontend/design-system/components/EmptyWrapper/Stories.tsx index fb8d54eb4..48b38297c 100644 --- a/src/frontend/design-system/components/EmptyWrapper/Stories.tsx +++ b/src/frontend/design-system/components/EmptyWrapper/Stories.tsx @@ -21,11 +21,6 @@ const Template: Story<IProps> = (args) => ( export const Default = Template.bind({}); Default.args = {}; -export const NoIcon = Template.bind({}); -NoIcon.args = { - hideIcon: true, -}; - export const WithChildren = Template.bind({}); WithChildren.args = { children: ( diff --git a/src/frontend/design-system/components/EmptyWrapper/index.tsx b/src/frontend/design-system/components/EmptyWrapper/index.tsx index 95f3f4845..676429eb1 100644 --- a/src/frontend/design-system/components/EmptyWrapper/index.tsx +++ b/src/frontend/design-system/components/EmptyWrapper/index.tsx @@ -6,7 +6,6 @@ import { Typo } from "frontend/design-system/primitives/Typo"; export interface IProps { text: string; - hideIcon?: true; children?: ReactNode; } @@ -18,12 +17,10 @@ const Root = styled.div` background: ${USE_ROOT_COLOR("base-color")}; `; -export function EmptyWrapper({ text, hideIcon, children }: IProps) { +export function EmptyWrapper({ text, children }: IProps) { return ( <Root> - {hideIcon ? null : ( - <Droplet size={50} color={USE_ROOT_COLOR("muted-text")} /> - )} + <Droplet size={50} color={USE_ROOT_COLOR("muted-text")} /> <br /> <br /> <Typo.MD color="muted"> {text} </Typo.MD> diff --git a/src/frontend/design-system/components/Form/FormSelect/Simple.tsx b/src/frontend/design-system/components/Form/FormSelect/Simple.tsx index 2bcbf2fc2..0fb1e033c 100644 --- a/src/frontend/design-system/components/Form/FormSelect/Simple.tsx +++ b/src/frontend/design-system/components/Form/FormSelect/Simple.tsx @@ -10,6 +10,7 @@ interface ISimpleSelect { value: number | string; fullWidth?: boolean; sm?: true; + ariaLabel?: string; } const SimpleSelectStyled = styled(Input)<{ fullWidth?: boolean }>` @@ -33,10 +34,12 @@ export function SimpleSelect({ value, fullWidth, sm, + ariaLabel, }: ISimpleSelect) { return ( <SimpleSelectStyled as="select" + aria-label={ariaLabel} value={value} sm={sm} fullWidth={fullWidth} diff --git a/src/frontend/design-system/components/Form/FormSelect/index.tsx b/src/frontend/design-system/components/Form/FormSelect/index.tsx index 390248f30..cab132fb7 100644 --- a/src/frontend/design-system/components/Form/FormSelect/index.tsx +++ b/src/frontend/design-system/components/Form/FormSelect/index.tsx @@ -22,12 +22,14 @@ interface IFormMultiSelect { selectData: ISelectData[]; values: string[]; onChange: (values: string[]) => void; + ariaLabel?: string; } export function FormMultiSelect({ selectData, values = [], onChange, + ariaLabel, }: IFormMultiSelect) { return ( <SelectStyled @@ -41,6 +43,7 @@ export function FormMultiSelect({ onChange={(newValues: any) => { onChange(newValues.map(({ value }: ISelectData) => value)); }} + aria-label={ariaLabel} options={selectData} /> ); diff --git a/src/frontend/design-system/components/SortList/SortList.spec.tsx b/src/frontend/design-system/components/SortList/SortList.spec.tsx index 1947575d5..d2a0634b2 100644 --- a/src/frontend/design-system/components/SortList/SortList.spec.tsx +++ b/src/frontend/design-system/components/SortList/SortList.spec.tsx @@ -29,7 +29,7 @@ jest.mock("react-easy-sort", () => ({ ), })); -describe.skip("SortList", () => { +describe("SortList", () => { it("should render labels if present else value", () => { render( <SortList diff --git a/src/frontend/design-system/components/Table/filters/Boolean.tsx b/src/frontend/design-system/components/Table/filters/Boolean.tsx index f55bd0344..c056fbb7c 100644 --- a/src/frontend/design-system/components/Table/filters/Boolean.tsx +++ b/src/frontend/design-system/components/Table/filters/Boolean.tsx @@ -17,6 +17,7 @@ export function FilterTableByBooleans({ value: value === "" ? undefined : value === "true", }); }} + ariaLabel="Select Boolean" fullWidth value={ // eslint-disable-next-line no-nested-ternary diff --git a/src/frontend/design-system/components/Table/filters/IdField.tsx b/src/frontend/design-system/components/Table/filters/IdField.tsx index 2869e82a0..16c16e19c 100644 --- a/src/frontend/design-system/components/Table/filters/IdField.tsx +++ b/src/frontend/design-system/components/Table/filters/IdField.tsx @@ -12,7 +12,7 @@ export function FilterTableByIdField({ onChange={(e: React.BaseSyntheticEvent) => { setFilter({ ...filterValue, - value: e.target.value || undefined, + value: e.target.value, }); }} placeholder="Enter value" diff --git a/src/frontend/design-system/components/Table/filters/Number.tsx b/src/frontend/design-system/components/Table/filters/Number.tsx index 208055000..5ba734253 100644 --- a/src/frontend/design-system/components/Table/filters/Number.tsx +++ b/src/frontend/design-system/components/Table/filters/Number.tsx @@ -16,9 +16,10 @@ export function FilterTableByNumbers({ onChange={(e) => setFilter({ ...filterValue, - value: +e.target.value || undefined, + value: +e.target.value, }) } + aria-label="Value 1" /> {filterValue?.operator === FilterOperators.BETWEEN && ( <> @@ -30,9 +31,10 @@ export function FilterTableByNumbers({ onChange={(e) => setFilter({ ...filterValue, - value2: +e.target.value || undefined, + value2: +e.target.value, }) } + aria-label="Value 2" /> </> )} diff --git a/src/frontend/design-system/components/Table/filters/Status.tsx b/src/frontend/design-system/components/Table/filters/Status.tsx index 6835e7543..2b1cb0d1b 100644 --- a/src/frontend/design-system/components/Table/filters/Status.tsx +++ b/src/frontend/design-system/components/Table/filters/Status.tsx @@ -13,6 +13,7 @@ export function FilterTableByStatus({ <FormMultiSelect selectData={bag} values={filterValue?.value || []} + ariaLabel="Select Status" onChange={(value) => { setFilter({ ...filterValue, diff --git a/src/frontend/design-system/components/Table/filters/Text.tsx b/src/frontend/design-system/components/Table/filters/Text.tsx index fb3903484..d80262109 100644 --- a/src/frontend/design-system/components/Table/filters/Text.tsx +++ b/src/frontend/design-system/components/Table/filters/Text.tsx @@ -12,7 +12,7 @@ export function FilterTableByText({ onChange={(e: React.BaseSyntheticEvent) => { setFilter({ ...filterValue, - value: e.target.value || undefined, + value: e.target.value, }); }} placeholder="Search" diff --git a/src/frontend/design-system/components/Table/filters/_FilterOperator.tsx b/src/frontend/design-system/components/Table/filters/_FilterOperator.tsx index d293b9aad..5c6bc9b13 100644 --- a/src/frontend/design-system/components/Table/filters/_FilterOperator.tsx +++ b/src/frontend/design-system/components/Table/filters/_FilterOperator.tsx @@ -44,11 +44,12 @@ export function RenderFilterOperator<T>({ label: FILTER_OPERATOR_LABELS[operator], })), ]} + ariaLabel="Select Filter Operator" fullWidth onChange={(value) => { setFilter({ ...filterValue, - operator: (value as FilterOperators) || undefined, + operator: value as FilterOperators, }); }} value={filterValue?.operator || ""} diff --git a/src/frontend/design-system/components/Table/filters/__tests__/index.spec.tsx b/src/frontend/design-system/components/Table/filters/__tests__/index.spec.tsx new file mode 100644 index 000000000..1b1d996f8 --- /dev/null +++ b/src/frontend/design-system/components/Table/filters/__tests__/index.spec.tsx @@ -0,0 +1,531 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { useState } from "react"; +import { FilterOperators, IColumnFilterBag } from "shared/types/data"; +import { TableFilter } from ".."; +import { TableFilterType } from "../types"; + +const setFilterValueJestFn = jest.fn(); + +function TestComponent({ + type, + defaultValue = {}, +}: { + defaultValue?: IColumnFilterBag<any>; + type: TableFilterType; +}) { + const [state, setState] = useState(defaultValue); + return ( + <TableFilter + type={type} + column={{ + setFilterValue: (value) => { + setState(value); + setFilterValueJestFn(value); + }, + getFilterValue: () => state, + }} + view="Test Column" + debounce={100} + /> + ); +} + +describe("Table Filters", () => { + describe("Strings", () => { + const type = { _type: "string", bag: undefined } as const; + + it("should have the correct options", async () => { + render(<TestComponent type={type} />); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Search") + ); + + expect(screen.getAllByRole("option").map((option) => option.textContent)) + .toMatchInlineSnapshot(` + [ + "Contains", + "Equal To", + "Not Equal To", + ] + `); + }); + + it("should filter by value correctly", async () => { + render(<TestComponent type={type} />); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Search") + ); + + await userEvent.type(screen.getByPlaceholderText("Search"), "Hello"); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "c", + value: "Hello", + }); + }); + + await userEvent.selectOptions( + screen.getByRole("combobox", { name: "Select Filter Operator" }), + "Equal To" + ); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "e", + value: "Hello", + }); + }); + }); + + it("should render default value correctly", async () => { + render( + <TestComponent + defaultValue={{ + operator: FilterOperators.NOT_EQUAL, + value: "Default Value", + }} + type={type} + /> + ); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Search Is Active") + ); + + expect(screen.getByPlaceholderText("Search")).toHaveValue( + "Default Value" + ); + expect( + screen.getByRole("combobox", { name: "Select Filter Operator" }) + ).toHaveValue("n"); + + await userEvent.type(screen.getByPlaceholderText("Search"), " Updated"); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "n", + value: "Default Value Updated", + }); + }); + }); + }); + + describe("Numbers", () => { + const type = { _type: "number", bag: undefined } as const; + + it("should have the correct options", async () => { + render(<TestComponent type={type} />); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Number") + ); + + expect(screen.getAllByRole("option").map((option) => option.textContent)) + .toMatchInlineSnapshot(` + [ + "Equal To", + "Not Equal To", + "Between", + "Greater Than", + "Less Than", + ] + `); + }); + + it("should filter by value correctly", async () => { + render(<TestComponent type={type} />); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Number") + ); + + await userEvent.type( + screen.getByRole("spinbutton", { name: "Value 1" }), + "123" + ); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "e", + value: 123, + }); + }); + + await userEvent.selectOptions( + screen.getByRole("combobox", { name: "Select Filter Operator" }), + "Between" + ); + + await userEvent.type( + screen.getByRole("spinbutton", { name: "Value 2" }), + "456" + ); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "b", + value: 123, + value2: 456, + }); + }); + }); + + it("should render default value correctly", async () => { + render( + <TestComponent + defaultValue={{ + operator: FilterOperators.BETWEEN, + value: "56", + value2: "78", + }} + type={type} + /> + ); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Number Is Active") + ); + + expect(screen.getByRole("spinbutton", { name: "Value 1" })).toHaveValue( + 56 + ); + + expect(screen.getByRole("spinbutton", { name: "Value 2" })).toHaveValue( + 78 + ); + + expect( + screen.getByRole("combobox", { name: "Select Filter Operator" }) + ).toHaveValue("b"); + + await userEvent.type( + screen.getByRole("spinbutton", { name: "Value 1" }), + "1" + ); + await userEvent.type( + screen.getByRole("spinbutton", { name: "Value 2" }), + "2" + ); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "b", + value: 561, + value2: 782, + }); + }); + }); + }); + + describe("Id", () => { + const type = { _type: "idField", bag: undefined } as const; + + it("should have the correct options", async () => { + render(<TestComponent type={type} />); + + await userEvent.click(screen.getByLabelText("Filter Test Column By Id")); + + expect( + screen + .getAllByRole("option", { hidden: true }) + .map((option) => option.textContent) + ).toMatchInlineSnapshot(` + [ + "Equal To", + ] + `); + }); + + it("should filter by value correctly", async () => { + render(<TestComponent type={type} />); + + await userEvent.click(screen.getByLabelText("Filter Test Column By Id")); + + await userEvent.type(screen.getByPlaceholderText("Enter value"), "12345"); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "e", + value: "12345", + }); + }); + + expect( + screen.queryByRole("combobox", { name: "Select Filter Operator" }) + ).not.toBeInTheDocument(); + }); + + it("should render default value correctly", async () => { + render( + <TestComponent + defaultValue={{ + operator: FilterOperators.EQUAL_TO, + value: "789", + }} + type={type} + /> + ); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Id Is Active") + ); + + expect(screen.getByPlaceholderText("Enter value")).toHaveValue("789"); + + await userEvent.type(screen.getByPlaceholderText("Enter value"), "0"); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "e", + value: "7890", + }); + }); + }); + }); + + describe("Boolean", () => { + const type: TableFilterType = { + _type: "boolean", + bag: [ + { + label: "True Option", + value: true, + }, + { + label: "False Option", + value: false, + }, + ], + }; + + it("should have the correct options", async () => { + render(<TestComponent type={type} />); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Boolean") + ); + + expect(screen.getAllByRole("option").map((option) => option.textContent)) + .toMatchInlineSnapshot(` + [ + "-- Select State --", + "True Option", + "False Option", + ] + `); + }); + + it("should filter by value correctly", async () => { + render(<TestComponent type={type} />); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Boolean") + ); + + await userEvent.selectOptions( + screen.getByRole("combobox", { name: "Select Boolean" }), + "True Option" + ); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "e", + value: true, + }); + }); + + await userEvent.selectOptions( + screen.getByRole("combobox", { name: "Select Boolean" }), + "False Option" + ); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "e", + value: false, + }); + }); + + await userEvent.selectOptions( + screen.getByRole("combobox", { name: "Select Boolean" }), + "-- Select State --" + ); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "e", + }); + }); + + expect( + screen.queryByRole("combobox", { name: "Select Filter Operator" }) + ).not.toBeInTheDocument(); + }); + + it("should render default value correctly", async () => { + render( + <TestComponent + defaultValue={{ + operator: FilterOperators.EQUAL_TO, + value: false, + }} + type={type} + /> + ); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Boolean Is Active") + ); + + expect( + screen.getByRole("combobox", { name: "Select Boolean" }) + ).toHaveValue("false"); + + await userEvent.selectOptions( + screen.getByRole("combobox", { name: "Select Boolean" }), + "True Option" + ); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "e", + value: true, + }); + }); + }); + }); + + describe("Status", () => { + const type: TableFilterType = { + _type: "status", + bag: [ + { + label: "Option 1 Label", + value: "option-1", + }, + { + label: "Option 2 Label", + value: "option-2", + }, + { + label: "Option 3 Label", + value: "option-3", + }, + ], + }; + + it("should have the correct options", async () => { + render(<TestComponent type={type} />); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Status") + ); + + expect(screen.getAllByRole("option").map((option) => option.textContent)) + .toMatchInlineSnapshot(` + [ + "In", + "Not In", + ] + `); + }); + + it("should filter by value correctly", async () => { + render(<TestComponent type={type} />); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Status") + ); + + await userEvent.type( + await screen.findByLabelText("Select Status"), + "Option 1 Label" + ); + + await userEvent.keyboard("{Enter}"); + + await userEvent.type( + await screen.findByLabelText("Select Status"), + "Option 2 Label" + ); + + await userEvent.keyboard("{Enter}"); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "i", + value: ["option-1", "option-2"], + }); + }); + + await userEvent.selectOptions( + screen.getByRole("combobox", { name: "Select Filter Operator" }), + "Not In" + ); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "t", + value: ["option-1", "option-2"], + }); + }); + }); + + it("should render default value correctly", async () => { + render( + <TestComponent + defaultValue={{ + operator: FilterOperators.NOT_IN, + value: ["option-3"], + }} + type={type} + /> + ); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Status Is Active") + ); + + await userEvent.type( + await screen.findByLabelText("Select Status"), + "Option 1 Label" + ); + + await userEvent.keyboard("{Enter}"); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "t", + value: ["option-3", "option-1"], + }); + }); + }); + }); + + it("should clear filters", async () => { + render(<TestComponent type={{ _type: "string", bag: undefined }} />); + + await userEvent.click( + screen.getByLabelText("Filter Test Column By Search") + ); + + await userEvent.type(screen.getByPlaceholderText("Search"), "Hello"); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "c", + value: "Hello", + }); + }); + + await userEvent.click(screen.getByRole("button", { name: "Reset" })); + + await waitFor(() => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith(undefined); + }); + }); +}); diff --git a/src/frontend/design-system/components/Table/filters/index.tsx b/src/frontend/design-system/components/Table/filters/index.tsx index caf7aa803..7591f0da1 100644 --- a/src/frontend/design-system/components/Table/filters/index.tsx +++ b/src/frontend/design-system/components/Table/filters/index.tsx @@ -11,15 +11,36 @@ const FILTER_DEBOUNCE_WAIT = 500; interface IProps { type: TableFilterType; - column: Column<Record<string, unknown>, unknown>; - view?: React.ReactNode; + column: Pick< + Column<Record<string, unknown>, unknown>, + "setFilterValue" | "getFilterValue" + >; + view?: React.ReactNode | string; + debounce?: number; } -export function TableFilter({ type, column, view }: IProps) { +export function TableFilter({ + type, + column, + view, + debounce = FILTER_DEBOUNCE_WAIT, +}: IProps) { const filterValue = column.getFilterValue() as IColumnFilterBag<any>; const setFilter = (value?: IColumnFilterBag<unknown>) => { - return column.setFilterValue(value); + if (value === undefined) { + column.setFilterValue(undefined); + return; + } + + return column.setFilterValue( + Object.fromEntries( + Object.entries(value).filter( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ([_, value$1]) => value$1 || typeof value$1 === "boolean" + ) + ) + ); }; const { filterHasValueImpl, operators, FilterComponent } = @@ -37,7 +58,7 @@ export function TableFilter({ type, column, view }: IProps) { () => { setFilter(localValue); }, - FILTER_DEBOUNCE_WAIT, + debounce, [localValue] ); diff --git a/src/frontend/design-system/components/Table/filters/types.ts b/src/frontend/design-system/components/Table/filters/types.ts index d2849e37b..7ae6f0bdb 100644 --- a/src/frontend/design-system/components/Table/filters/types.ts +++ b/src/frontend/design-system/components/Table/filters/types.ts @@ -19,3 +19,5 @@ export interface IFilterProps<T, K> { }; bag: K; } + +export const FOR_CODE_COV = 1; diff --git a/src/frontend/design-system/layouts/BaseLeftSideNav.tsx b/src/frontend/design-system/layouts/BaseLeftSideNav.tsx deleted file mode 100644 index 66eeb828c..000000000 --- a/src/frontend/design-system/layouts/BaseLeftSideNav.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import Link from "next/link"; -import React, { ReactNode } from "react"; -import styled from "styled-components"; -import { useThemeColorShade } from "../theme/useTheme"; - -const Logo = styled(Link)` - line-height: 52px; -`; - -const LogoSm = styled.img` - width: 28px; - margin-top: 16px; -`; - -const MenuContent = styled.div` - height: 100%; - padding-bottom: 30px; -`; - -const Brand = styled.div` - text-align: center; - height: 52px; - margin-top: 8px; -`; - -const Root = styled.div` - min-height: 100vh; - transition: 0.3s; - position: fixed; - bottom: 0; - top: 0; -`; - -interface IProps { - logo: string; - children: ReactNode; -} - -export function BaseLeftSideNav({ children, logo }: IProps) { - const colorShade = useThemeColorShade(); - return ( - <Root style={{ background: colorShade("primary-color", 30) }}> - <Brand> - <Logo href="/"> - <span> - <LogoSm src={logo} alt="small logo" /> - </span> - </Logo> - </Brand> - <MenuContent>{children}</MenuContent> - </Root> - ); -} diff --git a/src/frontend/design-system/layouts/Navigation.tsx b/src/frontend/design-system/layouts/Navigation.tsx deleted file mode 100644 index e7dfed7db..000000000 --- a/src/frontend/design-system/layouts/Navigation.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from "react"; -import styled, { css } from "styled-components"; -import Link from "next/link"; -import { USE_ROOT_COLOR } from "frontend/design-system/theme/root"; -import { ISelectionView } from "./types"; -import { PlainButton } from "../components/Button/TextButton"; - -interface IRenderNavigation { - navigation: Array<ISelectionView & { sideBarAction: () => void }>; - currentTitle: string; -} - -const LeftSideNavMenuList = styled.li<{ $isActive: boolean }>` - list-style: none; - display: block; - width: 100%; - padding: 6px 12px; - border-left: 2px solid transparent; - - ${(props) => - props.$isActive && - css` - border-color: ${USE_ROOT_COLOR("text-on-primary")}; - `} -`; - -const LeftSideNavMenuListAnchor = styled.a` - display: flex; - align-items: center; - width: 100%; - outline: none !important; - padding: 4px 0px; - font-size: 13px; - - transition: all 0.3s ease-out; - font-weight: 400; - background: inherit; - border: 0; - margin: 0; - - &:hover { - color: ${USE_ROOT_COLOR("primary-color")}; - } -`; - -const MenuIcon = styled.span<{ - $isActive?: boolean; -}>` - color: ${USE_ROOT_COLOR("text-on-primary")}; - margin-right: 0; - width: 28px; - height: 28px; - stroke-width: ${(props) => (props.$isActive ? 2 : 1)}px; - align-self: center; - display: inline-block; -`; - -const LeftSideNavMenu = styled.ul` - padding-left: 0; - margin-bottom: 0; - - hr:first-child { - display: none; - } -`; - -export function RenderNavigation({ - navigation, - currentTitle, -}: IRenderNavigation) { - return ( - <LeftSideNavMenu> - {navigation.map( - ({ title, icon, action, sideBarAction, secondaryAction }) => { - const isActive = currentTitle === title; - const content = <MenuIcon as={icon} $isActive={isActive} />; - return ( - <LeftSideNavMenuList - key={title} - $isActive={isActive} - className="brand-tooltip" - > - {typeof action === "string" ? ( - <Link href={action || ""} passHref> - <LeftSideNavMenuListAnchor - onClick={() => { - sideBarAction(); - secondaryAction?.(); - }} - > - {content} - </LeftSideNavMenuListAnchor> - </Link> - ) : ( - <LeftSideNavMenuListAnchor - as={PlainButton} - onClick={() => { - action?.(); - sideBarAction(); - secondaryAction?.(); - }} - > - {content} - </LeftSideNavMenuListAnchor> - )} - </LeftSideNavMenuList> - ); - } - )} - </LeftSideNavMenu> - ); -} diff --git a/src/frontend/design-system/layouts/sidebar.store.ts b/src/frontend/design-system/layouts/sidebar.store.ts deleted file mode 100644 index 4eb51ecfa..000000000 --- a/src/frontend/design-system/layouts/sidebar.store.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createStore } from "frontend/lib/store"; - -type IStore = { - currentMiniSideBar?: string; - currentTitle: string; - isFullSideBarOpen: boolean; - closeFullSideBar: () => void; - selectMiniSideBar: (selection: string) => void; - setCurrentTitle: (title: string) => void; -}; - -export const useSideBarStore = createStore<IStore>((set) => ({ - currentMiniSideBar: undefined, - isFullSideBarOpen: false, - currentTitle: "", - setCurrentTitle: (currentTitle) => - set(() => ({ - currentTitle, - })), - closeFullSideBar: () => - set(() => ({ - currentMiniSideBar: undefined, - isFullSideBarOpen: false, - })), - selectMiniSideBar: (selection: string) => - set(({ currentMiniSideBar, isFullSideBarOpen }) => ({ - currentMiniSideBar: selection, - isFullSideBarOpen: - currentMiniSideBar === selection ? !isFullSideBarOpen : true, - })), -})); diff --git a/src/frontend/design-system/layouts/types.ts b/src/frontend/design-system/layouts/types.ts deleted file mode 100644 index fa2229f42..000000000 --- a/src/frontend/design-system/layouts/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ReactNode } from "react"; -import { Icon } from "react-feather"; -import { DataStateKeys } from "frontend/lib/data/types"; -import { ButtonIconTypes } from "../components/Button/constants"; - -export interface IViewMenuItem { - value: string; - secondaryAction?: () => void; - action: string | (() => void); -} - -export interface IViewMenuItems { - menuItems: DataStateKeys<IViewMenuItem[]>; - getLabel: (value: string) => string; - singular?: string; - newItemLink?: string; - topAction?: { - title: string; - action: string | (() => void); - }; -} - -export interface ISelectionView { - title: string; - icon: Icon; - action?: string | (() => void); - secondaryAction?: () => void; - view?: ReactNode; - viewMenuItems?: IViewMenuItems; - description?: string; - iconButtons?: { action: () => void; icon: ButtonIconTypes }[]; -} - -export const FOR_CODE_COV = 1; diff --git a/src/frontend/hooks/auth/useAuthenticateUser.ts b/src/frontend/hooks/auth/useAuthenticateUser.ts index b22203429..5aeade9c0 100644 --- a/src/frontend/hooks/auth/useAuthenticateUser.ts +++ b/src/frontend/hooks/auth/useAuthenticateUser.ts @@ -4,15 +4,16 @@ import { useRouter } from "next/router"; import { useEffect } from "react"; import { createStore } from "frontend/lib/store"; import { STORAGE_CONSTANTS } from "frontend/lib/storage/constants"; +import { DataStates } from "frontend/lib/data/types"; import * as AuthStore from "./auth.store"; type IStore = { - isAuthenticated: "loading" | boolean; + isAuthenticated: DataStates.Loading | boolean; setIsAuthenticated: (state: boolean) => void; }; export const useIsAuthenticatedStore = createStore<IStore>((set) => ({ - isAuthenticated: "loading", + isAuthenticated: DataStates.Loading, setIsAuthenticated: (state: boolean) => set(() => ({ isAuthenticated: state, diff --git a/src/frontend/hooks/auth/useGuestCheck.ts b/src/frontend/hooks/auth/useGuestCheck.ts index 282dd16be..baee84356 100644 --- a/src/frontend/hooks/auth/useGuestCheck.ts +++ b/src/frontend/hooks/auth/useGuestCheck.ts @@ -3,6 +3,7 @@ import { useEffect } from "react"; import { NAVIGATION_LINKS } from "frontend/lib/routing/links"; import { TemporayStorageService } from "frontend/lib/storage"; import { STORAGE_CONSTANTS } from "frontend/lib/storage/constants"; +import { DataStates } from "frontend/lib/data/types"; import { useUserAuthenticatedState } from "./useAuthenticateUser"; export const useGuestCheck = () => { @@ -17,5 +18,5 @@ export const useGuestCheck = () => { } }, [typeof window, userAuthenticatedState]); - return userAuthenticatedState === "loading"; + return userAuthenticatedState === DataStates.Loading; }; diff --git a/src/frontend/hooks/auth/user.store.ts b/src/frontend/hooks/auth/user.store.ts index ddcc14a15..c1b16c685 100644 --- a/src/frontend/hooks/auth/user.store.ts +++ b/src/frontend/hooks/auth/user.store.ts @@ -5,6 +5,7 @@ import { canRoleDoThisSync } from "shared/logic/permissions"; import { useCallback } from "react"; import { useStorageApi } from "frontend/lib/data/useApi"; import { ToastService } from "frontend/lib/toast"; +import { DataStates } from "frontend/lib/data/types"; import { useIsAuthenticatedStore } from "./useAuthenticateUser"; import { ACCOUNT_PROFILE_CRUD_CONFIG } from "./constants"; import { useIsGranularCheck } from "./portal"; @@ -56,7 +57,7 @@ const doPermissionCheck = ( isGranularCheck: boolean ) => { if (isLoadingUser || !userData) { - return "loading"; + return DataStates.Loading; } const { role, permissions } = userData; @@ -87,12 +88,14 @@ export function useUserHasPermission(): (permision: string) => boolean { ); } -function useUserPermission(): (permision: string) => boolean | "loading" { +function useUserPermission(): ( + permision: string +) => boolean | DataStates.Loading { const userProfile = useAuthenticatedUserBag(); const isGranularCheck = useIsGranularCheck(); return useCallback( - (permission: string): boolean | "loading" => { + (permission: string): boolean | DataStates.Loading => { return doPermissionCheck( permission, userProfile.isLoading, @@ -106,11 +109,11 @@ function useUserPermission(): (permision: string) => boolean | "loading" { export function usePageRequiresPermission( permission: string -): "loading" | void { +): DataStates.Loading | void { const router = useRouter(); const canUser = useUserPermission(); - if (canUser(permission) === "loading") { - return "loading"; + if (canUser(permission) === DataStates.Loading) { + return DataStates.Loading; } if (!canUser(permission)) { ToastService.error("You dont have the permission to view this page"); diff --git a/src/frontend/hooks/data/data.store.ts b/src/frontend/hooks/data/data.store.ts index bf00f41b3..4ef55a8ba 100644 --- a/src/frontend/hooks/data/data.store.ts +++ b/src/frontend/hooks/data/data.store.ts @@ -9,6 +9,7 @@ import { useWaitForResponseMutationOptions } from "frontend/lib/data/useMutate/u import { SLUG_LOADING_VALUE } from "frontend/lib/routing/constants"; import { useApiQueries } from "frontend/lib/data/useApi/useApiQueries"; import { NAVIGATION_LINKS } from "frontend/lib/routing/links"; +import { DataStates } from "frontend/lib/data/types"; import { useEntityCrudConfig } from "../entity/entity.config"; import { useMultipleEntityReferenceFields } from "../entity/entity.store"; import { isRouterParamEnabled } from ".."; @@ -67,13 +68,16 @@ const buildFilterCountQueryString = ( export const useEntityFilterCount = ( entity: string, - filters: FieldQueryFilter[] | "loading" + filters: FieldQueryFilter[] | DataStates.Loading ) => { return useApi<{ count: number }>( - buildFilterCountQueryString(entity, filters === "loading" ? [] : filters), + buildFilterCountQueryString( + entity, + filters === DataStates.Loading ? [] : filters + ), { errorMessage: CRUD_CONFIG_NOT_FOUND(`${entity} count`), - enabled: filters !== "loading", + enabled: filters !== DataStates.Loading, defaultData: { count: 0 }, } ); diff --git a/src/frontend/hooks/index.ts b/src/frontend/hooks/index.ts index 70af8e5cf..852f8d76e 100644 --- a/src/frontend/hooks/index.ts +++ b/src/frontend/hooks/index.ts @@ -1,2 +1,4 @@ +import { SLUG_LOADING_VALUE } from "frontend/lib/routing/constants"; + export const isRouterParamEnabled = (entity: string): boolean => - !!entity && entity !== "loading"; + !!entity && entity !== SLUG_LOADING_VALUE; diff --git a/src/frontend/lib/data/types.ts b/src/frontend/lib/data/types.ts index d8f1990de..5ec7964c5 100644 --- a/src/frontend/lib/data/types.ts +++ b/src/frontend/lib/data/types.ts @@ -1,5 +1,11 @@ import { UseQueryResult } from "react-query"; +export enum DataStates { + Loading = "loading", + Error = "error", + Loaded = "loaded", +} + export type DataStateKeys<T> = Pick< UseQueryResult<T>, "data" | "isLoading" | "isRefetching" | "error" diff --git a/src/frontend/lib/data/useMutate/types.ts b/src/frontend/lib/data/useMutate/types.ts index 983c97608..dea4fbc49 100644 --- a/src/frontend/lib/data/useMutate/types.ts +++ b/src/frontend/lib/data/useMutate/types.ts @@ -5,7 +5,6 @@ type ToastMessageInput = export interface IApiMutateOptions<T, K, V> { dataQueryPath: string; otherEndpoints?: string[]; - isOnMockingMode?: true; onMutate: (oldData: T | undefined, form: K) => T; successMessage?: ToastMessageInput; smartSuccessMessage?: (formData: V) => ToastMessageInput; @@ -19,3 +18,5 @@ export interface IWaitForResponseMutationOptions<T> { successMessage?: ToastMessageInput; smartSuccessMessage?: (formData: T) => ToastMessageInput; } + +export const FOR_CODE_COV = 1; diff --git a/src/frontend/lib/data/useMutate/useApiMutateOptimisticOptions.ts b/src/frontend/lib/data/useMutate/useApiMutateOptimisticOptions.ts index 568040522..60777e77f 100644 --- a/src/frontend/lib/data/useMutate/useApiMutateOptimisticOptions.ts +++ b/src/frontend/lib/data/useMutate/useApiMutateOptimisticOptions.ts @@ -42,9 +42,6 @@ export function useApiMutateOptimisticOptions<T, K, V = void>( ); }, onSettled: () => { - if (options.isOnMockingMode) { - return; - } apiMutate.invalidate(); }, }; diff --git a/src/frontend/lib/form/types.ts b/src/frontend/lib/form/types.ts index d0b3cf81e..22a447db5 100644 --- a/src/frontend/lib/form/types.ts +++ b/src/frontend/lib/form/types.ts @@ -2,3 +2,5 @@ export interface IFormProps<T> { onSubmit: (arg0: T) => Promise<void>; initialValues?: Partial<T>; } + +export const FOR_CODE_COV = 1; diff --git a/src/frontend/lib/routing/useRouteParam.ts b/src/frontend/lib/routing/useRouteParam.ts index 481f959dd..0cb89041e 100644 --- a/src/frontend/lib/routing/useRouteParam.ts +++ b/src/frontend/lib/routing/useRouteParam.ts @@ -14,3 +14,15 @@ export function useRouteParam(name: string) { throw new Error("Unexpected handle given by Next.js"); return value; } + +export function useRouteParams() { + const router = useRouter(); + + if (typeof window === "undefined") return {}; + + const value = router.query; + + if (Array.isArray(value)) + throw new Error("Unexpected handle given by Next.js"); + return value; +} diff --git a/src/frontend/views/Dashboard/Widget/_render/Table/index.tsx b/src/frontend/views/Dashboard/Widget/_render/Table/index.tsx index 313268c39..d0241d03b 100644 --- a/src/frontend/views/Dashboard/Widget/_render/Table/index.tsx +++ b/src/frontend/views/Dashboard/Widget/_render/Table/index.tsx @@ -1,29 +1,28 @@ import { ITableColumn } from "frontend/design-system/components/Table/types"; import { Table } from "frontend/design-system/components/Table"; +import { userFriendlyCase } from "shared/lib/strings/friendly-case"; import { TableWidgetSchema } from "./types"; interface IProps { data: unknown; } export function TableWidget({ data }: IProps) { - const tableChartData = TableWidgetSchema.parse(data); + const tableData = TableWidgetSchema.parse(data); - const columns: ITableColumn[] = Object.keys(tableChartData[0]).map( - (column) => ({ - Header: column, - accessor: column, - disableSortBy: true, - }) - ); + const columns: ITableColumn[] = Object.keys(tableData[0]).map((column) => ({ + Header: userFriendlyCase(column), + accessor: column, + disableSortBy: true, + })); return ( <Table tableData={{ data: { - data: tableChartData, + data: tableData, pageIndex: 0, pageSize: 5, - totalRecords: tableChartData.length, + totalRecords: tableData.length, }, error: "", isLoading: false, diff --git a/src/frontend/views/data/Create/index.tsx b/src/frontend/views/data/Create/index.tsx index 1c92db1b2..888b747e3 100644 --- a/src/frontend/views/data/Create/index.tsx +++ b/src/frontend/views/data/Create/index.tsx @@ -3,7 +3,6 @@ import { SectionBox } from "frontend/design-system/components/Section/SectionBox import { useSetPageDetails } from "frontend/lib/routing/usePageDetails"; import { useNavigationStack } from "frontend/lib/routing/useNavigationStack"; import { META_USER_PERMISSIONS } from "shared/constants/user"; -import { useRouter } from "next/router"; import { AppLayout } from "frontend/_layouts/app"; import { useEntityCrudConfig, @@ -11,24 +10,13 @@ import { useHiddenEntityColumns, } from "frontend/hooks/entity/entity.config"; import { useEntityDataCreationMutation } from "frontend/hooks/data/data.store"; +import { useRouteParams } from "frontend/lib/routing/useRouteParam"; import { EntityActionTypes, useEntityActionMenuItems, } from "../../entity/constants"; import { BaseEntityForm } from "../_BaseEntityForm"; -export function useRouteParams() { - const router = useRouter(); - - if (typeof window === "undefined") return {}; - - const value = router.query; - - if (Array.isArray(value)) - throw new Error("Unexpected handle given by Next.js"); - return value; -} - export function EntityCreate() { const routeParams = useRouteParams(); const entity = useEntitySlug(); diff --git a/src/frontend/views/data/Details/DetailsView.tsx b/src/frontend/views/data/Details/DetailsView.tsx index 1911c1f28..591f9c190 100644 --- a/src/frontend/views/data/Details/DetailsView.tsx +++ b/src/frontend/views/data/Details/DetailsView.tsx @@ -19,6 +19,7 @@ import { useEntityFields, useEntityToOneReferenceFields, } from "frontend/hooks/entity/entity.store"; +import { DataStates } from "frontend/lib/data/types"; import { filterOutHiddenScalarColumns } from "../utils"; import { useEntityViewStateMachine } from "../useEntityViewStateMachine"; import { viewSpecialDataTypes } from "../viewSpecialDataTypes"; @@ -77,8 +78,10 @@ export function EntityDetailsView({ return ( <ViewStateMachine - loading={viewState.type === "loading" || !id} - error={viewState.type === "error" ? viewState.message : undefined} + loading={viewState.type === DataStates.Loading || !id} + error={ + viewState.type === DataStates.Error ? viewState.message : undefined + } loader={ <> {Array.from({ length: 7 }, (_, k) => k).map((key) => ( diff --git a/src/frontend/views/data/Details/_Layout.tsx b/src/frontend/views/data/Details/_Layout.tsx index 8bf60f6d9..ae570db6a 100644 --- a/src/frontend/views/data/Details/_Layout.tsx +++ b/src/frontend/views/data/Details/_Layout.tsx @@ -15,6 +15,7 @@ import { RenderList } from "frontend/design-system/components/RenderList"; import { ListSkeleton } from "frontend/design-system/components/Skeleton/List"; import { SectionListItem } from "frontend/design-system/components/Section/SectionList"; import { IDropDownMenuItem } from "frontend/design-system/components/DropdownMenu"; +import { DataStates } from "frontend/lib/data/types"; import { useEntityViewStateMachine } from "../useEntityViewStateMachine"; import { getEntitiesRelationsCount } from "./utils"; import { @@ -119,8 +120,12 @@ export function DetailsLayout({ <ContentLayout.Left> <SectionBox headLess title=""> <ViewStateMachine - loading={viewState.type === "loading"} - error={viewState.type === "error" ? viewState.message : undefined} + loading={viewState.type === DataStates.Loading} + error={ + viewState.type === DataStates.Error + ? viewState.message + : undefined + } loader={<ListSkeleton count={5} />} > <RenderList diff --git a/src/frontend/views/data/_BaseEntityForm.tsx b/src/frontend/views/data/_BaseEntityForm.tsx index 83abe4f68..800fbf336 100644 --- a/src/frontend/views/data/_BaseEntityForm.tsx +++ b/src/frontend/views/data/_BaseEntityForm.tsx @@ -18,7 +18,7 @@ import { import { IFormExtension } from "frontend/components/SchemaForm/types"; import { useMemo } from "react"; import { ViewStateMachine } from "frontend/components/ViewStateMachine"; -import { DataStateKeys } from "frontend/lib/data/types"; +import { DataStateKeys, DataStates } from "frontend/lib/data/types"; import { SLUG_LOADING_VALUE } from "frontend/lib/routing/constants"; import { buildAppliedSchemaFormConfig } from "./buildAppliedSchemaFormConfig"; import { useEntityViewStateMachine } from "./useEntityViewStateMachine"; @@ -111,8 +111,10 @@ export function BaseEntityForm({ return ( <ViewStateMachine - loading={viewState.type === "loading"} - error={viewState.type === "error" ? viewState.message : undefined} + loading={viewState.type === DataStates.Loading} + error={ + viewState.type === DataStates.Error ? viewState.message : undefined + } loader={ <FormSkeleton schema={[ diff --git a/src/frontend/views/data/useEntityViewStateMachine.ts b/src/frontend/views/data/useEntityViewStateMachine.ts index d97ee7e9c..fbb2994a2 100644 --- a/src/frontend/views/data/useEntityViewStateMachine.ts +++ b/src/frontend/views/data/useEntityViewStateMachine.ts @@ -3,6 +3,7 @@ import { useAppConfiguration } from "frontend/hooks/configuration/configuration. import { useEntitySlug } from "frontend/hooks/entity/entity.config"; import { IEntityCrudSettings } from "shared/configurations"; import { userFriendlyCase } from "shared/lib/strings/friendly-case"; +import { DataStates } from "frontend/lib/data/types"; import { useCanUserPerformCrudAction } from "./useCanUserPerformCrudAction"; export const useEntityViewStateMachine = ( @@ -11,29 +12,29 @@ export const useEntityViewStateMachine = ( actionKey: keyof IEntityCrudSettings, entityOverride?: string ): - | { type: "loading" } - | { type: "render" } - | { type: "error"; message: unknown } => { + | { type: DataStates.Loading } + | { type: DataStates.Loaded } + | { type: DataStates.Error; message: unknown } => { const entitiesToHide = useAppConfiguration<string[]>("disabled_entities"); const entity = useEntitySlug(entityOverride); const canUserPerformCrudAction = useCanUserPerformCrudAction(entity); if (isLoading || entitiesToHide.isLoading || !isRouterParamEnabled(entity)) { - return { type: "loading" }; + return { type: DataStates.Loading }; } if ( (actionKey && !canUserPerformCrudAction(actionKey)) || entitiesToHide.data?.includes(entity) ) { return { - type: "error", + type: DataStates.Error, message: `The '${userFriendlyCase( actionKey )}' Action For This Resource Is Not Available`, }; } if (error) { - return { type: "error", message: error }; + return { type: DataStates.Error, message: error }; } - return { type: "render" }; + return { type: DataStates.Loaded }; }; diff --git a/src/frontend/views/settings/Versions/index.tsx b/src/frontend/views/settings/Versions/index.tsx index 87194c48f..779ead921 100644 --- a/src/frontend/views/settings/Versions/index.tsx +++ b/src/frontend/views/settings/Versions/index.tsx @@ -11,7 +11,7 @@ import { Spacer } from "frontend/design-system/primitives/Spacer"; import { SETTINGS_VIEW_KEY } from "../constants"; import { BaseSettingsLayout } from "../_Base"; -export const SYSTEM_INFORMATION_CRUD_CONFIG = MAKE_CRUD_CONFIG({ +const SYSTEM_INFORMATION_CRUD_CONFIG = MAKE_CRUD_CONFIG({ path: "N/A", plural: "System Information", singular: "System Information",