From dd79434c35a992de4b3584336afc86fb06211836 Mon Sep 17 00:00:00 2001 From: Ayobami Akingbade Date: Wed, 20 Dec 2023 00:21:28 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(metadata-columsn):=20implement?= =?UTF-8?q?=20metadata=20columns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/_/api-handlers/config.ts | 4 + src/__tests__/admin/settings/data.spec.tsx | 148 ++++++++++++++++++ src/__tests__/admin/settings/date.spec.tsx | 93 ----------- src/backend/data/data.service.ts | 23 ++- .../Table/filters/__tests__/index.spec.tsx | 15 +- src/frontend/lib/routing/links.ts | 2 +- src/frontend/views/data/_BaseEntityForm.tsx | 17 +- .../Crud/EntityFieldsSelectionSettings.tsx | 12 +- src/frontend/views/settings/Data/index.tsx | 141 +++++++++++++++++ .../views/settings/DateFormat/index.tsx | 86 ---------- src/frontend/views/settings/_Base.tsx | 4 +- src/pages/admin/settings/data.tsx | 3 + src/pages/admin/settings/date.tsx | 3 - src/shared/configurations/base-types.ts | 1 + src/shared/configurations/constants.ts | 7 + 15 files changed, 359 insertions(+), 200 deletions(-) create mode 100644 src/__tests__/admin/settings/data.spec.tsx delete mode 100644 src/__tests__/admin/settings/date.spec.tsx create mode 100644 src/frontend/views/settings/Data/index.tsx delete mode 100644 src/frontend/views/settings/DateFormat/index.tsx create mode 100644 src/pages/admin/settings/data.tsx delete mode 100644 src/pages/admin/settings/date.tsx diff --git a/src/__tests__/_/api-handlers/config.ts b/src/__tests__/_/api-handlers/config.ts index 34ccb68c3..047fb1bd8 100644 --- a/src/__tests__/_/api-handlers/config.ts +++ b/src/__tests__/_/api-handlers/config.ts @@ -74,6 +74,10 @@ const CONFIG_VALUES = { primary: `#4b38b3`, primaryDark: `#111111`, }, + metadata_columns: { + createdAt: `created_at`, + updatedAt: `updated_at`, + }, disabled_entities: ["disabled-entity-1", "disabled-entity-2"], menu_entities_order: [], disabled_menu_entities: ["entity-3"], diff --git a/src/__tests__/admin/settings/data.spec.tsx b/src/__tests__/admin/settings/data.spec.tsx new file mode 100644 index 000000000..16a9913d1 --- /dev/null +++ b/src/__tests__/admin/settings/data.spec.tsx @@ -0,0 +1,148 @@ +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import { ApplicationRoot } from "frontend/components/ApplicationRoot"; +import userEvent from "@testing-library/user-event"; +import GeneralDataSettings from "pages/admin/settings/data"; + +import { setupApiHandlers } from "__tests__/_/setupApihandlers"; + +setupApiHandlers(); + +describe("pages/admin/settings/data", () => { + beforeAll(() => { + const useRouter = jest.spyOn(require("next/router"), "useRouter"); + useRouter.mockImplementation(() => ({ + asPath: "/", + })); + }); + + describe("Metadata", () => { + it("should display metadata columns", async () => { + render( + + + + ); + await waitFor(() => { + expect(screen.getByLabelText("Created At")).toHaveValue("created_at"); + }); + expect(screen.getByLabelText("Updated At")).toHaveValue("updated_at"); + }); + + it("should update metadata columns successfully", async () => { + render( + + + + ); + + await userEvent.type(screen.getByLabelText("Created At"), "-created"); + await userEvent.type(screen.getByLabelText("Updated At"), "-updated"); + + await userEvent.click( + screen.getByRole("button", { name: "Save Metadata Columns" }) + ); + + expect(await screen.findByRole("status")).toHaveTextContent( + "Metadata Columns Saved Successfully" + ); + }); + + it("should display updated date values", async () => { + render( + + + + ); + await waitFor(() => { + expect(screen.getByLabelText("Created At")).toHaveValue( + "created_at-created" + ); + }); + expect(screen.getByLabelText("Updated At")).toHaveValue( + "updated_at-updated" + ); + }); + }); + + describe("Date", () => { + it("should display date values", async () => { + render( + + + + ); + await waitFor(() => { + expect(screen.getByLabelText("Format")).toHaveValue("do MMM yyyy"); + }); + }); + + it("should update date successfully", async () => { + render( + + + + ); + + await userEvent.clear(screen.getByLabelText("Format")); + + await userEvent.type(screen.getByLabelText("Format"), "yyyy MMM do"); + + await userEvent.click( + screen.getByRole("button", { name: "Close Toast" }) + ); + + await userEvent.click( + screen.getByRole("button", { name: "Save Date Format" }) + ); + + expect(await screen.findByRole("status")).toHaveTextContent( + "Date Format Saved Successfully" + ); + }); + + it("should display updated date values", async () => { + render( + + + + ); + await waitFor(() => { + expect(screen.getByLabelText("Format")).toHaveValue("yyyy MMM do"); + }); + }); + + describe("invalid date formats", () => { + it("should not be updated", async () => { + render( + + + + ); + + await userEvent.clear(screen.getByLabelText("Format")); + + await userEvent.type(screen.getByLabelText("Format"), "yyYXXYY"); + + await userEvent.click( + screen.getByRole("button", { name: "Save Date Format" }) + ); + + expect((await screen.findAllByRole("status"))[0]).toHaveTextContent( + `Invalid Date Format!. Please go to https://date-fns.org/docs/format to see valid formats` + ); + }); + + it("should show date format", async () => { + render( + + + + ); + await waitFor(() => { + expect(screen.getByLabelText("Format")).toHaveValue("yyyy MMM do"); + }); + }); + }); + }); +}); diff --git a/src/__tests__/admin/settings/date.spec.tsx b/src/__tests__/admin/settings/date.spec.tsx deleted file mode 100644 index 5f72bdc99..000000000 --- a/src/__tests__/admin/settings/date.spec.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from "react"; -import { render, screen, waitFor } from "@testing-library/react"; -import { ApplicationRoot } from "frontend/components/ApplicationRoot"; -import userEvent from "@testing-library/user-event"; -import DateFormatSettings from "pages/admin/settings/date"; - -import { setupApiHandlers } from "__tests__/_/setupApihandlers"; - -setupApiHandlers(); - -describe("pages/admin/settings/date", () => { - beforeAll(() => { - const useRouter = jest.spyOn(require("next/router"), "useRouter"); - useRouter.mockImplementation(() => ({ - asPath: "/", - })); - }); - - it("should display date values", async () => { - render( - - - - ); - await waitFor(() => { - expect(screen.getByLabelText("Format")).toHaveValue("do MMM yyyy"); - }); - }); - - it("should update date successfully", async () => { - render( - - - - ); - - await userEvent.clear(screen.getByLabelText("Format")); - - await userEvent.type(screen.getByLabelText("Format"), "yyyy MMM do"); - - await userEvent.click( - screen.getByRole("button", { name: "Save Date Format" }) - ); - - expect(await screen.findByRole("status")).toHaveTextContent( - "Date Format Saved Successfully" - ); - }); - - it("should display updated date values", async () => { - render( - - - - ); - await waitFor(() => { - expect(screen.getByLabelText("Format")).toHaveValue("yyyy MMM do"); - }); - }); - - describe("invalid date formats", () => { - it("should not be updated", async () => { - render( - - - - ); - - await userEvent.clear(screen.getByLabelText("Format")); - - await userEvent.type(screen.getByLabelText("Format"), "yyYXXYY"); - - await userEvent.click( - screen.getByRole("button", { name: "Save Date Format" }) - ); - - expect((await screen.findAllByRole("status"))[0]).toHaveTextContent( - `Invalid Date Format!. Please go to https://date-fns.org/docs/format to see valid formats` - ); - }); - - it("should should show date format", async () => { - render( - - - - ); - await waitFor(() => { - expect(screen.getByLabelText("Format")).toHaveValue("yyyy MMM do"); - }); - }); - }); -}); diff --git a/src/backend/data/data.service.ts b/src/backend/data/data.service.ts index 8bb680505..fe2827524 100644 --- a/src/backend/data/data.service.ts +++ b/src/backend/data/data.service.ts @@ -230,11 +230,13 @@ export class DataApiService implements IDataApiService { data: Record, accountProfile: IAccountProfile ): Promise { - const [allowedFields, primaryField, entityValidations] = await Promise.all([ - this._entitiesApiService.getAllowedCrudsFieldsToShow(entity, "update"), - this._entitiesApiService.getEntityPrimaryField(entity), - this._configurationApiService.show("entity_validations", entity), - ]); + const [allowedFields, primaryField, entityValidations, metadataColumns] = + await Promise.all([ + this._entitiesApiService.getAllowedCrudsFieldsToShow(entity, "update"), + this._entitiesApiService.getEntityPrimaryField(entity), + this._configurationApiService.show("entity_validations", entity), + this._configurationApiService.show("metadata_columns"), + ]); // validate only the fields presents in 'data' noop(entityValidations); @@ -248,12 +250,21 @@ export class DataApiService implements IDataApiService { dataId: id, }); + const valueToUpdate = this.returnOnlyDataThatAreAllowed( + data, + allowedFields + ); + + if (allowedFields.includes(metadataColumns.updatedAt)) { + valueToUpdate[metadataColumns.updatedAt] = new Date(); + } + await this.getDataAccessInstance().update( entity, { [primaryField]: id, }, - this.returnOnlyDataThatAreAllowed(data, allowedFields) + valueToUpdate ); await PortalDataHooksService.afterUpdate({ 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 index fc7b07c38..bbcc49620 100644 --- a/src/frontend/design-system/components/Table/filters/__tests__/index.spec.tsx +++ b/src/frontend/design-system/components/Table/filters/__tests__/index.spec.tsx @@ -455,12 +455,15 @@ describe("Table Filters", () => { await userEvent.keyboard("{Enter}"); - await waitFor(() => { - expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ - operator: "i", - value: ["option-1", "option-2"], - }); - }); + await waitFor( + () => { + expect(setFilterValueJestFn).toHaveBeenLastCalledWith({ + operator: "i", + value: ["option-1", "option-2"], + }); + }, + { timeout: 5000 } + ); await userEvent.selectOptions( screen.getByRole("combobox", { name: "Select Filter Operator" }), diff --git a/src/frontend/lib/routing/links.ts b/src/frontend/lib/routing/links.ts index a52f3540a..cf5587657 100644 --- a/src/frontend/lib/routing/links.ts +++ b/src/frontend/lib/routing/links.ts @@ -48,7 +48,7 @@ export const NAVIGATION_LINKS = { ENTITIES: "/admin/settings/entities", MENU: "/admin/settings/menu", SYSTEM: "/admin/settings/system", - DATE: "/admin/settings/date", + DATA: "/admin/settings/data", SITE: "/admin/settings/site", THEME: "/admin/settings/theme", VARIABLES: "/admin/settings/variables", diff --git a/src/frontend/views/data/_BaseEntityForm.tsx b/src/frontend/views/data/_BaseEntityForm.tsx index 27eb7b5fb..45dbe5adf 100644 --- a/src/frontend/views/data/_BaseEntityForm.tsx +++ b/src/frontend/views/data/_BaseEntityForm.tsx @@ -3,7 +3,10 @@ import { FormSkeletonSchema, } from "frontend/design-system/components/Skeleton/Form"; import { SchemaForm } from "frontend/components/SchemaForm"; -import { useEntityConfiguration } from "frontend/hooks/configuration/configuration.store"; +import { + useAppConfiguration, + useEntityConfiguration, +} from "frontend/hooks/configuration/configuration.store"; import { useEntityFields, useEntityToOneReferenceFields, @@ -67,6 +70,8 @@ export function BaseEntityForm({ ); const entityToOneReferenceFields = useEntityToOneReferenceFields(entity); + const metaDataColumns = useAppConfiguration("metadata_columns"); + const error = entityFieldTypesMap.error || hiddenColumns.error || @@ -88,7 +93,15 @@ export function BaseEntityForm({ const viewState = useEntityViewStateMachine(isLoading, error, crudAction); const fields = filterOutHiddenScalarColumns( - entityFields.data.filter(({ isId }) => !isId), + entityFields.data + .filter(({ isId }) => !isId) + .filter( + ({ name }) => + ![ + metaDataColumns.data.createdAt, + metaDataColumns.data.updatedAt, + ].includes(name) + ), hiddenColumns.data ).map(({ name }) => name); diff --git a/src/frontend/views/entity/Crud/EntityFieldsSelectionSettings.tsx b/src/frontend/views/entity/Crud/EntityFieldsSelectionSettings.tsx index 09a49aabb..e92e124d2 100644 --- a/src/frontend/views/entity/Crud/EntityFieldsSelectionSettings.tsx +++ b/src/frontend/views/entity/Crud/EntityFieldsSelectionSettings.tsx @@ -13,6 +13,7 @@ import { useEntitySlug, } from "frontend/hooks/entity/entity.config"; import { IEntityCrudSettings } from "shared/configurations"; +import { useAppConfiguration } from "frontend/hooks/configuration/configuration.store"; import { ENTITY_CRUD_LABELS } from "../constants"; import { makeEntityFieldsSelectionKey } from "./constants"; @@ -43,6 +44,8 @@ export function EntityFieldsSelectionSettings({ const getEntityFieldLabels = useEntityFieldLabels(); + const metaDataColumns = useAppConfiguration("metadata_columns"); + const { toggleSelection, allSelections, selectMutiple, isSelected } = useStringSelections(makeEntityFieldsSelectionKey(entity, crudKey)); @@ -82,7 +85,14 @@ export function EntityFieldsSelectionSettings({ render={(menuItem) => { const isHidden = isSelected(menuItem.name); - const disabled = menuItem.isId || !toggling.enabled; + const disabled = + menuItem.isId || + !toggling.enabled || + ((crudKey === "create" || crudKey === "update") && + [ + metaDataColumns.data.createdAt, + metaDataColumns.data.updatedAt, + ].includes(menuItem.name)); return ( + + } + > + > + onSubmit={upsertConfigurationMutation.mutateAsync} + initialValues={metaDataColumns.data} + buttonText={META_DATA_CRUD_CONFIG.FORM_LANG.UPSERT} + icon="save" + fields={{ + createdAt: { + type: "text", + validations: [], + }, + updatedAt: { + type: "text", + validations: [], + }, + }} + /> + + + ); +} + +function DateSettings() { + const DATE_FORMAT_CRUD_CONFIG = MAKE_APP_CONFIGURATION_CRUD_CONFIG( + "default_date_format" + ); + const defaultDateFormat = useAppConfiguration("default_date_format"); + + const upsertDateFormatConfigurationMutation = useUpsertConfigurationMutation( + "default_date_format" + ); + + return ( + + } + > + + onSubmit={async ({ format }) => { + try { + dateFnsFormat(new Date(), format); + await upsertDateFormatConfigurationMutation.mutateAsync(format); + } catch (error) { + ToastService.error( + "Invalid Date Format!. Please go to https://date-fns.org/docs/format to see valid formats" + ); + } + }} + initialValues={{ format: defaultDateFormat.data }} + buttonText={DATE_FORMAT_CRUD_CONFIG.FORM_LANG.UPSERT} + icon="save" + fields={{ + format: { + type: "text", + validations: [ + { + validationType: "required", + }, + ], + }, + }} + /> + + + ); +} + +export function GeneralDataSettings() { + useSetPageDetails({ + pageTitle: "General Data Settings", + viewKey: SETTINGS_VIEW_KEY, + permission: USER_PERMISSIONS.CAN_CONFIGURE_APP, + }); + + return ( + + + + + + ); +} diff --git a/src/frontend/views/settings/DateFormat/index.tsx b/src/frontend/views/settings/DateFormat/index.tsx deleted file mode 100644 index c6ed2bb57..000000000 --- a/src/frontend/views/settings/DateFormat/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { SectionBox } from "frontend/design-system/components/Section/SectionBox"; -import { - FormSkeleton, - FormSkeletonSchema, -} from "frontend/design-system/components/Skeleton/Form"; -import { useSetPageDetails } from "frontend/lib/routing/usePageDetails"; -import { USER_PERMISSIONS } from "shared/constants/user"; -import { - useAppConfiguration, - useUpsertConfigurationMutation, -} from "frontend/hooks/configuration/configuration.store"; -import { ViewStateMachine } from "frontend/components/ViewStateMachine"; -import { format as dateFnsFormat } from "date-fns"; -import { MAKE_APP_CONFIGURATION_CRUD_CONFIG } from "frontend/hooks/configuration/configuration.constant"; -import { ToastService } from "frontend/lib/toast"; -import { SchemaForm } from "frontend/components/SchemaForm"; -import { BaseSettingsLayout } from "../_Base"; -import { SETTINGS_VIEW_KEY } from "../constants"; - -type IDateFormatSettings = { - format: string; -}; - -const CRUD_CONFIG = MAKE_APP_CONFIGURATION_CRUD_CONFIG("default_date_format"); - -export function DateFormatSettings() { - const defaultDateFormat = useAppConfiguration("default_date_format"); - - const upsertConfigurationMutation = useUpsertConfigurationMutation( - "default_date_format" - ); - - useSetPageDetails({ - pageTitle: CRUD_CONFIG.TEXT_LANG.TITLE, - viewKey: SETTINGS_VIEW_KEY, - permission: USER_PERMISSIONS.CAN_CONFIGURE_APP, - }); - - return ( - - - } - > - - onSubmit={async ({ format }) => { - try { - dateFnsFormat(new Date(), format); - await upsertConfigurationMutation.mutateAsync(format); - } catch (error) { - ToastService.error( - "Invalid Date Format!. Please go to https://date-fns.org/docs/format to see valid formats" - ); - } - }} - initialValues={{ format: defaultDateFormat.data }} - buttonText={CRUD_CONFIG.FORM_LANG.UPSERT} - icon="save" - fields={{ - format: { - type: "text", - validations: [ - { - validationType: "required", - }, - ], - }, - }} - /> - - - - ); -} diff --git a/src/frontend/views/settings/_Base.tsx b/src/frontend/views/settings/_Base.tsx index 19558f7f4..56b72a9f0 100644 --- a/src/frontend/views/settings/_Base.tsx +++ b/src/frontend/views/settings/_Base.tsx @@ -42,8 +42,8 @@ const baseMenuItems: IMenuSectionItem[] = [ order: 30, }, { - action: NAVIGATION_LINKS.SETTINGS.DATE, - name: "Date Format", + action: NAVIGATION_LINKS.SETTINGS.DATA, + name: "General Data Settings", IconComponent: Calendar, order: 40, }, diff --git a/src/pages/admin/settings/data.tsx b/src/pages/admin/settings/data.tsx new file mode 100644 index 000000000..3dba2f10c --- /dev/null +++ b/src/pages/admin/settings/data.tsx @@ -0,0 +1,3 @@ +import { GeneralDataSettings } from "frontend/views/settings/Data"; + +export default GeneralDataSettings; diff --git a/src/pages/admin/settings/date.tsx b/src/pages/admin/settings/date.tsx deleted file mode 100644 index 5c44130f7..000000000 --- a/src/pages/admin/settings/date.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { DateFormatSettings } from "frontend/views/settings/DateFormat"; - -export default DateFormatSettings; diff --git a/src/shared/configurations/base-types.ts b/src/shared/configurations/base-types.ts index c6d3302d0..6e374773b 100644 --- a/src/shared/configurations/base-types.ts +++ b/src/shared/configurations/base-types.ts @@ -24,6 +24,7 @@ export type BaseAppConfigurationKeys = | "system_settings" | "hidden_entity_relations" | "file_upload_settings" + | "metadata_columns" | "entity_relations_order"; export const FOR_CODE_COV = 1; diff --git a/src/shared/configurations/constants.ts b/src/shared/configurations/constants.ts index 54e9a27e1..415aae9db 100644 --- a/src/shared/configurations/constants.ts +++ b/src/shared/configurations/constants.ts @@ -117,6 +117,13 @@ export const APP_CONFIGURATION_CONFIG = { requireEntity: true, defaultValue: [] as string[], }, + metadata_columns: { + label: "Metadata Columns", + defaultValue: { + createdAt: "created_at", + updatedAt: "updated_at", + }, + }, entity_relation_template: { label: "Relation Template", requireEntity: true,