From f6280078c1b0c8a2e6c40e3f56fd869b17898119 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 26 Sep 2023 15:33:28 -0400 Subject: [PATCH] :sparkles: Add useLocalTableControlsWithUrlParams hook and update archetypes table to use it (#1392) When using the table-control hooks for a server-paginated table, you can use either `useTableControlState` or `useTableControlUrlParams` to provide state, then you take the returned object from that and use its properties to fetch API data, then pass the object with additional API-dependent properties to `useTableControlProps`. For a client-paginated table, there is no need to split this into two steps because the API request is not dependent on the table state (there's no need to take pagination/sort/filter state and include it in the API request), so the shorthand `useLocalTableControls` hook exists which is simply `useTableControlProps(useLocalTableControlState(args))` (`useLocalTableControlState` is similar to `useTableControlState` but performs client-side logic with the `getLocal[Feature]DerivedState` helpers in between calling each feature hook). However, `useLocalTableControls` only allows the use of React state as the source of truth rather than URL params. This PR adds an equivalent `useLocalTableControlsWithUrlParams` shorthand hook that is a drop-in replacement for `useLocalTableControls` which uses URL params instead of React state. The only additional argument available when switching to this new hook is `urlParamKeyPrefix`, which is optional and is used to distinguish the params for multiple tables on the same page. Even though the archetypes table is the only table on its page, I used it to be consistent in case we do add additional tables in modals or drawers, etc. like we did for Issues / Affected apps. With the archetypes table now using URL params for its filter state, we should now easily be able to navigate to the archetypes table filtered by tags (as discussed with @ibolton336) by using an approach similar to `getAffectedAppsUrl` on the issues page ([usage here](https://github.com/konveyor/tackle2-ui/blob/main/client/src/app/pages/issues/affected-apps-link.tsx#L22C1-L28), [implementation here](https://github.com/konveyor/tackle2-ui/blob/main/client/src/app/pages/issues/helpers.ts#L100)), which uses the `trimAndStringifyUrlParams` and `serializeFilterUrlParams` helpers. This implementation might look something like: ```ts export const getArchetypesUrlFilteredByTags = (tags: string[]) => { const baseUrl = Paths.archetypes; return `${baseUrl}?${trimAndStringifyUrlParams({ newPrefixedSerializedParams: { [`${TableURLParamKeyPrefix.archetypes}:filters`]: serializeFilterUrlParams({ tags }).filters, }, })}`; }; ``` Note (see the code comment in this diff): Because of the way we implemented `useUrlParams`, there is no way to conditionally use URL params or React state within the same hook based on an argument. This is why all the separate `use[Feature]State` and `use[Feature]UrlParams` hooks exist, and it also results in a moderate amount of duplication of code in this PR because of the logic required to feed the values returned from these feature hooks into each other. In the future we should refactor `useUrlParams` into something like `useStateOrUrlParams` to eliminate this issue and the duplication it causes (I also mentioned this in the initial docs in #1355). That will come in a future PR. Signed-off-by: Mike Turley Co-authored-by: Ian Bolton --- client/src/app/Constants.ts | 1 + .../useLocalTableControlState.ts | 92 ++++++++++++++++++- .../table-controls/useLocalTableControls.ts | 22 ++++- .../useTableControlUrlParams.ts | 2 +- .../app/pages/archetypes/archetypes-page.tsx | 6 +- 5 files changed, 112 insertions(+), 11 deletions(-) diff --git a/client/src/app/Constants.ts b/client/src/app/Constants.ts index 422963935c..f672a0faaf 100644 --- a/client/src/app/Constants.ts +++ b/client/src/app/Constants.ts @@ -242,4 +242,5 @@ export enum TableURLParamKeyPrefix { issuesAffectedFiles = "if", issuesRemainingIncidents = "ii", dependencyApplications = "da", + archetypes = "ar", } diff --git a/client/src/app/hooks/table-controls/useLocalTableControlState.ts b/client/src/app/hooks/table-controls/useLocalTableControlState.ts index 9f3f7062d9..3dd671b58a 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControlState.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControlState.ts @@ -1,13 +1,23 @@ import { useSelectionState } from "@migtools/lib-ui"; -import { getLocalFilterDerivedState, useFilterState } from "./filtering"; -import { useSortState, getLocalSortDerivedState } from "./sorting"; +import { + getLocalFilterDerivedState, + useFilterState, + useFilterUrlParams, +} from "./filtering"; +import { + useSortState, + getLocalSortDerivedState, + useSortUrlParams, +} from "./sorting"; import { getLocalPaginationDerivedState, usePaginationState, + usePaginationUrlParams, } from "./pagination"; -import { useExpansionState } from "./expansion"; -import { useActiveRowState } from "./active-row"; +import { useExpansionState, useExpansionUrlParams } from "./expansion"; +import { useActiveRowState, useActiveRowUrlParams } from "./active-row"; import { + IExtraArgsForURLParamHooks, IUseLocalTableControlStateArgs, IUseTableControlPropsArgs, } from "./types"; @@ -15,7 +25,7 @@ import { export const useLocalTableControlState = < TItem, TColumnKey extends string, - TSortableColumnKey extends TColumnKey + TSortableColumnKey extends TColumnKey, >( args: IUseLocalTableControlStateArgs ): IUseTableControlPropsArgs => { @@ -77,3 +87,75 @@ export const useLocalTableControlState = < currentPageItems: hasPagination ? currentPageItems : sortedItems, }; }; + +// TODO refactor useUrlParams so it can be used conditionally (e.g. useStateOrUrlParams) so we don't have to duplicate all this. +// this would mean all use[Feature]UrlParams hooks could be consolidated into use[Feature]State with a boolean option for whether to use URL params. + +export const useLocalTableControlUrlParams = < + TItem, + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, + TURLParamKeyPrefix extends string = string, +>( + args: IUseLocalTableControlStateArgs & + IExtraArgsForURLParamHooks +): IUseTableControlPropsArgs => { + const { + items, + filterCategories = [], + sortableColumns = [], + getSortValues, + initialSort = null, + hasPagination = true, + initialItemsPerPage = 10, + idProperty, + initialSelected, + isItemSelectable, + } = args; + + const filterState = useFilterUrlParams(args); + const { filteredItems } = getLocalFilterDerivedState({ + items, + filterCategories, + filterState, + }); + + const selectionState = useSelectionState({ + items: filteredItems, + isEqual: (a, b) => a[idProperty] === b[idProperty], + initialSelected, + isItemSelectable, + }); + + const sortState = useSortUrlParams({ ...args, sortableColumns, initialSort }); + const { sortedItems } = getLocalSortDerivedState({ + sortState, + items: filteredItems, + getSortValues, + }); + + const paginationState = usePaginationUrlParams({ + ...args, + initialItemsPerPage, + }); + const { currentPageItems } = getLocalPaginationDerivedState({ + paginationState, + items: sortedItems, + }); + + const expansionState = useExpansionUrlParams(args); + + const activeRowState = useActiveRowUrlParams(args); + + return { + ...args, + filterState, + expansionState, + selectionState, + sortState, + paginationState, + activeRowState, + totalItemCount: items.length, + currentPageItems: hasPagination ? currentPageItems : sortedItems, + }; +}; diff --git a/client/src/app/hooks/table-controls/useLocalTableControls.ts b/client/src/app/hooks/table-controls/useLocalTableControls.ts index b20714a8cc..4e5efc6762 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControls.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControls.ts @@ -1,11 +1,27 @@ -import { useLocalTableControlState } from "./useLocalTableControlState"; +import { + useLocalTableControlState, + useLocalTableControlUrlParams, +} from "./useLocalTableControlState"; import { useTableControlProps } from "./useTableControlProps"; -import { IUseLocalTableControlStateArgs } from "./types"; +import { + IExtraArgsForURLParamHooks, + IUseLocalTableControlStateArgs, +} from "./types"; export const useLocalTableControls = < TItem, TColumnKey extends string, - TSortableColumnKey extends TColumnKey + TSortableColumnKey extends TColumnKey, >( args: IUseLocalTableControlStateArgs ) => useTableControlProps(useLocalTableControlState(args)); + +export const useLocalTableControlsWithUrlParams = < + TItem, + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, + TURLParamKeyPrefix extends string = string, +>( + args: IUseLocalTableControlStateArgs & + IExtraArgsForURLParamHooks +) => useTableControlProps(useLocalTableControlUrlParams(args)); diff --git a/client/src/app/hooks/table-controls/useTableControlUrlParams.ts b/client/src/app/hooks/table-controls/useTableControlUrlParams.ts index 4e15c40fc4..7f8db39a73 100644 --- a/client/src/app/hooks/table-controls/useTableControlUrlParams.ts +++ b/client/src/app/hooks/table-controls/useTableControlUrlParams.ts @@ -10,7 +10,7 @@ export const useTableControlUrlParams = < TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, - TURLParamKeyPrefix extends string = string + TURLParamKeyPrefix extends string = string, >( args: ITableControlCommonArgs< TItem, diff --git a/client/src/app/pages/archetypes/archetypes-page.tsx b/client/src/app/pages/archetypes/archetypes-page.tsx index 60fad8847d..09cea4db16 100644 --- a/client/src/app/pages/archetypes/archetypes-page.tsx +++ b/client/src/app/pages/archetypes/archetypes-page.tsx @@ -39,7 +39,7 @@ import { TableHeaderContentWithControls, TableRowContentWithControls, } from "@app/components/TableControls"; -import { useLocalTableControls } from "@app/hooks/table-controls"; +import { useLocalTableControlsWithUrlParams } from "@app/hooks/table-controls"; import { useDeleteArchetypeMutation, useFetchArchetypes, @@ -57,6 +57,7 @@ import { formatPath, getAxiosErrorMessage } from "@app/utils/utils"; import { AxiosError } from "axios"; import { Paths } from "@app/Paths"; import { SimplePagination } from "@app/components/SimplePagination"; +import { TableURLParamKeyPrefix } from "@app/Constants"; const Archetypes: React.FC = () => { const { t } = useTranslation(); @@ -96,7 +97,8 @@ const Archetypes: React.FC = () => { onError ); - const tableControls = useLocalTableControls({ + const tableControls = useLocalTableControlsWithUrlParams({ + urlParamKeyPrefix: TableURLParamKeyPrefix.archetypes, idProperty: "id", items: archetypes, isLoading: isFetching,