From bdba322b34c61888096e5cbe905e23f53f5b4cef Mon Sep 17 00:00:00 2001 From: Ayobami Akingbade Date: Fri, 8 Dec 2023 14:11:35 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(sorting):=20impro?= =?UTF-8?q?ve=20and=20centralize=20sorting=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard-widgets.service.ts | 4 +- src/backend/entities/entities.service.ts | 10 +- src/backend/list-order/list-order.service.ts | 2 - src/backend/list-order/utils.ts | 23 ---- src/backend/menu/menu.service.ts | 4 +- .../components/SortList/index.tsx | 18 +-- .../Dashboard/Manage/_BaseManageDashboard.tsx | 4 +- src/frontend/views/data/_BaseEntityForm.tsx | 16 ++- src/frontend/views/data/portal/index.ts | 1 + src/frontend/views/data/portal/main.ts | 17 +++ src/frontend/views/settings/Menu/index.tsx | 4 +- src/shared/lib/array/move.ts | 20 +++ src/shared/lib/array/sort/index.ts | 17 +++ .../lib/array/sort/sort.spec.ts} | 13 +- .../entities/__tests__/sortByUtils.spec.ts | 124 ------------------ src/shared/logic/entities/sort.utils.ts | 19 --- 16 files changed, 92 insertions(+), 204 deletions(-) delete mode 100644 src/backend/list-order/utils.ts create mode 100644 src/frontend/views/data/portal/index.ts create mode 100644 src/frontend/views/data/portal/main.ts create mode 100644 src/shared/lib/array/move.ts create mode 100644 src/shared/lib/array/sort/index.ts rename src/{backend/list-order/utils.spec.ts => shared/lib/array/sort/sort.spec.ts} (72%) delete mode 100644 src/shared/logic/entities/__tests__/sortByUtils.spec.ts delete mode 100644 src/shared/logic/entities/sort.utils.ts diff --git a/src/backend/dashboard-widgets/dashboard-widgets.service.ts b/src/backend/dashboard-widgets/dashboard-widgets.service.ts index d68ad1dec..5b324685c 100644 --- a/src/backend/dashboard-widgets/dashboard-widgets.service.ts +++ b/src/backend/dashboard-widgets/dashboard-widgets.service.ts @@ -15,7 +15,6 @@ import { import { listOrderApiService, ListOrderApiService, - sortListByOrder, } from "backend/list-order/list-order.service"; import { rolesApiService, RolesApiService } from "backend/roles/roles.service"; import { userFriendlyCase } from "shared/lib/strings/friendly-case"; @@ -30,6 +29,7 @@ import { import { GranularEntityPermissions, IAccountProfile } from "shared/types/user"; import { relativeDateNotationToActualDate } from "backend/data/data-access/time.constants"; import { ILabelValue } from "shared/types/options"; +import { sortListByOrder } from "shared/lib/array/sort"; import { mutateGeneratedDashboardWidgets, PORTAL_DASHBOARD_PERMISSION, @@ -183,7 +183,7 @@ return [actual[0], relative[0]]; await this._dashboardWidgetsPersistenceService.getAllItemsIn(widgetList) ); - return sortListByOrder(widgetList, widgets); + return sortListByOrder(widgetList, widgets, "id"); } async listDashboardWidgets( diff --git a/src/backend/entities/entities.service.ts b/src/backend/entities/entities.service.ts index 336633a5b..09f1f0dd2 100644 --- a/src/backend/entities/entities.service.ts +++ b/src/backend/entities/entities.service.ts @@ -7,9 +7,9 @@ import { rolesApiService, RolesApiService } from "backend/roles/roles.service"; import { IApplicationService } from "backend/types"; import { noop } from "shared/lib/noop"; import { IDBSchema, IEntityField, IEntityRelation } from "shared/types/db"; -import { sortByList } from "shared/logic/entities/sort.utils"; import { DataCrudKeys } from "shared/types/data"; import { CRUD_KEY_CONFIG } from "shared/configurations/permissions"; +import { sortListByOrder } from "shared/lib/array/sort"; import { SchemasApiService, schemasApiService } from "../schema/schema.service"; import { PortalFieldsFilterService } from "./portal"; @@ -51,9 +51,9 @@ export class EntitiesApiService implements IApplicationService { this._configurationApiService.show("entity_fields_orders", entity), ]); - sortByList( - entityFields as unknown as Record[], + sortListByOrder( entityFieldsOrder, + entityFields as unknown as Record[], "name" as keyof IEntityField ); @@ -166,9 +166,9 @@ export class EntitiesApiService implements IApplicationService { "table" ); - sortByList( - allowedEntityRelation as unknown[] as Record[], + sortListByOrder( entityOrders, + allowedEntityRelation as unknown[] as Record[], "table" ); diff --git a/src/backend/list-order/list-order.service.ts b/src/backend/list-order/list-order.service.ts index 292944e54..dd292d76a 100644 --- a/src/backend/list-order/list-order.service.ts +++ b/src/backend/list-order/list-order.service.ts @@ -4,8 +4,6 @@ import { } from "backend/lib/config-persistence"; import { IApplicationService } from "backend/types"; -export { sortListByOrder } from "./utils"; - export class ListOrderApiService implements IApplicationService { constructor( private readonly _listOrderPersistenceService: AbstractConfigDataPersistenceService< diff --git a/src/backend/list-order/utils.ts b/src/backend/list-order/utils.ts deleted file mode 100644 index e3c55896d..000000000 --- a/src/backend/list-order/utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -export function sortListByOrder( - order: string[], - itemsToOrder: T[] -): T[] { - if (order.length === 0) { - return itemsToOrder; - } - const itemsMap = Object.fromEntries( - itemsToOrder.map((item) => [item.id, item]) - ); - - const orderedItems = order.map((item) => itemsMap[item]); - - if (order.length === itemsToOrder.length) { - return orderedItems; - } - - const remainingItems = itemsToOrder.filter( - (item) => !order.includes(item.id) - ); - - return [...orderedItems, ...remainingItems].filter(Boolean); -} diff --git a/src/backend/menu/menu.service.ts b/src/backend/menu/menu.service.ts index 8f29aa3df..4c44900b1 100644 --- a/src/backend/menu/menu.service.ts +++ b/src/backend/menu/menu.service.ts @@ -18,10 +18,10 @@ import { ConfigurationApiService, configurationApiService, } from "backend/configuration/configuration.service"; -import { sortByList } from "shared/logic/entities/sort.utils"; import { RolesApiService, rolesApiService } from "backend/roles/roles.service"; import { ILabelValue } from "shared/types/options"; import { ISingularPlural } from "shared/types/config"; +import { sortListByOrder } from "shared/lib/array/sort"; import { portalCheckIfIsMenuAllowed, getPortalMenuItems } from "./portal"; import { IBaseNavigationMenuApiService } from "./types"; @@ -148,7 +148,7 @@ export class NavigationMenuApiService .filter(({ value }) => !hiddenMenuEntities.includes(value)) .sort((a, b) => a.value.localeCompare(b.value)); - sortByList(menuEntities, entitiesOrder, "value"); + sortListByOrder(entitiesOrder, menuEntities, "value"); return menuEntities; } diff --git a/src/frontend/design-system/components/SortList/index.tsx b/src/frontend/design-system/components/SortList/index.tsx index fbfc90939..7ae8dfa41 100644 --- a/src/frontend/design-system/components/SortList/index.tsx +++ b/src/frontend/design-system/components/SortList/index.tsx @@ -7,6 +7,7 @@ import { Stack } from "frontend/design-system/primitives/Stack"; import { pluralize } from "shared/lib/strings"; import { Typo } from "frontend/design-system/primitives/Typo"; import { Spacer } from "frontend/design-system/primitives/Spacer"; +import { arrayMoveImmutable } from "shared/lib/array/move"; import { ErrorAlert } from "../Alert"; import { EmptyWrapper } from "../EmptyWrapper"; import { FormButton } from "../Button/FormButton"; @@ -14,23 +15,6 @@ import { defaultToEmptyArray } from "./utils"; import { ListSkeleton } from "../Skeleton/List"; import { SHADOW_CSS } from "../Card"; -function arrayMoveMutable(array: T[], fromIndex: number, toIndex: number) { - const startIndex = fromIndex < 0 ? array.length + fromIndex : fromIndex; - - if (startIndex >= 0 && startIndex < array.length) { - const endIndex = toIndex < 0 ? array.length + toIndex : toIndex; - - const [item] = array.splice(fromIndex, 1); - array.splice(endIndex, 0, item); - } -} - -function arrayMoveImmutable(array: T[], fromIndex: number, toIndex: number) { - const newArray = [...array]; - arrayMoveMutable(newArray, fromIndex, toIndex); - return newArray; -} - export interface IProps { data: DataStateKeys; onSave: (data: string[]) => Promise; diff --git a/src/frontend/views/Dashboard/Manage/_BaseManageDashboard.tsx b/src/frontend/views/Dashboard/Manage/_BaseManageDashboard.tsx index 587b3b95e..fd7714b45 100644 --- a/src/frontend/views/Dashboard/Manage/_BaseManageDashboard.tsx +++ b/src/frontend/views/Dashboard/Manage/_BaseManageDashboard.tsx @@ -2,12 +2,12 @@ import styled from "styled-components"; import { Check, Plus } from "react-feather"; import { USER_PERMISSIONS } from "shared/constants/user"; import { ViewStateMachine } from "frontend/components/ViewStateMachine"; -import arrayMove from "array-move"; import SortableList, { SortableItem } from "react-easy-sort"; import { useRouter } from "next/router"; import { useSetPageDetails } from "frontend/lib/routing/usePageDetails"; import { NAVIGATION_LINKS } from "frontend/lib/routing/links"; import { AppLayout } from "frontend/_layouts/app"; +import { arrayMoveImmutable } from "shared/lib/array/move"; import { useArrangeDashboardWidgetMutation, useDashboardWidgets, @@ -41,7 +41,7 @@ export function BaseManageDashboard({ dashboardId, doneLink, title }: IProps) { useArrangeDashboardWidgetMutation(dashboardId); const onSortEnd = (oldIndex: number, newIndex: number) => { - const newOrder = arrayMove(widgets.data, oldIndex, newIndex); + const newOrder = arrayMoveImmutable(widgets.data, oldIndex, newIndex); arrangeDashboardWidgetMutation.mutate(newOrder.map(({ id }) => id)); }; diff --git a/src/frontend/views/data/_BaseEntityForm.tsx b/src/frontend/views/data/_BaseEntityForm.tsx index a86d9c698..27eb7b5fb 100644 --- a/src/frontend/views/data/_BaseEntityForm.tsx +++ b/src/frontend/views/data/_BaseEntityForm.tsx @@ -22,6 +22,7 @@ import { ButtonIconTypes } from "frontend/design-system/components/Button/consta import { buildAppliedSchemaFormConfig } from "./buildAppliedSchemaFormConfig"; import { useEntityViewStateMachine } from "./useEntityViewStateMachine"; import { filterOutHiddenScalarColumns } from "./utils"; +import { usePortalExtendEntityFormConfig } from "./portal"; type IProps = { entity: string; @@ -57,6 +58,9 @@ export function BaseEntityForm({ "entity_columns_types", entity ); + + const extendEntityFormConfig = usePortalExtendEntityFormConfig(crudAction); + const entityFormExtension = useEntityConfiguration( "entity_form_extension", entity @@ -78,6 +82,7 @@ export function BaseEntityForm({ entityFormExtension.isLoading || entity === SLUG_LOADING_VALUE || entityFieldTypesMap.isLoading || + extendEntityFormConfig === "loading" || additionalDataState?.isLoading; const viewState = useEntityViewStateMachine(isLoading, error, crudAction); @@ -113,6 +118,11 @@ export function BaseEntityForm({ fields, }; + const formConfig = buildAppliedSchemaFormConfig( + formSchemaConfig, + allOptional + ); + return ( diff --git a/src/frontend/views/data/portal/index.ts b/src/frontend/views/data/portal/index.ts new file mode 100644 index 000000000..0f0a2fec5 --- /dev/null +++ b/src/frontend/views/data/portal/index.ts @@ -0,0 +1 @@ +export { usePortalExtendEntityFormConfig } from "./main"; diff --git a/src/frontend/views/data/portal/main.ts b/src/frontend/views/data/portal/main.ts new file mode 100644 index 000000000..7628d2ab1 --- /dev/null +++ b/src/frontend/views/data/portal/main.ts @@ -0,0 +1,17 @@ +import { IAppliedSchemaFormConfig } from "shared/form-schemas/types"; +import { noop } from "shared/lib/noop"; + +export const usePortalExtendEntityFormConfig = ( + crudAction: "update" | "create" +): + | "loading" + | (( + formConfig: IAppliedSchemaFormConfig + ) => IAppliedSchemaFormConfig) => { + noop(crudAction); + return ( + formConfig: IAppliedSchemaFormConfig + ): IAppliedSchemaFormConfig => { + return formConfig; + }; +}; diff --git a/src/frontend/views/settings/Menu/index.tsx b/src/frontend/views/settings/Menu/index.tsx index 0cb54b152..9bb03a254 100644 --- a/src/frontend/views/settings/Menu/index.tsx +++ b/src/frontend/views/settings/Menu/index.tsx @@ -19,8 +19,8 @@ import { } from "frontend/hooks/entity/entity.store"; import { loadedDataState } from "frontend/lib/data/constants/loadedDataState"; import { NAVIGATION_MENU_ENDPOINT } from "frontend/_layouts/app/LayoutImpl/constants"; -import { sortByList } from "shared/logic/entities/sort.utils"; import { AppLayout } from "frontend/_layouts/app"; +import { sortListByOrder } from "shared/lib/array/sort"; import { SETTINGS_VIEW_KEY } from "../constants"; import { EntitiesSelection } from "../Entities/Selection"; @@ -49,7 +49,7 @@ export function MenuSettings() { .filter(({ value }) => !menuEntitiesToHide.data.includes(value)) .sort((a, b) => a.value.localeCompare(b.value)); - sortByList(menuEntities, menuEntitiesOrder.data, "value"); + sortListByOrder(menuEntitiesOrder.data, menuEntities, "value"); const upsertHideFromMenuMutation = useUpsertConfigurationMutation( "disabled_menu_entities", diff --git a/src/shared/lib/array/move.ts b/src/shared/lib/array/move.ts new file mode 100644 index 000000000..e926c6633 --- /dev/null +++ b/src/shared/lib/array/move.ts @@ -0,0 +1,20 @@ +function arrayMoveMutable(array: T[], fromIndex: number, toIndex: number) { + const startIndex = fromIndex < 0 ? array.length + fromIndex : fromIndex; + + if (startIndex >= 0 && startIndex < array.length) { + const endIndex = toIndex < 0 ? array.length + toIndex : toIndex; + + const [item] = array.splice(fromIndex, 1); + array.splice(endIndex, 0, item); + } +} + +export function arrayMoveImmutable( + array: T[], + fromIndex: number, + toIndex: number +) { + const newArray = [...array]; + arrayMoveMutable(newArray, fromIndex, toIndex); + return newArray; +} diff --git a/src/shared/lib/array/sort/index.ts b/src/shared/lib/array/sort/index.ts new file mode 100644 index 000000000..892d8e3c6 --- /dev/null +++ b/src/shared/lib/array/sort/index.ts @@ -0,0 +1,17 @@ +export function sortListByOrder( + order: string[], + itemsToOrder: T[], + key: K +): T[] { + const indexOf = (entry: T) => { + const index = order.indexOf(entry[key] as unknown as string); + if (index === -1) { + return Infinity; + } + return index; + }; + + return itemsToOrder.sort((a, b) => { + return indexOf(a) - indexOf(b); + }); +} diff --git a/src/backend/list-order/utils.spec.ts b/src/shared/lib/array/sort/sort.spec.ts similarity index 72% rename from src/backend/list-order/utils.spec.ts rename to src/shared/lib/array/sort/sort.spec.ts index 888fa8df5..037a9e2ce 100644 --- a/src/backend/list-order/utils.spec.ts +++ b/src/shared/lib/array/sort/sort.spec.ts @@ -1,22 +1,25 @@ -import { sortListByOrder } from "./utils"; +import { sortListByOrder } from "."; describe("sortListByOrder", () => { it("should return the same list if the order is empty", () => { const list = [{ id: "1" }, { id: "2" }, { id: "3" }]; const order = []; - expect(sortListByOrder(order, list)).toEqual(list); + expect(sortListByOrder(order, list, "id")).toEqual(list); }); it("should return only items in the list even if present in the order", () => { const list = [{ id: "1" }, { id: "3" }]; const order = ["3", "2", "1"]; - expect(sortListByOrder(order, list)).toEqual([{ id: "3" }, { id: "1" }]); + expect(sortListByOrder(order, list, "id")).toEqual([ + { id: "3" }, + { id: "1" }, + ]); }); it("should order the list based on the order", () => { const list = [{ id: "1" }, { id: "2" }, { id: "3" }]; const order = ["3", "2", "1"]; - expect(sortListByOrder(order, list)).toEqual([ + expect(sortListByOrder(order, list, "id")).toEqual([ { id: "3" }, { id: "2" }, { id: "1" }, @@ -26,7 +29,7 @@ describe("sortListByOrder", () => { it("should append the remaining items at the end of the list", () => { const list = [{ id: "1" }, { id: "2" }, { id: "3" }, { id: "4" }]; const order = ["3", "2", "1"]; - expect(sortListByOrder(order, list)).toEqual([ + expect(sortListByOrder(order, list, "id")).toEqual([ { id: "3" }, { id: "2" }, { id: "1" }, diff --git a/src/shared/logic/entities/__tests__/sortByUtils.spec.ts b/src/shared/logic/entities/__tests__/sortByUtils.spec.ts deleted file mode 100644 index 05ea18ad0..000000000 --- a/src/shared/logic/entities/__tests__/sortByUtils.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { sortByList } from "../sort.utils"; - -describe("Sort By List", () => { - it("should sort list by provided key", () => { - const list = [{ name: "Foo" }, { name: "Boo" }]; - sortByList(list, ["Boo", "Foo"], "name"); - - expect(list).toMatchInlineSnapshot(` - [ - { - "name": "Boo", - }, - { - "name": "Foo", - }, - ] - `); - }); - - it("should send the items not in the sortlist to the end", () => { - const list = [ - { name: "Be Last" }, - { name: "Foo" }, - { name: "Boo" }, - { name: "Be Last Last" }, - ]; - sortByList(list, ["Boo", "Foo"], "name"); - - expect(list).toMatchInlineSnapshot(` - [ - { - "name": "Boo", - }, - { - "name": "Foo", - }, - { - "name": "Be Last", - }, - { - "name": "Be Last Last", - }, - ] - `); - }); - - it("should ignore items not in list but in the sort list", () => { - const list = [{ name: "Foo" }, { name: "Boo" }]; - sortByList(list, ["Boo", "Foo", "no damage be done"], "name"); - - expect(list).toMatchInlineSnapshot(` - [ - { - "name": "Boo", - }, - { - "name": "Foo", - }, - ] - `); - }); - - it("should sort large list", () => { - const list = [ - { name: "B" }, - { name: "A" }, - { name: "F" }, - { name: "C" }, - { name: "E" }, - { name: "G" }, - { name: "D" }, - ]; - sortByList(list, ["A", "B", "C", "D", "E", "F", "G"], "name"); - - expect(list).toMatchInlineSnapshot(` - [ - { - "name": "A", - }, - { - "name": "B", - }, - { - "name": "C", - }, - { - "name": "D", - }, - { - "name": "E", - }, - { - "name": "F", - }, - { - "name": "G", - }, - ] - `); - }); - - it("should not break on empty input", () => { - const list = []; - sortByList(list, ["Boo", "Foo"], "name"); - - expect(list).toHaveLength(0); - }); - - it("should leave input untouched on empty sort list", () => { - const list = [{ name: "Foo" }, { name: "Boo" }]; - sortByList(list, [], "name"); - - expect(list).toMatchInlineSnapshot(` - [ - { - "name": "Foo", - }, - { - "name": "Boo", - }, - ] - `); - }); -}); diff --git a/src/shared/logic/entities/sort.utils.ts b/src/shared/logic/entities/sort.utils.ts deleted file mode 100644 index 20b9d16ec..000000000 --- a/src/shared/logic/entities/sort.utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -export function sortByList< - T extends Record, - K extends keyof T ->(inputArray: T[], sortList: string[], key: K) { - if (sortList.length === 0) { - return inputArray; - } - const sortMap = Object.fromEntries( - sortList.map((item, index) => [item, index + 1]) - ); - - inputArray.sort( - (a, b) => - (sortMap[a[key] as string] || Infinity) - - (sortMap[b[key] as string] || Infinity) - ); - - return inputArray; -}