From 1ec3a550748580b4f269bbe71e44ab5878ab62f4 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 11 Sep 2023 15:38:18 -0400 Subject: [PATCH 001/102] TODOs for doc sections Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 client/src/app/hooks/table-controls/DOCS.md diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md new file mode 100644 index 0000000000..bdd79a3c2e --- /dev/null +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -0,0 +1,45 @@ +# table-controls + +## Documentation for our reusable hooks and components for managing state related to PatternFly tables + +# Goals + +TODO explain pros/cons of the legacy table vs composable table, and how the composability came at the cost of verbose code since page components are responsible for all the state +TODO explain that the goal is to restore some of the lost benefits of the legacy table abstraction without compromising the goals of the composable table. +TODO show tables with checks/Xs comparing legacy, composable, and composable w/ control hooks. goal is low-code but 100% composable with high refactorability. + +# Terms/Concerns/Features (TODO) + +TODO cover terms like filtering, sorting, pagination, expansion, selection and active-row concerns (is concerns the best word? maybe domains? functions? features?) + +# Usage + +## Table with client-side filtering/sorting/pagination logic + +TODO show top-level usage of useLocalTableControls with components for rendering + +## Table with server-side filtering/sorting/pagination logic + +TODO explain benefits and limitations of server-side table logic +TODO explain separating useLocalTableControls into useTableControlState/useTableControlUrlParams and useTableControlProps so we can leverage state when making API requests and have API data in scope for prop helpers +TODO show top-level usage of those separated hooks with components for rendering + +# Types + +TODO maybe move this to the bottom? +TODO cover the stuff in types.ts export by export referencing the usage in specific hooks and components + +# Hooks + +## Higher-level hooks (handle all state with combined options and return values) + +TODO list all the hooks used above and their signatures and implementation overview + +## Lower-level hooks (used internally by composite hooks but also usable standalone) + +TODO list all the hooks for each concern and how they flow into each other in the composite hooks + +# Components + +TODO summarize why it is still useful to have some component abstractions even though the goal is to preserve all composability / leave all control of JSX to the consumer +TODO list all the components exported from this directory, their signatures and why they are useful From bc97aa78f00bb68f50de6a20a5ff23bb7e553b57 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 12 Sep 2023 17:33:37 -0400 Subject: [PATCH 002/102] WIP Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 39 +++++++++++++++++---- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index bdd79a3c2e..ec8ce24f20 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -1,19 +1,46 @@ # table-controls -## Documentation for our reusable hooks and components for managing state related to PatternFly tables +Our reusable hooks and components for managing state related to composable PatternFly Tables # Goals -TODO explain pros/cons of the legacy table vs composable table, and how the composability came at the cost of verbose code since page components are responsible for all the state -TODO explain that the goal is to restore some of the lost benefits of the legacy table abstraction without compromising the goals of the composable table. -TODO show tables with checks/Xs comparing legacy, composable, and composable w/ control hooks. goal is low-code but 100% composable with high refactorability. +These hooks and components are intended as the missing "batteries" for the composable PatternFly Table. When PatternFly moved away from the "batteries included" legacy Table for a more flexible and maintainable solution with the new composable Table components, the tradeoff was that the table itself can no longer manage its own state and its usage became more verbose. -# Terms/Concerns/Features (TODO) +The table-controls hooks and components provide a pattern where state logic can be encapsulated with simple configuration and JSX for rendering table elements can be shortened via the use of "prop helpers", but the consumer retains full control over the JSX composition and has access to all the state at any level. With this pattern, tables are simpler to build but we don't have to sacrifice any of the benefits gained by migrating from legacy to composable table. The pattern also allows for only partial use of the encapsulated state alongside manually passed props on any element; if there is a future need for the state to be used for rendering in a way we don't currently anticipate, that should not be blocked by this abstraction. -TODO cover terms like filtering, sorting, pagination, expansion, selection and active-row concerns (is concerns the best word? maybe domains? functions? features?) +The high-level goal is to provide an easy way to implement a featureful table with code that is as short and readable as it is composable and refactorable. + +# Features + +The functionality of the table-controls hooks is broken down into the following features. Most features are defined by operations to be performed on API data before it is displayed in a table. Code is decoupled along these feature/operation boundaries where possible. + +Note that filtering, sorting and pagination are special because they must be performed in a specific order to work correctly: filter and sort data, then paginate it. Using the higher-level hooks like `useLocalTableControls` or `useTableControlState`+`useTableControlProps` will take care of this for you (see [Usage](#usage)), but if you are handling pagination yourself with the lower-level hooks you'll need to be mindful of this order (see [Hooks](#hooks)). + +## Filtering + +Filter the data according to user-selected filters defined by configurable filter categories. + +Implemented by `useFilterState` (or `useFilterUrlParams`), `getLocalFilterDerivedState`, `getFilterProps`, and `getFilterHubRequestParams`. + +All of these are used internally by the higher-level hooks (see [Hooks](#hooks)). + +Arguments: Filter definitions (`filterCategories`) +State: User filter selections (`filterValues`) + +TODO left off here + +## Sorting + +## Pagination + +## Expansion + +## Active Row # Usage +`` + ## Table with client-side filtering/sorting/pagination logic TODO show top-level usage of useLocalTableControls with components for rendering From 1228b5c167519bb44322d0542216d77fd5a66bed Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 13 Sep 2023 13:59:06 -0400 Subject: [PATCH 003/102] WIP Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 52 +++++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index ec8ce24f20..e1c6820c95 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -2,41 +2,59 @@ Our reusable hooks and components for managing state related to composable PatternFly Tables -# Goals +# Why? + +These hooks and components are intended as the missing "batteries" for the composable PatternFly Table. When PatternFly moved away from the "batteries included" legacy monolith Table towards the newer composable Table components, the price of the improved flexibility was that the table itself can no longer manage its own state and its usage became more verbose with more required boilerplate code. -These hooks and components are intended as the missing "batteries" for the composable PatternFly Table. When PatternFly moved away from the "batteries included" legacy Table for a more flexible and maintainable solution with the new composable Table components, the tradeoff was that the table itself can no longer manage its own state and its usage became more verbose. +The table-controls hooks and components provide a pattern where state logic can be encapsulated with simple configuration and JSX for rendering table elements can be shortened via the use of "prop helpers", but the consumer can retain full control over the JSX composition and have access to all the state at any level. With this pattern, tables are simpler to build and maintain but we don't have to sacrifice any of the benefits gained by migrating from the legacy to the composable table. -The table-controls hooks and components provide a pattern where state logic can be encapsulated with simple configuration and JSX for rendering table elements can be shortened via the use of "prop helpers", but the consumer retains full control over the JSX composition and has access to all the state at any level. With this pattern, tables are simpler to build but we don't have to sacrifice any of the benefits gained by migrating from legacy to composable table. The pattern also allows for only partial use of the encapsulated state alongside manually passed props on any element; if there is a future need for the state to be used for rendering in a way we don't currently anticipate, that should not be blocked by this abstraction. +# Goals -The high-level goal is to provide an easy way to implement a featureful table with code that is as short and readable as it is composable and refactorable. +- Featureful tables should be easy to implement with code that is short and readable without sacrificing composability and refactorability. +- The consumer should be able to override any and all props manually on any element of the table. If there is a future need for table state to be used for rendering in a way we don't currently anticipate, that should not be blocked by this abstraction. +- Client-paginated and server-paginated tables should be similar to implement and share reusable. If a table needs to be converted between client logic and server logic, that should be relatively easy. +- Strict TypeScript types with generics inferred from parameters should be used to provide a safe and convenient development experience without having to repeat type annotations all over the page-level code. +- All features should be optional and fall back to reasonable defaults if their options are omitted. +- Code for each feature should be isolated enough that it could be reasonably used on its own. # Features -The functionality of the table-controls hooks is broken down into the following features. Most features are defined by operations to be performed on API data before it is displayed in a table. Code is decoupled along these feature/operation boundaries where possible. +The functionality of the table-controls hooks is broken down into the following features. Most features are defined by operations to be performed on API data before it is displayed in a table. -Note that filtering, sorting and pagination are special because they must be performed in a specific order to work correctly: filter and sort data, then paginate it. Using the higher-level hooks like `useLocalTableControls` or `useTableControlState`+`useTableControlProps` will take care of this for you (see [Usage](#usage)), but if you are handling pagination yourself with the lower-level hooks you'll need to be mindful of this order (see [Hooks](#hooks)). +Note that filtering, sorting and pagination are special because they must be performed in a specific order to work correctly: filter and sort data, then paginate it. Using the higher-level hooks like `useLocalTableControls` or `useTableControlState` + `useTableControlProps` will take care of this for you (see [Usage](#usage)), but if you are handling pagination yourself with the lower-level hooks you'll need to be mindful of this order (see [Hooks and Helper Functions](#hooks-and-helper-functions)). ## Filtering -Filter the data according to user-selected filters defined by configurable filter categories. - -Implemented by `useFilterState` (or `useFilterUrlParams`), `getLocalFilterDerivedState`, `getFilterProps`, and `getFilterHubRequestParams`. +Items are filtered according to user-selected filter key/value pairs. Keys and filter types (search, select, etc) are defined by configurable `filterCategories`. Logic for client-side filtering is defined in each `FilterCategory` object with the `getItemValue` callback, which is not required when using server-side filtering. -All of these are used internally by the higher-level hooks (see [Hooks](#hooks)). +Filter state is provided by `useFilterState` or `useFilterUrlParams`. For client-side filtering, the filter logic is provided by `getLocalFilterDerivedState` (based on `getItemValue`). For server-side filtering, filter state is serialized for the API by `getFilterHubRequestParams`. Filter-related component props are provided by `getFilterProps`, and the filter inputs and chips are rendered by the `FilterToolbar` component. All of these are used internally by the higher-level hooks and helpers (see [Hooks and Helper Functions](#hooks-and-helper-functions)). -Arguments: Filter definitions (`filterCategories`) -State: User filter selections (`filterValues`) - -TODO left off here +⚠️ NOTE: The `FilterToolbar` component and `FilterCategory` type predate the table-controls pattern and are not located in this directory. The abstraction there may be a little too opaque and it does not take full advantage of TypeScript generics. We may want to adjust that code to better fit these patterns and move it here. ## Sorting +Items are sorted according to user-selected sort column and direction. Sortable columns are identified by a `sortableColumns` array of `TColumnKey` values (see [Unique Identifiers](#unique-identifiers)). Logic for client-side sorting is defined by the `getSortValues` callback, which is not required when using server-side sorting. + ## Pagination ## Expansion ## Active Row +# ❗ Important Data Structure Notes + +## Item Objects, Not Row Objects + +None of the code here treats "rows" as their own data structure, because the structure and style of a row is a presentation detail that should be limited to the JSX where rows are rendered. Instead, this code works with arrays of "items" (the API data objects themselves) and makes all of an item's properties available where they might be needed. An item has the generic type `TItem`, which is inferred either from the type of the `items` array passed into `useLocalTableControls` (for client-paginated tables) or from the `currentPageItems` array passed into `useTableControlProps` (for server-paginated tables). See [Types](#types). + +⚠️ NOTE: For server-paginated tables the item data is not in scope until after the API query hook is called. This means the inferred `TItem` type is not available when passing arguments to `useTableControlState` (which must be called before API queries because its return values are needed to serialize filter/sort/pagination params for the API). `TItem` is resolved as `unknown` in this scope, which is usually fine since the arguments there don't need to know what type of items they are working with. If the item type is needed for any of these arguments it can be explicitly passed as a type param, but due to the way TypeScript type params currently work this means all other inferred params must also be explicitly passed (including all of the `TFilterCategoryKey`s). This makes for a lot of repeated code (although TypeScript will still enforce that it is all consistent). There is an upcoming TypeScript feature which allows partial inference in type params and may alleviate this in the future. See TypeScript pull requests [#26349](https://github.com/microsoft/TypeScript/pull/26349) and [#54047](https://github.com/microsoft/TypeScript/pull/54047). + +## Unique Identifiers + +Columns are identified by unique keys which are statically inferred from the keys of the `columnNames` object (used in many places via the generic type `TColumnKey`. See [Types](#types)). Any state which keeps track of something by column (such as which columns are sorted, and which columns are expanded in a compound-expandable row) uses these column keys as identifiers, and the user-facing column names can be looked up from the `columnNames` object anywhere a `columnKey` is present. Valid column keys are enforced via TypeScript generics; if a `columnKey` value is used that is not present in `columnNames`, you should get a type error. + +Item objects must contain some unique identifier which is either a string or number. The property key of this identifier must be passed into the hooks as `idProperty`, which will usually be `"id"`. If no unique identifier is present in the API data, an artificial one can be injected before passing the data into these hooks (see instances of `_ui_unique_id`), which can be done a the useQuery `select` callback. Any state which keeps track of something by item (i.e. by row) makes use of `item[idProperty]` as an identifier. Examples of this include selected rows, expandable rows and active rows. Valid `idProperty` values are also enforced by TypeScript generics; if an `idProperty` is provided that is not a property on the `TItem` type, you should get a type error. + # Usage `` @@ -56,7 +74,7 @@ TODO show top-level usage of those separated hooks with components for rendering TODO maybe move this to the bottom? TODO cover the stuff in types.ts export by export referencing the usage in specific hooks and components -# Hooks +# Hooks and Helper Functions ## Higher-level hooks (handle all state with combined options and return values) @@ -70,3 +88,7 @@ TODO list all the hooks for each concern and how they flow into each other in th TODO summarize why it is still useful to have some component abstractions even though the goal is to preserve all composability / leave all control of JSX to the consumer TODO list all the components exported from this directory, their signatures and why they are useful + +# Future Features and Improvements + +- It would be nice to support inline editable rows with a clean abstraction that fits into this pattern. From b3b725e2c681b8c8dfe8ceeb923efc2e5708f53b Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 13 Sep 2023 16:30:02 -0400 Subject: [PATCH 004/102] WIP - describe features Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 119 +++++++++++++++----- 1 file changed, 91 insertions(+), 28 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index e1c6820c95..87f0088e8c 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -2,13 +2,13 @@ Our reusable hooks and components for managing state related to composable PatternFly Tables -# Why? +## Why? These hooks and components are intended as the missing "batteries" for the composable PatternFly Table. When PatternFly moved away from the "batteries included" legacy monolith Table towards the newer composable Table components, the price of the improved flexibility was that the table itself can no longer manage its own state and its usage became more verbose with more required boilerplate code. The table-controls hooks and components provide a pattern where state logic can be encapsulated with simple configuration and JSX for rendering table elements can be shortened via the use of "prop helpers", but the consumer can retain full control over the JSX composition and have access to all the state at any level. With this pattern, tables are simpler to build and maintain but we don't have to sacrifice any of the benefits gained by migrating from the legacy to the composable table. -# Goals +## Goals - Featureful tables should be easy to implement with code that is short and readable without sacrificing composability and refactorability. - The consumer should be able to override any and all props manually on any element of the table. If there is a future need for table state to be used for rendering in a way we don't currently anticipate, that should not be blocked by this abstraction. @@ -17,78 +17,141 @@ The table-controls hooks and components provide a pattern where state logic can - All features should be optional and fall back to reasonable defaults if their options are omitted. - Code for each feature should be isolated enough that it could be reasonably used on its own. -# Features +## Features The functionality of the table-controls hooks is broken down into the following features. Most features are defined by operations to be performed on API data before it is displayed in a table. Note that filtering, sorting and pagination are special because they must be performed in a specific order to work correctly: filter and sort data, then paginate it. Using the higher-level hooks like `useLocalTableControls` or `useTableControlState` + `useTableControlProps` will take care of this for you (see [Usage](#usage)), but if you are handling pagination yourself with the lower-level hooks you'll need to be mindful of this order (see [Hooks and Helper Functions](#hooks-and-helper-functions)). -## Filtering +The state used by these features can be stored either in React state (provided by `use[Feature]State` hooks) or in the browser's URL query parameters (provided by `use[Feature]UrlParams` hooks). If URL params are used, the user's current filters, sort, pagination state, expanded/active rows and more are preserved when reloading the browser, using the browser Back and Forward buttons, or loading a bookmark. -Items are filtered according to user-selected filter key/value pairs. Keys and filter types (search, select, etc) are defined by configurable `filterCategories`. Logic for client-side filtering is defined in each `FilterCategory` object with the `getItemValue` callback, which is not required when using server-side filtering. +> ⚠️ TECH DEBT NOTE: This URL param behavior is currently all-or-nothing if you're using the higher-level hooks and not available when using the shorthand hook `useLocalTableControls`. This is because the generic hook for manipulating URL params (`useUrlParams`) cannot be used conditionally. We may want to refactor this later such that we have a hook like `useStateOrUrlParams` and make URL params opt-in on a feature-by-feature basis. This would also mean we could combine the `useTableControlState` and `useTableControlUrlParams` which currently are interchangeable, as well as combine each set of `use[Feature]State` and `use[Feature]UrlParams` hooks. See [Hooks and Helper Functions](#hooks-and-helper-functions). -Filter state is provided by `useFilterState` or `useFilterUrlParams`. For client-side filtering, the filter logic is provided by `getLocalFilterDerivedState` (based on `getItemValue`). For server-side filtering, filter state is serialized for the API by `getFilterHubRequestParams`. Filter-related component props are provided by `getFilterProps`, and the filter inputs and chips are rendered by the `FilterToolbar` component. All of these are used internally by the higher-level hooks and helpers (see [Hooks and Helper Functions](#hooks-and-helper-functions)). +All of the hooks and helpers described in this section are used internally by the higher-level hooks and helpers, and do not need to be used directly (see [Hooks and Helper Functions](#hooks-and-helper-functions) and [Usage](#usage)). -⚠️ NOTE: The `FilterToolbar` component and `FilterCategory` type predate the table-controls pattern and are not located in this directory. The abstraction there may be a little too opaque and it does not take full advantage of TypeScript generics. We may want to adjust that code to better fit these patterns and move it here. +### Filtering -## Sorting +Items are filtered according to the user-selected filter key/value pairs. -Items are sorted according to user-selected sort column and direction. Sortable columns are identified by a `sortableColumns` array of `TColumnKey` values (see [Unique Identifiers](#unique-identifiers)). Logic for client-side sorting is defined by the `getSortValues` callback, which is not required when using server-side sorting. +- Keys and filter types (search, select, etc) are defined by the `filterCategories` array config argument. The `key` properties of each of these `FilterCategory` objects are the source of truth for the inferred generic type `TFilterCategoryKeys` (see [Types](#types)). +- Filter state is provided by `useFilterState` or `useFilterUrlParams`. +- For client-side filtering, the filter logic is provided by `getLocalFilterDerivedState` (based on the `getItemValue` callback defined on each `FilterCategory` object, which is not required when using server-side filtering). +- For server-side filtering, filter state is serialized for the API by `getFilterHubRequestParams`. +- Filter-related component props are provided by `getFilterProps`. +- Filter inputs and chips are rendered by the `FilterToolbar` component. -## Pagination +> ⚠️ TECH DEBT NOTE: The `FilterToolbar` component and `FilterCategory` type predate the table-controls pattern and are not located in this directory. The abstraction there may be a little too opaque and it does not take full advantage of TypeScript generics. We may want to adjust that code to better fit these patterns and move it here. -## Expansion +### Sorting -## Active Row +Items are sorted according to the user-selected sort column and direction. -# ❗ Important Data Structure Notes +- Sortable columns are defined by a `sortableColumns` array of `TColumnKey` values (see [Unique Identifiers](#unique-identifiers)). +- Sort state is provided by `useSortState` or `useSortUrlParams`. +- For client-side sorting, the sort logic is provided by `getLocalSortDerivedState` (based on the `getSortValues` config argument, which is not required when using server-side sorting). +- For server-side sorting, sort state is serialized for the API by `getSortHubRequestParams`. +- Sort-related component props are provided by `getSortProps`. +- Sort inputs are rendered by the table's `Th` component. -## Item Objects, Not Row Objects +### Pagination -None of the code here treats "rows" as their own data structure, because the structure and style of a row is a presentation detail that should be limited to the JSX where rows are rendered. Instead, this code works with arrays of "items" (the API data objects themselves) and makes all of an item's properties available where they might be needed. An item has the generic type `TItem`, which is inferred either from the type of the `items` array passed into `useLocalTableControls` (for client-paginated tables) or from the `currentPageItems` array passed into `useTableControlProps` (for server-paginated tables). See [Types](#types). +Items are paginated according to the user-selected page number and items-per-page count. -⚠️ NOTE: For server-paginated tables the item data is not in scope until after the API query hook is called. This means the inferred `TItem` type is not available when passing arguments to `useTableControlState` (which must be called before API queries because its return values are needed to serialize filter/sort/pagination params for the API). `TItem` is resolved as `unknown` in this scope, which is usually fine since the arguments there don't need to know what type of items they are working with. If the item type is needed for any of these arguments it can be explicitly passed as a type param, but due to the way TypeScript type params currently work this means all other inferred params must also be explicitly passed (including all of the `TFilterCategoryKey`s). This makes for a lot of repeated code (although TypeScript will still enforce that it is all consistent). There is an upcoming TypeScript feature which allows partial inference in type params and may alleviate this in the future. See TypeScript pull requests [#26349](https://github.com/microsoft/TypeScript/pull/26349) and [#54047](https://github.com/microsoft/TypeScript/pull/54047). +- The only config argument for pagination is the optional `initialItemsPerPage` which defaults to 10. +- Pagination state is provided by `usePaginationState` or `usePaginationUrlParams`. +- For client-side pagination, the pagination logic is provided by `getLocalPaginationDerivedState`. +- For server-side pagination, pagination state is serialized for the API by `getPaginationHubRequestParams`. +- Pagination-related component props are provided by `getPaginationProps`. +- A `useEffect` call which prevents invalid state after an item is deleted is provided by `usePaginationEffects`. +- Pagination inputs are rendered by our `SimplePagination` component which is a thin wrapper around the PatternFly `Pagination` component. -## Unique Identifiers +> ⚠️ TECH DEBT NOTE: Do we really need `SimplePagination`? -Columns are identified by unique keys which are statically inferred from the keys of the `columnNames` object (used in many places via the generic type `TColumnKey`. See [Types](#types)). Any state which keeps track of something by column (such as which columns are sorted, and which columns are expanded in a compound-expandable row) uses these column keys as identifiers, and the user-facing column names can be looked up from the `columnNames` object anywhere a `columnKey` is present. Valid column keys are enforced via TypeScript generics; if a `columnKey` value is used that is not present in `columnNames`, you should get a type error. +### Expansion + +Item details can be expanded, either with a "single expansion" variant where an entire row is expanded to show more detail or a "compound expansion" variant where individual cells in a row are expanded. This is tracked in state by a mapping of item ids (derived from the `idProperty` config argument) to an array of either boolean values (for single expansion) or `columnKey` values that are expanded for that item (for compound expansion). See [Unique Identifiers](#unique-identifiers) for more on `idProperty` and `columnKey`. + +- Single or compound expansion is defined by the optional `expandableVariant` config argument which defaults to `"single"`. +- Expansion state is provided by `useExpansionState` or `useExpansionUrlParams`. +- Expansion shorthand functions are provided by `getExpansionDerivedState`. +- Expansion is never managed server-side. +- Expansion-related component props are provided inside `useTableControlProps` in the `getSingleExpandTdProps` and `getCompoundExpandTdProps` functions. +- Expansion inputs are rendered by the table's `Td` component and expanded content is managed at the consumer level by conditionally rendering a second row with full colSpan in a `Tbody` component. The `numRenderedColumns` value returned by `useTableControlProps` can be used for the correct colSpan here. + +> ⚠️ TECH DEBT NOTE: `getSingleExpandTdProps` and `getCompoundExpandTdProps` should probably be factored out of `useTableControlProps` into a decoupled `getExpansionProps` helper. + +### Active Row + +An item can be clicked to mark it as "active", which usually opens a drawer on the page to show more detail. Note that this is distinct from expansion and selection and these features can all be used together. Active row state is simply a single id value (number or string) for the active item, derived from the `idProperty` config argument (see [Unique Identifiers](#unique-identifiers)). + +- The active row feature requires no config arguments. +- Active row state is provided by `useActiveRowState` or `useActiveRowUrlParams`. +- Active row shorthand functions are provided by `getActiveRowDerivedState`. +- A `useEffect` call which prevents invalid state after an item is deleted is provided by `useActiveRowEffects`. + +⚠️ TECH DEBT NOTE: We may want to rename the "active row" feature and code to "active item" to be consistent about using "item" naming rather than "row" naming outside the rendering code (see [Item Objects, Not Row Objects](#item-objects-not-row-objects). + +### Selection + +Items can be selected with checkboxes on each row or with a bulk select control that provides actions like "select all", "select none" and "select page". + +> ⚠️ TECH DEBT NOTE: Currently, selection state has not been refactored to be a part of the table-controls pattern and we are still relying on the old `useSelectionState` from lib-ui (which dates back to Forklift). The return value of `useSelectionState` is required by `useTableControlProps`. Mike is working on a refactor to bring selection state hooks into this directory. + +## ❗ Important Data Structure Notes + +### Item Objects, Not Row Objects + +None of the code here treats "rows" as their own data structure, because the structure and style of a row is a presentation detail that should be limited to the JSX where rows are rendered. Instead, this code works with arrays of "items" (the API data objects themselves) and makes all of an item's properties available where they might be needed without extra lookups. An item object has the generic type `TItem`, which is inferred either from the type of the `items` array passed into `useLocalTableControls` (for client-paginated tables) or from the `currentPageItems` array passed into `useTableControlProps` (for server-paginated tables). See [Types](#types). + +⚠️ CAVEAT / TECH DEBT NOTE: For server-paginated tables the item data is not in scope until after the API query hook is called, but the `useTableControlState` or `useTableControlUrlParams` hook must be called _before_ API queries because its return values are needed to serialize filter/sort/pagination params for the API. This means the inferred `TItem` type is not available when passing arguments to `useTableControlState` or `useTableControlUrlParams`. `TItem` resolves to `unknown` in this scope, which is usually fine since the arguments there don't need to know what type of items they are working with. If the item type is needed for any of these arguments it can be explicitly passed as a type param. However, since TypeScript generic type param lists are all-or-nothing (you must either omit the list and infer all generics for a function or pass them all explicitly), this means all other normally inferred type params must be explicitly passed (including all of the `TColumnKey`s and `TFilterCategoryKey`s). This makes for some redundant code, although TypeScript will still enforce that it is all consistent. There is a possible upcoming TypeScript feature which would allow partial inference in type param lists and may alleviate this in the future. See TypeScript pull requests [#26349](https://github.com/microsoft/TypeScript/pull/26349) and [#54047](https://github.com/microsoft/TypeScript/pull/54047). + +### Unique Identifiers + +Columns are identified by unique keys which are statically inferred from the keys of the `columnNames` object (used in many places via the inferred generic type `TColumnKey`. See [Types](#types)). Any state which keeps track of something by column (such as which columns are sorted, and which columns are expanded in a compound-expandable row) uses these column keys as identifiers, and the user-facing column names can be looked up from the `columnNames` object anywhere a `columnKey` is present. Valid column keys are enforced via TypeScript generics; if a `columnKey` value is used that is not present in `columnNames`, you should get a type error. Item objects must contain some unique identifier which is either a string or number. The property key of this identifier must be passed into the hooks as `idProperty`, which will usually be `"id"`. If no unique identifier is present in the API data, an artificial one can be injected before passing the data into these hooks (see instances of `_ui_unique_id`), which can be done a the useQuery `select` callback. Any state which keeps track of something by item (i.e. by row) makes use of `item[idProperty]` as an identifier. Examples of this include selected rows, expandable rows and active rows. Valid `idProperty` values are also enforced by TypeScript generics; if an `idProperty` is provided that is not a property on the `TItem` type, you should get a type error. -# Usage +### Top-level Config Arguments and the `tableControls` Object + +TODO explain how the args and return values from each hook get bundled together into a shared object. +TODO is this the best place in the docs for this section? +TODO tech debt note about how the structure of this is currently inferred from return values of all the hooks and should probably be explicitly defined in the types.ts file + +## Usage -`` +TODO -## Table with client-side filtering/sorting/pagination logic +### Table with client-side filtering/sorting/pagination logic TODO show top-level usage of useLocalTableControls with components for rendering -## Table with server-side filtering/sorting/pagination logic +### Table with server-side filtering/sorting/pagination logic TODO explain benefits and limitations of server-side table logic TODO explain separating useLocalTableControls into useTableControlState/useTableControlUrlParams and useTableControlProps so we can leverage state when making API requests and have API data in scope for prop helpers TODO show top-level usage of those separated hooks with components for rendering -# Types +## Types TODO maybe move this to the bottom? TODO cover the stuff in types.ts export by export referencing the usage in specific hooks and components -# Hooks and Helper Functions +## Hooks and Helper Functions -## Higher-level hooks (handle all state with combined options and return values) +### Higher-level hooks (handle all state with combined options and return values) TODO list all the hooks used above and their signatures and implementation overview -## Lower-level hooks (used internally by composite hooks but also usable standalone) +### Lower-level hooks (used internally by composite hooks but also usable standalone) TODO list all the hooks for each concern and how they flow into each other in the composite hooks -# Components +## Components TODO summarize why it is still useful to have some component abstractions even though the goal is to preserve all composability / leave all control of JSX to the consumer TODO list all the components exported from this directory, their signatures and why they are useful -# Future Features and Improvements +## Future Features and Improvements +- Tech debt notes above should be addressed. - It would be nice to support inline editable rows with a clean abstraction that fits into this pattern. From 6d2f94f7542247e44282d09e3d760188b7947359 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 13 Sep 2023 17:28:18 -0400 Subject: [PATCH 005/102] WIP - flesh out all sections through some of Usage Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 71 ++++++++++++++++----- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 87f0088e8c..8873ab68f9 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -13,6 +13,7 @@ The table-controls hooks and components provide a pattern where state logic can - Featureful tables should be easy to implement with code that is short and readable without sacrificing composability and refactorability. - The consumer should be able to override any and all props manually on any element of the table. If there is a future need for table state to be used for rendering in a way we don't currently anticipate, that should not be blocked by this abstraction. - Client-paginated and server-paginated tables should be similar to implement and share reusable. If a table needs to be converted between client logic and server logic, that should be relatively easy. +- There should not be a concept of a "row object" because rows are presentational details and defining them as a separate model from the API data causes unnecessary complexity. See [Item Objects, Not Row Objects](#item-objects-not-row-objects). - Strict TypeScript types with generics inferred from parameters should be used to provide a safe and convenient development experience without having to repeat type annotations all over the page-level code. - All features should be optional and fall back to reasonable defaults if their options are omitted. - Code for each feature should be isolated enough that it could be reasonably used on its own. @@ -31,7 +32,7 @@ All of the hooks and helpers described in this section are used internally by th ### Filtering -Items are filtered according to the user-selected filter key/value pairs. +Items are filtered according to user-selected filter key/value pairs. - Keys and filter types (search, select, etc) are defined by the `filterCategories` array config argument. The `key` properties of each of these `FilterCategory` objects are the source of truth for the inferred generic type `TFilterCategoryKeys` (see [Types](#types)). - Filter state is provided by `useFilterState` or `useFilterUrlParams`. @@ -93,23 +94,61 @@ An item can be clicked to mark it as "active", which usually opens a drawer on t ### Selection -Items can be selected with checkboxes on each row or with a bulk select control that provides actions like "select all", "select none" and "select page". +Items can be selected with checkboxes on each row or with a bulk select control that provides actions like "select all", "select none" and "select page". The list of selected item ids in state can be used to perform bulk actions. -> ⚠️ TECH DEBT NOTE: Currently, selection state has not been refactored to be a part of the table-controls pattern and we are still relying on the old `useSelectionState` from lib-ui (which dates back to Forklift). The return value of `useSelectionState` is required by `useTableControlProps`. Mike is working on a refactor to bring selection state hooks into this directory. +> ⚠️ TECH DEBT NOTE: Currently, selection state has not yet been refactored to be a part of the table-controls pattern and we are still relying on [the old `useSelectionState` from lib-ui](https://migtools.github.io/lib-ui/?path=/docs/hooks-useselectionstate--checkboxes) which dates back to older migtools projects. The return value of this legacy `useSelectionState` is required by `useTableControlProps`. Mike is working on a refactor to bring selection state hooks into this directory. -## ❗ Important Data Structure Notes +## Important Data Structure Notes ### Item Objects, Not Row Objects -None of the code here treats "rows" as their own data structure, because the structure and style of a row is a presentation detail that should be limited to the JSX where rows are rendered. Instead, this code works with arrays of "items" (the API data objects themselves) and makes all of an item's properties available where they might be needed without extra lookups. An item object has the generic type `TItem`, which is inferred either from the type of the `items` array passed into `useLocalTableControls` (for client-paginated tables) or from the `currentPageItems` array passed into `useTableControlProps` (for server-paginated tables). See [Types](#types). +None of the code here treats "rows" as their own data structure. The content and style of a row is a presentational detail that should be limited to the JSX where rows are rendered. When an array of row objects is used, those objects tend to duplicate API data with a different structure and the code must reason about two different representations of the data. Instead, this code works directly with arrays of "items" (the API data objects themselves) and makes all of an item's properties available where they might be needed without extra lookups. The consumer maps over item objects and derives row components from them only at render time. -⚠️ CAVEAT / TECH DEBT NOTE: For server-paginated tables the item data is not in scope until after the API query hook is called, but the `useTableControlState` or `useTableControlUrlParams` hook must be called _before_ API queries because its return values are needed to serialize filter/sort/pagination params for the API. This means the inferred `TItem` type is not available when passing arguments to `useTableControlState` or `useTableControlUrlParams`. `TItem` resolves to `unknown` in this scope, which is usually fine since the arguments there don't need to know what type of items they are working with. If the item type is needed for any of these arguments it can be explicitly passed as a type param. However, since TypeScript generic type param lists are all-or-nothing (you must either omit the list and infer all generics for a function or pass them all explicitly), this means all other normally inferred type params must be explicitly passed (including all of the `TColumnKey`s and `TFilterCategoryKey`s). This makes for some redundant code, although TypeScript will still enforce that it is all consistent. There is a possible upcoming TypeScript feature which would allow partial inference in type param lists and may alleviate this in the future. See TypeScript pull requests [#26349](https://github.com/microsoft/TypeScript/pull/26349) and [#54047](https://github.com/microsoft/TypeScript/pull/54047). +An item object has the generic type `TItem`, which is inferred either from the type of the `items` array passed into `useLocalTableControls` (for client-paginated tables) or from the `currentPageItems` array passed into `useTableControlProps` (for server-paginated tables). See [Types](#types). + +> ℹ️ CAVEAT: For server-paginated tables the item data is not in scope until after the API query hook is called, but the `useTableControlState` or `useTableControlUrlParams` hook must be called _before_ API queries because its return values are needed to serialize filter/sort/pagination params for the API. This means the inferred `TItem` type is not available when passing arguments to `useTableControlState` or `useTableControlUrlParams`. `TItem` resolves to `unknown` in this scope, which is usually fine since the arguments there don't need to know what type of items they are working with. If the item type is needed for any of these arguments it can be explicitly passed as a type param. However... +> +> ⚠️ TECH DEBT NOTE: Since TypeScript generic type param lists are all-or-nothing (you must either omit the list and infer all generics for a function or pass them all explicitly), this means all other type params which are normally inferred must be explicitly passed (including all of the `TColumnKey`s and `TFilterCategoryKey`s). This makes for some redundant code, although TypeScript will still enforce that it is all consistent. There is a possible upcoming TypeScript language feature which would allow partial inference in type param lists and may alleviate this in the future. See TypeScript pull requests [#26349](https://github.com/microsoft/TypeScript/pull/26349) and [#54047](https://github.com/microsoft/TypeScript/pull/54047). ### Unique Identifiers -Columns are identified by unique keys which are statically inferred from the keys of the `columnNames` object (used in many places via the inferred generic type `TColumnKey`. See [Types](#types)). Any state which keeps track of something by column (such as which columns are sorted, and which columns are expanded in a compound-expandable row) uses these column keys as identifiers, and the user-facing column names can be looked up from the `columnNames` object anywhere a `columnKey` is present. Valid column keys are enforced via TypeScript generics; if a `columnKey` value is used that is not present in `columnNames`, you should get a type error. +Table columns are identified by unique keys which are statically inferred from the keys of the `columnNames` object (used in many places via the inferred generic type `TColumnKey`. See [Types](#types)). Any state which keeps track of something by column (such as which columns are sorted, and which columns are expanded in a compound-expandable row) uses these column keys as identifiers, and the user-facing column names can be looked up from the `columnNames` object anywhere a `columnKey` is present. Valid column keys are enforced via TypeScript generics; if a `columnKey` value is used that is not present in `columnNames`, you should get a type error. + +Item objects must contain some unique identifier which is either a string or number. The property key of this identifier is a required config argument called `idProperty`, which will usually be `"id"`. If no unique identifier is present in the API data, an artificial one can be injected before passing the data into these hooks, which can be done in the useQuery `select` callback (see instances where we have used `"_ui_unique_id"`). Any state which keeps track of something by item (i.e. by row) makes use of `item[idProperty]` as an identifier. Examples of this include selected rows, expanded rows and active rows. Valid `idProperty` values are also enforced by TypeScript generics; if an `idProperty` is provided that is not a property on the `TItem` type, you should get a type error. + +## Usage + +### Should I Use Client or Server Logic? + +If the API endpoints you're using support server-side pagination parameters, it is generally a good idea to use them for better performance and scalability. If you do use server-side pagination, you'll need to also use server-side filtering and sorting. + +If the endpoints do not support these parameters or you need to have the entire collection of items in memory at once for some other reason, you'll need a client-paginated table. It is also slightly easier to implement a client-paginated table. + +### Which Hooks/Functions Do I Need? + +In most cases, you'll only need to use these higher-level hooks and helpers to build a table: + +- For client-paginated tables: `useLocalTableControls` is all you need. + - Internally it uses `useTableControlState`, `useTableControlProps` and the `getLocal[Feature]DerivedState` helpers. The config arguments object is a combination of the arguments required by `useTableControlState` and `useTableControlProps`. + - The return value (an object we generally name `tableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. +- For server-paginated tables: `useTableControlState` (or `useTableControlUrlParams`), `getHubRequestParams`, and `useTableControlProps`. + - Choose whether you want to use React state or URL params as the source of truth, and use `useTableControlState` or `useTableControlUrlParams` which are interchangeable. + - Take the object returned by that hook (generally named `tableControlState`) and pass it to `getHubRequestParams` function (you may need to spread it and add additional properties like `hubSortFieldKeys`). + - Call your API query hooks, using the `hubRequestParams` as needed. + - Call `useTableControlProps` and pass it an object including all properties from `tableControlState` along with additional config arguments. Some of these arguments will be derived from your API data, such as `currentPageItems`, `totalItemCount` and `isLoading`. Others are simply passed here rather than above because they are used only for rendering and not required for state management. + - The return value (the same `tableControls` object returned by `useLocalTableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. -Item objects must contain some unique identifier which is either a string or number. The property key of this identifier must be passed into the hooks as `idProperty`, which will usually be `"id"`. If no unique identifier is present in the API data, an artificial one can be injected before passing the data into these hooks (see instances of `_ui_unique_id`), which can be done a the useQuery `select` callback. Any state which keeps track of something by item (i.e. by row) makes use of `item[idProperty]` as an identifier. Examples of this include selected rows, expandable rows and active rows. Valid `idProperty` values are also enforced by TypeScript generics; if an `idProperty` is provided that is not a property on the `TItem` type, you should get a type error. +If desired, you can use the lower-level hooks provided here on their own (for example, if you really only need pagination and you're not rendering a full table). However, if you are using more than one or two of them you may want to consider using the higher-level hooks above even if you don't need all the features. You can omit the config arguments for any features you don't need and then just don't use the relevant `propHelpers`. + +--- + +


+ +# NOTE: Sections below this line are WIP. Ask Mike for clarification if you need it before he finishes writing this. + +


+ +--- ### Top-level Config Arguments and the `tableControls` Object @@ -117,19 +156,15 @@ TODO explain how the args and return values from each hook get bundled together TODO is this the best place in the docs for this section? TODO tech debt note about how the structure of this is currently inferred from return values of all the hooks and should probably be explicitly defined in the types.ts file -## Usage - -TODO - -### Table with client-side filtering/sorting/pagination logic +### Example Table with client-side filtering/sorting/pagination logic +TODO explain benefits and limitations of client-side table logic TODO show top-level usage of useLocalTableControls with components for rendering -### Table with server-side filtering/sorting/pagination logic +### Example Table with server-side filtering/sorting/pagination logic TODO explain benefits and limitations of server-side table logic -TODO explain separating useLocalTableControls into useTableControlState/useTableControlUrlParams and useTableControlProps so we can leverage state when making API requests and have API data in scope for prop helpers -TODO show top-level usage of those separated hooks with components for rendering +TODO show top-level usage of useTableControlState/useTableControlUrlParams with components for rendering ## Types @@ -138,6 +173,9 @@ TODO cover the stuff in types.ts export by export referencing the usage in speci ## Hooks and Helper Functions +TODO maybe this section isn't necessary anymore if we go into enough detail in features and usage +TODO if we remove this remember to remove/change anchor links above + ### Higher-level hooks (handle all state with combined options and return values) TODO list all the hooks used above and their signatures and implementation overview @@ -148,6 +186,7 @@ TODO list all the hooks for each concern and how they flow into each other in th ## Components +TODO maybe this section isn't necessary anymore if we go into enough detail in features and usage TODO summarize why it is still useful to have some component abstractions even though the goal is to preserve all composability / leave all control of JSX to the consumer TODO list all the components exported from this directory, their signatures and why they are useful From 3b97a63ec2f756807503b54e1cbf60aefb503d56 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 14 Sep 2023 10:15:17 -0400 Subject: [PATCH 006/102] Typo Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 8873ab68f9..967964afe0 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -12,7 +12,7 @@ The table-controls hooks and components provide a pattern where state logic can - Featureful tables should be easy to implement with code that is short and readable without sacrificing composability and refactorability. - The consumer should be able to override any and all props manually on any element of the table. If there is a future need for table state to be used for rendering in a way we don't currently anticipate, that should not be blocked by this abstraction. -- Client-paginated and server-paginated tables should be similar to implement and share reusable. If a table needs to be converted between client logic and server logic, that should be relatively easy. +- Client-paginated and server-paginated tables should be similar to implement and share reusable code. If a table needs to be converted between client logic and server logic, that should be relatively easy. - There should not be a concept of a "row object" because rows are presentational details and defining them as a separate model from the API data causes unnecessary complexity. See [Item Objects, Not Row Objects](#item-objects-not-row-objects). - Strict TypeScript types with generics inferred from parameters should be used to provide a safe and convenient development experience without having to repeat type annotations all over the page-level code. - All features should be optional and fall back to reasonable defaults if their options are omitted. @@ -26,7 +26,7 @@ Note that filtering, sorting and pagination are special because they must be per The state used by these features can be stored either in React state (provided by `use[Feature]State` hooks) or in the browser's URL query parameters (provided by `use[Feature]UrlParams` hooks). If URL params are used, the user's current filters, sort, pagination state, expanded/active rows and more are preserved when reloading the browser, using the browser Back and Forward buttons, or loading a bookmark. -> ⚠️ TECH DEBT NOTE: This URL param behavior is currently all-or-nothing if you're using the higher-level hooks and not available when using the shorthand hook `useLocalTableControls`. This is because the generic hook for manipulating URL params (`useUrlParams`) cannot be used conditionally. We may want to refactor this later such that we have a hook like `useStateOrUrlParams` and make URL params opt-in on a feature-by-feature basis. This would also mean we could combine the `useTableControlState` and `useTableControlUrlParams` which currently are interchangeable, as well as combine each set of `use[Feature]State` and `use[Feature]UrlParams` hooks. See [Hooks and Helper Functions](#hooks-and-helper-functions). +> ⚠️ TECH DEBT NOTE: This URL param behavior is currently all-or-nothing if you're using the higher-level hooks and not available when using the shorthand hook `useLocalTableControls`. This is because the generic hook for manipulating URL params (`useUrlParams`) cannot be used conditionally. We may want to refactor this later such that we have a hook like `useStateOrUrlParams` and make URL params opt-in on a feature-by-feature basis. This would also mean we could combine `useTableControlState` and `useTableControlUrlParams` which currently are interchangeable, as well as combine each set of `use[Feature]State` and `use[Feature]UrlParams` hooks. See [Hooks and Helper Functions](#hooks-and-helper-functions). All of the hooks and helpers described in this section are used internally by the higher-level hooks and helpers, and do not need to be used directly (see [Hooks and Helper Functions](#hooks-and-helper-functions) and [Usage](#usage)). From 880758502421af70d7ae59e3989bd34803bba30b Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 14 Sep 2023 11:04:52 -0400 Subject: [PATCH 007/102] Clarify expansion state Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 967964afe0..964e7db841 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -70,7 +70,7 @@ Items are paginated according to the user-selected page number and items-per-pag ### Expansion -Item details can be expanded, either with a "single expansion" variant where an entire row is expanded to show more detail or a "compound expansion" variant where individual cells in a row are expanded. This is tracked in state by a mapping of item ids (derived from the `idProperty` config argument) to an array of either boolean values (for single expansion) or `columnKey` values that are expanded for that item (for compound expansion). See [Unique Identifiers](#unique-identifiers) for more on `idProperty` and `columnKey`. +Item details can be expanded, either with a "single expansion" variant where an entire row is expanded to show more detail or a "compound expansion" variant where an individual cell in a row (one at a time per row) is expanded. This is tracked in state by a mapping of item ids (derived from the `idProperty` config argument) to either a boolean value (for single expansion) or a `columnKey` value (for compound expansion). See [Unique Identifiers](#unique-identifiers) for more on `idProperty` and `columnKey`. - Single or compound expansion is defined by the optional `expandableVariant` config argument which defaults to `"single"`. - Expansion state is provided by `useExpansionState` or `useExpansionUrlParams`. From a4d6a2530df610a232ee9b4c3f7e762d1b523e22 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 14 Sep 2023 11:08:04 -0400 Subject: [PATCH 008/102] WIP Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 964e7db841..46769e48fa 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -90,7 +90,7 @@ An item can be clicked to mark it as "active", which usually opens a drawer on t - Active row shorthand functions are provided by `getActiveRowDerivedState`. - A `useEffect` call which prevents invalid state after an item is deleted is provided by `useActiveRowEffects`. -⚠️ TECH DEBT NOTE: We may want to rename the "active row" feature and code to "active item" to be consistent about using "item" naming rather than "row" naming outside the rendering code (see [Item Objects, Not Row Objects](#item-objects-not-row-objects). +> ⚠️ TECH DEBT NOTE: We may want to rename the "active row" feature and code to "active item" to be consistent about using "item" naming rather than "row" naming outside the rendering code (see [Item Objects, Not Row Objects](#item-objects-not-row-objects)). ### Selection From 60f717bbc99e2388107f464e25df648350c7eade Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 22 Sep 2023 17:06:47 -0400 Subject: [PATCH 009/102] Add code examples and rearrange some sections Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 257 ++++++++++++++++---- 1 file changed, 216 insertions(+), 41 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 46769e48fa..6e562f570e 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -18,6 +18,222 @@ The table-controls hooks and components provide a pattern where state logic can - All features should be optional and fall back to reasonable defaults if their options are omitted. - Code for each feature should be isolated enough that it could be reasonably used on its own. +## Usage + +### Example table with client-side filtering/sorting/pagination + +For client-paginated tables, we only need the `useLocalTableControls` hook. All arguments can be passed to it in one object, and the `tableControls` object returned by it contains everything we need to render the composable table. + +This simple example includes only the filtering, sorting and pagination features and excludes arguments and properties related to the other features (see [Features](#features)). + +```tsx +// In a real table, this API data would come from a useQuery call. +const isLoading = false; +const isError = false; +const things: Thing[] = [ + { id: 1, name: "Thing 1", description: "Something from the API" }, + { id: 2, name: "Thing 2", description: "Something else from the API" }, +]; + +const tableControls = useLocalTableControls({ + idProperty: "id", // The name of a unique string or number property on the data items. + items: things, // The generic type `TItem` is inferred from the items passed here. + columnNames: { + // The keys of this object define the inferred generic type `TColumnKey`. See "Unique Identifiers". + name: "Name", + description: "Description", + }, + filterCategories: [ + { + key: "name", + title: "Name", + type: FilterType.search, + placeholderText: + t("actions.filterBy", { + what: t("terms.name").toLowerCase(), + }) + "...", + getItemValue: (thing) => thing.name || "", + }, + ], + sortableColumns: ["name", "description"], + getSortValues: (thing) => ({ + name: thing.name || "", + description: thing.description || "", + }), + initialSort: { columnKey: "name", direction: "asc" }, + isLoading, +}); + +// Here we destructure some of the properties from `tableControls` for rendering. +// Later we also spread the entire `tableControls` object onto components whose props include subsets of this object. +const { + currentPageItems, + numRenderedColumns, + // `numRenderedColumns` is based on which features are used (selection, expansion, etc) and the number of columnNames. + // It is used as the colSpan when rendering a full-table-wide cell (e.g. expanded content when using expansion). + selectionState: { selectedItems }, + // The objects and functions in `propHelpers` correspond to the props needed for specific PatternFly or Tackle components and are provided to reduce prop-drilling and make the rendering code as short as possible. + propHelpers: { + toolbarProps, + filterToolbarProps, + paginationToolbarItemProps, + paginationProps, + tableProps, + getThProps, + getTdProps, + }, +} = tableControls; + +return ( + <> + + + + {/* You can render whatever other custom toolbar items you may need here! */} + + + + + + + + + + + + + + + No things available + + + } + numRenderedColumns={numRenderedColumns} + > + + {currentPageItems?.map((thing, rowIndex) => ( + + + + + + + ))} + + +
+ + +
+ {thing.name} + + {thing.description} +
+ + <> +); +``` + +### Example table with server-side filtering/sorting/pagination + +The usage is similar here, but some arguments are no longer required (like `getSortValues` and the `getItemValue` property of the filter category) and we break up the arguments object passed to `useLocalTableControls` into two separate objects passed to `useTableControlState` and `useTableControlProps` based on when they are needed. You'll note that the object passed to the latter contains all the properties of the object passed to the former in addition to things derived from the fetched API data. Those arguments are all also included in the `tableControls` object returned by `useTableControlProps` (and `useLocalTableControls` above). This way, we have one big object we can pass around to any components or functions that need any of the configuration, state, derived state, or props present on it. + +Note also: the destructuring and rendering part of the example code is not included here because **_it is identical to the example above_**. The only difference between client-paginated and server-paginated tables is the hook usage; the `tableControls` object and its usage are the same for both. + +```tsx +// In a real table, this API data would come from a useQuery call. +const tableControlState = useTableControlState({ + columnNames: { + name: "Name", + description: "Description", + }, + filterCategories: [ + { + key: "name", + title: "Name", + type: FilterType.search, + placeholderText: + t("actions.filterBy", { + what: t("terms.name").toLowerCase(), + }) + "...", + getItemValue: (thing) => thing.name || "", + }, + ], + sortableColumns: ["name", "description"], + initialSort: { columnKey: "name", direction: "asc" }, +}); + +const hubRequestParams = getHubRequestParams({ + ...tableControlState, // Includes filterState, sortState and paginationState + hubSortFieldKeys: { + // The keys required for sorting on the server, in case they aren't the same as our columns here + name: "name", + description: "description", + }, +}); + +// `useFetchThings` is an example of a custom hook that calls a react-query `useQuery` and the `serializeRequestParamsForHub` helper. +// Any API fetch implementation could be used here as long as it will re-fetch when `hubRequestParams` changes. +// The `data` returned here has been paginated, filtered and sorted on the server. +const { data, totalItemCount, isLoading, isError } = + useFetchThings(hubRequestParams); + +const tableControls = useTableControlProps({ + ...tableControlState, // Includes filterState, sortState and paginationState + idProperty: "id", + currentPageItems: data, + totalItemCount, + isLoading, +}); + +// Everything else (destructuring `tableControls` and returning JSX) is the same as the client-side example! +``` + +### Kitchen sink example with all features + +TODO + +### Should I Use Client or Server Logic? + +If the API endpoints you're using support server-side pagination parameters, it is generally a good idea to use them for better performance and scalability. If you do use server-side pagination, you'll need to also use server-side filtering and sorting. + +If the endpoints do not support these parameters or you need to have the entire collection of items in memory at once for some other reason, you'll need a client-paginated table. It is also slightly easier to implement a client-paginated table. + +### Which Hooks/Functions Do I Need? + +In most cases, you'll only need to use these higher-level hooks and helpers to build a table: + +- For client-paginated tables: `useLocalTableControls` or `useLocalTableControlsWithUrlParams` is all you need. These have the same signature and are interchangeable. + - Internally they use `useTableControlState` (or `useTableControlUrlParams`), `useTableControlProps` and the `getLocal[Feature]DerivedState` helpers. The config arguments object is a combination of the arguments required by `useTableControlState` and `useTableControlProps`. + - The return value (an object we generally name `tableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. +- For server-paginated tables: `useTableControlState` (or `useTableControlUrlParams`), `getHubRequestParams`, and `useTableControlProps`. + - Choose whether you want to use React state or URL params as the source of truth, and use `useTableControlState` or `useTableControlUrlParams` which are interchangeable. + - Take the object returned by that hook (generally named `tableControlState`) and pass it to `getHubRequestParams` function (you may need to spread it and add additional properties like `hubSortFieldKeys`). + - Call your API query hooks, using the `hubRequestParams` as needed. + - Call `useTableControlProps` and pass it an object including all properties from `tableControlState` along with additional config arguments. Some of these arguments will be derived from your API data, such as `currentPageItems`, `totalItemCount` and `isLoading`. Others are simply passed here rather than above because they are used only for rendering and not required for state management. + - The return value (the same `tableControls` object returned by `useLocalTableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. + +> ⚠️ TECH DEBT NOTE: The `tableControls` object returned by the higher-level hooks here currently has no explicit type. Its type is inferred from the return values of `useTableControlState` and `useTableControlProps`, which was a choice made to ease the original development. However, this makes it difficult to see what properties are available for table rendering without using `console.log` or reading the source. We probably should add an explicit type interface for this object. + +If desired, you can use the lower-level feature-specific hooks (see [Features](#features)) on their own (for example, if you really only need pagination and you're not rendering a full table). However, if you are using more than one or two of them you may want to consider using these higher-level hooks even if you don't need all the features. You can omit the config arguments for any features you don't need and then just don't use the relevant `propHelpers`. + ## Features The functionality of the table-controls hooks is broken down into the following features. Most features are defined by operations to be performed on API data before it is displayed in a table. @@ -116,30 +332,6 @@ Table columns are identified by unique keys which are statically inferred from t Item objects must contain some unique identifier which is either a string or number. The property key of this identifier is a required config argument called `idProperty`, which will usually be `"id"`. If no unique identifier is present in the API data, an artificial one can be injected before passing the data into these hooks, which can be done in the useQuery `select` callback (see instances where we have used `"_ui_unique_id"`). Any state which keeps track of something by item (i.e. by row) makes use of `item[idProperty]` as an identifier. Examples of this include selected rows, expanded rows and active rows. Valid `idProperty` values are also enforced by TypeScript generics; if an `idProperty` is provided that is not a property on the `TItem` type, you should get a type error. -## Usage - -### Should I Use Client or Server Logic? - -If the API endpoints you're using support server-side pagination parameters, it is generally a good idea to use them for better performance and scalability. If you do use server-side pagination, you'll need to also use server-side filtering and sorting. - -If the endpoints do not support these parameters or you need to have the entire collection of items in memory at once for some other reason, you'll need a client-paginated table. It is also slightly easier to implement a client-paginated table. - -### Which Hooks/Functions Do I Need? - -In most cases, you'll only need to use these higher-level hooks and helpers to build a table: - -- For client-paginated tables: `useLocalTableControls` is all you need. - - Internally it uses `useTableControlState`, `useTableControlProps` and the `getLocal[Feature]DerivedState` helpers. The config arguments object is a combination of the arguments required by `useTableControlState` and `useTableControlProps`. - - The return value (an object we generally name `tableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. -- For server-paginated tables: `useTableControlState` (or `useTableControlUrlParams`), `getHubRequestParams`, and `useTableControlProps`. - - Choose whether you want to use React state or URL params as the source of truth, and use `useTableControlState` or `useTableControlUrlParams` which are interchangeable. - - Take the object returned by that hook (generally named `tableControlState`) and pass it to `getHubRequestParams` function (you may need to spread it and add additional properties like `hubSortFieldKeys`). - - Call your API query hooks, using the `hubRequestParams` as needed. - - Call `useTableControlProps` and pass it an object including all properties from `tableControlState` along with additional config arguments. Some of these arguments will be derived from your API data, such as `currentPageItems`, `totalItemCount` and `isLoading`. Others are simply passed here rather than above because they are used only for rendering and not required for state management. - - The return value (the same `tableControls` object returned by `useLocalTableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. - -If desired, you can use the lower-level hooks provided here on their own (for example, if you really only need pagination and you're not rendering a full table). However, if you are using more than one or two of them you may want to consider using the higher-level hooks above even if you don't need all the features. You can omit the config arguments for any features you don't need and then just don't use the relevant `propHelpers`. - ---


@@ -150,25 +342,8 @@ If desired, you can use the lower-level hooks provided here on their own (for ex --- -### Top-level Config Arguments and the `tableControls` Object - -TODO explain how the args and return values from each hook get bundled together into a shared object. -TODO is this the best place in the docs for this section? -TODO tech debt note about how the structure of this is currently inferred from return values of all the hooks and should probably be explicitly defined in the types.ts file - -### Example Table with client-side filtering/sorting/pagination logic - -TODO explain benefits and limitations of client-side table logic -TODO show top-level usage of useLocalTableControls with components for rendering - -### Example Table with server-side filtering/sorting/pagination logic - -TODO explain benefits and limitations of server-side table logic -TODO show top-level usage of useTableControlState/useTableControlUrlParams with components for rendering - ## Types -TODO maybe move this to the bottom? TODO cover the stuff in types.ts export by export referencing the usage in specific hooks and components ## Hooks and Helper Functions From 6097846a083a3067152c0643aed9479fbb69bb48 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 22 Sep 2023 17:15:00 -0400 Subject: [PATCH 010/102] Shorten comments in example code to prevent horizontal scroll in rendered markdown Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 6e562f570e..dc71c557e0 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -22,7 +22,7 @@ The table-controls hooks and components provide a pattern where state logic can ### Example table with client-side filtering/sorting/pagination -For client-paginated tables, we only need the `useLocalTableControls` hook. All arguments can be passed to it in one object, and the `tableControls` object returned by it contains everything we need to render the composable table. +For client-paginated tables, the only hook we need is `useLocalTableControls`. All arguments can be passed to it in one object, and the `tableControls` object returned by it contains everything we need to render the composable table. This simple example includes only the filtering, sorting and pagination features and excludes arguments and properties related to the other features (see [Features](#features)). @@ -65,14 +65,16 @@ const tableControls = useLocalTableControls({ }); // Here we destructure some of the properties from `tableControls` for rendering. -// Later we also spread the entire `tableControls` object onto components whose props include subsets of this object. +// Later we also spread the entire `tableControls` object onto components whose props include subsets of it. const { currentPageItems, numRenderedColumns, - // `numRenderedColumns` is based on which features are used (selection, expansion, etc) and the number of columnNames. - // It is used as the colSpan when rendering a full-table-wide cell (e.g. expanded content when using expansion). + // `numRenderedColumns` is based on the number of columnNames and additional columns needed for + // rendering controls related to features like selection, expansion, etc. + // It is used as the colSpan when rendering a full-table-wide cell. selectionState: { selectedItems }, - // The objects and functions in `propHelpers` correspond to the props needed for specific PatternFly or Tackle components and are provided to reduce prop-drilling and make the rendering code as short as possible. + // The objects and functions in `propHelpers` correspond to the props needed for specific PatternFly or Tackle + // components and are provided to reduce prop-drilling and make the rendering code as short as possible. propHelpers: { toolbarProps, filterToolbarProps, @@ -189,7 +191,8 @@ const hubRequestParams = getHubRequestParams({ }, }); -// `useFetchThings` is an example of a custom hook that calls a react-query `useQuery` and the `serializeRequestParamsForHub` helper. +// `useFetchThings` is an example of a custom hook that calls a react-query `useQuery` +// and the `serializeRequestParamsForHub` helper. // Any API fetch implementation could be used here as long as it will re-fetch when `hubRequestParams` changes. // The `data` returned here has been paginated, filtered and sorted on the server. const { data, totalItemCount, isLoading, isError } = From cc61b43b63013c3f387ccf0656c69cb35dcdfbf9 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 22 Sep 2023 18:30:17 -0400 Subject: [PATCH 011/102] Remove usage of translation fn from example code Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index dc71c557e0..0f5719a5fe 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -48,10 +48,7 @@ const tableControls = useLocalTableControls({ key: "name", title: "Name", type: FilterType.search, - placeholderText: - t("actions.filterBy", { - what: t("terms.name").toLowerCase(), - }) + "...", + placeholderText: "Filter by name...", getItemValue: (thing) => thing.name || "", }, ], @@ -171,10 +168,7 @@ const tableControlState = useTableControlState({ key: "name", title: "Name", type: FilterType.search, - placeholderText: - t("actions.filterBy", { - what: t("terms.name").toLowerCase(), - }) + "...", + placeholderText: "Filter by name...", getItemValue: (thing) => thing.name || "", }, ], From d04d3169d8f1f026d6c0a720013fe063a4869ebd Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 22 Sep 2023 18:30:55 -0400 Subject: [PATCH 012/102] Remove unnecessary getItemValue from server example Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 0f5719a5fe..d6b7461d43 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -169,7 +169,6 @@ const tableControlState = useTableControlState({ title: "Name", type: FilterType.search, placeholderText: "Filter by name...", - getItemValue: (thing) => thing.name || "", }, ], sortableColumns: ["name", "description"], From a4e3fe6dc134bc7d4ce77400e166b8af6ea4292c Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 22 Sep 2023 18:34:48 -0400 Subject: [PATCH 013/102] Remove selection code and clean up comments Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index d6b7461d43..ff2d399324 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -64,12 +64,11 @@ const tableControls = useLocalTableControls({ // Here we destructure some of the properties from `tableControls` for rendering. // Later we also spread the entire `tableControls` object onto components whose props include subsets of it. const { - currentPageItems, - numRenderedColumns, + currentPageItems, // These items have already been paginated. // `numRenderedColumns` is based on the number of columnNames and additional columns needed for // rendering controls related to features like selection, expansion, etc. // It is used as the colSpan when rendering a full-table-wide cell. - selectionState: { selectedItems }, + numRenderedColumns, // The objects and functions in `propHelpers` correspond to the props needed for specific PatternFly or Tackle // components and are provided to reduce prop-drilling and make the rendering code as short as possible. propHelpers: { From 23aacc68d3ea5ff81668bc8970909a1e97f0ec20 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Sun, 24 Sep 2023 21:36:38 -0400 Subject: [PATCH 014/102] Remove copypasted irrelevant comment Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index ff2d399324..69d8d04e8d 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -156,7 +156,6 @@ The usage is similar here, but some arguments are no longer required (like `getS Note also: the destructuring and rendering part of the example code is not included here because **_it is identical to the example above_**. The only difference between client-paginated and server-paginated tables is the hook usage; the `tableControls` object and its usage are the same for both. ```tsx -// In a real table, this API data would come from a useQuery call. const tableControlState = useTableControlState({ columnNames: { name: "Name", From 7b990648dd1667cb53b224490ab5d7d7f85ca54f Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Sun, 1 Oct 2023 01:43:55 -0400 Subject: [PATCH 015/102] Skeleton/TODOs for usePersistedState and next steps Signed-off-by: Mike Turley --- client/src/app/hooks/usePersistedState.ts | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 client/src/app/hooks/usePersistedState.ts diff --git a/client/src/app/hooks/usePersistedState.ts b/client/src/app/hooks/usePersistedState.ts new file mode 100644 index 0000000000..166f947c67 --- /dev/null +++ b/client/src/app/hooks/usePersistedState.ts @@ -0,0 +1,26 @@ +import React from "react"; +import { useUrlParams } from "./useUrlParams"; +import { useLocalStorage, useSessionStorage } from "@migtools/lib-ui"; + +export const usePersistedState = ( + initialValue: T +): [T | undefined, React.Dispatch>] => { + const storage = { + state: React.useState(initialValue), + url: useUrlParams(), // TODO + localStorage: useLocalStorage(), // TODO + sessionStorage: useSessionStorage(), // TODO + }; + // TODO based on options, return only one of the above? + // TODO make sure none of the disabled ones have any effect on URL or storage +}; + +// TODO combine all the use[Feature]State and use[Feature]UrlParams hooks +// TODO combine/rename useTableControlUrlParams into single useTableControlState hook with persistence options +// TODO bring in useSelectionState as a persistable thing +// TODO add JSdoc comments for all inputs and outputs +// TODO explore the state contract needed for using useTableControlProps with custom state logic +// TODO rename args to options in all types and code +// TODO rename active-row to active-item +// TODO decouple SimplePagination +// TODO decouple FilterToolbar? -- can we make a toolbar-batteries hook? useFilterToolbar? option to hook it up to table batteries or not? From 7540d41b0a5273218e20e16cf1470ce1e7ffc113 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Sun, 1 Oct 2023 01:48:20 -0400 Subject: [PATCH 016/102] More TODOs Signed-off-by: Mike Turley --- client/src/app/hooks/usePersistedState.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/src/app/hooks/usePersistedState.ts b/client/src/app/hooks/usePersistedState.ts index 166f947c67..419d041137 100644 --- a/client/src/app/hooks/usePersistedState.ts +++ b/client/src/app/hooks/usePersistedState.ts @@ -5,12 +5,10 @@ import { useLocalStorage, useSessionStorage } from "@migtools/lib-ui"; export const usePersistedState = ( initialValue: T ): [T | undefined, React.Dispatch>] => { - const storage = { - state: React.useState(initialValue), - url: useUrlParams(), // TODO - localStorage: useLocalStorage(), // TODO - sessionStorage: useSessionStorage(), // TODO - }; + const state = React.useState(initialValue); + const url = useUrlParams(); // TODO - add a disabled flag for the url param behavior? + const localStorage = useLocalStorage(); // TODO + const sessionStorage = useSessionStorage(); // TODO // TODO based on options, return only one of the above? // TODO make sure none of the disabled ones have any effect on URL or storage }; @@ -24,3 +22,4 @@ export const usePersistedState = ( // TODO rename active-row to active-item // TODO decouple SimplePagination // TODO decouple FilterToolbar? -- can we make a toolbar-batteries hook? useFilterToolbar? option to hook it up to table batteries or not? +// TODO decouple useUrlParams from react-router? can we do everything from the document.location.search? From 4db8a6688d8dd7d087dc25e0cf33cf09252e1f21 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Sun, 1 Oct 2023 02:26:19 -0400 Subject: [PATCH 017/102] WIP Signed-off-by: Mike Turley --- client/src/app/hooks/usePersistedState.ts | 49 +++++++++++++++++++---- client/src/app/hooks/useUrlParams.ts | 8 ++-- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/client/src/app/hooks/usePersistedState.ts b/client/src/app/hooks/usePersistedState.ts index 419d041137..feb01ac208 100644 --- a/client/src/app/hooks/usePersistedState.ts +++ b/client/src/app/hooks/usePersistedState.ts @@ -1,14 +1,47 @@ import React from "react"; -import { useUrlParams } from "./useUrlParams"; +import { TSerializedParams, useUrlParams } from "./useUrlParams"; import { useLocalStorage, useSessionStorage } from "@migtools/lib-ui"; +import { DisallowCharacters } from "@app/utils/type-utils"; + +type UsePersistedStateOptions< + T, + TURLParamKey extends string, + TKeyPrefix extends string, +> = { + defaultValue: T; +} & ( + | { + mode: null; + } + | { + mode: "url"; + keyPrefix?: DisallowCharacters; + keys: DisallowCharacters[]; + serialize: (params: Partial) => TSerializedParams; + deserialize: (serializedParams: TSerializedParams) => T; + } + | { + mode: "localStorage" | "sessionStorage"; + key: string; + } +); + +export const usePersistedState = < + T, + TURLParamKey extends string, + TKeyPrefix extends string, +>( + options: UsePersistedStateOptions +): [T, (value: T) => void] => { + const storage = { + state: React.useState(options.defaultValue), + url: useUrlParams(options), // TODO - add a disabled flag for the url param behavior? + localStorage: useLocalStorage(options.key, options.defaultValue), // TODO + sessionStorage: useSessionStorage(options.key, options.defaultValue), // TODO + }; + if (!options.mode) return storage.state; + return storage[options.mode]; -export const usePersistedState = ( - initialValue: T -): [T | undefined, React.Dispatch>] => { - const state = React.useState(initialValue); - const url = useUrlParams(); // TODO - add a disabled flag for the url param behavior? - const localStorage = useLocalStorage(); // TODO - const sessionStorage = useSessionStorage(); // TODO // TODO based on options, return only one of the above? // TODO make sure none of the disabled ones have any effect on URL or storage }; diff --git a/client/src/app/hooks/useUrlParams.ts b/client/src/app/hooks/useUrlParams.ts index d0d9354685..44b63b84c8 100644 --- a/client/src/app/hooks/useUrlParams.ts +++ b/client/src/app/hooks/useUrlParams.ts @@ -16,14 +16,14 @@ import { useLocation, useHistory } from "react-router-dom"; // The keys of TDeserializedParams and TSerializedParams have the prefixes omitted. // Prefixes are only used at the very first/last step when reading/writing from/to the URLSearchParams object. -type TSerializedParams = Partial< +export type TSerializedParams = Partial< Record >; export interface IUseUrlParamsArgs< TURLParamKey extends string, TKeyPrefix extends string, - TDeserializedParams + TDeserializedParams, > { keyPrefix?: DisallowCharacters; keys: DisallowCharacters[]; @@ -38,13 +38,13 @@ export interface IUseUrlParamsArgs< export type TURLParamStateTuple = readonly [ TDeserializedParams, - (newParams: Partial) => void + (newParams: Partial) => void, ]; export const useUrlParams = < TURLParamKey extends string, TKeyPrefix extends string, - TDeserializedParams + TDeserializedParams, >({ keyPrefix, keys, From a11bcf086ff379f60eb411d5d2f860aeeac76257 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 2 Oct 2023 17:09:58 -0400 Subject: [PATCH 018/102] WIP / TODOs Signed-off-by: Mike Turley --- .../table-controls/sorting/useSortState.ts | 2 +- client/src/app/hooks/usePersistedState.ts | 30 ++++++++++++------- client/src/app/hooks/useUrlParams.ts | 29 +++++++++++------- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index 8ada4d5de1..71e001a45e 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -34,7 +34,7 @@ export const useSortState = ({ export const useSortUrlParams = < TSortableColumnKey extends string, - TURLParamKeyPrefix extends string = string + TURLParamKeyPrefix extends string = string, >({ sortableColumns = [], initialSort = getDefaultSort(sortableColumns), diff --git a/client/src/app/hooks/usePersistedState.ts b/client/src/app/hooks/usePersistedState.ts index feb01ac208..a84fa791ab 100644 --- a/client/src/app/hooks/usePersistedState.ts +++ b/client/src/app/hooks/usePersistedState.ts @@ -1,7 +1,6 @@ import React from "react"; -import { TSerializedParams, useUrlParams } from "./useUrlParams"; +import { IUseUrlParamsArgs, useUrlParams } from "./useUrlParams"; import { useLocalStorage, useSessionStorage } from "@migtools/lib-ui"; -import { DisallowCharacters } from "@app/utils/type-utils"; type UsePersistedStateOptions< T, @@ -13,19 +12,28 @@ type UsePersistedStateOptions< | { mode: null; } - | { + | ({ mode: "url"; - keyPrefix?: DisallowCharacters; - keys: DisallowCharacters[]; - serialize: (params: Partial) => TSerializedParams; - deserialize: (serializedParams: TSerializedParams) => T; - } + } & IUseUrlParamsArgs) | { mode: "localStorage" | "sessionStorage"; key: string; } ); +// TODO FIXME - Original problem: can't call hooks conditionally with eslint rules +// (but we could do so if we make sure the conditional never changes between renders) +// That's hacky though, what's better? +// Calling all 4 hooks and only returning the stuff from the active one +// isn't the most performant in theory but is fine in practice +// The problem there is TypeScript. can't narrow the type of `options` +// without a conditional, so we'd hit the same issue. +// The workaround for that may be TS assertions? +// But it's not safe to just assert that options in there are defined +// because all 4 hooks always get called with whatever you pass. +// So they all need an `isEnabled` option and they need to never +// try to access other options if it's false. + export const usePersistedState = < T, TURLParamKey extends string, @@ -35,8 +43,10 @@ export const usePersistedState = < ): [T, (value: T) => void] => { const storage = { state: React.useState(options.defaultValue), - url: useUrlParams(options), // TODO - add a disabled flag for the url param behavior? - localStorage: useLocalStorage(options.key, options.defaultValue), // TODO + url: useUrlParams( + options as IUseUrlParamsArgs // TODO is this a bad idea? + ), // TODO - add a disabled flag for the url param behavior? + localStorage: useLocalStorage(options.key, options.defaultValue), // TODO - disabled flag for these too? sessionStorage: useSessionStorage(options.key, options.defaultValue), // TODO }; if (!options.mode) return storage.state; diff --git a/client/src/app/hooks/useUrlParams.ts b/client/src/app/hooks/useUrlParams.ts index 44b63b84c8..449cb27964 100644 --- a/client/src/app/hooks/useUrlParams.ts +++ b/client/src/app/hooks/useUrlParams.ts @@ -25,6 +25,7 @@ export interface IUseUrlParamsArgs< TKeyPrefix extends string, TDeserializedParams, > { + isEnabled?: boolean; keyPrefix?: DisallowCharacters; keys: DisallowCharacters[]; defaultValue: TDeserializedParams; @@ -37,7 +38,7 @@ export interface IUseUrlParamsArgs< } export type TURLParamStateTuple = readonly [ - TDeserializedParams, + TDeserializedParams | null, (newParams: Partial) => void, ]; @@ -46,6 +47,7 @@ export const useUrlParams = < TKeyPrefix extends string, TDeserializedParams, >({ + isEnabled = true, keyPrefix, keys, defaultValue, @@ -96,18 +98,25 @@ export const useUrlParams = < // We use useLocation here so we are re-rendering when the params change. const urlParams = new URLSearchParams(useLocation().search); // We un-prefix the params object here so the deserialize function doesn't have to care about the keyPrefix. - const serializedParams = keys.reduce( - (obj, key) => ({ - ...obj, - [key]: urlParams.get(withPrefix(key)), - }), - {} as TSerializedParams - ); - const allParamsEmpty = keys.every((key) => !serializedParams[key]); - const params = allParamsEmpty ? defaultValue : deserialize(serializedParams); + + let allParamsEmpty = true; + let params: TDeserializedParams | null = null; + if (isEnabled) { + const serializedParams = keys.reduce( + (obj, key) => ({ + ...obj, + [key]: urlParams.get(withPrefix(key)), + }), + {} as TSerializedParams + ); + allParamsEmpty = keys.every((key) => !serializedParams[key]); + params = allParamsEmpty ? defaultValue : deserialize(serializedParams); + } React.useEffect(() => { if (allParamsEmpty) setParams(defaultValue); + // Leaving this rule enabled results in a cascade of unnecessary useCallbacks: + // eslint-disable-next-line react-hooks/exhaustive-deps }, [allParamsEmpty]); return [params, setParams]; From aebeb757c3e59868504675030ab2bcc4176b2447 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 2 Oct 2023 17:25:19 -0400 Subject: [PATCH 019/102] Upgrade lib-ui, implement usePersistedState with assertions Signed-off-by: Mike Turley --- client/package.json | 2 +- client/src/app/hooks/usePersistedState.ts | 46 ++++++++++------------- client/src/app/hooks/useUrlParams.ts | 2 +- package-lock.json | 8 ++-- 4 files changed, 26 insertions(+), 32 deletions(-) diff --git a/client/package.json b/client/package.json index 4872140f87..01b186eb47 100644 --- a/client/package.json +++ b/client/package.json @@ -24,7 +24,7 @@ "@dnd-kit/sortable": "^7.0.2", "@hookform/resolvers": "^2.9.11", "@hot-loader/react-dom": "^17.0.2", - "@migtools/lib-ui": "^9.0.3", + "@migtools/lib-ui": "^10.0.0", "@patternfly/patternfly": "^5.0.2", "@patternfly/react-charts": "^7.1.0", "@patternfly/react-code-editor": "^5.1.0", diff --git a/client/src/app/hooks/usePersistedState.ts b/client/src/app/hooks/usePersistedState.ts index a84fa791ab..93675fafbc 100644 --- a/client/src/app/hooks/usePersistedState.ts +++ b/client/src/app/hooks/usePersistedState.ts @@ -1,6 +1,10 @@ import React from "react"; import { IUseUrlParamsArgs, useUrlParams } from "./useUrlParams"; -import { useLocalStorage, useSessionStorage } from "@migtools/lib-ui"; +import { + UseStorageTypeOptions, + useLocalStorage, + useSessionStorage, +} from "@migtools/lib-ui"; type UsePersistedStateOptions< T, @@ -10,7 +14,7 @@ type UsePersistedStateOptions< defaultValue: T; } & ( | { - mode: null; + mode?: "state"; } | ({ mode: "url"; @@ -21,39 +25,29 @@ type UsePersistedStateOptions< } ); -// TODO FIXME - Original problem: can't call hooks conditionally with eslint rules -// (but we could do so if we make sure the conditional never changes between renders) -// That's hacky though, what's better? -// Calling all 4 hooks and only returning the stuff from the active one -// isn't the most performant in theory but is fine in practice -// The problem there is TypeScript. can't narrow the type of `options` -// without a conditional, so we'd hit the same issue. -// The workaround for that may be TS assertions? -// But it's not safe to just assert that options in there are defined -// because all 4 hooks always get called with whatever you pass. -// So they all need an `isEnabled` option and they need to never -// try to access other options if it's false. - export const usePersistedState = < T, TURLParamKey extends string, TKeyPrefix extends string, >( options: UsePersistedStateOptions -): [T, (value: T) => void] => { +): [T | null, (value: T) => void] => { const storage = { state: React.useState(options.defaultValue), - url: useUrlParams( - options as IUseUrlParamsArgs // TODO is this a bad idea? - ), // TODO - add a disabled flag for the url param behavior? - localStorage: useLocalStorage(options.key, options.defaultValue), // TODO - disabled flag for these too? - sessionStorage: useSessionStorage(options.key, options.defaultValue), // TODO + url: useUrlParams({ + ...(options as IUseUrlParamsArgs), + isEnabled: options.mode === "url", + }), + localStorage: useLocalStorage({ + ...(options as UseStorageTypeOptions), + isEnabled: options.mode === "localStorage", + }), + sessionStorage: useSessionStorage({ + ...(options as UseStorageTypeOptions), + isEnabled: options.mode === "sessionStorage", + }), }; - if (!options.mode) return storage.state; - return storage[options.mode]; - - // TODO based on options, return only one of the above? - // TODO make sure none of the disabled ones have any effect on URL or storage + return storage[options.mode || "state"]; }; // TODO combine all the use[Feature]State and use[Feature]UrlParams hooks diff --git a/client/src/app/hooks/useUrlParams.ts b/client/src/app/hooks/useUrlParams.ts index 449cb27964..5f48256e79 100644 --- a/client/src/app/hooks/useUrlParams.ts +++ b/client/src/app/hooks/useUrlParams.ts @@ -37,7 +37,7 @@ export interface IUseUrlParamsArgs< ) => TDeserializedParams; } -export type TURLParamStateTuple = readonly [ +export type TURLParamStateTuple = [ TDeserializedParams | null, (newParams: Partial) => void, ]; diff --git a/package-lock.json b/package-lock.json index 71f4825c64..e396517424 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "@dnd-kit/sortable": "^7.0.2", "@hookform/resolvers": "^2.9.11", "@hot-loader/react-dom": "^17.0.2", - "@migtools/lib-ui": "^9.0.3", + "@migtools/lib-ui": "^10.0.0", "@patternfly/patternfly": "^5.0.2", "@patternfly/react-charts": "^7.1.0", "@patternfly/react-code-editor": "^5.1.0", @@ -1539,9 +1539,9 @@ "dev": true }, "node_modules/@migtools/lib-ui": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@migtools/lib-ui/-/lib-ui-9.0.3.tgz", - "integrity": "sha512-ncs1eos8pTIEM1CGyZOXMjlzWoUjBfkyt0lsRJ59gFEUobl0oV+JlKQGD0hwAhr+F8518ZH5zeMBwagC6px5Dg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@migtools/lib-ui/-/lib-ui-10.0.0.tgz", + "integrity": "sha512-UMF9o/M0PKjPIdPoX8GpMAAf2Vv8ZgVqJa38Se4zKTGPGIcYsNOiZ6ZfmaymUwD1jT7d/h7QQFMcpVpU4Ca1kg==", "dependencies": { "@tanstack/react-query": "^4.26.1", "fast-deep-equal": "^3.1.3", From ec2d3e5ce58d2bc9968594ab7afaa8680fc17cd5 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 2 Oct 2023 17:26:13 -0400 Subject: [PATCH 020/102] TODO Signed-off-by: Mike Turley --- client/src/app/hooks/usePersistedState.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/app/hooks/usePersistedState.ts b/client/src/app/hooks/usePersistedState.ts index 93675fafbc..cada3b9097 100644 --- a/client/src/app/hooks/usePersistedState.ts +++ b/client/src/app/hooks/usePersistedState.ts @@ -35,6 +35,8 @@ export const usePersistedState = < const storage = { state: React.useState(options.defaultValue), url: useUrlParams({ + // TODO can we avoid these assertions? how can we narrow the type of `options` depending on mode without conditionals? + // something with `satisfies`? read TS docs on narrowing types with hints ...(options as IUseUrlParamsArgs), isEnabled: options.mode === "url", }), From 1af3009f170417243cc11222bc91aa3b4c8f822a Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 3 Oct 2023 11:23:51 -0400 Subject: [PATCH 021/102] Refactor in progress to combine hooks with `persistIn` option Signed-off-by: Mike Turley --- client/src/app/Constants.ts | 1 + client/src/app/hooks/table-controls/DOCS.md | 12 +++--- .../filtering/useFilterState.ts | 12 +++--- client/src/app/hooks/table-controls/index.ts | 4 +- client/src/app/hooks/table-controls/types.ts | 37 ++++++++++++---- .../table-controls/useTableControlState.ts | 43 +++++++++++++++++++ .../useTableControlUrlParams.ts | 43 ------------------- client/src/app/hooks/usePersistedState.ts | 17 ++++---- .../app/pages/dependencies/dependencies.tsx | 9 ++-- .../dependencies/dependency-apps-table.tsx | 7 +-- .../affected-applications.tsx | 7 +-- .../file-all-incidents-table.tsx | 7 +-- .../issue-affected-files-table.tsx | 13 +++--- client/src/app/pages/issues/issues-table.tsx | 7 +-- 14 files changed, 120 insertions(+), 99 deletions(-) create mode 100644 client/src/app/hooks/table-controls/useTableControlState.ts delete mode 100644 client/src/app/hooks/table-controls/useTableControlUrlParams.ts diff --git a/client/src/app/Constants.ts b/client/src/app/Constants.ts index f672a0faaf..cf53fe00de 100644 --- a/client/src/app/Constants.ts +++ b/client/src/app/Constants.ts @@ -238,6 +238,7 @@ export enum LocalStorageKey { // URL param prefixes: should be short, must be unique for each table that uses one export enum TableURLParamKeyPrefix { issues = "i", + dependencies = "d", issuesAffectedApps = "ia", issuesAffectedFiles = "if", issuesRemainingIncidents = "ii", diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 69d8d04e8d..a1ff56ecbe 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -215,10 +215,10 @@ If the endpoints do not support these parameters or you need to have the entire In most cases, you'll only need to use these higher-level hooks and helpers to build a table: - For client-paginated tables: `useLocalTableControls` or `useLocalTableControlsWithUrlParams` is all you need. These have the same signature and are interchangeable. - - Internally they use `useTableControlState` (or `useTableControlUrlParams`), `useTableControlProps` and the `getLocal[Feature]DerivedState` helpers. The config arguments object is a combination of the arguments required by `useTableControlState` and `useTableControlProps`. + - Internally they use `useTableControlState`, `useTableControlProps` and the `getLocal[Feature]DerivedState` helpers. The config arguments object is a combination of the arguments required by `useTableControlState` and `useTableControlProps`. - The return value (an object we generally name `tableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. -- For server-paginated tables: `useTableControlState` (or `useTableControlUrlParams`), `getHubRequestParams`, and `useTableControlProps`. - - Choose whether you want to use React state or URL params as the source of truth, and use `useTableControlState` or `useTableControlUrlParams` which are interchangeable. +- For server-paginated tables: `useTableControlState`, `getHubRequestParams`, and `useTableControlProps`. + - Choose whether you want to use React state, URL params or localStorage/sessionStorage as the source of truth, and call `useTableControlState` with the appropriate `storageType` option and the required options for each type. - Take the object returned by that hook (generally named `tableControlState`) and pass it to `getHubRequestParams` function (you may need to spread it and add additional properties like `hubSortFieldKeys`). - Call your API query hooks, using the `hubRequestParams` as needed. - Call `useTableControlProps` and pass it an object including all properties from `tableControlState` along with additional config arguments. Some of these arguments will be derived from your API data, such as `currentPageItems`, `totalItemCount` and `isLoading`. Others are simply passed here rather than above because they are used only for rendering and not required for state management. @@ -234,9 +234,7 @@ The functionality of the table-controls hooks is broken down into the following Note that filtering, sorting and pagination are special because they must be performed in a specific order to work correctly: filter and sort data, then paginate it. Using the higher-level hooks like `useLocalTableControls` or `useTableControlState` + `useTableControlProps` will take care of this for you (see [Usage](#usage)), but if you are handling pagination yourself with the lower-level hooks you'll need to be mindful of this order (see [Hooks and Helper Functions](#hooks-and-helper-functions)). -The state used by these features can be stored either in React state (provided by `use[Feature]State` hooks) or in the browser's URL query parameters (provided by `use[Feature]UrlParams` hooks). If URL params are used, the user's current filters, sort, pagination state, expanded/active rows and more are preserved when reloading the browser, using the browser Back and Forward buttons, or loading a bookmark. - -> ⚠️ TECH DEBT NOTE: This URL param behavior is currently all-or-nothing if you're using the higher-level hooks and not available when using the shorthand hook `useLocalTableControls`. This is because the generic hook for manipulating URL params (`useUrlParams`) cannot be used conditionally. We may want to refactor this later such that we have a hook like `useStateOrUrlParams` and make URL params opt-in on a feature-by-feature basis. This would also mean we could combine `useTableControlState` and `useTableControlUrlParams` which currently are interchangeable, as well as combine each set of `use[Feature]State` and `use[Feature]UrlParams` hooks. See [Hooks and Helper Functions](#hooks-and-helper-functions). +The state used by these features (provided by `use[Feature]State` hooks) can be stored either in React state, in the browser's URL query parameters, or in the browser's `localStorage` or `sessionStorage`. If URL params are used, the user's current filters, sort, pagination state, expanded/active rows and more are preserved when reloading the browser, using the browser Back and Forward buttons, or loading a bookmark. All of the hooks and helpers described in this section are used internally by the higher-level hooks and helpers, and do not need to be used directly (see [Hooks and Helper Functions](#hooks-and-helper-functions) and [Usage](#usage)). @@ -316,7 +314,7 @@ None of the code here treats "rows" as their own data structure. The content and An item object has the generic type `TItem`, which is inferred either from the type of the `items` array passed into `useLocalTableControls` (for client-paginated tables) or from the `currentPageItems` array passed into `useTableControlProps` (for server-paginated tables). See [Types](#types). -> ℹ️ CAVEAT: For server-paginated tables the item data is not in scope until after the API query hook is called, but the `useTableControlState` or `useTableControlUrlParams` hook must be called _before_ API queries because its return values are needed to serialize filter/sort/pagination params for the API. This means the inferred `TItem` type is not available when passing arguments to `useTableControlState` or `useTableControlUrlParams`. `TItem` resolves to `unknown` in this scope, which is usually fine since the arguments there don't need to know what type of items they are working with. If the item type is needed for any of these arguments it can be explicitly passed as a type param. However... +> ℹ️ CAVEAT: For server-paginated tables the item data is not in scope until after the API query hook is called, but the `useTableControlState` hook must be called _before_ API queries because its return values are needed to serialize filter/sort/pagination params for the API. This means the inferred `TItem` type is not available when passing arguments to `useTableControlState`. `TItem` resolves to `unknown` in this scope, which is usually fine since the arguments there don't need to know what type of items they are working with. If the item type is needed for any of these arguments it can be explicitly passed as a type param. However... > > ⚠️ TECH DEBT NOTE: Since TypeScript generic type param lists are all-or-nothing (you must either omit the list and infer all generics for a function or pass them all explicitly), this means all other type params which are normally inferred must be explicitly passed (including all of the `TColumnKey`s and `TFilterCategoryKey`s). This makes for some redundant code, although TypeScript will still enforce that it is all consistent. There is a possible upcoming TypeScript language feature which would allow partial inference in type param lists and may alleviate this in the future. See TypeScript pull requests [#26349](https://github.com/microsoft/TypeScript/pull/26349) and [#54047](https://github.com/microsoft/TypeScript/pull/54047). diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index 08fb2af669..2ef0604120 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -26,10 +26,10 @@ export const useFilterState = ({ TFilterCategoryKey >): IFilterState => { const state = React.useState>({}); - const storage = useSessionStorage>( - filterStorageKey || "", - {} - ); + const storage = useSessionStorage>({ + key: filterStorageKey || "", + defaultValue: {}, + }); const [filterValues, setFilterValues] = filterStorageKey ? storage : state; return { filterValues, setFilterValues }; }; @@ -56,7 +56,7 @@ export const serializeFilterUrlParams = ( }; export const deserializeFilterUrlParams = < - TFilterCategoryKey extends string + TFilterCategoryKey extends string, >(serializedParams: { filters?: string | null; }): Partial> => { @@ -69,7 +69,7 @@ export const deserializeFilterUrlParams = < export const useFilterUrlParams = < TFilterCategoryKey extends string, - TURLParamKeyPrefix extends string = string + TURLParamKeyPrefix extends string = string, >({ urlParamKeyPrefix, }: IExtraArgsForURLParamHooks = {}): IFilterState => { diff --git a/client/src/app/hooks/table-controls/index.ts b/client/src/app/hooks/table-controls/index.ts index 0dc3eb49f3..d00fbdfd1e 100644 --- a/client/src/app/hooks/table-controls/index.ts +++ b/client/src/app/hooks/table-controls/index.ts @@ -1,9 +1,9 @@ export * from "./types"; export * from "./utils"; -export * from "./useLocalTableControlState"; +export * from "./useTableControlState"; export * from "./useTableControlProps"; +export * from "./useLocalTableControlState"; export * from "./useLocalTableControls"; -export * from "./useTableControlUrlParams"; export * from "./getHubRequestParams"; export * from "./filtering"; export * from "./sorting"; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 1695b1e6b1..6e4a0ab0d5 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -25,20 +25,20 @@ import { IActiveRowDerivedStateArgs } from "./active-row"; // TSortableColumnKey - A subset of column keys that have sorting enabled // TFilterCategoryKey - Union type of unique identifier strings for filters (not necessarily the same as column keys) -// TODO when calling useTableControlUrlParams, the TItem type is not inferred and some of the params have it inferred as `unknown`. +// TODO when calling useTableControlState, the TItem type is not inferred and some of the params have it inferred as `unknown`. // this currently doesn't seem to matter since TItem becomes inferred later when currentPageItems is in scope, // but we should see if we can fix that (maybe not depend on TItem in the extended types here, or find a way // to pass TItem while still letting the rest of the generics be inferred. // This may be resolved in a newer TypeScript version after https://github.com/microsoft/TypeScript/pull/54047 is merged! // Common args -// - Used by both useLocalTableControlState and useTableControlUrlParams +// - Used by both useLocalTableControlState and useTableControlState // - Does not require any state or query values in scope export interface ITableControlCommonArgs< TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, - TFilterCategoryKey extends string = string + TFilterCategoryKey extends string = string, > extends IFilterStateArgs, ISortStateArgs, IPaginationStateArgs { @@ -51,14 +51,33 @@ export interface ITableControlCommonArgs< } // URL-param-specific args -// - Extra args needed for useTableControlUrlParams and each concern-specific use*UrlParams hook +// - Extra args needed for useTableControlState and each concern-specific use*State hook in URL params mode // - Does not require any state or query values in scope export interface IExtraArgsForURLParamHooks< - TURLParamKeyPrefix extends string = string + TURLParamKeyPrefix extends string = string, > { - urlParamKeyPrefix?: DisallowCharacters; + urlParamKeyPrefix: DisallowCharacters; } +export type IUseTableControlStateArgs< + TItem, + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, + TFilterCategoryKey extends string = string, + TURLParamKeyPrefix extends string = string, +> = ITableControlCommonArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey +> & + ( + | { persistIn?: "state" | "localStorage" | "sessionStorage" } + | ({ + persistIn: "urlParams"; + } & IExtraArgsForURLParamHooks) + ); + // Data-dependent args // - Used by both useLocalTableControlState and useTableControlProps // - Requires query values and defined TItem type in scope but not state values @@ -75,7 +94,7 @@ export type IUseLocalTableControlStateArgs< TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, - TFilterCategoryKey extends string = string + TFilterCategoryKey extends string = string, > = ITableControlCommonArgs< TItem, TColumnKey, @@ -94,12 +113,12 @@ export type IUseLocalTableControlStateArgs< // - Requires state and query values in scope // - Combines all args above with either: // - The return values of useLocalTableControlState -// - The return values of useTableControlUrlParams and args derived from server-side filtering/sorting/pagination +// - The return values of useTableControlState and args derived from server-side filtering/sorting/pagination export interface IUseTableControlPropsArgs< TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, - TFilterCategoryKey extends string = string + TFilterCategoryKey extends string = string, > extends ITableControlCommonArgs< TItem, TColumnKey, diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts new file mode 100644 index 0000000000..73cafe111f --- /dev/null +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -0,0 +1,43 @@ +import { IUseTableControlStateArgs } from "./types"; +import { useFilterState } from "./filtering"; +import { useSortState } from "./sorting"; +import { usePaginationState } from "./pagination"; +import { useActiveRowState } from "./active-row"; +import { useExpansionState } from "./expansion"; + +export const useTableControlState = < + TItem, + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, + TFilterCategoryKey extends string = string, + TURLParamKeyPrefix extends string = string, +>( + args: IUseTableControlStateArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TURLParamKeyPrefix + > +) => { + // Must pass type params because they can't all be inferred from the required args of useFilterState + const filterState = useFilterState< + TFilterCategoryKey, // Must pass this because no required args here have categories to infer from + TURLParamKeyPrefix + >(args); + const sortState = useSortState(args); // Type params inferred from args + const paginationState = usePaginationState(args); // Type params inferred from args + // Must pass type params because they can't all be inferred from the required args of useExpansionState + const expansionState = useExpansionState( + args + ); + const activeRowState = useActiveRowState(args); // Type params inferred from args + return { + ...args, + filterState, + sortState, + paginationState, + expansionState, + activeRowState, + }; +}; diff --git a/client/src/app/hooks/table-controls/useTableControlUrlParams.ts b/client/src/app/hooks/table-controls/useTableControlUrlParams.ts deleted file mode 100644 index 7f8db39a73..0000000000 --- a/client/src/app/hooks/table-controls/useTableControlUrlParams.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { IExtraArgsForURLParamHooks, ITableControlCommonArgs } from "./types"; -import { useFilterUrlParams } from "./filtering"; -import { useSortUrlParams } from "./sorting"; -import { usePaginationUrlParams } from "./pagination"; -import { useActiveRowUrlParams } from "./active-row"; -import { useExpansionUrlParams } from "./expansion"; - -export const useTableControlUrlParams = < - TItem, - TColumnKey extends string, - TSortableColumnKey extends TColumnKey, - TFilterCategoryKey extends string = string, - TURLParamKeyPrefix extends string = string, ->( - args: ITableControlCommonArgs< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey - > & - IExtraArgsForURLParamHooks -) => { - // Must pass type params because they can't all be inferred from the required args of useFilterUrlParams - const filterState = useFilterUrlParams< - TFilterCategoryKey, // Must pass this because no required args here have categories to infer from - TURLParamKeyPrefix - >(args); - const sortState = useSortUrlParams(args); // Type params inferred from args - const paginationState = usePaginationUrlParams(args); // Type params inferred from args - // Must pass type params because they can't all be inferred from the required args of useExpansionUrlParams - const expansionState = useExpansionUrlParams( - args - ); - const activeRowState = useActiveRowUrlParams(args); // Type params inferred from args - return { - ...args, - filterState, - sortState, - paginationState, - expansionState, - activeRowState, - }; -}; diff --git a/client/src/app/hooks/usePersistedState.ts b/client/src/app/hooks/usePersistedState.ts index cada3b9097..96336ae494 100644 --- a/client/src/app/hooks/usePersistedState.ts +++ b/client/src/app/hooks/usePersistedState.ts @@ -14,13 +14,13 @@ type UsePersistedStateOptions< defaultValue: T; } & ( | { - mode?: "state"; + persistIn?: "state"; } | ({ - mode: "url"; + persistIn: "urlParams"; } & IUseUrlParamsArgs) | { - mode: "localStorage" | "sessionStorage"; + persistIn: "localStorage" | "sessionStorage"; key: string; } ); @@ -34,26 +34,25 @@ export const usePersistedState = < ): [T | null, (value: T) => void] => { const storage = { state: React.useState(options.defaultValue), - url: useUrlParams({ + urlParams: useUrlParams({ // TODO can we avoid these assertions? how can we narrow the type of `options` depending on mode without conditionals? // something with `satisfies`? read TS docs on narrowing types with hints ...(options as IUseUrlParamsArgs), - isEnabled: options.mode === "url", + isEnabled: options.persistIn === "urlParams", }), localStorage: useLocalStorage({ ...(options as UseStorageTypeOptions), - isEnabled: options.mode === "localStorage", + isEnabled: options.persistIn === "localStorage", }), sessionStorage: useSessionStorage({ ...(options as UseStorageTypeOptions), - isEnabled: options.mode === "sessionStorage", + isEnabled: options.persistIn === "sessionStorage", }), }; - return storage[options.mode || "state"]; + return storage[options.persistIn || "state"]; }; // TODO combine all the use[Feature]State and use[Feature]UrlParams hooks -// TODO combine/rename useTableControlUrlParams into single useTableControlState hook with persistence options // TODO bring in useSelectionState as a persistable thing // TODO add JSdoc comments for all inputs and outputs // TODO explore the state contract needed for using useTableControlProps with custom state logic diff --git a/client/src/app/pages/dependencies/dependencies.tsx b/client/src/app/pages/dependencies/dependencies.tsx index fe19caf66e..af23037c2d 100644 --- a/client/src/app/pages/dependencies/dependencies.tsx +++ b/client/src/app/pages/dependencies/dependencies.tsx @@ -15,9 +15,11 @@ import { useTranslation } from "react-i18next"; import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; import { - useTableControlUrlParams, + useTableControlState, useTableControlProps, + getHubRequestParams, } from "@app/hooks/table-controls"; +import { TableURLParamKeyPrefix } from "@app/Constants"; import { SimplePagination } from "@app/components/SimplePagination"; import { ConditionalTableBody, @@ -26,7 +28,6 @@ import { } from "@app/components/TableControls"; import { useFetchDependencies } from "@app/queries/dependencies"; import { useSelectionState } from "@migtools/lib-ui"; -import { getHubRequestParams } from "@app/hooks/table-controls"; import { DependencyAppsDetailDrawer } from "./dependency-apps-detail-drawer"; import { useSharedAffectedApplicationFilterCategories } from "../issues/helpers"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; @@ -37,7 +38,9 @@ export const Dependencies: React.FC = () => { const allAffectedApplicationsFilterCategories = useSharedAffectedApplicationFilterCategories(); - const tableControlState = useTableControlUrlParams({ + const tableControlState = useTableControlState({ + persistIn: "urlParams", + urlParamKeyPrefix: TableURLParamKeyPrefix.dependencies, columnNames: { name: "Dependency name", foundIn: "Found in", diff --git a/client/src/app/pages/dependencies/dependency-apps-table.tsx b/client/src/app/pages/dependencies/dependency-apps-table.tsx index 83989e9207..46da5b46b6 100644 --- a/client/src/app/pages/dependencies/dependency-apps-table.tsx +++ b/client/src/app/pages/dependencies/dependency-apps-table.tsx @@ -6,9 +6,9 @@ import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { useSelectionState } from "@migtools/lib-ui"; import { AnalysisDependency } from "@app/api/models"; import { - getHubRequestParams, + useTableControlState, useTableControlProps, - useTableControlUrlParams, + getHubRequestParams, } from "@app/hooks/table-controls"; import { TableURLParamKeyPrefix } from "@app/Constants"; import { @@ -33,7 +33,8 @@ export const DependencyAppsTable: React.FC = ({ const { businessServices } = useFetchBusinessServices(); const { tags } = useFetchTags(); - const tableControlState = useTableControlUrlParams({ + const tableControlState = useTableControlState({ + persistIn: "urlParams", urlParamKeyPrefix: TableURLParamKeyPrefix.dependencyApplications, columnNames: { name: "Application", diff --git a/client/src/app/pages/issues/affected-applications/affected-applications.tsx b/client/src/app/pages/issues/affected-applications/affected-applications.tsx index 4d5a0f5017..f4a2c0b412 100644 --- a/client/src/app/pages/issues/affected-applications/affected-applications.tsx +++ b/client/src/app/pages/issues/affected-applications/affected-applications.tsx @@ -17,9 +17,9 @@ import { useSelectionState } from "@migtools/lib-ui"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { AppPlaceholder } from "@app/components/AppPlaceholder"; import { - useTableControlUrlParams, - getHubRequestParams, + useTableControlState, useTableControlProps, + getHubRequestParams, } from "@app/hooks/table-controls"; import { SimplePagination } from "@app/components/SimplePagination"; import { @@ -52,7 +52,8 @@ export const AffectedApplications: React.FC = () => { new URLSearchParams(useLocation().search).get("issueTitle") || "Active rule"; - const tableControlState = useTableControlUrlParams({ + const tableControlState = useTableControlState({ + persistIn: "urlParams", urlParamKeyPrefix: TableURLParamKeyPrefix.issuesAffectedApps, columnNames: { name: "Name", diff --git a/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx b/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx index 5a83401bb6..1670d099af 100644 --- a/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx +++ b/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx @@ -11,9 +11,9 @@ import { TableRowContentWithControls, } from "@app/components/TableControls"; import { - getHubRequestParams, + useTableControlState, useTableControlProps, - useTableControlUrlParams, + getHubRequestParams, } from "@app/hooks/table-controls"; import ReactMarkdown from "react-markdown"; import { markdownPFComponents } from "@app/components/markdownPFComponents"; @@ -25,7 +25,8 @@ export interface IFileRemainingIncidentsTableProps { export const FileAllIncidentsTable: React.FC< IFileRemainingIncidentsTableProps > = ({ fileReport }) => { - const tableControlState = useTableControlUrlParams({ + const tableControlState = useTableControlState({ + persistIn: "urlParams", urlParamKeyPrefix: TableURLParamKeyPrefix.issuesRemainingIncidents, columnNames: { line: "Line #", diff --git a/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx b/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx index e642e2f098..cb46f3eb24 100644 --- a/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx +++ b/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx @@ -9,15 +9,11 @@ import { import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { useSelectionState } from "@migtools/lib-ui"; +import { AnalysisFileReport, AnalysisIssue } from "@app/api/models"; import { - AnalysisFileReport, - AnalysisAppReport, - AnalysisIssue, -} from "@app/api/models"; -import { - getHubRequestParams, + useTableControlState, useTableControlProps, - useTableControlUrlParams, + getHubRequestParams, } from "@app/hooks/table-controls"; import { useFetchFileReports } from "@app/queries/issues"; import { TableURLParamKeyPrefix } from "@app/Constants"; @@ -39,7 +35,8 @@ export const IssueAffectedFilesTable: React.FC< > = ({ issue }) => { const { t } = useTranslation(); - const tableControlState = useTableControlUrlParams({ + const tableControlState = useTableControlState({ + persistIn: "urlParams", urlParamKeyPrefix: TableURLParamKeyPrefix.issuesAffectedFiles, columnNames: { file: "File", diff --git a/client/src/app/pages/issues/issues-table.tsx b/client/src/app/pages/issues/issues-table.tsx index b76f6f3318..4bfcee0464 100644 --- a/client/src/app/pages/issues/issues-table.tsx +++ b/client/src/app/pages/issues/issues-table.tsx @@ -47,9 +47,9 @@ import { TableRowContentWithControls, } from "@app/components/TableControls"; import { - useTableControlUrlParams, - getHubRequestParams, + useTableControlState, useTableControlProps, + getHubRequestParams, } from "@app/hooks/table-controls"; import { @@ -95,7 +95,8 @@ export const IssuesTable: React.FC = ({ mode }) => { const allIssuesSpecificFilterCategories = useSharedAffectedApplicationFilterCategories(); - const tableControlState = useTableControlUrlParams({ + const tableControlState = useTableControlState({ + persistIn: "urlParams", urlParamKeyPrefix: TableURLParamKeyPrefix.issues, columnNames: { description: "Issue", From 3314d4640d75cae2f9ee72e36cf9b33c644b4b9b Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 3 Oct 2023 14:19:42 -0400 Subject: [PATCH 022/102] WIP: Refactored useFilterState but not all usages yet Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 2 +- .../filtering/useFilterState.ts | 117 +++++++----------- client/src/app/hooks/table-controls/types.ts | 64 ++++------ .../useLocalTableControlState.ts | 7 +- client/src/app/hooks/usePersistedState.ts | 95 +++++++++----- client/src/app/hooks/useUrlParams.ts | 24 ++-- 6 files changed, 148 insertions(+), 161 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index a1ff56ecbe..e94ac5fdde 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -243,7 +243,7 @@ All of the hooks and helpers described in this section are used internally by th Items are filtered according to user-selected filter key/value pairs. - Keys and filter types (search, select, etc) are defined by the `filterCategories` array config argument. The `key` properties of each of these `FilterCategory` objects are the source of truth for the inferred generic type `TFilterCategoryKeys` (see [Types](#types)). -- Filter state is provided by `useFilterState` or `useFilterUrlParams`. +- Filter state is provided by `useFilterState`. - For client-side filtering, the filter logic is provided by `getLocalFilterDerivedState` (based on the `getItemValue` callback defined on each `FilterCategory` object, which is not required when using server-side filtering). - For server-side filtering, filter state is serialized for the API by `getFilterHubRequestParams`. - Filter-related component props are provided by `getFilterProps`. diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index 2ef0604120..cb2901b85c 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -1,84 +1,63 @@ -import * as React from "react"; -import { useSessionStorage } from "@migtools/lib-ui"; +import { FilterCategory, IFilterValues } from "@app/components/FilterToolbar"; +import { IPersistenceOptions } from "../types"; import { - FilterCategory, - FilterValue, - IFilterValues, -} from "@app/components/FilterToolbar"; -import { useUrlParams } from "../../useUrlParams"; -import { objectKeys } from "@app/utils/utils"; -import { IExtraArgsForURLParamHooks } from "../types"; + BaseUsePersistedStateOptions, + usePersistedState, +} from "@app/hooks/usePersistedState"; +import { serializeFilterUrlParams } from "./helpers"; +import { deserializeFilterUrlParams } from "./helpers"; export interface IFilterState { filterValues: IFilterValues; setFilterValues: (values: IFilterValues) => void; } -export interface IFilterStateArgs { - filterStorageKey?: string; +export type IFilterStateArgs< + TItem, + TFilterCategoryKey extends string, + TPersistenceKeyPrefix extends string = string, +> = { filterCategories?: FilterCategory[]; -} +} & IPersistenceOptions; -export const useFilterState = ({ - filterStorageKey, -}: IFilterStateArgs< +export const useFilterState = < TItem, - TFilterCategoryKey ->): IFilterState => { - const state = React.useState>({}); - const storage = useSessionStorage>({ - key: filterStorageKey || "", - defaultValue: {}, - }); - const [filterValues, setFilterValues] = filterStorageKey ? storage : state; - return { filterValues, setFilterValues }; -}; + TFilterCategoryKey extends string, + TPersistenceKeyPrefix extends string = string, +>( + args: IFilterStateArgs +): IFilterState => { + const { persistIn = "state", persistenceKeyPrefix } = args; -export const serializeFilterUrlParams = ( - filterValues: IFilterValues -): { filters?: string | null } => { - // If a filter value is empty/cleared, don't put it in the object in URL params - const trimmedFilterValues = { ...filterValues }; - objectKeys(trimmedFilterValues).forEach((filterCategoryKey) => { - if ( - !trimmedFilterValues[filterCategoryKey] || - trimmedFilterValues[filterCategoryKey]?.length === 0 - ) { - delete trimmedFilterValues[filterCategoryKey]; - } - }); - return { - filters: - objectKeys(trimmedFilterValues).length > 0 - ? JSON.stringify(trimmedFilterValues) - : null, // If there are no filters, remove the filters param from the URL entirely. + const baseOptions: BaseUsePersistedStateOptions< + IFilterValues + > = { + defaultValue: {}, + persistenceKeyPrefix, }; -}; - -export const deserializeFilterUrlParams = < - TFilterCategoryKey extends string, ->(serializedParams: { - filters?: string | null; -}): Partial> => { - try { - return JSON.parse(serializedParams.filters || "{}"); - } catch (e) { - return {}; - } -}; -export const useFilterUrlParams = < - TFilterCategoryKey extends string, - TURLParamKeyPrefix extends string = string, ->({ - urlParamKeyPrefix, -}: IExtraArgsForURLParamHooks = {}): IFilterState => { - const [filterValues, setFilterValues] = useUrlParams({ - keyPrefix: urlParamKeyPrefix, - keys: ["filters"], - defaultValue: {} as IFilterValues, - serialize: serializeFilterUrlParams, - deserialize: deserializeFilterUrlParams, - }); + // Note: for the discriminated union here to work without TypeScript getting confused + // (e.g. require the urlParams-specific options when persistIn === "urlParams"), + // we need to pass persistIn inside each type-narrowed options object instead of outside the ternary. + const [filterValues, setFilterValues] = usePersistedState( + persistIn === "urlParams" + ? { + ...baseOptions, + persistIn, + keys: ["filters"], + serialize: serializeFilterUrlParams, + deserialize: deserializeFilterUrlParams, + } + : persistIn === "localStorage" || persistIn === "sessionStorage" + ? { + ...baseOptions, + persistIn, + key: "filters", + } + : { + ...baseOptions, + persistIn, + } + ); return { filterValues, setFilterValues }; }; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 6e4a0ab0d5..e37846d9a2 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -31,52 +31,34 @@ import { IActiveRowDerivedStateArgs } from "./active-row"; // to pass TItem while still letting the rest of the generics be inferred. // This may be resolved in a newer TypeScript version after https://github.com/microsoft/TypeScript/pull/54047 is merged! -// Common args -// - Used by both useLocalTableControlState and useTableControlState -// - Does not require any state or query values in scope -export interface ITableControlCommonArgs< - TItem, - TColumnKey extends string, - TSortableColumnKey extends TColumnKey, - TFilterCategoryKey extends string = string, -> extends IFilterStateArgs, - ISortStateArgs, - IPaginationStateArgs { - columnNames: Record; // An ordered mapping of unique keys to human-readable column name strings - isSelectable?: boolean; - hasPagination?: boolean; - expandableVariant?: "single" | "compound" | null; - hasActionsColumn?: boolean; - variant?: TableProps["variant"]; -} - -// URL-param-specific args +// Persistence-specific args // - Extra args needed for useTableControlState and each concern-specific use*State hook in URL params mode // - Does not require any state or query values in scope -export interface IExtraArgsForURLParamHooks< - TURLParamKeyPrefix extends string = string, -> { - urlParamKeyPrefix: DisallowCharacters; -} +export type IPersistenceOptions = + { + persistIn?: "state" | "urlParams" | "localStorage" | "sessionStorage"; + persistenceKeyPrefix?: DisallowCharacters; + }; +// Common args +// - Used by both useLocalTableControlState and useTableControlState +// - Does not require any state or query values in scope export type IUseTableControlStateArgs< TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, - TURLParamKeyPrefix extends string = string, -> = ITableControlCommonArgs< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey -> & - ( - | { persistIn?: "state" | "localStorage" | "sessionStorage" } - | ({ - persistIn: "urlParams"; - } & IExtraArgsForURLParamHooks) - ); +> = IFilterStateArgs & + ISortStateArgs & + IPaginationStateArgs & + IPersistenceOptions & { + columnNames: Record; // An ordered mapping of unique keys to human-readable column name strings + isSelectable?: boolean; + hasPagination?: boolean; + expandableVariant?: "single" | "compound" | null; + hasActionsColumn?: boolean; + variant?: TableProps["variant"]; + }; // Data-dependent args // - Used by both useLocalTableControlState and useTableControlProps @@ -95,7 +77,7 @@ export type IUseLocalTableControlStateArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, -> = ITableControlCommonArgs< +> = IUseTableControlStateArgs< TItem, TColumnKey, TSortableColumnKey, @@ -106,7 +88,7 @@ export type IUseLocalTableControlStateArgs< IFilterStateArgs & ILocalSortDerivedStateArgs & ILocalPaginationDerivedStateArgs & - Pick, "initialSelected" | "isItemSelectable">; + Pick, "initialSelected" | "isItemSelectable">; // TODO ??? // Rendering args // - Used by only useTableControlProps @@ -119,7 +101,7 @@ export interface IUseTableControlPropsArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, -> extends ITableControlCommonArgs< +> extends IUseTableControlStateArgs< TItem, TColumnKey, TSortableColumnKey, diff --git a/client/src/app/hooks/table-controls/useLocalTableControlState.ts b/client/src/app/hooks/table-controls/useLocalTableControlState.ts index 3dd671b58a..987543f1e2 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControlState.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControlState.ts @@ -1,9 +1,5 @@ import { useSelectionState } from "@migtools/lib-ui"; -import { - getLocalFilterDerivedState, - useFilterState, - useFilterUrlParams, -} from "./filtering"; +import { getLocalFilterDerivedState, useFilterState } from "./filtering"; import { useSortState, getLocalSortDerivedState, @@ -17,7 +13,6 @@ import { import { useExpansionState, useExpansionUrlParams } from "./expansion"; import { useActiveRowState, useActiveRowUrlParams } from "./active-row"; import { - IExtraArgsForURLParamHooks, IUseLocalTableControlStateArgs, IUseTableControlPropsArgs, } from "./types"; diff --git a/client/src/app/hooks/usePersistedState.ts b/client/src/app/hooks/usePersistedState.ts index 96336ae494..ef342f00d4 100644 --- a/client/src/app/hooks/usePersistedState.ts +++ b/client/src/app/hooks/usePersistedState.ts @@ -6,50 +6,80 @@ import { useSessionStorage, } from "@migtools/lib-ui"; -type UsePersistedStateOptions< - T, - TURLParamKey extends string, - TKeyPrefix extends string, +export type BaseUsePersistedStateOptions< + TPersistedState, + TPersistenceKeyPrefix extends string = string, > = { - defaultValue: T; -} & ( - | { - persistIn?: "state"; - } - | ({ - persistIn: "urlParams"; - } & IUseUrlParamsArgs) - | { - persistIn: "localStorage" | "sessionStorage"; - key: string; - } -); + defaultValue: TPersistedState; + isEnabled?: boolean; + persistenceKeyPrefix?: TPersistenceKeyPrefix; +}; + +export type UsePersistedStateOptions< + TPersistedState, + TPersistenceKeyPrefix extends string, + TURLParamKey extends string, +> = BaseUsePersistedStateOptions & + ( + | { + persistIn?: "state"; + } + | ({ + persistIn: "urlParams"; + } & IUseUrlParamsArgs< + TPersistedState, + TPersistenceKeyPrefix, + TURLParamKey + >) + | ({ + persistIn: "localStorage" | "sessionStorage"; + } & UseStorageTypeOptions) + ); export const usePersistedState = < - T, + TPersistedState, + TPersistenceKeyPrefix extends string, TURLParamKey extends string, - TKeyPrefix extends string, >( - options: UsePersistedStateOptions -): [T | null, (value: T) => void] => { - const storage = { - state: React.useState(options.defaultValue), + options: UsePersistedStateOptions< + TPersistedState, + TPersistenceKeyPrefix, + TURLParamKey + > +): [TPersistedState, (value: TPersistedState) => void] => { + const { + defaultValue, + isEnabled = true, + persistIn, + persistenceKeyPrefix, + } = options; + const urlParamOptions = options as IUseUrlParamsArgs< + TPersistedState, + TPersistenceKeyPrefix, + TURLParamKey + >; + const storageOptions = options as UseStorageTypeOptions; + const prefixedStorageKey = persistenceKeyPrefix + ? `${persistenceKeyPrefix}:${storageOptions.key}` + : storageOptions.key; + const persistence = { + state: React.useState(defaultValue), urlParams: useUrlParams({ - // TODO can we avoid these assertions? how can we narrow the type of `options` depending on mode without conditionals? - // something with `satisfies`? read TS docs on narrowing types with hints - ...(options as IUseUrlParamsArgs), - isEnabled: options.persistIn === "urlParams", + ...urlParamOptions, + isEnabled: isEnabled && persistIn === "urlParams", }), localStorage: useLocalStorage({ - ...(options as UseStorageTypeOptions), - isEnabled: options.persistIn === "localStorage", + ...storageOptions, + isEnabled: isEnabled && persistIn === "localStorage", + key: prefixedStorageKey, }), sessionStorage: useSessionStorage({ - ...(options as UseStorageTypeOptions), - isEnabled: options.persistIn === "sessionStorage", + ...(options as UseStorageTypeOptions), + isEnabled: isEnabled && persistIn === "sessionStorage", + key: prefixedStorageKey, }), }; - return storage[options.persistIn || "state"]; + return persistence[persistIn || "state"]; }; // TODO combine all the use[Feature]State and use[Feature]UrlParams hooks @@ -61,3 +91,4 @@ export const usePersistedState = < // TODO decouple SimplePagination // TODO decouple FilterToolbar? -- can we make a toolbar-batteries hook? useFilterToolbar? option to hook it up to table batteries or not? // TODO decouple useUrlParams from react-router? can we do everything from the document.location.search? +``; diff --git a/client/src/app/hooks/useUrlParams.ts b/client/src/app/hooks/useUrlParams.ts index 5f48256e79..18a9d44e90 100644 --- a/client/src/app/hooks/useUrlParams.ts +++ b/client/src/app/hooks/useUrlParams.ts @@ -21,12 +21,12 @@ export type TSerializedParams = Partial< >; export interface IUseUrlParamsArgs< - TURLParamKey extends string, - TKeyPrefix extends string, TDeserializedParams, + TPersistenceKeyPrefix extends string, + TURLParamKey extends string, > { isEnabled?: boolean; - keyPrefix?: DisallowCharacters; + persistenceKeyPrefix?: DisallowCharacters; keys: DisallowCharacters[]; defaultValue: TDeserializedParams; serialize: ( @@ -38,37 +38,37 @@ export interface IUseUrlParamsArgs< } export type TURLParamStateTuple = [ - TDeserializedParams | null, + TDeserializedParams, (newParams: Partial) => void, ]; export const useUrlParams = < - TURLParamKey extends string, - TKeyPrefix extends string, TDeserializedParams, + TKeyPrefix extends string, + TURLParamKey extends string, >({ isEnabled = true, - keyPrefix, + persistenceKeyPrefix, keys, defaultValue, serialize, deserialize, }: IUseUrlParamsArgs< - TURLParamKey, + TDeserializedParams, TKeyPrefix, - TDeserializedParams + TURLParamKey >): TURLParamStateTuple => { type TPrefixedURLParamKey = TURLParamKey | `${TKeyPrefix}:${TURLParamKey}`; const history = useHistory(); const withPrefix = (key: TURLParamKey): TPrefixedURLParamKey => - keyPrefix ? `${keyPrefix}:${key}` : key; + persistenceKeyPrefix ? `${persistenceKeyPrefix}:${key}` : key; const withPrefixes = ( serializedParams: TSerializedParams ): TSerializedParams => - keyPrefix + persistenceKeyPrefix ? objectKeys(serializedParams).reduce( (obj, key) => ({ ...obj, @@ -100,7 +100,7 @@ export const useUrlParams = < // We un-prefix the params object here so the deserialize function doesn't have to care about the keyPrefix. let allParamsEmpty = true; - let params: TDeserializedParams | null = null; + let params: TDeserializedParams = defaultValue; if (isEnabled) { const serializedParams = keys.reduce( (obj, key) => ({ From f3d0a1d76837b489bae210bb4b00cfb8b5830d64 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 3 Oct 2023 14:29:45 -0400 Subject: [PATCH 023/102] Combine useLocalTableControls and useLocalTableControlsWithUrlParams, fix usage in archetypes table Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 2 +- .../filtering/useFilterState.ts | 16 ++-- client/src/app/hooks/table-controls/types.ts | 2 +- .../useLocalTableControlState.ts | 94 +++---------------- .../table-controls/useLocalTableControls.ts | 20 +--- client/src/app/hooks/usePersistedState.ts | 16 ++-- .../app/pages/archetypes/archetypes-page.tsx | 7 +- .../app/pages/dependencies/dependencies.tsx | 2 +- .../dependencies/dependency-apps-table.tsx | 2 +- .../affected-applications.tsx | 2 +- .../file-all-incidents-table.tsx | 2 +- .../issue-affected-files-table.tsx | 2 +- client/src/app/pages/issues/issues-table.tsx | 2 +- 13 files changed, 41 insertions(+), 128 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index e94ac5fdde..06d7e4d7a1 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -214,7 +214,7 @@ If the endpoints do not support these parameters or you need to have the entire In most cases, you'll only need to use these higher-level hooks and helpers to build a table: -- For client-paginated tables: `useLocalTableControls` or `useLocalTableControlsWithUrlParams` is all you need. These have the same signature and are interchangeable. +- For client-paginated tables: `useLocalTableControls` is all you need. These have the same signature and are interchangeable. - Internally they use `useTableControlState`, `useTableControlProps` and the `getLocal[Feature]DerivedState` helpers. The config arguments object is a combination of the arguments required by `useTableControlState` and `useTableControlProps`. - The return value (an object we generally name `tableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. - For server-paginated tables: `useTableControlState`, `getHubRequestParams`, and `useTableControlProps`. diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index cb2901b85c..531e35066b 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -27,7 +27,7 @@ export const useFilterState = < >( args: IFilterStateArgs ): IFilterState => { - const { persistIn = "state", persistenceKeyPrefix } = args; + const { persistTo = "state", persistenceKeyPrefix } = args; const baseOptions: BaseUsePersistedStateOptions< IFilterValues @@ -37,26 +37,26 @@ export const useFilterState = < }; // Note: for the discriminated union here to work without TypeScript getting confused - // (e.g. require the urlParams-specific options when persistIn === "urlParams"), - // we need to pass persistIn inside each type-narrowed options object instead of outside the ternary. + // (e.g. require the urlParams-specific options when persistTo === "urlParams"), + // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. const [filterValues, setFilterValues] = usePersistedState( - persistIn === "urlParams" + persistTo === "urlParams" ? { ...baseOptions, - persistIn, + persistTo, keys: ["filters"], serialize: serializeFilterUrlParams, deserialize: deserializeFilterUrlParams, } - : persistIn === "localStorage" || persistIn === "sessionStorage" + : persistTo === "localStorage" || persistTo === "sessionStorage" ? { ...baseOptions, - persistIn, + persistTo, key: "filters", } : { ...baseOptions, - persistIn, + persistTo, } ); return { filterValues, setFilterValues }; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index e37846d9a2..05f5f85346 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -36,7 +36,7 @@ import { IActiveRowDerivedStateArgs } from "./active-row"; // - Does not require any state or query values in scope export type IPersistenceOptions = { - persistIn?: "state" | "urlParams" | "localStorage" | "sessionStorage"; + persistTo?: "state" | "urlParams" | "localStorage" | "sessionStorage"; persistenceKeyPrefix?: DisallowCharacters; }; diff --git a/client/src/app/hooks/table-controls/useLocalTableControlState.ts b/client/src/app/hooks/table-controls/useLocalTableControlState.ts index 987543f1e2..fad276dae7 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControlState.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControlState.ts @@ -1,17 +1,12 @@ import { useSelectionState } from "@migtools/lib-ui"; import { getLocalFilterDerivedState, useFilterState } from "./filtering"; -import { - useSortState, - getLocalSortDerivedState, - useSortUrlParams, -} from "./sorting"; +import { useSortState, getLocalSortDerivedState } from "./sorting"; import { getLocalPaginationDerivedState, usePaginationState, - usePaginationUrlParams, } from "./pagination"; -import { useExpansionState, useExpansionUrlParams } from "./expansion"; -import { useActiveRowState, useActiveRowUrlParams } from "./active-row"; +import { useExpansionState } from "./expansion"; +import { useActiveRowState } from "./active-row"; import { IUseLocalTableControlStateArgs, IUseTableControlPropsArgs, @@ -39,108 +34,41 @@ export const useLocalTableControlState = < const filterState = useFilterState(args); const { filteredItems } = getLocalFilterDerivedState({ + ...args, items, filterCategories, filterState, }); - const selectionState = useSelectionState({ - items: filteredItems, - isEqual: (a, b) => a[idProperty] === b[idProperty], - initialSelected, - isItemSelectable, - }); - - const sortState = useSortState({ sortableColumns, initialSort }); + const sortState = useSortState({ ...args, sortableColumns, initialSort }); const { sortedItems } = getLocalSortDerivedState({ + ...args, sortState, items: filteredItems, getSortValues, }); const paginationState = usePaginationState({ + ...args, initialItemsPerPage, }); const { currentPageItems } = getLocalPaginationDerivedState({ - paginationState, - items: sortedItems, - }); - - const expansionState = useExpansionState(); - - const activeRowState = useActiveRowState(); - - return { ...args, - filterState, - expansionState, - selectionState, - sortState, paginationState, - activeRowState, - totalItemCount: items.length, - 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, + items: sortedItems, }); const selectionState = useSelectionState({ + ...args, 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 expansionState = useExpansionState(args); - const activeRowState = useActiveRowUrlParams(args); + const activeRowState = useActiveRowState(args); return { ...args, diff --git a/client/src/app/hooks/table-controls/useLocalTableControls.ts b/client/src/app/hooks/table-controls/useLocalTableControls.ts index 4e5efc6762..e797487c6d 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControls.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControls.ts @@ -1,12 +1,6 @@ -import { - useLocalTableControlState, - useLocalTableControlUrlParams, -} from "./useLocalTableControlState"; +import { useLocalTableControlState } from "./useLocalTableControlState"; import { useTableControlProps } from "./useTableControlProps"; -import { - IExtraArgsForURLParamHooks, - IUseLocalTableControlStateArgs, -} from "./types"; +import { IUseLocalTableControlStateArgs } from "./types"; export const useLocalTableControls = < TItem, @@ -15,13 +9,3 @@ export const useLocalTableControls = < >( 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/usePersistedState.ts b/client/src/app/hooks/usePersistedState.ts index ef342f00d4..f3493d553a 100644 --- a/client/src/app/hooks/usePersistedState.ts +++ b/client/src/app/hooks/usePersistedState.ts @@ -22,17 +22,17 @@ export type UsePersistedStateOptions< > = BaseUsePersistedStateOptions & ( | { - persistIn?: "state"; + persistTo?: "state"; } | ({ - persistIn: "urlParams"; + persistTo: "urlParams"; } & IUseUrlParamsArgs< TPersistedState, TPersistenceKeyPrefix, TURLParamKey >) | ({ - persistIn: "localStorage" | "sessionStorage"; + persistTo: "localStorage" | "sessionStorage"; } & UseStorageTypeOptions) ); @@ -50,7 +50,7 @@ export const usePersistedState = < const { defaultValue, isEnabled = true, - persistIn, + persistTo, persistenceKeyPrefix, } = options; const urlParamOptions = options as IUseUrlParamsArgs< @@ -66,20 +66,20 @@ export const usePersistedState = < state: React.useState(defaultValue), urlParams: useUrlParams({ ...urlParamOptions, - isEnabled: isEnabled && persistIn === "urlParams", + isEnabled: isEnabled && persistTo === "urlParams", }), localStorage: useLocalStorage({ ...storageOptions, - isEnabled: isEnabled && persistIn === "localStorage", + isEnabled: isEnabled && persistTo === "localStorage", key: prefixedStorageKey, }), sessionStorage: useSessionStorage({ ...(options as UseStorageTypeOptions), - isEnabled: isEnabled && persistIn === "sessionStorage", + isEnabled: isEnabled && persistTo === "sessionStorage", key: prefixedStorageKey, }), }; - return persistence[persistIn || "state"]; + return persistence[persistTo || "state"]; }; // TODO combine all the use[Feature]State and use[Feature]UrlParams hooks diff --git a/client/src/app/pages/archetypes/archetypes-page.tsx b/client/src/app/pages/archetypes/archetypes-page.tsx index 94a14b9541..4ffa16f1e4 100644 --- a/client/src/app/pages/archetypes/archetypes-page.tsx +++ b/client/src/app/pages/archetypes/archetypes-page.tsx @@ -38,7 +38,7 @@ import { TableHeaderContentWithControls, TableRowContentWithControls, } from "@app/components/TableControls"; -import { useLocalTableControlsWithUrlParams } from "@app/hooks/table-controls"; +import { useLocalTableControls } from "@app/hooks/table-controls"; import { useDeleteArchetypeMutation, useFetchArchetypes, @@ -98,8 +98,9 @@ const Archetypes: React.FC = () => { onError ); - const tableControls = useLocalTableControlsWithUrlParams({ - urlParamKeyPrefix: TableURLParamKeyPrefix.archetypes, + const tableControls = useLocalTableControls({ + persistTo: "urlParams", + persistenceKeyPrefix: TableURLParamKeyPrefix.archetypes, idProperty: "id", items: archetypes, isLoading: isFetching, diff --git a/client/src/app/pages/dependencies/dependencies.tsx b/client/src/app/pages/dependencies/dependencies.tsx index af23037c2d..77823da35d 100644 --- a/client/src/app/pages/dependencies/dependencies.tsx +++ b/client/src/app/pages/dependencies/dependencies.tsx @@ -39,7 +39,7 @@ export const Dependencies: React.FC = () => { useSharedAffectedApplicationFilterCategories(); const tableControlState = useTableControlState({ - persistIn: "urlParams", + persistTo: "urlParams", urlParamKeyPrefix: TableURLParamKeyPrefix.dependencies, columnNames: { name: "Dependency name", diff --git a/client/src/app/pages/dependencies/dependency-apps-table.tsx b/client/src/app/pages/dependencies/dependency-apps-table.tsx index 46da5b46b6..7c6fdd77af 100644 --- a/client/src/app/pages/dependencies/dependency-apps-table.tsx +++ b/client/src/app/pages/dependencies/dependency-apps-table.tsx @@ -34,7 +34,7 @@ export const DependencyAppsTable: React.FC = ({ const { tags } = useFetchTags(); const tableControlState = useTableControlState({ - persistIn: "urlParams", + persistTo: "urlParams", urlParamKeyPrefix: TableURLParamKeyPrefix.dependencyApplications, columnNames: { name: "Application", diff --git a/client/src/app/pages/issues/affected-applications/affected-applications.tsx b/client/src/app/pages/issues/affected-applications/affected-applications.tsx index f4a2c0b412..8aa4c1754e 100644 --- a/client/src/app/pages/issues/affected-applications/affected-applications.tsx +++ b/client/src/app/pages/issues/affected-applications/affected-applications.tsx @@ -53,7 +53,7 @@ export const AffectedApplications: React.FC = () => { "Active rule"; const tableControlState = useTableControlState({ - persistIn: "urlParams", + persistTo: "urlParams", urlParamKeyPrefix: TableURLParamKeyPrefix.issuesAffectedApps, columnNames: { name: "Name", diff --git a/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx b/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx index 1670d099af..d1c21f05a3 100644 --- a/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx +++ b/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx @@ -26,7 +26,7 @@ export const FileAllIncidentsTable: React.FC< IFileRemainingIncidentsTableProps > = ({ fileReport }) => { const tableControlState = useTableControlState({ - persistIn: "urlParams", + persistTo: "urlParams", urlParamKeyPrefix: TableURLParamKeyPrefix.issuesRemainingIncidents, columnNames: { line: "Line #", diff --git a/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx b/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx index cb46f3eb24..267e261681 100644 --- a/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx +++ b/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx @@ -36,7 +36,7 @@ export const IssueAffectedFilesTable: React.FC< const { t } = useTranslation(); const tableControlState = useTableControlState({ - persistIn: "urlParams", + persistTo: "urlParams", urlParamKeyPrefix: TableURLParamKeyPrefix.issuesAffectedFiles, columnNames: { file: "File", diff --git a/client/src/app/pages/issues/issues-table.tsx b/client/src/app/pages/issues/issues-table.tsx index 4bfcee0464..8534fadc40 100644 --- a/client/src/app/pages/issues/issues-table.tsx +++ b/client/src/app/pages/issues/issues-table.tsx @@ -96,7 +96,7 @@ export const IssuesTable: React.FC = ({ mode }) => { useSharedAffectedApplicationFilterCategories(); const tableControlState = useTableControlState({ - persistIn: "urlParams", + persistTo: "urlParams", urlParamKeyPrefix: TableURLParamKeyPrefix.issues, columnNames: { description: "Issue", From f2a08572ac97d71e325975ac57d339372a299212 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 3 Oct 2023 14:33:29 -0400 Subject: [PATCH 024/102] Commit missing file Signed-off-by: Mike Turley --- .../hooks/table-controls/filtering/helpers.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 client/src/app/hooks/table-controls/filtering/helpers.ts diff --git a/client/src/app/hooks/table-controls/filtering/helpers.ts b/client/src/app/hooks/table-controls/filtering/helpers.ts new file mode 100644 index 0000000000..265da451fa --- /dev/null +++ b/client/src/app/hooks/table-controls/filtering/helpers.ts @@ -0,0 +1,34 @@ +import { FilterValue, IFilterValues } from "@app/components/FilterToolbar"; +import { objectKeys } from "@app/utils/utils"; + +export const serializeFilterUrlParams = ( + filterValues: IFilterValues +): { filters?: string | null } => { + // If a filter value is empty/cleared, don't put it in the object in URL params + const trimmedFilterValues = { ...filterValues }; + objectKeys(trimmedFilterValues).forEach((filterCategoryKey) => { + if ( + !trimmedFilterValues[filterCategoryKey] || + trimmedFilterValues[filterCategoryKey]?.length === 0 + ) { + delete trimmedFilterValues[filterCategoryKey]; + } + }); + return { + filters: + objectKeys(trimmedFilterValues).length > 0 + ? JSON.stringify(trimmedFilterValues) + : null, // If there are no filters, remove the filters param from the URL entirely. + }; +}; +export const deserializeFilterUrlParams = < + TFilterCategoryKey extends string, +>(serializedParams: { + filters?: string | null; +}): Partial> => { + try { + return JSON.parse(serializedParams.filters || "{}"); + } catch (e) { + return {}; + } +}; From 384686398cce9a7626ef5dfc6409acb5cbe09980 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 3 Oct 2023 14:33:45 -0400 Subject: [PATCH 025/102] Fix useLegacyFilterState (remove unused storagekey) Signed-off-by: Mike Turley --- client/src/app/hooks/useLegacyFilterState.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/app/hooks/useLegacyFilterState.ts b/client/src/app/hooks/useLegacyFilterState.ts index b9e48a4dd0..5622ccaa64 100644 --- a/client/src/app/hooks/useLegacyFilterState.ts +++ b/client/src/app/hooks/useLegacyFilterState.ts @@ -16,11 +16,10 @@ export interface IFilterStateHook { export const useLegacyFilterState = ( items: TItem[], - filterCategories: FilterCategory[], - filterStorageKey?: string + filterCategories: FilterCategory[] ): IFilterStateHook => { const { filterValues, setFilterValues } = useFilterState({ - filterStorageKey, + filterCategories, }); const { filteredItems } = getLocalFilterDerivedState({ items, From 9eea5486b939c20f806f8b82da59d64a575833a4 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 3 Oct 2023 14:42:00 -0400 Subject: [PATCH 026/102] Resolving errors with useFilterState Signed-off-by: Mike Turley --- .../table-controls/expansion/useExpansionState.ts | 12 ++++++------ client/src/app/hooks/table-controls/types.ts | 3 ++- .../app/hooks/table-controls/useTableControlState.ts | 10 ++++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index 0080710258..4f01837cbb 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -1,7 +1,4 @@ import React from "react"; -import { useUrlParams } from "../../useUrlParams"; -import { objectKeys } from "@app/utils/utils"; -import { IExtraArgsForURLParamHooks } from "../types"; // TExpandedCells maps item[idProperty] values to either: // - The key of an expanded column in that row, if the table is compound-expandable @@ -19,17 +16,19 @@ export interface IExpansionState { } export const useExpansionState = < - TColumnKey extends string + TColumnKey extends string, + TPersistenceKeyPrefix extends string = string, >(): IExpansionState => { + // TODO use usePersistedState here const [expandedCells, setExpandedCells] = React.useState< TExpandedCells >({}); return { expandedCells, setExpandedCells }; }; - +/* export const useExpansionUrlParams = < TColumnKey extends string, - TURLParamKeyPrefix extends string = string + TURLParamKeyPrefix extends string = string, >({ urlParamKeyPrefix, }: IExtraArgsForURLParamHooks = {}): IExpansionState => { @@ -52,3 +51,4 @@ export const useExpansionUrlParams = < }); return { expandedCells, setExpandedCells }; }; +*/ diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 05f5f85346..bba516670d 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -48,7 +48,8 @@ export type IUseTableControlStateArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, -> = IFilterStateArgs & + TPersistenceKeyPrefix extends string = string, +> = IFilterStateArgs & ISortStateArgs & IPaginationStateArgs & IPersistenceOptions & { diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 73cafe111f..5b5d7703e5 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -10,25 +10,27 @@ export const useTableControlState = < TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, - TURLParamKeyPrefix extends string = string, + TPersistenceKeyPrefix extends string = string, >( args: IUseTableControlStateArgs< TItem, TColumnKey, TSortableColumnKey, TFilterCategoryKey, - TURLParamKeyPrefix + TPersistenceKeyPrefix > ) => { // Must pass type params because they can't all be inferred from the required args of useFilterState const filterState = useFilterState< + TItem, TFilterCategoryKey, // Must pass this because no required args here have categories to infer from - TURLParamKeyPrefix + TPersistenceKeyPrefix >(args); const sortState = useSortState(args); // Type params inferred from args const paginationState = usePaginationState(args); // Type params inferred from args // Must pass type params because they can't all be inferred from the required args of useExpansionState - const expansionState = useExpansionState( + // TODO is that still true here? + const expansionState = useExpansionState( args ); const activeRowState = useActiveRowState(args); // Type params inferred from args From 66027fd602581bf7918d30dbe9a8fa1cc3f0a2a6 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 3 Oct 2023 14:43:03 -0400 Subject: [PATCH 027/102] TODOs Signed-off-by: Mike Turley --- client/src/app/hooks/usePersistedState.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/app/hooks/usePersistedState.ts b/client/src/app/hooks/usePersistedState.ts index f3493d553a..f6b97cfcaa 100644 --- a/client/src/app/hooks/usePersistedState.ts +++ b/client/src/app/hooks/usePersistedState.ts @@ -83,6 +83,8 @@ export const usePersistedState = < }; // TODO combine all the use[Feature]State and use[Feature]UrlParams hooks +// TODO search for and make sure there are no more hooks with "urlParams" in the name +// TODO TEST IT - should build and work at this point // TODO bring in useSelectionState as a persistable thing // TODO add JSdoc comments for all inputs and outputs // TODO explore the state contract needed for using useTableControlProps with custom state logic From 2b274e778524c11e4895ac37009e262ee5c270c1 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 3 Oct 2023 15:27:38 -0400 Subject: [PATCH 028/102] Remove stray characters Signed-off-by: Mike Turley --- client/src/app/hooks/usePersistedState.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/app/hooks/usePersistedState.ts b/client/src/app/hooks/usePersistedState.ts index f6b97cfcaa..bd2c01642b 100644 --- a/client/src/app/hooks/usePersistedState.ts +++ b/client/src/app/hooks/usePersistedState.ts @@ -93,4 +93,3 @@ export const usePersistedState = < // TODO decouple SimplePagination // TODO decouple FilterToolbar? -- can we make a toolbar-batteries hook? useFilterToolbar? option to hook it up to table batteries or not? // TODO decouple useUrlParams from react-router? can we do everything from the document.location.search? -``; From 1f515472be3f87ad78a3b282ab51a852cdca4a76 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 3 Oct 2023 16:29:45 -0400 Subject: [PATCH 029/102] WIP refactor of useSortState Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 2 +- .../expansion/useExpansionState.ts | 2 +- .../filtering/useFilterState.ts | 34 +++---- .../sorting/getLocalSortDerivedState.ts | 4 +- .../sorting/getSortHubRequestParams.ts | 2 +- .../table-controls/sorting/getSortProps.ts | 6 +- .../table-controls/sorting/useSortState.ts | 93 ++++++++++--------- client/src/app/hooks/table-controls/types.ts | 8 +- .../useLocalTableControlState.ts | 8 +- ...ersistedState.ts => usePersistentState.ts} | 39 ++++---- 10 files changed, 100 insertions(+), 98 deletions(-) rename client/src/app/hooks/{usePersistedState.ts => usePersistentState.ts} (75%) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 06d7e4d7a1..22cefbc1ed 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -256,7 +256,7 @@ Items are filtered according to user-selected filter key/value pairs. Items are sorted according to the user-selected sort column and direction. - Sortable columns are defined by a `sortableColumns` array of `TColumnKey` values (see [Unique Identifiers](#unique-identifiers)). -- Sort state is provided by `useSortState` or `useSortUrlParams`. +- Sort state is provided by `useSortState`. - For client-side sorting, the sort logic is provided by `getLocalSortDerivedState` (based on the `getSortValues` config argument, which is not required when using server-side sorting). - For server-side sorting, sort state is serialized for the API by `getSortHubRequestParams`. - Sort-related component props are provided by `getSortProps`. diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index 4f01837cbb..51d3503854 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -19,7 +19,7 @@ export const useExpansionState = < TColumnKey extends string, TPersistenceKeyPrefix extends string = string, >(): IExpansionState => { - // TODO use usePersistedState here + // TODO use usePersistentState here const [expandedCells, setExpandedCells] = React.useState< TExpandedCells >({}); diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index 531e35066b..f4c270e25c 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -1,9 +1,9 @@ import { FilterCategory, IFilterValues } from "@app/components/FilterToolbar"; import { IPersistenceOptions } from "../types"; import { - BaseUsePersistedStateOptions, - usePersistedState, -} from "@app/hooks/usePersistedState"; + BaseUsePersistentStateOptions, + usePersistentState, +} from "@app/hooks/usePersistentState"; import { serializeFilterUrlParams } from "./helpers"; import { deserializeFilterUrlParams } from "./helpers"; @@ -12,24 +12,21 @@ export interface IFilterState { setFilterValues: (values: IFilterValues) => void; } -export type IFilterStateArgs< - TItem, - TFilterCategoryKey extends string, - TPersistenceKeyPrefix extends string = string, -> = { +export type IFilterStateArgs = { filterCategories?: FilterCategory[]; -} & IPersistenceOptions; +}; export const useFilterState = < TItem, TFilterCategoryKey extends string, TPersistenceKeyPrefix extends string = string, >( - args: IFilterStateArgs + args: IFilterStateArgs & + IPersistenceOptions ): IFilterState => { const { persistTo = "state", persistenceKeyPrefix } = args; - const baseOptions: BaseUsePersistedStateOptions< + const baseStateOptions: BaseUsePersistentStateOptions< IFilterValues > = { defaultValue: {}, @@ -39,25 +36,18 @@ export const useFilterState = < // Note: for the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. - const [filterValues, setFilterValues] = usePersistedState( + const [filterValues, setFilterValues] = usePersistentState( persistTo === "urlParams" ? { - ...baseOptions, + ...baseStateOptions, persistTo, keys: ["filters"], serialize: serializeFilterUrlParams, deserialize: deserializeFilterUrlParams, } : persistTo === "localStorage" || persistTo === "sessionStorage" - ? { - ...baseOptions, - persistTo, - key: "filters", - } - : { - ...baseOptions, - persistTo, - } + ? { ...baseStateOptions, persistTo, key: "filters" } + : { ...baseStateOptions, persistTo } ); return { filterValues, setFilterValues }; }; diff --git a/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts b/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts index 6903650718..a9fe8ab1ca 100644 --- a/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts +++ b/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts @@ -3,7 +3,7 @@ import { ISortState } from "./useSortState"; export interface ILocalSortDerivedStateArgs< TItem, - TSortableColumnKey extends string + TSortableColumnKey extends string, > { items: TItem[]; getSortValues?: ( @@ -13,7 +13,7 @@ export interface ILocalSortDerivedStateArgs< export const getLocalSortDerivedState = < TItem, - TSortableColumnKey extends string + TSortableColumnKey extends string, >({ items, getSortValues, diff --git a/client/src/app/hooks/table-controls/sorting/getSortHubRequestParams.ts b/client/src/app/hooks/table-controls/sorting/getSortHubRequestParams.ts index b807963c4d..ce788b360c 100644 --- a/client/src/app/hooks/table-controls/sorting/getSortHubRequestParams.ts +++ b/client/src/app/hooks/table-controls/sorting/getSortHubRequestParams.ts @@ -2,7 +2,7 @@ import { HubRequestParams } from "@app/api/models"; import { ISortState } from "./useSortState"; export interface IGetSortHubRequestParamsArgs< - TSortableColumnKey extends string + TSortableColumnKey extends string, > { sortState?: ISortState; hubSortFieldKeys?: Record; diff --git a/client/src/app/hooks/table-controls/sorting/getSortProps.ts b/client/src/app/hooks/table-controls/sorting/getSortProps.ts index 7c91d08c78..5bd14a7477 100644 --- a/client/src/app/hooks/table-controls/sorting/getSortProps.ts +++ b/client/src/app/hooks/table-controls/sorting/getSortProps.ts @@ -4,7 +4,7 @@ import { ISortState } from "./useSortState"; // Args that are part of IUseTableControlPropsArgs (the args for useTableControlProps) export interface ISortPropsArgs< TColumnKey extends string, - TSortableColumnKey extends TColumnKey + TSortableColumnKey extends TColumnKey, > { sortState: ISortState; } @@ -12,7 +12,7 @@ export interface ISortPropsArgs< // Additional args that need to be passed in on a per-column basis export interface IUseSortPropsArgs< TColumnKey extends string, - TSortableColumnKey extends TColumnKey + TSortableColumnKey extends TColumnKey, > extends ISortPropsArgs { columnKeys: TColumnKey[]; columnKey: TSortableColumnKey; @@ -20,7 +20,7 @@ export interface IUseSortPropsArgs< export const getSortProps = < TColumnKey extends string, - TSortableColumnKey extends TColumnKey + TSortableColumnKey extends TColumnKey, >({ sortState: { activeSort, setActiveSort }, columnKeys, diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index 71e001a45e..bb2944c7c3 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -1,6 +1,6 @@ -import * as React from "react"; -import { useUrlParams } from "../../useUrlParams"; -import { IExtraArgsForURLParamHooks } from "../types"; +import { IPersistenceOptions } from ".."; +import { usePersistentState } from "@app/hooks/usePersistentState"; +import { BaseUsePersistentStateOptions } from "@app/hooks/usePersistentState"; export interface IActiveSort { columnKey: TSortableColumnKey; @@ -12,50 +12,57 @@ export interface ISortState { setActiveSort: (sort: IActiveSort) => void; } -export interface ISortStateArgs { +export type ISortStateArgs = { sortableColumns?: TSortableColumnKey[]; initialSort?: IActiveSort | null; -} - -const getDefaultSort = ( - sortableColumns: TSortableColumnKey[] -): IActiveSort | null => - sortableColumns[0] - ? { columnKey: sortableColumns[0], direction: "asc" } - : null; - -export const useSortState = ({ - sortableColumns = [], - initialSort = getDefaultSort(sortableColumns), -}: ISortStateArgs): ISortState => { - const [activeSort, setActiveSort] = React.useState(initialSort); - return { activeSort, setActiveSort }; }; -export const useSortUrlParams = < +export const useSortState = < TSortableColumnKey extends string, - TURLParamKeyPrefix extends string = string, ->({ - sortableColumns = [], - initialSort = getDefaultSort(sortableColumns), - urlParamKeyPrefix, -}: ISortStateArgs & - IExtraArgsForURLParamHooks): ISortState => { - const [activeSort, setActiveSort] = useUrlParams({ - keyPrefix: urlParamKeyPrefix, - keys: ["sortColumn", "sortDirection"], - defaultValue: initialSort, - serialize: (activeSort) => ({ - sortColumn: activeSort?.columnKey || null, - sortDirection: activeSort?.direction || null, - }), - deserialize: (urlParams) => - urlParams.sortColumn && urlParams.sortDirection - ? { - columnKey: urlParams.sortColumn as TSortableColumnKey, - direction: urlParams.sortDirection as "asc" | "desc", - } - : null, - }); + TPersistenceKeyPrefix extends string = string, +>( + args: ISortStateArgs & + IPersistenceOptions +): ISortState => { + const { + persistTo = "state", + persistenceKeyPrefix, + sortableColumns = [], + initialSort = sortableColumns[0] + ? { columnKey: sortableColumns[0], direction: "asc" } + : null, + } = args; + + const baseStateOptions: BaseUsePersistentStateOptions | null> = + { + defaultValue: initialSort, + persistenceKeyPrefix, + }; + + // Note: for the discriminated union here to work without TypeScript getting confused + // (e.g. require the urlParams-specific options when persistTo === "urlParams"), + // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. + const [activeSort, setActiveSort] = usePersistentState( + persistTo === "urlParams" + ? { + ...baseStateOptions, + persistTo, + keys: ["sortColumn", "sortDirection"], + serialize: (activeSort) => ({ + sortColumn: activeSort?.columnKey || null, + sortDirection: activeSort?.direction || null, + }), + deserialize: (urlParams) => + urlParams.sortColumn && urlParams.sortDirection + ? { + columnKey: urlParams.sortColumn as TSortableColumnKey, + direction: urlParams.sortDirection as "asc" | "desc", + } + : null, + } + : persistTo === "localStorage" || persistTo === "sessionStorage" + ? { ...baseStateOptions, persistTo, key: "sort" } + : { ...baseStateOptions, persistTo } + ); return { activeSort, setActiveSort }; }; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index bba516670d..af8f85dc6e 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -49,10 +49,10 @@ export type IUseTableControlStateArgs< TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, -> = IFilterStateArgs & +> = IFilterStateArgs & ISortStateArgs & IPaginationStateArgs & - IPersistenceOptions & { + IPersistenceOptions & { columnNames: Record; // An ordered mapping of unique keys to human-readable column name strings isSelectable?: boolean; hasPagination?: boolean; @@ -78,11 +78,13 @@ export type IUseLocalTableControlStateArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, + TPersistenceKeyPrefix extends string = string, > = IUseTableControlStateArgs< TItem, TColumnKey, TSortableColumnKey, - TFilterCategoryKey + TFilterCategoryKey, + TPersistenceKeyPrefix > & ITableControlDataDependentArgs & ILocalFilterDerivedStateArgs & diff --git a/client/src/app/hooks/table-controls/useLocalTableControlState.ts b/client/src/app/hooks/table-controls/useLocalTableControlState.ts index fad276dae7..883c17c43f 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControlState.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControlState.ts @@ -16,8 +16,14 @@ export const useLocalTableControlState = < TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, + TPersistenceKeyPrefix extends string = string, >( - args: IUseLocalTableControlStateArgs + args: IUseLocalTableControlStateArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TPersistenceKeyPrefix + > ): IUseTableControlPropsArgs => { const { items, diff --git a/client/src/app/hooks/usePersistedState.ts b/client/src/app/hooks/usePersistentState.ts similarity index 75% rename from client/src/app/hooks/usePersistedState.ts rename to client/src/app/hooks/usePersistentState.ts index bd2c01642b..ddd0742682 100644 --- a/client/src/app/hooks/usePersistedState.ts +++ b/client/src/app/hooks/usePersistentState.ts @@ -6,47 +6,43 @@ import { useSessionStorage, } from "@migtools/lib-ui"; -export type BaseUsePersistedStateOptions< - TPersistedState, +export type BaseUsePersistentStateOptions< + TValue, TPersistenceKeyPrefix extends string = string, > = { - defaultValue: TPersistedState; + defaultValue: TValue; isEnabled?: boolean; persistenceKeyPrefix?: TPersistenceKeyPrefix; }; -export type UsePersistedStateOptions< - TPersistedState, +export type UsePersistentStateOptions< + TValue, TPersistenceKeyPrefix extends string, TURLParamKey extends string, -> = BaseUsePersistedStateOptions & +> = BaseUsePersistentStateOptions & ( | { persistTo?: "state"; } | ({ persistTo: "urlParams"; - } & IUseUrlParamsArgs< - TPersistedState, - TPersistenceKeyPrefix, - TURLParamKey - >) + } & IUseUrlParamsArgs) | ({ persistTo: "localStorage" | "sessionStorage"; - } & UseStorageTypeOptions) + } & UseStorageTypeOptions) ); -export const usePersistedState = < - TPersistedState, +export const usePersistentState = < + TValue, TPersistenceKeyPrefix extends string, TURLParamKey extends string, >( - options: UsePersistedStateOptions< - TPersistedState, + options: UsePersistentStateOptions< + TValue, TPersistenceKeyPrefix, TURLParamKey > -): [TPersistedState, (value: TPersistedState) => void] => { +): [TValue, (value: TValue) => void] => { const { defaultValue, isEnabled = true, @@ -54,11 +50,11 @@ export const usePersistedState = < persistenceKeyPrefix, } = options; const urlParamOptions = options as IUseUrlParamsArgs< - TPersistedState, + TValue, TPersistenceKeyPrefix, TURLParamKey >; - const storageOptions = options as UseStorageTypeOptions; + const storageOptions = options as UseStorageTypeOptions; const prefixedStorageKey = persistenceKeyPrefix ? `${persistenceKeyPrefix}:${storageOptions.key}` : storageOptions.key; @@ -74,7 +70,7 @@ export const usePersistedState = < key: prefixedStorageKey, }), sessionStorage: useSessionStorage({ - ...(options as UseStorageTypeOptions), + ...(options as UseStorageTypeOptions), isEnabled: isEnabled && persistTo === "sessionStorage", key: prefixedStorageKey, }), @@ -84,10 +80,11 @@ export const usePersistedState = < // TODO combine all the use[Feature]State and use[Feature]UrlParams hooks // TODO search for and make sure there are no more hooks with "urlParams" in the name -// TODO TEST IT - should build and work at this point +// TODO TEST IT - should build and work at this point. verify ALL state is persisted in all targets // TODO bring in useSelectionState as a persistable thing // TODO add JSdoc comments for all inputs and outputs // TODO explore the state contract needed for using useTableControlProps with custom state logic +// - replace IUseTableControlPropsArgs with something like a TableState object // TODO rename args to options in all types and code // TODO rename active-row to active-item // TODO decouple SimplePagination From 9f8cfbd0f0e510b4a0da826a958f0a0f9b58c36a Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 3 Oct 2023 16:40:02 -0400 Subject: [PATCH 030/102] Move TODOs to notes app Signed-off-by: Mike Turley --- client/src/app/hooks/usePersistentState.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/client/src/app/hooks/usePersistentState.ts b/client/src/app/hooks/usePersistentState.ts index ddd0742682..643c47d9ed 100644 --- a/client/src/app/hooks/usePersistentState.ts +++ b/client/src/app/hooks/usePersistentState.ts @@ -77,16 +77,3 @@ export const usePersistentState = < }; return persistence[persistTo || "state"]; }; - -// TODO combine all the use[Feature]State and use[Feature]UrlParams hooks -// TODO search for and make sure there are no more hooks with "urlParams" in the name -// TODO TEST IT - should build and work at this point. verify ALL state is persisted in all targets -// TODO bring in useSelectionState as a persistable thing -// TODO add JSdoc comments for all inputs and outputs -// TODO explore the state contract needed for using useTableControlProps with custom state logic -// - replace IUseTableControlPropsArgs with something like a TableState object -// TODO rename args to options in all types and code -// TODO rename active-row to active-item -// TODO decouple SimplePagination -// TODO decouple FilterToolbar? -- can we make a toolbar-batteries hook? useFilterToolbar? option to hook it up to table batteries or not? -// TODO decouple useUrlParams from react-router? can we do everything from the document.location.search? From c83b78d7a4880cd09dcb4ff31a2303c136a9bbd9 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 5 Oct 2023 12:52:53 -0400 Subject: [PATCH 031/102] Remove remaining references to urlparam hooks from docs file Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 22cefbc1ed..dd01fe42d1 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -267,7 +267,7 @@ Items are sorted according to the user-selected sort column and direction. Items are paginated according to the user-selected page number and items-per-page count. - The only config argument for pagination is the optional `initialItemsPerPage` which defaults to 10. -- Pagination state is provided by `usePaginationState` or `usePaginationUrlParams`. +- Pagination state is provided by `usePaginationState`. - For client-side pagination, the pagination logic is provided by `getLocalPaginationDerivedState`. - For server-side pagination, pagination state is serialized for the API by `getPaginationHubRequestParams`. - Pagination-related component props are provided by `getPaginationProps`. @@ -281,7 +281,7 @@ Items are paginated according to the user-selected page number and items-per-pag Item details can be expanded, either with a "single expansion" variant where an entire row is expanded to show more detail or a "compound expansion" variant where an individual cell in a row (one at a time per row) is expanded. This is tracked in state by a mapping of item ids (derived from the `idProperty` config argument) to either a boolean value (for single expansion) or a `columnKey` value (for compound expansion). See [Unique Identifiers](#unique-identifiers) for more on `idProperty` and `columnKey`. - Single or compound expansion is defined by the optional `expandableVariant` config argument which defaults to `"single"`. -- Expansion state is provided by `useExpansionState` or `useExpansionUrlParams`. +- Expansion state is provided by `useExpansionState`. - Expansion shorthand functions are provided by `getExpansionDerivedState`. - Expansion is never managed server-side. - Expansion-related component props are provided inside `useTableControlProps` in the `getSingleExpandTdProps` and `getCompoundExpandTdProps` functions. @@ -294,7 +294,7 @@ Item details can be expanded, either with a "single expansion" variant where an An item can be clicked to mark it as "active", which usually opens a drawer on the page to show more detail. Note that this is distinct from expansion and selection and these features can all be used together. Active row state is simply a single id value (number or string) for the active item, derived from the `idProperty` config argument (see [Unique Identifiers](#unique-identifiers)). - The active row feature requires no config arguments. -- Active row state is provided by `useActiveRowState` or `useActiveRowUrlParams`. +- Active row state is provided by `useActiveRowState`. - Active row shorthand functions are provided by `getActiveRowDerivedState`. - A `useEffect` call which prevents invalid state after an item is deleted is provided by `useActiveRowEffects`. From 70220048dcec1c5f5e774ada9be995a9f01d0f6e Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 6 Oct 2023 16:52:21 -0400 Subject: [PATCH 032/102] WIP refactor of usePaginationState Signed-off-by: Mike Turley --- .../filtering/useFilterState.ts | 8 +- .../pagination/usePaginationState.ts | 109 ++++++++++-------- .../table-controls/sorting/useSortState.ts | 6 +- 3 files changed, 76 insertions(+), 47 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index f4c270e25c..f26077467f 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -46,7 +46,13 @@ export const useFilterState = < deserialize: deserializeFilterUrlParams, } : persistTo === "localStorage" || persistTo === "sessionStorage" - ? { ...baseStateOptions, persistTo, key: "filters" } + ? { + ...baseStateOptions, + persistTo, + key: `${ + persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : "" + }filters`, + } : { ...baseStateOptions, persistTo } ); return { filterValues, setFilterValues }; diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 7d6e91ff0b..43fc0597fb 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -1,6 +1,8 @@ -import * as React from "react"; -import { useUrlParams } from "../../useUrlParams"; -import { IExtraArgsForURLParamHooks } from "../types"; +import { + BaseUsePersistentStateOptions, + usePersistentState, +} from "@app/hooks/usePersistentState"; +import { IPersistenceOptions } from "../types"; export interface IPaginationState { pageNumber: number; @@ -13,50 +15,67 @@ export interface IPaginationStateArgs { initialItemsPerPage?: number; } -export const usePaginationState = ({ - initialItemsPerPage = 10, -}: IPaginationStateArgs): IPaginationState => { - const [pageNumber, baseSetPageNumber] = React.useState(1); - const setPageNumber = (num: number) => baseSetPageNumber(num >= 1 ? num : 1); - const [itemsPerPage, setItemsPerPage] = React.useState(initialItemsPerPage); - return { pageNumber, setPageNumber, itemsPerPage, setItemsPerPage }; -}; +export const usePaginationState = < + TPersistenceKeyPrefix extends string = string, +>( + args: IPaginationStateArgs & IPersistenceOptions +): IPaginationState => { + const { + persistTo = "state", + persistenceKeyPrefix, + initialItemsPerPage = 10, + } = args; -export const usePaginationUrlParams = < - TURLParamKeyPrefix extends string = string ->({ - initialItemsPerPage = 10, - urlParamKeyPrefix, -}: IPaginationStateArgs & - IExtraArgsForURLParamHooks): IPaginationState => { const defaultValue = { pageNumber: 1, itemsPerPage: initialItemsPerPage }; - const [paginationState, setPaginationState] = useUrlParams({ - keyPrefix: urlParamKeyPrefix, - keys: ["pageNumber", "itemsPerPage"], - defaultValue, - serialize: ({ pageNumber, itemsPerPage }) => ({ - pageNumber: pageNumber ? String(pageNumber) : undefined, - itemsPerPage: itemsPerPage ? String(itemsPerPage) : undefined, - }), - deserialize: ({ pageNumber, itemsPerPage }) => - pageNumber && itemsPerPage - ? { - pageNumber: parseInt(pageNumber, 10), - itemsPerPage: parseInt(itemsPerPage, 10), - } - : defaultValue, - }); + const baseStateOptions: BaseUsePersistentStateOptions< + typeof defaultValue | null + > = { defaultValue, persistenceKeyPrefix }; - const setPageNumber = (pageNumber: number) => - setPaginationState({ pageNumber: pageNumber >= 1 ? pageNumber : 1 }); + const [paginationState, setPaginationState] = usePersistentState( + persistTo === "urlParams" + ? { + ...baseStateOptions, + persistTo, + keys: ["pageNumber", "itemsPerPage"], + serialize: (state) => { + const { pageNumber, itemsPerPage } = state || {}; + return { + pageNumber: pageNumber ? String(pageNumber) : undefined, + itemsPerPage: itemsPerPage ? String(itemsPerPage) : undefined, + }; + }, + deserialize: (urlParams) => { + const { pageNumber, itemsPerPage } = urlParams || {}; + return pageNumber && itemsPerPage + ? { + pageNumber: parseInt(pageNumber, 10), + itemsPerPage: parseInt(itemsPerPage, 10), + } + : defaultValue; + }, + } + : persistTo === "localStorage" || persistTo === "sessionStorage" + ? { + ...baseStateOptions, + persistTo, + key: `${ + persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : "" + }pagination`, + } + : { ...baseStateOptions, persistTo } + ); + const { pageNumber, itemsPerPage } = paginationState || defaultValue; + const setPageNumber = (num: number) => + setPaginationState({ + pageNumber: num >= 1 ? num : 1, + itemsPerPage: paginationState?.itemsPerPage || initialItemsPerPage, + }); const setItemsPerPage = (itemsPerPage: number) => - setPaginationState({ itemsPerPage }); - - const { pageNumber, itemsPerPage } = paginationState; - return { - pageNumber: pageNumber || 1, - itemsPerPage: itemsPerPage || initialItemsPerPage, - setPageNumber, - setItemsPerPage, - }; + setPaginationState({ + pageNumber: paginationState?.pageNumber || 1, + itemsPerPage, + }); + return { pageNumber, setPageNumber, itemsPerPage, setItemsPerPage }; }; + +// TODO look for and replace all usages of usePaginationUrlParams and fix usage of usePaginationState diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index bb2944c7c3..1b88a3c301 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -61,7 +61,11 @@ export const useSortState = < : null, } : persistTo === "localStorage" || persistTo === "sessionStorage" - ? { ...baseStateOptions, persistTo, key: "sort" } + ? { + ...baseStateOptions, + persistTo, + key: `${persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : ""}sort`, + } : { ...baseStateOptions, persistTo } ); return { activeSort, setActiveSort }; From eb12fc32a059c2a13101a9c811d71600fdd193d8 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 10 Oct 2023 13:20:16 -0400 Subject: [PATCH 033/102] WIP refactor of useExpansionState, fix mismatched type params Signed-off-by: Mike Turley --- .../expansion/useExpansionState.ts | 80 +++++++++++-------- .../pagination/usePaginationState.ts | 2 - .../useLocalTableControlState.ts | 5 +- .../table-controls/useTableControlState.ts | 20 ++--- client/src/app/hooks/usePersistentState.ts | 3 +- 5 files changed, 60 insertions(+), 50 deletions(-) diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index 51d3503854..0c498c32f9 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -1,4 +1,9 @@ -import React from "react"; +import { + BaseUsePersistentStateOptions, + usePersistentState, +} from "@app/hooks/usePersistentState"; +import { objectKeys } from "@app/utils/utils"; +import { IPersistenceOptions } from "../types"; // TExpandedCells maps item[idProperty] values to either: // - The key of an expanded column in that row, if the table is compound-expandable @@ -18,37 +23,48 @@ export interface IExpansionState { export const useExpansionState = < TColumnKey extends string, TPersistenceKeyPrefix extends string = string, ->(): IExpansionState => { - // TODO use usePersistentState here - const [expandedCells, setExpandedCells] = React.useState< +>( + args: IPersistenceOptions +): IExpansionState => { + const { persistTo = "state", persistenceKeyPrefix } = args; + const baseStateOptions: BaseUsePersistentStateOptions< TExpandedCells - >({}); - return { expandedCells, setExpandedCells }; -}; -/* -export const useExpansionUrlParams = < - TColumnKey extends string, - TURLParamKeyPrefix extends string = string, ->({ - urlParamKeyPrefix, -}: IExtraArgsForURLParamHooks = {}): IExpansionState => { - const [expandedCells, setExpandedCells] = useUrlParams({ - keyPrefix: urlParamKeyPrefix, - keys: ["expandedCells"], - defaultValue: {} as TExpandedCells, - serialize: (expandedCellsObj) => { - if (objectKeys(expandedCellsObj).length === 0) - return { expandedCells: null }; - return { expandedCells: JSON.stringify(expandedCellsObj) }; - }, - deserialize: ({ expandedCells: expandedCellsStr }) => { - try { - return JSON.parse(expandedCellsStr || "{}"); - } catch (e) { - return {}; - } - }, - }); + > = { + defaultValue: {}, + persistenceKeyPrefix, + }; + + // Note: for the discriminated union here to work without TypeScript getting confused + // (e.g. require the urlParams-specific options when persistTo === "urlParams"), + // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. + const [expandedCells, setExpandedCells] = usePersistentState( + persistTo === "urlParams" + ? { + ...baseStateOptions, + persistTo, + keys: ["expandedCells"], + serialize: (expandedCellsObj) => { + if (!expandedCellsObj || objectKeys(expandedCellsObj).length === 0) + return { expandedCells: null }; + return { expandedCells: JSON.stringify(expandedCellsObj) }; + }, + deserialize: ({ expandedCells: expandedCellsStr }) => { + try { + return JSON.parse(expandedCellsStr || "{}"); + } catch (e) { + return {}; + } + }, + } + : persistTo === "localStorage" || persistTo === "sessionStorage" + ? { + ...baseStateOptions, + persistTo, + key: `${ + persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : "" + }expandedCells`, + } + : { ...baseStateOptions, persistTo } + ); return { expandedCells, setExpandedCells }; }; -*/ diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 43fc0597fb..5d86e55b42 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -77,5 +77,3 @@ export const usePaginationState = < }); return { pageNumber, setPageNumber, itemsPerPage, setItemsPerPage }; }; - -// TODO look for and replace all usages of usePaginationUrlParams and fix usage of usePaginationState diff --git a/client/src/app/hooks/table-controls/useLocalTableControlState.ts b/client/src/app/hooks/table-controls/useLocalTableControlState.ts index 883c17c43f..ebe4688b46 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControlState.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControlState.ts @@ -16,12 +16,14 @@ export const useLocalTableControlState = < TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, + TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, >( args: IUseLocalTableControlStateArgs< TItem, TColumnKey, TSortableColumnKey, + TFilterCategoryKey, TPersistenceKeyPrefix > ): IUseTableControlPropsArgs => { @@ -64,6 +66,7 @@ export const useLocalTableControlState = < items: sortedItems, }); + // TODO bring useSelectionState in from lib-ui, but maybe don't use the persistence because we want to keep the whole selected item object references const selectionState = useSelectionState({ ...args, items: filteredItems, @@ -72,7 +75,7 @@ export const useLocalTableControlState = < isItemSelectable, }); - const expansionState = useExpansionState(args); + const expansionState = useExpansionState(args); const activeRowState = useActiveRowState(args); diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 5b5d7703e5..a930b89fce 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -20,20 +20,12 @@ export const useTableControlState = < TPersistenceKeyPrefix > ) => { - // Must pass type params because they can't all be inferred from the required args of useFilterState - const filterState = useFilterState< - TItem, - TFilterCategoryKey, // Must pass this because no required args here have categories to infer from - TPersistenceKeyPrefix - >(args); - const sortState = useSortState(args); // Type params inferred from args - const paginationState = usePaginationState(args); // Type params inferred from args - // Must pass type params because they can't all be inferred from the required args of useExpansionState - // TODO is that still true here? - const expansionState = useExpansionState( - args - ); - const activeRowState = useActiveRowState(args); // Type params inferred from args + // Generic type params for each of these hooks are inferred from the args object + const filterState = useFilterState(args); + const sortState = useSortState(args); + const paginationState = usePaginationState(args); + const expansionState = useExpansionState(args); + const activeRowState = useActiveRowState(args); return { ...args, filterState, diff --git a/client/src/app/hooks/usePersistentState.ts b/client/src/app/hooks/usePersistentState.ts index 643c47d9ed..caadc30c6a 100644 --- a/client/src/app/hooks/usePersistentState.ts +++ b/client/src/app/hooks/usePersistentState.ts @@ -5,6 +5,7 @@ import { useLocalStorage, useSessionStorage, } from "@migtools/lib-ui"; +import { DisallowCharacters } from "@app/utils/type-utils"; export type BaseUsePersistentStateOptions< TValue, @@ -12,7 +13,7 @@ export type BaseUsePersistentStateOptions< > = { defaultValue: TValue; isEnabled?: boolean; - persistenceKeyPrefix?: TPersistenceKeyPrefix; + persistenceKeyPrefix?: DisallowCharacters; }; export type UsePersistentStateOptions< From 3769dabe557d74e77ba71fc515cdec486e829c22 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 10 Oct 2023 13:28:16 -0400 Subject: [PATCH 034/102] WIP refactor of useActiveRowState Signed-off-by: Mike Turley --- .../active-row/useActiveRowState.ts | 64 ++++++++++++------- .../expansion/useExpansionState.ts | 2 +- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts index 478edf7cfb..936f4ade93 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts @@ -1,33 +1,51 @@ -import * as React from "react"; -import { useUrlParams } from "../../useUrlParams"; -import { IExtraArgsForURLParamHooks } from "../types"; import { parseMaybeNumericString } from "@app/utils/utils"; +import { IPersistenceOptions } from "../types"; +import { + BaseUsePersistentStateOptions, + usePersistentState, +} from "@app/hooks/usePersistentState"; export interface IActiveRowState { activeRowId: string | number | null; setActiveRowId: (id: string | number | null) => void; } -export const useActiveRowState = (): IActiveRowState => { - const [activeRowId, setActiveRowId] = React.useState( - null - ); - return { activeRowId, setActiveRowId }; -}; +export const useActiveRowState = < + TPersistenceKeyPrefix extends string = string, +>( + args: IPersistenceOptions = {} +): IActiveRowState => { + const { persistTo, persistenceKeyPrefix } = args; + const baseStateOptions: BaseUsePersistentStateOptions< + string | number | null + > = { + defaultValue: null, + persistenceKeyPrefix, + }; -export const useActiveRowUrlParams = < - TURLParamKeyPrefix extends string = string ->({ - urlParamKeyPrefix, -}: IExtraArgsForURLParamHooks = {}): IActiveRowState => { - const [activeRowId, setActiveRowId] = useUrlParams({ - keyPrefix: urlParamKeyPrefix, - keys: ["activeRow"], - defaultValue: null as string | number | null, - serialize: (activeRowId) => ({ - activeRow: activeRowId !== null ? String(activeRowId) : null, - }), - deserialize: ({ activeRow }) => parseMaybeNumericString(activeRow), - }); + // Note: for the discriminated union here to work without TypeScript getting confused + // (e.g. require the urlParams-specific options when persistTo === "urlParams"), + // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. + const [activeRowId, setActiveRowId] = usePersistentState( + persistTo === "urlParams" + ? { + ...baseStateOptions, + persistTo, + keys: ["activeRow"], + serialize: (activeRowId) => ({ + activeRow: activeRowId !== null ? String(activeRowId) : null, + }), + deserialize: ({ activeRow }) => parseMaybeNumericString(activeRow), + } + : persistTo === "localStorage" || persistTo === "sessionStorage" + ? { + ...baseStateOptions, + persistTo, + key: `${ + persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : "" + }activeRow`, + } + : { ...baseStateOptions, persistTo } + ); return { activeRowId, setActiveRowId }; }; diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index 0c498c32f9..6117040f56 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -24,7 +24,7 @@ export const useExpansionState = < TColumnKey extends string, TPersistenceKeyPrefix extends string = string, >( - args: IPersistenceOptions + args: IPersistenceOptions = {} ): IExpansionState => { const { persistTo = "state", persistenceKeyPrefix } = args; const baseStateOptions: BaseUsePersistentStateOptions< From 917d97737c77c0fd7b194bbd9ca430793e8de8ba Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 10 Oct 2023 13:31:56 -0400 Subject: [PATCH 035/102] Explicitly pass type params in higher level hooks to be safe Signed-off-by: Mike Turley --- .../useLocalTableControlState.ts | 20 ++++++++++++++----- .../table-controls/useTableControlState.ts | 19 ++++++++++++------ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/client/src/app/hooks/table-controls/useLocalTableControlState.ts b/client/src/app/hooks/table-controls/useLocalTableControlState.ts index ebe4688b46..83a83d3d32 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControlState.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControlState.ts @@ -40,7 +40,11 @@ export const useLocalTableControlState = < isItemSelectable, } = args; - const filterState = useFilterState(args); + const filterState = useFilterState< + TItem, + TFilterCategoryKey, + TPersistenceKeyPrefix + >(args); const { filteredItems } = getLocalFilterDerivedState({ ...args, items, @@ -48,7 +52,11 @@ export const useLocalTableControlState = < filterState, }); - const sortState = useSortState({ ...args, sortableColumns, initialSort }); + const sortState = useSortState({ + ...args, + sortableColumns, + initialSort, + }); const { sortedItems } = getLocalSortDerivedState({ ...args, sortState, @@ -56,7 +64,7 @@ export const useLocalTableControlState = < getSortValues, }); - const paginationState = usePaginationState({ + const paginationState = usePaginationState({ ...args, initialItemsPerPage, }); @@ -75,9 +83,11 @@ export const useLocalTableControlState = < isItemSelectable, }); - const expansionState = useExpansionState(args); + const expansionState = useExpansionState( + args + ); - const activeRowState = useActiveRowState(args); + const activeRowState = useActiveRowState(args); return { ...args, diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index a930b89fce..251215ba90 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -20,12 +20,19 @@ export const useTableControlState = < TPersistenceKeyPrefix > ) => { - // Generic type params for each of these hooks are inferred from the args object - const filterState = useFilterState(args); - const sortState = useSortState(args); - const paginationState = usePaginationState(args); - const expansionState = useExpansionState(args); - const activeRowState = useActiveRowState(args); + const filterState = useFilterState< + TItem, + TFilterCategoryKey, + TPersistenceKeyPrefix + >(args); + const sortState = useSortState( + args + ); + const paginationState = usePaginationState(args); + const expansionState = useExpansionState( + args + ); + const activeRowState = useActiveRowState(args); return { ...args, filterState, From e65ec7fd9560ccd4ea564f31fdd3a1def0d0d0db Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 10 Oct 2023 13:40:26 -0400 Subject: [PATCH 036/102] Fix type errors Signed-off-by: Mike Turley --- client/src/app/Constants.ts | 2 +- client/src/app/hooks/table-controls/filtering/index.ts | 1 + client/src/app/hooks/table-controls/index.ts | 2 ++ client/src/app/layout/SidebarApp/SidebarApp.tsx | 5 ++++- client/src/app/pages/archetypes/archetypes-page.tsx | 4 ++-- client/src/app/pages/dependencies/dependencies.tsx | 4 ++-- .../src/app/pages/dependencies/dependency-apps-table.tsx | 4 ++-- .../affected-applications/affected-applications.tsx | 4 ++-- client/src/app/pages/issues/helpers.ts | 8 ++++---- .../file-all-incidents-table.tsx | 4 ++-- .../issue-detail-drawer/issue-affected-files-table.tsx | 4 ++-- client/src/app/pages/issues/issues-table.tsx | 4 ++-- client/src/app/pages/issues/issues.tsx | 4 ++-- 13 files changed, 28 insertions(+), 22 deletions(-) diff --git a/client/src/app/Constants.ts b/client/src/app/Constants.ts index cf53fe00de..59d8974062 100644 --- a/client/src/app/Constants.ts +++ b/client/src/app/Constants.ts @@ -236,7 +236,7 @@ export enum LocalStorageKey { } // URL param prefixes: should be short, must be unique for each table that uses one -export enum TableURLParamKeyPrefix { +export enum TablePersistenceKeyPrefix { issues = "i", dependencies = "d", issuesAffectedApps = "ia", diff --git a/client/src/app/hooks/table-controls/filtering/index.ts b/client/src/app/hooks/table-controls/filtering/index.ts index aeb3f6c370..4628cd70ae 100644 --- a/client/src/app/hooks/table-controls/filtering/index.ts +++ b/client/src/app/hooks/table-controls/filtering/index.ts @@ -2,3 +2,4 @@ export * from "./useFilterState"; export * from "./getLocalFilterDerivedState"; export * from "./getFilterProps"; export * from "./getFilterHubRequestParams"; +export * from "./helpers"; diff --git a/client/src/app/hooks/table-controls/index.ts b/client/src/app/hooks/table-controls/index.ts index d00fbdfd1e..a203793fe5 100644 --- a/client/src/app/hooks/table-controls/index.ts +++ b/client/src/app/hooks/table-controls/index.ts @@ -8,3 +8,5 @@ export * from "./getHubRequestParams"; export * from "./filtering"; export * from "./sorting"; export * from "./pagination"; +export * from "./expansion"; +export * from "./active-row"; diff --git a/client/src/app/layout/SidebarApp/SidebarApp.tsx b/client/src/app/layout/SidebarApp/SidebarApp.tsx index 6388cd4b61..8c660a3045 100644 --- a/client/src/app/layout/SidebarApp/SidebarApp.tsx +++ b/client/src/app/layout/SidebarApp/SidebarApp.tsx @@ -70,7 +70,10 @@ export const SidebarApp: React.FC = () => { ]; const [selectedPersona, setSelectedPersona] = - useLocalStorage(LocalStorageKey.selectedPersona, null); + useLocalStorage({ + key: LocalStorageKey.selectedPersona, + defaultValue: null, + }); useEffect(() => { if (!selectedPersona) { diff --git a/client/src/app/pages/archetypes/archetypes-page.tsx b/client/src/app/pages/archetypes/archetypes-page.tsx index 4ffa16f1e4..3598183ab0 100644 --- a/client/src/app/pages/archetypes/archetypes-page.tsx +++ b/client/src/app/pages/archetypes/archetypes-page.tsx @@ -56,7 +56,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"; +import { TablePersistenceKeyPrefix } from "@app/Constants"; const Archetypes: React.FC = () => { const { t } = useTranslation(); @@ -100,7 +100,7 @@ const Archetypes: React.FC = () => { const tableControls = useLocalTableControls({ persistTo: "urlParams", - persistenceKeyPrefix: TableURLParamKeyPrefix.archetypes, + persistenceKeyPrefix: TablePersistenceKeyPrefix.archetypes, idProperty: "id", items: archetypes, isLoading: isFetching, diff --git a/client/src/app/pages/dependencies/dependencies.tsx b/client/src/app/pages/dependencies/dependencies.tsx index 77823da35d..e388a09d5e 100644 --- a/client/src/app/pages/dependencies/dependencies.tsx +++ b/client/src/app/pages/dependencies/dependencies.tsx @@ -19,7 +19,7 @@ import { useTableControlProps, getHubRequestParams, } from "@app/hooks/table-controls"; -import { TableURLParamKeyPrefix } from "@app/Constants"; +import { TablePersistenceKeyPrefix } from "@app/Constants"; import { SimplePagination } from "@app/components/SimplePagination"; import { ConditionalTableBody, @@ -40,7 +40,7 @@ export const Dependencies: React.FC = () => { const tableControlState = useTableControlState({ persistTo: "urlParams", - urlParamKeyPrefix: TableURLParamKeyPrefix.dependencies, + persistenceKeyPrefix: TablePersistenceKeyPrefix.dependencies, columnNames: { name: "Dependency name", foundIn: "Found in", diff --git a/client/src/app/pages/dependencies/dependency-apps-table.tsx b/client/src/app/pages/dependencies/dependency-apps-table.tsx index 7c6fdd77af..35d2630b02 100644 --- a/client/src/app/pages/dependencies/dependency-apps-table.tsx +++ b/client/src/app/pages/dependencies/dependency-apps-table.tsx @@ -10,7 +10,7 @@ import { useTableControlProps, getHubRequestParams, } from "@app/hooks/table-controls"; -import { TableURLParamKeyPrefix } from "@app/Constants"; +import { TablePersistenceKeyPrefix } from "@app/Constants"; import { ConditionalTableBody, TableHeaderContentWithControls, @@ -35,7 +35,7 @@ export const DependencyAppsTable: React.FC = ({ const tableControlState = useTableControlState({ persistTo: "urlParams", - urlParamKeyPrefix: TableURLParamKeyPrefix.dependencyApplications, + persistenceKeyPrefix: TablePersistenceKeyPrefix.dependencyApplications, columnNames: { name: "Application", version: "Version", diff --git a/client/src/app/pages/issues/affected-applications/affected-applications.tsx b/client/src/app/pages/issues/affected-applications/affected-applications.tsx index 8aa4c1754e..1b9cffea4c 100644 --- a/client/src/app/pages/issues/affected-applications/affected-applications.tsx +++ b/client/src/app/pages/issues/affected-applications/affected-applications.tsx @@ -35,7 +35,7 @@ import { useSharedAffectedApplicationFilterCategories, } from "../helpers"; import { IssueDetailDrawer } from "../issue-detail-drawer"; -import { TableURLParamKeyPrefix } from "@app/Constants"; +import { TablePersistenceKeyPrefix } from "@app/Constants"; interface IAffectedApplicationsRouteParams { ruleset: string; @@ -54,7 +54,7 @@ export const AffectedApplications: React.FC = () => { const tableControlState = useTableControlState({ persistTo: "urlParams", - urlParamKeyPrefix: TableURLParamKeyPrefix.issuesAffectedApps, + persistenceKeyPrefix: TablePersistenceKeyPrefix.issuesAffectedApps, columnNames: { name: "Name", description: "Description", diff --git a/client/src/app/pages/issues/helpers.ts b/client/src/app/pages/issues/helpers.ts index 23ae1b64aa..6cec0692c2 100644 --- a/client/src/app/pages/issues/helpers.ts +++ b/client/src/app/pages/issues/helpers.ts @@ -15,7 +15,7 @@ import { } from "@app/hooks/table-controls"; import { trimAndStringifyUrlParams } from "@app/hooks/useUrlParams"; import { Paths } from "@app/Paths"; -import { TableURLParamKeyPrefix } from "@app/Constants"; +import { TablePersistenceKeyPrefix } from "@app/Constants"; import { IssueFilterGroups } from "./issues"; import { useFetchBusinessServices } from "@app/queries/businessservices"; import { useFetchTags } from "@app/queries/tags"; @@ -116,7 +116,7 @@ export const getAffectedAppsUrl = ({ .replace("/:ruleset/", `/${encodeURIComponent(ruleReport.ruleset)}/`) .replace("/:rule/", `/${encodeURIComponent(ruleReport.rule)}/`); const prefix = (key: string) => - `${TableURLParamKeyPrefix.issuesAffectedApps}:${key}`; + `${TablePersistenceKeyPrefix.issuesAffectedApps}:${key}`; return `${baseUrl}?${trimAndStringifyUrlParams({ newPrefixedSerializedParams: { @@ -143,7 +143,7 @@ export const getBackToAllIssuesUrl = ({ new URLSearchParams(fromIssuesParams) ); // Pull the filters param out of that - const prefix = (key: string) => `${TableURLParamKeyPrefix.issues}:${key}`; + const prefix = (key: string) => `${TablePersistenceKeyPrefix.issues}:${key}`; const filterValuesToRestore = deserializeFilterUrlParams({ filters: prefixedParamsToRestore[prefix("filters")], }); @@ -183,7 +183,7 @@ export const getIssuesSingleAppSelectedLocation = ( const existingFiltersParam = fromLocation && new URLSearchParams(fromLocation.search).get( - `${TableURLParamKeyPrefix.issues}:filters` + `${TablePersistenceKeyPrefix.issues}:filters` ); return { pathname: Paths.issuesSingleAppSelected.replace( diff --git a/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx b/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx index d1c21f05a3..352f7494a1 100644 --- a/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx +++ b/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table"; import { useSelectionState } from "@migtools/lib-ui"; -import { TableURLParamKeyPrefix } from "@app/Constants"; +import { TablePersistenceKeyPrefix } from "@app/Constants"; import { AnalysisFileReport } from "@app/api/models"; import { useFetchIncidents } from "@app/queries/issues"; import { SimplePagination } from "@app/components/SimplePagination"; @@ -27,7 +27,7 @@ export const FileAllIncidentsTable: React.FC< > = ({ fileReport }) => { const tableControlState = useTableControlState({ persistTo: "urlParams", - urlParamKeyPrefix: TableURLParamKeyPrefix.issuesRemainingIncidents, + persistenceKeyPrefix: TablePersistenceKeyPrefix.issuesRemainingIncidents, columnNames: { line: "Line #", message: "Message", diff --git a/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx b/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx index 267e261681..5dc060870e 100644 --- a/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx +++ b/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx @@ -16,7 +16,7 @@ import { getHubRequestParams, } from "@app/hooks/table-controls"; import { useFetchFileReports } from "@app/queries/issues"; -import { TableURLParamKeyPrefix } from "@app/Constants"; +import { TablePersistenceKeyPrefix } from "@app/Constants"; import { ConditionalTableBody, TableHeaderContentWithControls, @@ -37,7 +37,7 @@ export const IssueAffectedFilesTable: React.FC< const tableControlState = useTableControlState({ persistTo: "urlParams", - urlParamKeyPrefix: TableURLParamKeyPrefix.issuesAffectedFiles, + persistenceKeyPrefix: TablePersistenceKeyPrefix.issuesAffectedFiles, columnNames: { file: "File", incidents: "Incidents", diff --git a/client/src/app/pages/issues/issues-table.tsx b/client/src/app/pages/issues/issues-table.tsx index 8534fadc40..5de31d8fb6 100644 --- a/client/src/app/pages/issues/issues-table.tsx +++ b/client/src/app/pages/issues/issues-table.tsx @@ -33,7 +33,7 @@ import { useSelectionState } from "@migtools/lib-ui"; import { AppPlaceholder } from "@app/components/AppPlaceholder"; import { OptionWithValue, SimpleSelect } from "@app/components/SimpleSelect"; -import { TableURLParamKeyPrefix } from "@app/Constants"; +import { TablePersistenceKeyPrefix } from "@app/Constants"; import { useFetchIssueReports, useFetchRuleReports } from "@app/queries/issues"; import { FilterType, @@ -97,7 +97,7 @@ export const IssuesTable: React.FC = ({ mode }) => { const tableControlState = useTableControlState({ persistTo: "urlParams", - urlParamKeyPrefix: TableURLParamKeyPrefix.issues, + persistenceKeyPrefix: TablePersistenceKeyPrefix.issues, columnNames: { description: "Issue", category: "Category", diff --git a/client/src/app/pages/issues/issues.tsx b/client/src/app/pages/issues/issues.tsx index 265e9b7391..a7fe2ae7cf 100644 --- a/client/src/app/pages/issues/issues.tsx +++ b/client/src/app/pages/issues/issues.tsx @@ -17,7 +17,7 @@ import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { Paths } from "@app/Paths"; import { IssuesTable } from "./issues-table"; import { ConfirmDialog } from "@app/components/ConfirmDialog"; -import { TableURLParamKeyPrefix } from "@app/Constants"; +import { TablePersistenceKeyPrefix } from "@app/Constants"; export enum IssueFilterGroups { ApplicationInventory = "Application inventory", @@ -59,7 +59,7 @@ export const Issues: React.FC = () => { activeKey={activeTabPath} onSelect={(_event, tabPath) => { const pageHasFilters = new URLSearchParams(location.search).has( - `${TableURLParamKeyPrefix.issues}:filters` + `${TablePersistenceKeyPrefix.issues}:filters` ); if (pageHasFilters) { setNavConfirmPath(tabPath as IssuesTabPath); From a4649bf50649bb9e12335fdcd8b93687a503b6f7 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 10 Oct 2023 13:46:28 -0400 Subject: [PATCH 037/102] Fix missing type params in useLocalTableControls Signed-off-by: Mike Turley --- .../app/hooks/table-controls/useLocalTableControls.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/src/app/hooks/table-controls/useLocalTableControls.ts b/client/src/app/hooks/table-controls/useLocalTableControls.ts index e797487c6d..c0a8c3375e 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControls.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControls.ts @@ -6,6 +6,14 @@ export const useLocalTableControls = < TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, + TFilterCategoryKey extends string = string, + TPersistenceKeyPrefix extends string = string, >( - args: IUseLocalTableControlStateArgs + args: IUseLocalTableControlStateArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + > ) => useTableControlProps(useLocalTableControlState(args)); From 6f4c8a4c2307a1ab9be029a9d751ffcb1c7e095c Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 10 Oct 2023 16:17:54 -0400 Subject: [PATCH 038/102] Fix fallback behavior in usePersistentState Signed-off-by: Mike Turley --- client/src/app/hooks/usePersistentState.ts | 88 +++++++++++++--------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/client/src/app/hooks/usePersistentState.ts b/client/src/app/hooks/usePersistentState.ts index caadc30c6a..8e75dcf7ea 100644 --- a/client/src/app/hooks/usePersistentState.ts +++ b/client/src/app/hooks/usePersistentState.ts @@ -16,21 +16,29 @@ export type BaseUsePersistentStateOptions< persistenceKeyPrefix?: DisallowCharacters; }; +type PersistToStateOptions = { persistTo?: "state" }; + +type PersistToUrlParamsOptions< + TValue, + TPersistenceKeyPrefix extends string, + TURLParamKey extends string, +> = { + persistTo: "urlParams"; +} & IUseUrlParamsArgs; + +type PersistToStorageOptions = { + persistTo: "localStorage" | "sessionStorage"; +} & UseStorageTypeOptions; + export type UsePersistentStateOptions< TValue, TPersistenceKeyPrefix extends string, TURLParamKey extends string, > = BaseUsePersistentStateOptions & ( - | { - persistTo?: "state"; - } - | ({ - persistTo: "urlParams"; - } & IUseUrlParamsArgs) - | ({ - persistTo: "localStorage" | "sessionStorage"; - } & UseStorageTypeOptions) + | PersistToStateOptions + | PersistToUrlParamsOptions + | PersistToStorageOptions ); export const usePersistentState = < @@ -44,37 +52,47 @@ export const usePersistentState = < TURLParamKey > ): [TValue, (value: TValue) => void] => { - const { - defaultValue, - isEnabled = true, - persistTo, - persistenceKeyPrefix, - } = options; - const urlParamOptions = options as IUseUrlParamsArgs< + const { defaultValue, persistTo, persistenceKeyPrefix } = options; + + const isUrlParamsOptions = ( + o: typeof options + ): o is PersistToUrlParamsOptions< TValue, TPersistenceKeyPrefix, TURLParamKey - >; - const storageOptions = options as UseStorageTypeOptions; - const prefixedStorageKey = persistenceKeyPrefix - ? `${persistenceKeyPrefix}:${storageOptions.key}` - : storageOptions.key; + > => o.persistTo === "urlParams"; + + const isStorageOptions = ( + o: typeof options + ): o is PersistToStorageOptions => + o.persistTo === "localStorage" || o.persistTo === "sessionStorage"; + + const prefixKey = (key: string) => + persistenceKeyPrefix ? `${persistenceKeyPrefix}:${key}` : key; + const persistence = { state: React.useState(defaultValue), - urlParams: useUrlParams({ - ...urlParamOptions, - isEnabled: isEnabled && persistTo === "urlParams", - }), - localStorage: useLocalStorage({ - ...storageOptions, - isEnabled: isEnabled && persistTo === "localStorage", - key: prefixedStorageKey, - }), - sessionStorage: useSessionStorage({ - ...(options as UseStorageTypeOptions), - isEnabled: isEnabled && persistTo === "sessionStorage", - key: prefixedStorageKey, - }), + urlParams: useUrlParams( + isUrlParamsOptions(options) + ? options + : { + ...options, + isEnabled: false, + keys: [], + serialize: () => ({}), + deserialize: () => defaultValue, + } + ), + localStorage: useLocalStorage( + isStorageOptions(options) + ? { ...options, key: prefixKey(options.key) } + : { ...options, isEnabled: false, key: "" } + ), + sessionStorage: useSessionStorage( + isStorageOptions(options) + ? { ...options, key: prefixKey(options.key) } + : { ...options, isEnabled: false, key: "" } + ), }; return persistence[persistTo || "state"]; }; From b32293fc0e4ebe5ad111930664949a2469458e68 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 10 Oct 2023 16:58:55 -0400 Subject: [PATCH 039/102] Simplify usage of usePersistentState with explicit type params and no more base params object Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 2 +- .../active-row/useActiveRowState.ts | 33 +++++++--------- .../expansion/useExpansionState.ts | 33 +++++++--------- .../filtering/useFilterState.ts | 39 +++++++------------ .../pagination/usePaginationState.ts | 29 +++++++------- .../table-controls/sorting/useSortState.ts | 31 +++++++-------- client/src/app/hooks/table-controls/types.ts | 1 + client/src/app/hooks/usePersistentState.ts | 24 +++++------- 8 files changed, 87 insertions(+), 105 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index dd01fe42d1..ba62851f2b 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -316,7 +316,7 @@ An item object has the generic type `TItem`, which is inferred either from the t > ℹ️ CAVEAT: For server-paginated tables the item data is not in scope until after the API query hook is called, but the `useTableControlState` hook must be called _before_ API queries because its return values are needed to serialize filter/sort/pagination params for the API. This means the inferred `TItem` type is not available when passing arguments to `useTableControlState`. `TItem` resolves to `unknown` in this scope, which is usually fine since the arguments there don't need to know what type of items they are working with. If the item type is needed for any of these arguments it can be explicitly passed as a type param. However... > -> ⚠️ TECH DEBT NOTE: Since TypeScript generic type param lists are all-or-nothing (you must either omit the list and infer all generics for a function or pass them all explicitly), this means all other type params which are normally inferred must be explicitly passed (including all of the `TColumnKey`s and `TFilterCategoryKey`s). This makes for some redundant code, although TypeScript will still enforce that it is all consistent. There is a possible upcoming TypeScript language feature which would allow partial inference in type param lists and may alleviate this in the future. See TypeScript pull requests [#26349](https://github.com/microsoft/TypeScript/pull/26349) and [#54047](https://github.com/microsoft/TypeScript/pull/54047). +> ⚠️ TECH DEBT NOTE: Since TypeScript generic type param lists are all-or-nothing (you must either omit the list and infer all generics for a function or pass them all explicitly), this means all other type params which are normally inferred must be explicitly passed (including all of the `TColumnKey`s and `TFilterCategoryKey`s). This makes for some redundant code, although TypeScript will still enforce that it is all consistent. There is a possible upcoming TypeScript language feature which would allow partial inference in type param lists and may alleviate this in the future. See TypeScript pull requests [#26349](https://github.com/microsoft/TypeScript/pull/26349) and [#54047](https://github.com/microsoft/TypeScript/pull/54047), and our issue [#1456](https://github.com/konveyor/tackle2-ui/issues/1456). ### Unique Identifiers diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts index 936f4ade93..4c508fdaf3 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts @@ -1,9 +1,6 @@ import { parseMaybeNumericString } from "@app/utils/utils"; import { IPersistenceOptions } from "../types"; -import { - BaseUsePersistentStateOptions, - usePersistentState, -} from "@app/hooks/usePersistentState"; +import { usePersistentState } from "@app/hooks/usePersistentState"; export interface IActiveRowState { activeRowId: string | number | null; @@ -16,20 +13,21 @@ export const useActiveRowState = < args: IPersistenceOptions = {} ): IActiveRowState => { const { persistTo, persistenceKeyPrefix } = args; - const baseStateOptions: BaseUsePersistentStateOptions< - string | number | null - > = { + + // We won't need to pass the latter two type params here if TS adds support for partial inference. + // See https://github.com/konveyor/tackle2-ui/issues/1456 + const [activeRowId, setActiveRowId] = usePersistentState< + string | number | null, + TPersistenceKeyPrefix, + "activeRow" + >({ defaultValue: null, persistenceKeyPrefix, - }; - - // Note: for the discriminated union here to work without TypeScript getting confused - // (e.g. require the urlParams-specific options when persistTo === "urlParams"), - // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. - const [activeRowId, setActiveRowId] = usePersistentState( - persistTo === "urlParams" + // Note: For the discriminated union here to work without TypeScript getting confused + // (e.g. require the urlParams-specific options when persistTo === "urlParams"), + // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. + ...(persistTo === "urlParams" ? { - ...baseStateOptions, persistTo, keys: ["activeRow"], serialize: (activeRowId) => ({ @@ -39,13 +37,12 @@ export const useActiveRowState = < } : persistTo === "localStorage" || persistTo === "sessionStorage" ? { - ...baseStateOptions, persistTo, key: `${ persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : "" }activeRow`, } - : { ...baseStateOptions, persistTo } - ); + : { persistTo }), + }); return { activeRowId, setActiveRowId }; }; diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index 6117040f56..ee8c6ee1c4 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -1,7 +1,4 @@ -import { - BaseUsePersistentStateOptions, - usePersistentState, -} from "@app/hooks/usePersistentState"; +import { usePersistentState } from "@app/hooks/usePersistentState"; import { objectKeys } from "@app/utils/utils"; import { IPersistenceOptions } from "../types"; @@ -27,20 +24,21 @@ export const useExpansionState = < args: IPersistenceOptions = {} ): IExpansionState => { const { persistTo = "state", persistenceKeyPrefix } = args; - const baseStateOptions: BaseUsePersistentStateOptions< - TExpandedCells - > = { + + // We won't need to pass the latter two type params here if TS adds support for partial inference. + // See https://github.com/konveyor/tackle2-ui/issues/1456 + const [expandedCells, setExpandedCells] = usePersistentState< + TExpandedCells, + TPersistenceKeyPrefix, + "expandedCells" + >({ defaultValue: {}, persistenceKeyPrefix, - }; - - // Note: for the discriminated union here to work without TypeScript getting confused - // (e.g. require the urlParams-specific options when persistTo === "urlParams"), - // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. - const [expandedCells, setExpandedCells] = usePersistentState( - persistTo === "urlParams" + // Note: For the discriminated union here to work without TypeScript getting confused + // (e.g. require the urlParams-specific options when persistTo === "urlParams"), + // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. + ...(persistTo === "urlParams" ? { - ...baseStateOptions, persistTo, keys: ["expandedCells"], serialize: (expandedCellsObj) => { @@ -58,13 +56,12 @@ export const useExpansionState = < } : persistTo === "localStorage" || persistTo === "sessionStorage" ? { - ...baseStateOptions, persistTo, key: `${ persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : "" }expandedCells`, } - : { ...baseStateOptions, persistTo } - ); + : { persistTo }), + }); return { expandedCells, setExpandedCells }; }; diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index f26077467f..937ef1b68f 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -1,9 +1,6 @@ import { FilterCategory, IFilterValues } from "@app/components/FilterToolbar"; import { IPersistenceOptions } from "../types"; -import { - BaseUsePersistentStateOptions, - usePersistentState, -} from "@app/hooks/usePersistentState"; +import { usePersistentState } from "@app/hooks/usePersistentState"; import { serializeFilterUrlParams } from "./helpers"; import { deserializeFilterUrlParams } from "./helpers"; @@ -26,34 +23,28 @@ export const useFilterState = < ): IFilterState => { const { persistTo = "state", persistenceKeyPrefix } = args; - const baseStateOptions: BaseUsePersistentStateOptions< - IFilterValues - > = { + // We won't need to pass the latter two type params here if TS adds support for partial inference. + // See https://github.com/konveyor/tackle2-ui/issues/1456 + const [filterValues, setFilterValues] = usePersistentState< + IFilterValues, + TPersistenceKeyPrefix, + "filters" + >({ defaultValue: {}, persistenceKeyPrefix, - }; - - // Note: for the discriminated union here to work without TypeScript getting confused - // (e.g. require the urlParams-specific options when persistTo === "urlParams"), - // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. - const [filterValues, setFilterValues] = usePersistentState( - persistTo === "urlParams" + // Note: For the discriminated union here to work without TypeScript getting confused + // (e.g. require the urlParams-specific options when persistTo === "urlParams"), + // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. + ...(persistTo === "urlParams" ? { - ...baseStateOptions, persistTo, keys: ["filters"], serialize: serializeFilterUrlParams, deserialize: deserializeFilterUrlParams, } : persistTo === "localStorage" || persistTo === "sessionStorage" - ? { - ...baseStateOptions, - persistTo, - key: `${ - persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : "" - }filters`, - } - : { ...baseStateOptions, persistTo } - ); + ? { persistTo, key: "filters" } + : { persistTo }), + }); return { filterValues, setFilterValues }; }; diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 5d86e55b42..0db2babb34 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -1,7 +1,4 @@ -import { - BaseUsePersistentStateOptions, - usePersistentState, -} from "@app/hooks/usePersistentState"; +import { usePersistentState } from "@app/hooks/usePersistentState"; import { IPersistenceOptions } from "../types"; export interface IPaginationState { @@ -27,14 +24,21 @@ export const usePaginationState = < } = args; const defaultValue = { pageNumber: 1, itemsPerPage: initialItemsPerPage }; - const baseStateOptions: BaseUsePersistentStateOptions< - typeof defaultValue | null - > = { defaultValue, persistenceKeyPrefix }; - const [paginationState, setPaginationState] = usePersistentState( - persistTo === "urlParams" + // We won't need to pass the latter two type params here if TS adds support for partial inference. + // See https://github.com/konveyor/tackle2-ui/issues/1456 + const [paginationState, setPaginationState] = usePersistentState< + typeof defaultValue, + TPersistenceKeyPrefix, + "pageNumber" | "itemsPerPage" + >({ + defaultValue, + persistenceKeyPrefix, + // Note: For the discriminated union here to work without TypeScript getting confused + // (e.g. require the urlParams-specific options when persistTo === "urlParams"), + // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. + ...(persistTo === "urlParams" ? { - ...baseStateOptions, persistTo, keys: ["pageNumber", "itemsPerPage"], serialize: (state) => { @@ -56,14 +60,13 @@ export const usePaginationState = < } : persistTo === "localStorage" || persistTo === "sessionStorage" ? { - ...baseStateOptions, persistTo, key: `${ persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : "" }pagination`, } - : { ...baseStateOptions, persistTo } - ); + : { persistTo }), + }); const { pageNumber, itemsPerPage } = paginationState || defaultValue; const setPageNumber = (num: number) => setPaginationState({ diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index 1b88a3c301..7db779cf35 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -1,6 +1,5 @@ import { IPersistenceOptions } from ".."; import { usePersistentState } from "@app/hooks/usePersistentState"; -import { BaseUsePersistentStateOptions } from "@app/hooks/usePersistentState"; export interface IActiveSort { columnKey: TSortableColumnKey; @@ -33,19 +32,20 @@ export const useSortState = < : null, } = args; - const baseStateOptions: BaseUsePersistentStateOptions | null> = - { - defaultValue: initialSort, - persistenceKeyPrefix, - }; - - // Note: for the discriminated union here to work without TypeScript getting confused - // (e.g. require the urlParams-specific options when persistTo === "urlParams"), - // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. - const [activeSort, setActiveSort] = usePersistentState( - persistTo === "urlParams" + // We won't need to pass the latter two type params here if TS adds support for partial inference. + // See https://github.com/konveyor/tackle2-ui/issues/1456 + const [activeSort, setActiveSort] = usePersistentState< + IActiveSort | null, + TPersistenceKeyPrefix, + "sortColumn" | "sortDirection" + >({ + defaultValue: initialSort, + persistenceKeyPrefix, + // Note: For the discriminated union here to work without TypeScript getting confused + // (e.g. require the urlParams-specific options when persistTo === "urlParams"), + // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. + ...(persistTo === "urlParams" ? { - ...baseStateOptions, persistTo, keys: ["sortColumn", "sortDirection"], serialize: (activeSort) => ({ @@ -62,11 +62,10 @@ export const useSortState = < } : persistTo === "localStorage" || persistTo === "sessionStorage" ? { - ...baseStateOptions, persistTo, key: `${persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : ""}sort`, } - : { ...baseStateOptions, persistTo } - ); + : { persistTo }), + }); return { activeSort, setActiveSort }; }; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index af8f85dc6e..416b61f717 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -30,6 +30,7 @@ import { IActiveRowDerivedStateArgs } from "./active-row"; // but we should see if we can fix that (maybe not depend on TItem in the extended types here, or find a way // to pass TItem while still letting the rest of the generics be inferred. // This may be resolved in a newer TypeScript version after https://github.com/microsoft/TypeScript/pull/54047 is merged! +// See https://github.com/konveyor/tackle2-ui/issues/1456 // Persistence-specific args // - Extra args needed for useTableControlState and each concern-specific use*State hook in URL params mode diff --git a/client/src/app/hooks/usePersistentState.ts b/client/src/app/hooks/usePersistentState.ts index 8e75dcf7ea..f7b23354f5 100644 --- a/client/src/app/hooks/usePersistentState.ts +++ b/client/src/app/hooks/usePersistentState.ts @@ -7,15 +7,6 @@ import { } from "@migtools/lib-ui"; import { DisallowCharacters } from "@app/utils/type-utils"; -export type BaseUsePersistentStateOptions< - TValue, - TPersistenceKeyPrefix extends string = string, -> = { - defaultValue: TValue; - isEnabled?: boolean; - persistenceKeyPrefix?: DisallowCharacters; -}; - type PersistToStateOptions = { persistTo?: "state" }; type PersistToUrlParamsOptions< @@ -34,12 +25,15 @@ export type UsePersistentStateOptions< TValue, TPersistenceKeyPrefix extends string, TURLParamKey extends string, -> = BaseUsePersistentStateOptions & - ( - | PersistToStateOptions - | PersistToUrlParamsOptions - | PersistToStorageOptions - ); +> = { + defaultValue: TValue; + isEnabled?: boolean; + persistenceKeyPrefix?: DisallowCharacters; +} & ( + | PersistToStateOptions + | PersistToUrlParamsOptions + | PersistToStorageOptions +); export const usePersistentState = < TValue, From 96b48d457b782aaaecc35f52ea566ec6c5dea5aa Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 10 Oct 2023 17:08:20 -0400 Subject: [PATCH 040/102] Fix bug with double prefix on local/sessionStorage Signed-off-by: Mike Turley --- .../app/hooks/table-controls/active-row/useActiveRowState.ts | 4 +--- .../app/hooks/table-controls/expansion/useExpansionState.ts | 4 +--- .../app/hooks/table-controls/pagination/usePaginationState.ts | 4 +--- client/src/app/hooks/table-controls/sorting/useSortState.ts | 2 +- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts index 4c508fdaf3..7b5db1e8e0 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts @@ -38,9 +38,7 @@ export const useActiveRowState = < : persistTo === "localStorage" || persistTo === "sessionStorage" ? { persistTo, - key: `${ - persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : "" - }activeRow`, + key: "activeRow", } : { persistTo }), }); diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index ee8c6ee1c4..3e0b1761fd 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -57,9 +57,7 @@ export const useExpansionState = < : persistTo === "localStorage" || persistTo === "sessionStorage" ? { persistTo, - key: `${ - persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : "" - }expandedCells`, + key: "expandedCells", } : { persistTo }), }); diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 0db2babb34..b663fad470 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -61,9 +61,7 @@ export const usePaginationState = < : persistTo === "localStorage" || persistTo === "sessionStorage" ? { persistTo, - key: `${ - persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : "" - }pagination`, + key: "pagination", } : { persistTo }), }); diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index 7db779cf35..d456f1fb75 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -63,7 +63,7 @@ export const useSortState = < : persistTo === "localStorage" || persistTo === "sessionStorage" ? { persistTo, - key: `${persistenceKeyPrefix ? `${persistenceKeyPrefix}:` : ""}sort`, + key: "sort", } : { persistTo }), }); From 5ec0c0b0dabfc3ba76f84c04d7c990312f0251aa Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 11:26:43 -0400 Subject: [PATCH 041/102] Make persistence configurable by feature Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/types.ts | 19 ++++++++-- .../useLocalTableControlState.ts | 24 ++++++++++--- .../table-controls/useTableControlState.ts | 35 +++++++++++++------ 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 416b61f717..b26217ff50 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -32,12 +32,18 @@ import { IActiveRowDerivedStateArgs } from "./active-row"; // This may be resolved in a newer TypeScript version after https://github.com/microsoft/TypeScript/pull/54047 is merged! // See https://github.com/konveyor/tackle2-ui/issues/1456 +export type PersistTarget = + | "state" + | "urlParams" + | "localStorage" + | "sessionStorage"; + // Persistence-specific args // - Extra args needed for useTableControlState and each concern-specific use*State hook in URL params mode // - Does not require any state or query values in scope export type IPersistenceOptions = { - persistTo?: "state" | "urlParams" | "localStorage" | "sessionStorage"; + persistTo?: PersistTarget; persistenceKeyPrefix?: DisallowCharacters; }; @@ -53,7 +59,16 @@ export type IUseTableControlStateArgs< > = IFilterStateArgs & ISortStateArgs & IPaginationStateArgs & - IPersistenceOptions & { + Omit, "persistTo"> & { + persistTo?: + | PersistTarget + | { + filters?: PersistTarget; + sort?: PersistTarget; + pagination?: PersistTarget; + expansion?: PersistTarget; + activeRow?: PersistTarget; + }; columnNames: Record; // An ordered mapping of unique keys to human-readable column name strings isSelectable?: boolean; hasPagination?: boolean; diff --git a/client/src/app/hooks/table-controls/useLocalTableControlState.ts b/client/src/app/hooks/table-controls/useLocalTableControlState.ts index 83a83d3d32..4cb3b4edf3 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControlState.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControlState.ts @@ -10,6 +10,7 @@ import { useActiveRowState } from "./active-row"; import { IUseLocalTableControlStateArgs, IUseTableControlPropsArgs, + PersistTarget, } from "./types"; export const useLocalTableControlState = < @@ -40,11 +41,18 @@ export const useLocalTableControlState = < isItemSelectable, } = args; + const getPersistTo = ( + feature: "filters" | "sort" | "pagination" | "expansion" | "activeRow" + ): PersistTarget | undefined => + !args.persistTo || typeof args.persistTo === "string" + ? args.persistTo + : args.persistTo[feature]; + const filterState = useFilterState< TItem, TFilterCategoryKey, TPersistenceKeyPrefix - >(args); + >({ ...args, persistTo: getPersistTo("filters") }); const { filteredItems } = getLocalFilterDerivedState({ ...args, items, @@ -54,6 +62,7 @@ export const useLocalTableControlState = < const sortState = useSortState({ ...args, + persistTo: getPersistTo("sort"), sortableColumns, initialSort, }); @@ -66,6 +75,7 @@ export const useLocalTableControlState = < const paginationState = usePaginationState({ ...args, + persistTo: getPersistTo("pagination"), initialItemsPerPage, }); const { currentPageItems } = getLocalPaginationDerivedState({ @@ -83,11 +93,15 @@ export const useLocalTableControlState = < isItemSelectable, }); - const expansionState = useExpansionState( - args - ); + const expansionState = useExpansionState({ + ...args, + persistTo: getPersistTo("expansion"), + }); - const activeRowState = useActiveRowState(args); + const activeRowState = useActiveRowState({ + ...args, + persistTo: getPersistTo("activeRow"), + }); return { ...args, diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 251215ba90..cdf361cb75 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -1,4 +1,4 @@ -import { IUseTableControlStateArgs } from "./types"; +import { IUseTableControlStateArgs, PersistTarget } from "./types"; import { useFilterState } from "./filtering"; import { useSortState } from "./sorting"; import { usePaginationState } from "./pagination"; @@ -20,19 +20,34 @@ export const useTableControlState = < TPersistenceKeyPrefix > ) => { + const getPersistTo = ( + feature: "filters" | "sort" | "pagination" | "expansion" | "activeRow" + ): PersistTarget | undefined => + !args.persistTo || typeof args.persistTo === "string" + ? args.persistTo + : args.persistTo[feature]; + const filterState = useFilterState< TItem, TFilterCategoryKey, TPersistenceKeyPrefix - >(args); - const sortState = useSortState( - args - ); - const paginationState = usePaginationState(args); - const expansionState = useExpansionState( - args - ); - const activeRowState = useActiveRowState(args); + >({ ...args, persistTo: getPersistTo("filters") }); + const sortState = useSortState({ + ...args, + persistTo: getPersistTo("sort"), + }); + const paginationState = usePaginationState({ + ...args, + persistTo: getPersistTo("pagination"), + }); + const expansionState = useExpansionState({ + ...args, + persistTo: getPersistTo("expansion"), + }); + const activeRowState = useActiveRowState({ + ...args, + persistTo: getPersistTo("activeRow"), + }); return { ...args, filterState, From 506e9606a55c91a2f645d64c79755b5925cd61c4 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 11:33:05 -0400 Subject: [PATCH 042/102] Update docs Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index ba62851f2b..892d792178 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -202,7 +202,7 @@ const tableControls = useTableControlProps({ ### Kitchen sink example with all features -TODO +TODO - use all features, plus use an object with different persistTo options here ### Should I Use Client or Server Logic? @@ -218,7 +218,9 @@ In most cases, you'll only need to use these higher-level hooks and helpers to b - Internally they use `useTableControlState`, `useTableControlProps` and the `getLocal[Feature]DerivedState` helpers. The config arguments object is a combination of the arguments required by `useTableControlState` and `useTableControlProps`. - The return value (an object we generally name `tableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. - For server-paginated tables: `useTableControlState`, `getHubRequestParams`, and `useTableControlProps`. - - Choose whether you want to use React state, URL params or localStorage/sessionStorage as the source of truth, and call `useTableControlState` with the appropriate `storageType` option and the required options for each type. + - Choose whether you want to use React state, URL params or localStorage/sessionStorage as the source of truth, and call `useTableControlState` with the appropriate `persistTo` option and optional `persistenceKeyPrefix` (to namespace persisted state for multiple tables on the same page). + - `persistTo` can be `"state" | "urlParams" | "localStorage" | "sessionStorage"`, and defaults to `"state"` if omitted (falls back to regular React state). + - You can also use a different type of storage for the state of each feature by passing an object for `persistTo`. See the [Kitchen sink example with all features](#kitchen-sink-example-with-all-features). - Take the object returned by that hook (generally named `tableControlState`) and pass it to `getHubRequestParams` function (you may need to spread it and add additional properties like `hubSortFieldKeys`). - Call your API query hooks, using the `hubRequestParams` as needed. - Call `useTableControlProps` and pass it an object including all properties from `tableControlState` along with additional config arguments. Some of these arguments will be derived from your API data, such as `currentPageItems`, `totalItemCount` and `isLoading`. Others are simply passed here rather than above because they are used only for rendering and not required for state management. From 8fd16c7cfb73727299f3ec1363c735f440225e38 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 11:36:45 -0400 Subject: [PATCH 043/102] Allow default for persistTo Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/types.ts | 1 + client/src/app/hooks/table-controls/useTableControlState.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index b26217ff50..5d281c9e60 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -63,6 +63,7 @@ export type IUseTableControlStateArgs< persistTo?: | PersistTarget | { + default?: PersistTarget; filters?: PersistTarget; sort?: PersistTarget; pagination?: PersistTarget; diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index cdf361cb75..96abc8418a 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -25,7 +25,7 @@ export const useTableControlState = < ): PersistTarget | undefined => !args.persistTo || typeof args.persistTo === "string" ? args.persistTo - : args.persistTo[feature]; + : args.persistTo[feature] || args.persistTo.default; const filterState = useFilterState< TItem, From c9d965e81406999fb5e92a744674c9f9d6a70be2 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 11:43:19 -0400 Subject: [PATCH 044/102] Remove unnecessary repeated properties in useLocalTableControlState Signed-off-by: Mike Turley --- .../hooks/table-controls/useLocalTableControlState.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/client/src/app/hooks/table-controls/useLocalTableControlState.ts b/client/src/app/hooks/table-controls/useLocalTableControlState.ts index 4cb3b4edf3..a7eff41ec1 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControlState.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControlState.ts @@ -30,12 +30,7 @@ export const useLocalTableControlState = < ): IUseTableControlPropsArgs => { const { items, - filterCategories = [], - sortableColumns = [], - getSortValues, - initialSort = null, hasPagination = true, - initialItemsPerPage = 10, idProperty, initialSelected, isItemSelectable, @@ -56,27 +51,22 @@ export const useLocalTableControlState = < const { filteredItems } = getLocalFilterDerivedState({ ...args, items, - filterCategories, filterState, }); const sortState = useSortState({ ...args, persistTo: getPersistTo("sort"), - sortableColumns, - initialSort, }); const { sortedItems } = getLocalSortDerivedState({ ...args, sortState, items: filteredItems, - getSortValues, }); const paginationState = usePaginationState({ ...args, persistTo: getPersistTo("pagination"), - initialItemsPerPage, }); const { currentPageItems } = getLocalPaginationDerivedState({ ...args, From 54694d617eaa07ce142acd79ac59d97e2cc2021e Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 11:57:23 -0400 Subject: [PATCH 045/102] Deduplicate logic from useLocalTableControlState Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/types.ts | 6 +- .../useLocalTableControlState.ts | 68 +++---------------- 2 files changed, 13 insertions(+), 61 deletions(-) diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 5d281c9e60..7c9e493aec 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -113,9 +113,9 @@ export type IUseLocalTableControlStateArgs< // Rendering args // - Used by only useTableControlProps // - Requires state and query values in scope -// - Combines all args above with either: -// - The return values of useLocalTableControlState -// - The return values of useTableControlState and args derived from server-side filtering/sorting/pagination +// - Combines all args above with the return values of useTableControlState and args derived from either: +// - Server-side filtering/sorting/pagination +// - `getLocal[Feature]DerivedState` logic export interface IUseTableControlPropsArgs< TItem, TColumnKey extends string, diff --git a/client/src/app/hooks/table-controls/useLocalTableControlState.ts b/client/src/app/hooks/table-controls/useLocalTableControlState.ts index a7eff41ec1..b958a1f9f8 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControlState.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControlState.ts @@ -1,17 +1,12 @@ import { useSelectionState } from "@migtools/lib-ui"; -import { getLocalFilterDerivedState, useFilterState } from "./filtering"; -import { useSortState, getLocalSortDerivedState } from "./sorting"; -import { - getLocalPaginationDerivedState, - usePaginationState, -} from "./pagination"; -import { useExpansionState } from "./expansion"; -import { useActiveRowState } from "./active-row"; +import { getLocalFilterDerivedState } from "./filtering"; +import { getLocalSortDerivedState } from "./sorting"; +import { getLocalPaginationDerivedState } from "./pagination"; import { IUseLocalTableControlStateArgs, IUseTableControlPropsArgs, - PersistTarget, } from "./types"; +import { useTableControlState } from "./useTableControlState"; export const useLocalTableControlState = < TItem, @@ -28,49 +23,22 @@ export const useLocalTableControlState = < TPersistenceKeyPrefix > ): IUseTableControlPropsArgs => { - const { - items, - hasPagination = true, - idProperty, - initialSelected, - isItemSelectable, - } = args; + const { items, hasPagination = true, idProperty } = args; - const getPersistTo = ( - feature: "filters" | "sort" | "pagination" | "expansion" | "activeRow" - ): PersistTarget | undefined => - !args.persistTo || typeof args.persistTo === "string" - ? args.persistTo - : args.persistTo[feature]; + const tableControlState = useTableControlState(args); - const filterState = useFilterState< - TItem, - TFilterCategoryKey, - TPersistenceKeyPrefix - >({ ...args, persistTo: getPersistTo("filters") }); const { filteredItems } = getLocalFilterDerivedState({ - ...args, + ...tableControlState, items, - filterState, }); - const sortState = useSortState({ - ...args, - persistTo: getPersistTo("sort"), - }); const { sortedItems } = getLocalSortDerivedState({ - ...args, - sortState, + ...tableControlState, items: filteredItems, }); - const paginationState = usePaginationState({ - ...args, - persistTo: getPersistTo("pagination"), - }); const { currentPageItems } = getLocalPaginationDerivedState({ - ...args, - paginationState, + ...tableControlState, items: sortedItems, }); @@ -79,28 +47,12 @@ export const useLocalTableControlState = < ...args, items: filteredItems, isEqual: (a, b) => a[idProperty] === b[idProperty], - initialSelected, - isItemSelectable, - }); - - const expansionState = useExpansionState({ - ...args, - persistTo: getPersistTo("expansion"), - }); - - const activeRowState = useActiveRowState({ - ...args, - persistTo: getPersistTo("activeRow"), }); return { ...args, - filterState, - expansionState, + ...tableControlState, selectionState, - sortState, - paginationState, - activeRowState, totalItemCount: items.length, currentPageItems: hasPagination ? currentPageItems : sortedItems, }; From 9757de0965acdbfdff56421413076cb4ded25fa8 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 12:35:56 -0400 Subject: [PATCH 046/102] Update type comments Signed-off-by: Mike Turley --- ...ts => getLocalTableControlDerivedState.ts} | 36 ++++-------- client/src/app/hooks/table-controls/index.ts | 2 +- client/src/app/hooks/table-controls/types.ts | 58 ++++++++++++++----- .../table-controls/useLocalTableControls.ts | 21 +++++-- 4 files changed, 73 insertions(+), 44 deletions(-) rename client/src/app/hooks/table-controls/{useLocalTableControlState.ts => getLocalTableControlDerivedState.ts} (52%) diff --git a/client/src/app/hooks/table-controls/useLocalTableControlState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts similarity index 52% rename from client/src/app/hooks/table-controls/useLocalTableControlState.ts rename to client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index b958a1f9f8..fe067c05b7 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControlState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -1,58 +1,44 @@ -import { useSelectionState } from "@migtools/lib-ui"; import { getLocalFilterDerivedState } from "./filtering"; import { getLocalSortDerivedState } from "./sorting"; import { getLocalPaginationDerivedState } from "./pagination"; import { - IUseLocalTableControlStateArgs, + IGetLocalTableControlDerivedStateArgs, IUseTableControlPropsArgs, } from "./types"; -import { useTableControlState } from "./useTableControlState"; -export const useLocalTableControlState = < +export const getLocalTableControlDerivedState = < TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, >( - args: IUseLocalTableControlStateArgs< + args: IGetLocalTableControlDerivedStateArgs< TItem, TColumnKey, TSortableColumnKey, TFilterCategoryKey, TPersistenceKeyPrefix > -): IUseTableControlPropsArgs => { - const { items, hasPagination = true, idProperty } = args; - - const tableControlState = useTableControlState(args); - +): Omit< + IUseTableControlPropsArgs, + "selectionState" // TODO we won't need to omit this once selection state is part of useTableControlState +> => { + const { items, hasPagination = true } = args; const { filteredItems } = getLocalFilterDerivedState({ - ...tableControlState, + ...args, items, }); - const { sortedItems } = getLocalSortDerivedState({ - ...tableControlState, + ...args, items: filteredItems, }); - const { currentPageItems } = getLocalPaginationDerivedState({ - ...tableControlState, - items: sortedItems, - }); - - // TODO bring useSelectionState in from lib-ui, but maybe don't use the persistence because we want to keep the whole selected item object references - const selectionState = useSelectionState({ ...args, - items: filteredItems, - isEqual: (a, b) => a[idProperty] === b[idProperty], + items: sortedItems, }); - return { ...args, - ...tableControlState, - selectionState, totalItemCount: items.length, currentPageItems: hasPagination ? currentPageItems : sortedItems, }; diff --git a/client/src/app/hooks/table-controls/index.ts b/client/src/app/hooks/table-controls/index.ts index a203793fe5..4c06b16c9d 100644 --- a/client/src/app/hooks/table-controls/index.ts +++ b/client/src/app/hooks/table-controls/index.ts @@ -2,7 +2,7 @@ export * from "./types"; export * from "./utils"; export * from "./useTableControlState"; export * from "./useTableControlProps"; -export * from "./useLocalTableControlState"; +export * from "./getLocalTableControlDerivedState"; export * from "./useLocalTableControls"; export * from "./getHubRequestParams"; export * from "./filtering"; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 7c9e493aec..fca1ebc5fe 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -18,12 +18,14 @@ import { } from "./pagination"; import { IExpansionDerivedStateArgs } from "./expansion"; import { IActiveRowDerivedStateArgs } from "./active-row"; +import { useTableControlState } from "."; // Generic type params used here: // TItem - The actual API objects represented by rows in the table. Can be any object. // TColumnKey - Union type of unique identifier strings for the columns in the table // TSortableColumnKey - A subset of column keys that have sorting enabled // TFilterCategoryKey - Union type of unique identifier strings for filters (not necessarily the same as column keys) +// TPersistenceKeyPrefix - String (must not include a `:` character) used to distinguish persisted state for multiple tables // TODO when calling useTableControlState, the TItem type is not inferred and some of the params have it inferred as `unknown`. // this currently doesn't seem to matter since TItem becomes inferred later when currentPageItems is in scope, @@ -47,9 +49,9 @@ export type IPersistenceOptions = persistenceKeyPrefix?: DisallowCharacters; }; -// Common args -// - Used by both useLocalTableControlState and useTableControlState -// - Does not require any state or query values in scope +// State args +// - Used by useTableControlState +// - Does not require any state or query data in scope export type IUseTableControlStateArgs< TItem, TColumnKey extends string, @@ -79,8 +81,9 @@ export type IUseTableControlStateArgs< }; // Data-dependent args -// - Used by both useLocalTableControlState and useTableControlProps -// - Requires query values and defined TItem type in scope but not state values +// - Used by both getLocalTableControlDerivedState and useTableControlProps +// - Also used indirectly by the useLocalTableControls shorthand +// - Requires query data and defined TItem type in scope but not state values export interface ITableControlDataDependentArgs { isLoading?: boolean; idProperty: KeyWithValueType; @@ -88,9 +91,35 @@ export interface ITableControlDataDependentArgs { } // Derived state option args -// - Used by only useLocalTableControlState (client-side filtering/sorting/pagination) -// - Requires state and query values in scope -export type IUseLocalTableControlStateArgs< +// - Used by getLocalTableControlDerivedState (client-side filtering/sorting/pagination) +// - Also used indirectly by the useLocalTableControls shorthand +// - Requires state and query data in scope +export type IGetLocalTableControlDerivedStateArgs< + TItem, + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, + TFilterCategoryKey extends string = string, + TPersistenceKeyPrefix extends string = string, +> = ReturnType< + // TODO make this an explicit type + typeof useTableControlState< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + > +> & + ITableControlDataDependentArgs & + ILocalFilterDerivedStateArgs & + IFilterStateArgs & // TODO ??? + ILocalSortDerivedStateArgs & + ILocalPaginationDerivedStateArgs & + Pick, "initialSelected" | "isItemSelectable">; // TODO ??? + +// Args for useLocalTableControls shorthand hook +// - Combines args for useTableControlState and getLocalTableControlDerivedState +export type IUseLocalTableControlsArgs< TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, @@ -103,12 +132,13 @@ export type IUseLocalTableControlStateArgs< TFilterCategoryKey, TPersistenceKeyPrefix > & - ITableControlDataDependentArgs & - ILocalFilterDerivedStateArgs & - IFilterStateArgs & - ILocalSortDerivedStateArgs & - ILocalPaginationDerivedStateArgs & - Pick, "initialSelected" | "isItemSelectable">; // TODO ??? + IGetLocalTableControlDerivedStateArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + >; // Rendering args // - Used by only useTableControlProps diff --git a/client/src/app/hooks/table-controls/useLocalTableControls.ts b/client/src/app/hooks/table-controls/useLocalTableControls.ts index c0a8c3375e..2985631926 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControls.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControls.ts @@ -1,6 +1,8 @@ -import { useLocalTableControlState } from "./useLocalTableControlState"; import { useTableControlProps } from "./useTableControlProps"; -import { IUseLocalTableControlStateArgs } from "./types"; +import { IUseLocalTableControlsArgs } from "./types"; +import { getLocalTableControlDerivedState } from "./getLocalTableControlDerivedState"; +import { useTableControlState } from "./useTableControlState"; +import { useSelectionState } from "@migtools/lib-ui"; export const useLocalTableControls = < TItem, @@ -9,11 +11,22 @@ export const useLocalTableControls = < TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, >( - args: IUseLocalTableControlStateArgs< + args: IUseLocalTableControlsArgs< TItem, TColumnKey, TSortableColumnKey, TFilterCategoryKey, TPersistenceKeyPrefix > -) => useTableControlProps(useLocalTableControlState(args)); +) => + useTableControlProps({ + ...getLocalTableControlDerivedState({ + ...args, + ...useTableControlState(args), + }), + // TODO we won't need this here once selection state is part of useTableControlState + selectionState: useSelectionState({ + ...args, + isEqual: (a, b) => a[args.idProperty] === b[args.idProperty], + }), + }); From 7a27846441b5f419b8fb90cee0671c42af5861a6 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 12:48:08 -0400 Subject: [PATCH 047/102] More type cleanup Signed-off-by: Mike Turley --- .../getLocalTableControlDerivedState.ts | 20 ++++++++--- client/src/app/hooks/table-controls/types.ts | 36 ++++++------------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index fe067c05b7..5674d118d6 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -2,9 +2,10 @@ import { getLocalFilterDerivedState } from "./filtering"; import { getLocalSortDerivedState } from "./sorting"; import { getLocalPaginationDerivedState } from "./pagination"; import { - IGetLocalTableControlDerivedStateArgs, + ITableControlLocalDerivedStateArgs, IUseTableControlPropsArgs, } from "./types"; +import { useTableControlState } from "./useTableControlState"; export const getLocalTableControlDerivedState = < TItem, @@ -13,13 +14,22 @@ export const getLocalTableControlDerivedState = < TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, >( - args: IGetLocalTableControlDerivedStateArgs< + args: ITableControlLocalDerivedStateArgs< TItem, TColumnKey, TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - > + TFilterCategoryKey + > & + // TODO make this an explicit type + ReturnType< + typeof useTableControlState< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + > + > ): Omit< IUseTableControlPropsArgs, "selectionState" // TODO we won't need to omit this once selection state is part of useTableControlState diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index fca1ebc5fe..5e7cf08b44 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -18,7 +18,6 @@ import { } from "./pagination"; import { IExpansionDerivedStateArgs } from "./expansion"; import { IActiveRowDerivedStateArgs } from "./active-row"; -import { useTableControlState } from "."; // Generic type params used here: // TItem - The actual API objects represented by rows in the table. Can be any object. @@ -90,32 +89,19 @@ export interface ITableControlDataDependentArgs { forceNumRenderedColumns?: number; } -// Derived state option args +// Local derived state args // - Used by getLocalTableControlDerivedState (client-side filtering/sorting/pagination) // - Also used indirectly by the useLocalTableControls shorthand // - Requires state and query data in scope -export type IGetLocalTableControlDerivedStateArgs< +export type ITableControlLocalDerivedStateArgs< TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, - TPersistenceKeyPrefix extends string = string, -> = ReturnType< - // TODO make this an explicit type - typeof useTableControlState< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - > -> & - ITableControlDataDependentArgs & +> = ITableControlDataDependentArgs & ILocalFilterDerivedStateArgs & - IFilterStateArgs & // TODO ??? ILocalSortDerivedStateArgs & - ILocalPaginationDerivedStateArgs & - Pick, "initialSelected" | "isItemSelectable">; // TODO ??? + ILocalPaginationDerivedStateArgs; // Args for useLocalTableControls shorthand hook // - Combines args for useTableControlState and getLocalTableControlDerivedState @@ -132,20 +118,20 @@ export type IUseLocalTableControlsArgs< TFilterCategoryKey, TPersistenceKeyPrefix > & - IGetLocalTableControlDerivedStateArgs< + ITableControlLocalDerivedStateArgs< TItem, TColumnKey, TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - >; + TFilterCategoryKey + > & + Pick, "initialSelected" | "isItemSelectable">; // TODO this won't be included here when selection is part of useTableControlState // Rendering args // - Used by only useTableControlProps // - Requires state and query values in scope // - Combines all args above with the return values of useTableControlState and args derived from either: -// - Server-side filtering/sorting/pagination -// - `getLocal[Feature]DerivedState` logic +// - Server-side filtering/sorting/pagination provided by the consumer +// - getLocalTableControlDerivedState (client-side filtering/sorting/pagination) export interface IUseTableControlPropsArgs< TItem, TColumnKey extends string, @@ -164,5 +150,5 @@ export interface IUseTableControlPropsArgs< IExpansionDerivedStateArgs, IActiveRowDerivedStateArgs { currentPageItems: TItem[]; - selectionState: ReturnType>; // TODO make this optional? fold it in? + selectionState: ReturnType>; // TODO this won't be included here when selection is part of useTableControlState } From 35961be2b9e639dbfd731773e9fe16b0d54f7c20 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 13:51:37 -0400 Subject: [PATCH 048/102] Even more type cleanup Signed-off-by: Mike Turley --- .../getLocalTableControlDerivedState.ts | 8 +-- client/src/app/hooks/table-controls/types.ts | 60 ++++++++++++------- .../table-controls/useLocalTableControls.ts | 14 +++-- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index 5674d118d6..34c962fd6f 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -3,7 +3,7 @@ import { getLocalSortDerivedState } from "./sorting"; import { getLocalPaginationDerivedState } from "./pagination"; import { ITableControlLocalDerivedStateArgs, - IUseTableControlPropsArgs, + ITableControlDerivedState, } from "./types"; import { useTableControlState } from "./useTableControlState"; @@ -30,10 +30,7 @@ export const getLocalTableControlDerivedState = < TPersistenceKeyPrefix > > -): Omit< - IUseTableControlPropsArgs, - "selectionState" // TODO we won't need to omit this once selection state is part of useTableControlState -> => { +): ITableControlDerivedState => { const { items, hasPagination = true } = args; const { filteredItems } = getLocalFilterDerivedState({ ...args, @@ -48,7 +45,6 @@ export const getLocalTableControlDerivedState = < items: sortedItems, }); return { - ...args, totalItemCount: items.length, currentPageItems: hasPagination ? currentPageItems : sortedItems, }; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 5e7cf08b44..f87b442fb1 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -79,15 +79,13 @@ export type IUseTableControlStateArgs< variant?: TableProps["variant"]; }; -// Data-dependent args +// Data-dependent args (TODO is there a better name?) // - Used by both getLocalTableControlDerivedState and useTableControlProps // - Also used indirectly by the useLocalTableControls shorthand // - Requires query data and defined TItem type in scope but not state values -export interface ITableControlDataDependentArgs { - isLoading?: boolean; +export type ITableControlDataDependentArgs = { idProperty: KeyWithValueType; - forceNumRenderedColumns?: number; -} +}; // Local derived state args // - Used by getLocalTableControlDerivedState (client-side filtering/sorting/pagination) @@ -103,8 +101,22 @@ export type ITableControlLocalDerivedStateArgs< ILocalSortDerivedStateArgs & ILocalPaginationDerivedStateArgs; +export type ITableControlDerivedState = { + currentPageItems: TItem[]; + totalItemCount: number; +}; + +// Rendering args +// - Used by useTableControlProps +// - Also used indirectly by the useLocalTableControls shorthand +// - Requires state and query values in scope +export type ITableControlRenderingArgs = { + isLoading?: boolean; + forceNumRenderedColumns?: number; +}; + // Args for useLocalTableControls shorthand hook -// - Combines args for useTableControlState and getLocalTableControlDerivedState +// - Combines args for useTableControlState, getLocalTableControlDerivedState and useTableControlProps export type IUseLocalTableControlsArgs< TItem, TColumnKey extends string, @@ -124,31 +136,33 @@ export type IUseLocalTableControlsArgs< TSortableColumnKey, TFilterCategoryKey > & + ITableControlRenderingArgs & Pick, "initialSelected" | "isItemSelectable">; // TODO this won't be included here when selection is part of useTableControlState -// Rendering args +// Rendering args (TODO rename) // - Used by only useTableControlProps // - Requires state and query values in scope // - Combines all args above with the return values of useTableControlState and args derived from either: // - Server-side filtering/sorting/pagination provided by the consumer // - getLocalTableControlDerivedState (client-side filtering/sorting/pagination) -export interface IUseTableControlPropsArgs< +export type IUseTableControlPropsArgs< TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, -> extends IUseTableControlStateArgs< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey - >, - ITableControlDataDependentArgs, - IFilterPropsArgs, - ISortPropsArgs, - IPaginationPropsArgs, - IExpansionDerivedStateArgs, - IActiveRowDerivedStateArgs { - currentPageItems: TItem[]; - selectionState: ReturnType>; // TODO this won't be included here when selection is part of useTableControlState -} +> = IUseTableControlStateArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey +> & + ITableControlDataDependentArgs & + IFilterPropsArgs & + ISortPropsArgs & + IPaginationPropsArgs & + IExpansionDerivedStateArgs & + IActiveRowDerivedStateArgs & + ITableControlDerivedState & + ITableControlRenderingArgs & { + selectionState: ReturnType>; // TODO this won't be included here when selection is part of useTableControlState + }; diff --git a/client/src/app/hooks/table-controls/useLocalTableControls.ts b/client/src/app/hooks/table-controls/useLocalTableControls.ts index 2985631926..18137a27fd 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControls.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControls.ts @@ -18,15 +18,17 @@ export const useLocalTableControls = < TFilterCategoryKey, TPersistenceKeyPrefix > -) => - useTableControlProps({ - ...getLocalTableControlDerivedState({ - ...args, - ...useTableControlState(args), - }), +) => { + const state = useTableControlState(args); + const derivedState = getLocalTableControlDerivedState({ ...args, ...state }); + return useTableControlProps({ + ...args, + ...state, + ...derivedState, // TODO we won't need this here once selection state is part of useTableControlState selectionState: useSelectionState({ ...args, isEqual: (a, b) => a[args.idProperty] === b[args.idProperty], }), }); +}; From 4a9d9256955b2eae65f674484069609fa2033cb9 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 14:52:30 -0400 Subject: [PATCH 049/102] Yet again more type cleanup Signed-off-by: Mike Turley --- .../getLocalTableControlDerivedState.ts | 17 +- client/src/app/hooks/table-controls/types.ts | 150 +++++++++++------- .../table-controls/useTableControlState.ts | 14 +- .../file-all-incidents-table.tsx | 2 +- .../issue-affected-files-table.tsx | 2 +- 5 files changed, 117 insertions(+), 68 deletions(-) diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index 34c962fd6f..a5a7e1efa3 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -4,8 +4,8 @@ import { getLocalPaginationDerivedState } from "./pagination"; import { ITableControlLocalDerivedStateArgs, ITableControlDerivedState, + ITableControlState, } from "./types"; -import { useTableControlState } from "./useTableControlState"; export const getLocalTableControlDerivedState = < TItem, @@ -20,15 +20,12 @@ export const getLocalTableControlDerivedState = < TSortableColumnKey, TFilterCategoryKey > & - // TODO make this an explicit type - ReturnType< - typeof useTableControlState< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - > + ITableControlState< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix > ): ITableControlDerivedState => { const { items, hasPagination = true } = args; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index f87b442fb1..1fc18f3adc 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -1,23 +1,27 @@ import { TableProps } from "@patternfly/react-table"; import { ISelectionStateArgs, useSelectionState } from "@migtools/lib-ui"; -import { DisallowCharacters, KeyWithValueType } from "@app/utils/type-utils"; +import { DisallowCharacters } from "@app/utils/type-utils"; import { IFilterStateArgs, ILocalFilterDerivedStateArgs, IFilterPropsArgs, + IFilterState, } from "./filtering"; import { ILocalSortDerivedStateArgs, ISortPropsArgs, + ISortState, ISortStateArgs, } from "./sorting"; import { IPaginationStateArgs, ILocalPaginationDerivedStateArgs, IPaginationPropsArgs, + IPaginationState, } from "./pagination"; -import { IExpansionDerivedStateArgs } from "./expansion"; -import { IActiveRowDerivedStateArgs } from "./active-row"; +import { IExpansionDerivedStateArgs, IExpansionState } from "./expansion"; +import { IActiveRowDerivedStateArgs, IActiveRowState } from "./active-row"; +import { useTableControlState } from "./useTableControlState"; // Generic type params used here: // TItem - The actual API objects represented by rows in the table. Can be any object. @@ -40,8 +44,8 @@ export type PersistTarget = | "sessionStorage"; // Persistence-specific args -// - Extra args needed for useTableControlState and each concern-specific use*State hook in URL params mode -// - Does not require any state or query values in scope +// - Extra args needed for useTableControlState and each feature-specific use*State hook for persisting state +// - Does not require any state or query data in scope export type IPersistenceOptions = { persistTo?: PersistTarget; @@ -60,6 +64,8 @@ export type IUseTableControlStateArgs< > = IFilterStateArgs & ISortStateArgs & IPaginationStateArgs & + // There is no IExpansionStateArgs because the only args there are the IPersistenceOptions + // There is no IActiveRowStateArgs because the only args there are the IPersistenceOptions Omit, "persistTo"> & { persistTo?: | PersistTarget @@ -73,22 +79,38 @@ export type IUseTableControlStateArgs< }; columnNames: Record; // An ordered mapping of unique keys to human-readable column name strings isSelectable?: boolean; - hasPagination?: boolean; - expandableVariant?: "single" | "compound" | null; - hasActionsColumn?: boolean; - variant?: TableProps["variant"]; + hasPagination?: boolean; // TODO disable pagination state stuff if this is falsy + expandableVariant?: "single" | "compound" | null; // TODO disable expandable state stuff if this is falsy + // TODO do we want to have a featuresEnabled object instead? }; -// Data-dependent args (TODO is there a better name?) -// - Used by both getLocalTableControlDerivedState and useTableControlProps -// - Also used indirectly by the useLocalTableControls shorthand -// - Requires query data and defined TItem type in scope but not state values -export type ITableControlDataDependentArgs = { - idProperty: KeyWithValueType; +// State object +// - Returned by useTableControlState +// - Contains persisted state for all features +// - Also includes all of useTableControlState's args for convenience, since useTableControlProps requires them along with the state itself +export type ITableControlState< + TItem, + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, + TFilterCategoryKey extends string = string, + TPersistenceKeyPrefix extends string = string, +> = IUseTableControlStateArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix +> & { + filterState: IFilterState; + sortState: ISortState; + paginationState: IPaginationState; + expansionState: IExpansionState; + activeRowState: IActiveRowState; }; // Local derived state args // - Used by getLocalTableControlDerivedState (client-side filtering/sorting/pagination) +// - getLocalTableControlDerivedState also requires the return values from useTableControlState // - Also used indirectly by the useLocalTableControls shorthand // - Requires state and query data in scope export type ITableControlLocalDerivedStateArgs< @@ -96,27 +118,56 @@ export type ITableControlLocalDerivedStateArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, -> = ITableControlDataDependentArgs & - ILocalFilterDerivedStateArgs & +> = ILocalFilterDerivedStateArgs & ILocalSortDerivedStateArgs & ILocalPaginationDerivedStateArgs; +// There is no ILocalExpansionDerivedStateArgs because expansion derived state is always local an internal to useTableControlProps +// There is no ILocalActiveRowDerivedStateArgs because expansion derived state is always local an internal to useTableControlProps +// Table-level derived state object +// - Used by useTableControlProps +// - Provided by either: +// - Return values of getLocalTableControlDerivedState (client-side filtering/sorting/pagination) +// - The consumer directly (server-side filtering/sorting/pagination) export type ITableControlDerivedState = { currentPageItems: TItem[]; totalItemCount: number; }; // Rendering args -// - Used by useTableControlProps -// - Also used indirectly by the useLocalTableControls shorthand +// - Used by only useTableControlProps // - Requires state and query values in scope -export type ITableControlRenderingArgs = { - isLoading?: boolean; - forceNumRenderedColumns?: number; -}; +// - Combines all args above with the return values of useTableControlState, args used only for rendering, and args derived from either: +// - Server-side filtering/sorting/pagination provided by the consumer +// - getLocalTableControlDerivedState (client-side filtering/sorting/pagination) +export type IUseTableControlPropsArgs< + TItem, + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, + TFilterCategoryKey extends string = string, +> = IUseTableControlStateArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey +> & + IFilterPropsArgs & + ISortPropsArgs & + IPaginationPropsArgs & + IExpansionDerivedStateArgs & // TODO should this be IExpansionPropsArgs? should we lift that stuff out of useTableControlProps? + IActiveRowDerivedStateArgs & // TODO should this be IActiveRowPropsArgs? should we lift that stuff out of useTableControlProps? + ITableControlDerivedState & { + isLoading?: boolean; + forceNumRenderedColumns?: number; + variant?: TableProps["variant"]; + hasActionsColumn?: boolean; + selectionState: ReturnType>; // TODO this won't be included here when selection is part of useTableControlState + }; -// Args for useLocalTableControls shorthand hook -// - Combines args for useTableControlState, getLocalTableControlDerivedState and useTableControlProps +// Combined args for locally-paginated tables +// - Used by useLocalTableControls shorthand hook +// - Combines args for useTableControlState, getLocalTableControlDerivedState and useTableControlProps, +// omitting args for useTableControlProps that come from return values of useTableControlState and getLocalTableControlDerivedState. export type IUseLocalTableControlsArgs< TItem, TColumnKey extends string, @@ -136,33 +187,24 @@ export type IUseLocalTableControlsArgs< TSortableColumnKey, TFilterCategoryKey > & - ITableControlRenderingArgs & + Omit< + IUseTableControlPropsArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey + >, + | keyof (ITableControlDerivedState & + // TODO make this an explicit type + ReturnType< + typeof useTableControlState< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + > + >) + | "selectionState" // TODO this won't be included here when selection is part of useTableControlState + > & Pick, "initialSelected" | "isItemSelectable">; // TODO this won't be included here when selection is part of useTableControlState - -// Rendering args (TODO rename) -// - Used by only useTableControlProps -// - Requires state and query values in scope -// - Combines all args above with the return values of useTableControlState and args derived from either: -// - Server-side filtering/sorting/pagination provided by the consumer -// - getLocalTableControlDerivedState (client-side filtering/sorting/pagination) -export type IUseTableControlPropsArgs< - TItem, - TColumnKey extends string, - TSortableColumnKey extends TColumnKey, - TFilterCategoryKey extends string = string, -> = IUseTableControlStateArgs< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey -> & - ITableControlDataDependentArgs & - IFilterPropsArgs & - ISortPropsArgs & - IPaginationPropsArgs & - IExpansionDerivedStateArgs & - IActiveRowDerivedStateArgs & - ITableControlDerivedState & - ITableControlRenderingArgs & { - selectionState: ReturnType>; // TODO this won't be included here when selection is part of useTableControlState - }; diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 96abc8418a..be2db9ea81 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -1,4 +1,8 @@ -import { IUseTableControlStateArgs, PersistTarget } from "./types"; +import { + ITableControlState, + IUseTableControlStateArgs, + PersistTarget, +} from "./types"; import { useFilterState } from "./filtering"; import { useSortState } from "./sorting"; import { usePaginationState } from "./pagination"; @@ -19,7 +23,13 @@ export const useTableControlState = < TFilterCategoryKey, TPersistenceKeyPrefix > -) => { +): ITableControlState< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix +> => { const getPersistTo = ( feature: "filters" | "sort" | "pagination" | "expansion" | "activeRow" ): PersistTarget | undefined => diff --git a/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx b/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx index 352f7494a1..b95f9ae670 100644 --- a/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx +++ b/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx @@ -35,7 +35,6 @@ export const FileAllIncidentsTable: React.FC< sortableColumns: ["line", "message"], initialSort: { columnKey: "line", direction: "asc" }, initialItemsPerPage: 10, - variant: "compact", }); const { @@ -60,6 +59,7 @@ export const FileAllIncidentsTable: React.FC< forceNumRenderedColumns: 3, totalItemCount, isLoading: isFetching, + variant: "compact", // TODO FIXME - we don't need selectionState but it's required by this hook? selectionState: useSelectionState({ items: currentPageIncidents, diff --git a/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx b/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx index 5dc060870e..ebfd8628ef 100644 --- a/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx +++ b/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx @@ -58,7 +58,6 @@ export const IssueAffectedFilesTable: React.FC< }, ], initialItemsPerPage: 10, - variant: "compact", }); const { @@ -83,6 +82,7 @@ export const IssueAffectedFilesTable: React.FC< currentPageItems: currentPageFileReports, totalItemCount, isLoading: isFetching, + variant: "compact", // TODO FIXME - we don't need selectionState but it's required by this hook? selectionState: useSelectionState({ items: currentPageFileReports, From 929c9aa9543f37717bb80bfe9766485ab9ae1f1e Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 15:36:36 -0400 Subject: [PATCH 050/102] Update lib-ui Signed-off-by: Mike Turley --- client/package.json | 2 +- package-lock.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/package.json b/client/package.json index 01b186eb47..11e0475873 100644 --- a/client/package.json +++ b/client/package.json @@ -24,7 +24,7 @@ "@dnd-kit/sortable": "^7.0.2", "@hookform/resolvers": "^2.9.11", "@hot-loader/react-dom": "^17.0.2", - "@migtools/lib-ui": "^10.0.0", + "@migtools/lib-ui": "^10.0.1", "@patternfly/patternfly": "^5.0.2", "@patternfly/react-charts": "^7.1.0", "@patternfly/react-code-editor": "^5.1.0", diff --git a/package-lock.json b/package-lock.json index e396517424..6ed9531d8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "@dnd-kit/sortable": "^7.0.2", "@hookform/resolvers": "^2.9.11", "@hot-loader/react-dom": "^17.0.2", - "@migtools/lib-ui": "^10.0.0", + "@migtools/lib-ui": "^10.0.1", "@patternfly/patternfly": "^5.0.2", "@patternfly/react-charts": "^7.1.0", "@patternfly/react-code-editor": "^5.1.0", @@ -1539,9 +1539,9 @@ "dev": true }, "node_modules/@migtools/lib-ui": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@migtools/lib-ui/-/lib-ui-10.0.0.tgz", - "integrity": "sha512-UMF9o/M0PKjPIdPoX8GpMAAf2Vv8ZgVqJa38Se4zKTGPGIcYsNOiZ6ZfmaymUwD1jT7d/h7QQFMcpVpU4Ca1kg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@migtools/lib-ui/-/lib-ui-10.0.1.tgz", + "integrity": "sha512-CiFw0gz8tssRzSvQ9TjH1kUTg/TIOnTfXqsv6Hn7Yd/0oNW5hxdQYYeIaznNDTj7D/moPzV7U4oKW9BKYu2KKw==", "dependencies": { "@tanstack/react-query": "^4.26.1", "fast-deep-equal": "^3.1.3", From d7da7181ff87435d29ba8db9dd2a0ab0318dab35 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 15:52:35 -0400 Subject: [PATCH 051/102] Clean up types in pagination state Signed-off-by: Mike Turley --- .../pagination/usePaginationState.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index b663fad470..16ca4fc01f 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -1,10 +1,13 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; import { IPersistenceOptions } from "../types"; -export interface IPaginationState { +export interface IActivePagination { pageNumber: number; - setPageNumber: (pageNumber: number) => void; itemsPerPage: number; +} + +export interface IPaginationState extends IActivePagination { + setPageNumber: (pageNumber: number) => void; setItemsPerPage: (numItems: number) => void; } @@ -23,12 +26,15 @@ export const usePaginationState = < initialItemsPerPage = 10, } = args; - const defaultValue = { pageNumber: 1, itemsPerPage: initialItemsPerPage }; + const defaultValue: IActivePagination = { + pageNumber: 1, + itemsPerPage: initialItemsPerPage, + }; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 const [paginationState, setPaginationState] = usePersistentState< - typeof defaultValue, + IActivePagination, TPersistenceKeyPrefix, "pageNumber" | "itemsPerPage" >({ From 1d66543383b29785bf9c57436351382249c54f78 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 16:37:37 -0400 Subject: [PATCH 052/102] Add featuresEnabled arg Signed-off-by: Mike Turley --- .../active-row/useActiveRowState.ts | 12 ++- .../expansion/useExpansionState.ts | 12 ++- .../filtering/useFilterState.ts | 8 +- .../pagination/usePaginationState.ts | 7 +- .../table-controls/sorting/useSortState.ts | 7 +- client/src/app/hooks/table-controls/types.ts | 79 ++++++++++++------- .../table-controls/useTableControlProps.ts | 20 ++++- .../table-controls/useTableControlState.ts | 32 +++++--- client/src/app/hooks/table-controls/utils.ts | 31 +++++++- 9 files changed, 149 insertions(+), 59 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts index 7b5db1e8e0..e110bdb2af 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts @@ -1,5 +1,5 @@ import { parseMaybeNumericString } from "@app/utils/utils"; -import { IPersistenceOptions } from "../types"; +import { IFeaturePersistenceArgs } from "../types"; import { usePersistentState } from "@app/hooks/usePersistentState"; export interface IActiveRowState { @@ -7,12 +7,17 @@ export interface IActiveRowState { setActiveRowId: (id: string | number | null) => void; } +export interface IActiveRowStateArgs { + isEnabled?: boolean; +} + export const useActiveRowState = < TPersistenceKeyPrefix extends string = string, >( - args: IPersistenceOptions = {} + args: IActiveRowStateArgs & + IFeaturePersistenceArgs = {} ): IActiveRowState => { - const { persistTo, persistenceKeyPrefix } = args; + const { persistTo, persistenceKeyPrefix, isEnabled } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 @@ -23,6 +28,7 @@ export const useActiveRowState = < >({ defaultValue: null, persistenceKeyPrefix, + isEnabled, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index 3e0b1761fd..37e5c98f7a 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -1,6 +1,6 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; import { objectKeys } from "@app/utils/utils"; -import { IPersistenceOptions } from "../types"; +import { IFeaturePersistenceArgs } from "../types"; // TExpandedCells maps item[idProperty] values to either: // - The key of an expanded column in that row, if the table is compound-expandable @@ -17,13 +17,18 @@ export interface IExpansionState { ) => void; } +export interface IExpansionStateArgs { + isEnabled?: boolean; +} + export const useExpansionState = < TColumnKey extends string, TPersistenceKeyPrefix extends string = string, >( - args: IPersistenceOptions = {} + args: IExpansionStateArgs & + IFeaturePersistenceArgs = {} ): IExpansionState => { - const { persistTo = "state", persistenceKeyPrefix } = args; + const { persistTo = "state", persistenceKeyPrefix, isEnabled } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 @@ -34,6 +39,7 @@ export const useExpansionState = < >({ defaultValue: {}, persistenceKeyPrefix, + isEnabled, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index 937ef1b68f..ef3911452b 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -1,5 +1,5 @@ import { FilterCategory, IFilterValues } from "@app/components/FilterToolbar"; -import { IPersistenceOptions } from "../types"; +import { IFeaturePersistenceArgs } from "../types"; import { usePersistentState } from "@app/hooks/usePersistentState"; import { serializeFilterUrlParams } from "./helpers"; import { deserializeFilterUrlParams } from "./helpers"; @@ -11,6 +11,7 @@ export interface IFilterState { export type IFilterStateArgs = { filterCategories?: FilterCategory[]; + isEnabled?: boolean; }; export const useFilterState = < @@ -19,9 +20,9 @@ export const useFilterState = < TPersistenceKeyPrefix extends string = string, >( args: IFilterStateArgs & - IPersistenceOptions + IFeaturePersistenceArgs ): IFilterState => { - const { persistTo = "state", persistenceKeyPrefix } = args; + const { persistTo = "state", persistenceKeyPrefix, isEnabled } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 @@ -32,6 +33,7 @@ export const useFilterState = < >({ defaultValue: {}, persistenceKeyPrefix, + isEnabled, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 16ca4fc01f..0491e2cd4b 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -1,5 +1,5 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; -import { IPersistenceOptions } from "../types"; +import { IFeaturePersistenceArgs } from "../types"; export interface IActivePagination { pageNumber: number; @@ -13,17 +13,19 @@ export interface IPaginationState extends IActivePagination { export interface IPaginationStateArgs { initialItemsPerPage?: number; + isEnabled?: boolean; } export const usePaginationState = < TPersistenceKeyPrefix extends string = string, >( - args: IPaginationStateArgs & IPersistenceOptions + args: IPaginationStateArgs & IFeaturePersistenceArgs ): IPaginationState => { const { persistTo = "state", persistenceKeyPrefix, initialItemsPerPage = 10, + isEnabled, } = args; const defaultValue: IActivePagination = { @@ -40,6 +42,7 @@ export const usePaginationState = < >({ defaultValue, persistenceKeyPrefix, + isEnabled, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index d456f1fb75..578f237161 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -1,4 +1,4 @@ -import { IPersistenceOptions } from ".."; +import { IFeaturePersistenceArgs } from ".."; import { usePersistentState } from "@app/hooks/usePersistentState"; export interface IActiveSort { @@ -14,6 +14,7 @@ export interface ISortState { export type ISortStateArgs = { sortableColumns?: TSortableColumnKey[]; initialSort?: IActiveSort | null; + isEnabled?: boolean; }; export const useSortState = < @@ -21,7 +22,7 @@ export const useSortState = < TPersistenceKeyPrefix extends string = string, >( args: ISortStateArgs & - IPersistenceOptions + IFeaturePersistenceArgs ): ISortState => { const { persistTo = "state", @@ -30,6 +31,7 @@ export const useSortState = < initialSort = sortableColumns[0] ? { columnKey: sortableColumns[0], direction: "asc" } : null, + isEnabled, } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. @@ -41,6 +43,7 @@ export const useSortState = < >({ defaultValue: initialSort, persistenceKeyPrefix, + isEnabled, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 1fc18f3adc..aa8d39bbde 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -19,8 +19,16 @@ import { IPaginationPropsArgs, IPaginationState, } from "./pagination"; -import { IExpansionDerivedStateArgs, IExpansionState } from "./expansion"; -import { IActiveRowDerivedStateArgs, IActiveRowState } from "./active-row"; +import { + IExpansionDerivedStateArgs, + IExpansionState, + IExpansionStateArgs, +} from "./expansion"; +import { + IActiveRowDerivedStateArgs, + IActiveRowState, + IActiveRowStateArgs, +} from "./active-row"; import { useTableControlState } from "./useTableControlState"; // Generic type params used here: @@ -37,6 +45,14 @@ import { useTableControlState } from "./useTableControlState"; // This may be resolved in a newer TypeScript version after https://github.com/microsoft/TypeScript/pull/54047 is merged! // See https://github.com/konveyor/tackle2-ui/issues/1456 +export type TableFeature = + | "filter" + | "sort" + | "pagination" + | "selection" + | "expansion" + | "activeRow"; + export type PersistTarget = | "state" | "urlParams" @@ -46,13 +62,28 @@ export type PersistTarget = // Persistence-specific args // - Extra args needed for useTableControlState and each feature-specific use*State hook for persisting state // - Does not require any state or query data in scope -export type IPersistenceOptions = - { - persistTo?: PersistTarget; - persistenceKeyPrefix?: DisallowCharacters; - }; +// Common: +export type ICommonPersistenceArgs< + TPersistenceKeyPrefix extends string = string, +> = { + persistenceKeyPrefix?: DisallowCharacters; +}; +// Feature-level: +export type IFeaturePersistenceArgs< + TPersistenceKeyPrefix extends string = string, +> = ICommonPersistenceArgs & { + persistTo?: PersistTarget; +}; +// Table-level: +export type ITablePersistenceArgs< + TPersistenceKeyPrefix extends string = string, +> = ICommonPersistenceArgs & { + persistTo?: + | PersistTarget + | Partial>; +}; -// State args +// Table-level state args // - Used by useTableControlState // - Does not require any state or query data in scope export type IUseTableControlStateArgs< @@ -61,30 +92,20 @@ export type IUseTableControlStateArgs< TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, -> = IFilterStateArgs & - ISortStateArgs & - IPaginationStateArgs & - // There is no IExpansionStateArgs because the only args there are the IPersistenceOptions - // There is no IActiveRowStateArgs because the only args there are the IPersistenceOptions - Omit, "persistTo"> & { - persistTo?: - | PersistTarget - | { - default?: PersistTarget; - filters?: PersistTarget; - sort?: PersistTarget; - pagination?: PersistTarget; - expansion?: PersistTarget; - activeRow?: PersistTarget; - }; +> = Omit, "isEnabled"> & + Omit, "isEnabled"> & + Omit & + Omit & + Omit & + ITablePersistenceArgs & { + featuresEnabled?: Partial>; columnNames: Record; // An ordered mapping of unique keys to human-readable column name strings - isSelectable?: boolean; hasPagination?: boolean; // TODO disable pagination state stuff if this is falsy expandableVariant?: "single" | "compound" | null; // TODO disable expandable state stuff if this is falsy // TODO do we want to have a featuresEnabled object instead? }; -// State object +// Table-level state object // - Returned by useTableControlState // - Contains persisted state for all features // - Also includes all of useTableControlState's args for convenience, since useTableControlProps requires them along with the state itself @@ -108,7 +129,7 @@ export type ITableControlState< activeRowState: IActiveRowState; }; -// Local derived state args +// Table-level local derived state args // - Used by getLocalTableControlDerivedState (client-side filtering/sorting/pagination) // - getLocalTableControlDerivedState also requires the return values from useTableControlState // - Also used indirectly by the useLocalTableControls shorthand @@ -121,8 +142,8 @@ export type ITableControlLocalDerivedStateArgs< > = ILocalFilterDerivedStateArgs & ILocalSortDerivedStateArgs & ILocalPaginationDerivedStateArgs; -// There is no ILocalExpansionDerivedStateArgs because expansion derived state is always local an internal to useTableControlProps -// There is no ILocalActiveRowDerivedStateArgs because expansion derived state is always local an internal to useTableControlProps +// There is no ILocalExpansionDerivedStateArgs type because expansion derived state is always local and internal to useTableControlProps +// There is no ILocalActiveRowDerivedStateArgs type because activeRow derived state is always local and internal to useTableControlProps // Table-level derived state object // - Used by useTableControlProps diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index bd0c05cba5..5dca77da1e 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -10,7 +10,10 @@ import { getFilterProps } from "./filtering"; import { getSortProps } from "./sorting"; import { getPaginationProps, usePaginationEffects } from "./pagination"; import { getActiveRowDerivedState, useActiveRowEffects } from "./active-row"; -import { handlePropagatedRowClick } from "./utils"; +import { + getFeaturesEnabledWithFallbacks, + handlePropagatedRowClick, +} from "./utils"; import { getExpansionDerivedState } from "./expansion"; export const useTableControlProps = < @@ -46,21 +49,24 @@ export const useTableControlProps = < }, columnNames, sortableColumns = [], - isSelectable = false, expandableVariant = null, hasActionsColumn = false, variant, idProperty, } = args; + const featuresEnabled = getFeaturesEnabledWithFallbacks(args.featuresEnabled); + // TODO make sure we are not passing stuff for any disabled features + const columnKeys = objectKeys(columnNames); // Some table controls rely on extra columns inserted before or after the ones included in columnNames. // We need to account for those when dealing with props based on column index and colSpan. let numColumnsBeforeData = 0; let numColumnsAfterData = 0; - if (isSelectable) numColumnsBeforeData++; - if (expandableVariant === "single") numColumnsBeforeData++; + if (featuresEnabled.selection) numColumnsBeforeData++; + if (featuresEnabled.expansion && expandableVariant === "single") + numColumnsBeforeData++; if (hasActionsColumn) numColumnsAfterData++; const numRenderedColumns = forceNumRenderedColumns || @@ -120,6 +126,8 @@ export const useTableControlProps = < children: columnNames[columnKey], }); + // TODO move this into a getActiveRowProps helper? + // TODO have the consumer always call getTrProps and only include clickable stuff if the feature is enabled const getClickableTrProps = ({ onRowClick, item, @@ -149,6 +157,7 @@ export const useTableControlProps = < dataLabel: columnNames[columnKey], }); + // TODO move this into a getSelectionProps helper somehow? const getSelectCheckboxTdProps = ({ item, rowIndex, @@ -165,6 +174,7 @@ export const useTableControlProps = < }, }); + // TODO move this into a getExpansionProps helper somehow? const getSingleExpandTdProps = ({ item, rowIndex, @@ -184,6 +194,7 @@ export const useTableControlProps = < }, }); + // TODO move this into a getExpansionProps helper somehow? const getCompoundExpandTdProps = ({ item, rowIndex, @@ -208,6 +219,7 @@ export const useTableControlProps = < }, }); + // TODO move this into a getExpansionProps helper somehow? const getExpandedContentTdProps = ({ item, }: { diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index be2db9ea81..e5db2925c0 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -1,13 +1,14 @@ import { ITableControlState, IUseTableControlStateArgs, - PersistTarget, + TableFeature, } from "./types"; import { useFilterState } from "./filtering"; import { useSortState } from "./sorting"; import { usePaginationState } from "./pagination"; import { useActiveRowState } from "./active-row"; import { useExpansionState } from "./expansion"; +import { getFeaturesEnabledWithFallbacks } from "./utils"; export const useTableControlState = < TItem, @@ -30,34 +31,41 @@ export const useTableControlState = < TFilterCategoryKey, TPersistenceKeyPrefix > => { - const getPersistTo = ( - feature: "filters" | "sort" | "pagination" | "expansion" | "activeRow" - ): PersistTarget | undefined => - !args.persistTo || typeof args.persistTo === "string" - ? args.persistTo - : args.persistTo[feature] || args.persistTo.default; + const featuresEnabled = getFeaturesEnabledWithFallbacks(args.featuresEnabled); + + const getFeatureArgs = (feature: TableFeature) => ({ + isEnabled: featuresEnabled[feature], + persistTo: + !args.persistTo || typeof args.persistTo === "string" + ? args.persistTo + : args.persistTo[feature] || args.persistTo.default, + }); const filterState = useFilterState< TItem, TFilterCategoryKey, TPersistenceKeyPrefix - >({ ...args, persistTo: getPersistTo("filters") }); + >({ + ...args, + ...getFeatureArgs("filter"), + }); const sortState = useSortState({ ...args, - persistTo: getPersistTo("sort"), + ...getFeatureArgs("sort"), }); const paginationState = usePaginationState({ ...args, - persistTo: getPersistTo("pagination"), + ...getFeatureArgs("pagination"), }); const expansionState = useExpansionState({ ...args, - persistTo: getPersistTo("expansion"), + ...getFeatureArgs("expansion"), }); const activeRowState = useActiveRowState({ ...args, - persistTo: getPersistTo("activeRow"), + ...getFeatureArgs("activeRow"), }); + // TODO include selectionState here when we move it from lib-ui return { ...args, filterState, diff --git a/client/src/app/hooks/table-controls/utils.ts b/client/src/app/hooks/table-controls/utils.ts index 60b625f506..22cdd0ee26 100644 --- a/client/src/app/hooks/table-controls/utils.ts +++ b/client/src/app/hooks/table-controls/utils.ts @@ -1,7 +1,8 @@ import React from "react"; +import { TableFeature } from "./types"; export const handlePropagatedRowClick = < - E extends React.KeyboardEvent | React.MouseEvent + E extends React.KeyboardEvent | React.MouseEvent, >( event: E | undefined, onRowClick: (event: E) => void @@ -29,3 +30,31 @@ export const handlePropagatedRowClick = < onRowClick(event); } }; + +export const getFeaturesEnabledWithFallbacks = ( + featuresEnabled?: Partial> +) => { + // Most tables have only filtering, sorting and pagination. + const defaultFeaturesEnabled: Record = { + filter: true, + sort: true, + pagination: true, + selection: false, + expansion: false, + activeRow: false, + }; + if (!featuresEnabled) return defaultFeaturesEnabled; + // Omitting a feature from featuresEnabled shouldn't explicitly disable it, so we need to check for undefined. + const getFeatureEnabled = (feature: TableFeature) => + featuresEnabled[feature] !== undefined + ? (featuresEnabled[feature] as boolean) + : defaultFeaturesEnabled[feature]; + return { + filter: getFeatureEnabled("filter"), + sort: getFeatureEnabled("sort"), + pagination: getFeatureEnabled("pagination"), + selection: getFeatureEnabled("selection"), + expansion: getFeatureEnabled("expansion"), + activeRow: getFeatureEnabled("activeRow"), + } satisfies Record; +}; From 4728cbe8466ffa334715a8f38ae0b34dce282d5e Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 17:00:35 -0400 Subject: [PATCH 053/102] Revert "Add featuresEnabled arg" This reverts commit e20e91cc4c079b5b287b30d6cdb93b0a0f5b86e1. Signed-off-by: Mike Turley --- .../active-row/useActiveRowState.ts | 12 +-- .../expansion/useExpansionState.ts | 12 +-- .../filtering/useFilterState.ts | 8 +- .../pagination/usePaginationState.ts | 7 +- .../table-controls/sorting/useSortState.ts | 7 +- client/src/app/hooks/table-controls/types.ts | 79 +++++++------------ .../table-controls/useTableControlProps.ts | 20 +---- .../table-controls/useTableControlState.ts | 32 +++----- client/src/app/hooks/table-controls/utils.ts | 31 +------- 9 files changed, 59 insertions(+), 149 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts index e110bdb2af..7b5db1e8e0 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts @@ -1,5 +1,5 @@ import { parseMaybeNumericString } from "@app/utils/utils"; -import { IFeaturePersistenceArgs } from "../types"; +import { IPersistenceOptions } from "../types"; import { usePersistentState } from "@app/hooks/usePersistentState"; export interface IActiveRowState { @@ -7,17 +7,12 @@ export interface IActiveRowState { setActiveRowId: (id: string | number | null) => void; } -export interface IActiveRowStateArgs { - isEnabled?: boolean; -} - export const useActiveRowState = < TPersistenceKeyPrefix extends string = string, >( - args: IActiveRowStateArgs & - IFeaturePersistenceArgs = {} + args: IPersistenceOptions = {} ): IActiveRowState => { - const { persistTo, persistenceKeyPrefix, isEnabled } = args; + const { persistTo, persistenceKeyPrefix } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 @@ -28,7 +23,6 @@ export const useActiveRowState = < >({ defaultValue: null, persistenceKeyPrefix, - isEnabled, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index 37e5c98f7a..3e0b1761fd 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -1,6 +1,6 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; import { objectKeys } from "@app/utils/utils"; -import { IFeaturePersistenceArgs } from "../types"; +import { IPersistenceOptions } from "../types"; // TExpandedCells maps item[idProperty] values to either: // - The key of an expanded column in that row, if the table is compound-expandable @@ -17,18 +17,13 @@ export interface IExpansionState { ) => void; } -export interface IExpansionStateArgs { - isEnabled?: boolean; -} - export const useExpansionState = < TColumnKey extends string, TPersistenceKeyPrefix extends string = string, >( - args: IExpansionStateArgs & - IFeaturePersistenceArgs = {} + args: IPersistenceOptions = {} ): IExpansionState => { - const { persistTo = "state", persistenceKeyPrefix, isEnabled } = args; + const { persistTo = "state", persistenceKeyPrefix } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 @@ -39,7 +34,6 @@ export const useExpansionState = < >({ defaultValue: {}, persistenceKeyPrefix, - isEnabled, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index ef3911452b..937ef1b68f 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -1,5 +1,5 @@ import { FilterCategory, IFilterValues } from "@app/components/FilterToolbar"; -import { IFeaturePersistenceArgs } from "../types"; +import { IPersistenceOptions } from "../types"; import { usePersistentState } from "@app/hooks/usePersistentState"; import { serializeFilterUrlParams } from "./helpers"; import { deserializeFilterUrlParams } from "./helpers"; @@ -11,7 +11,6 @@ export interface IFilterState { export type IFilterStateArgs = { filterCategories?: FilterCategory[]; - isEnabled?: boolean; }; export const useFilterState = < @@ -20,9 +19,9 @@ export const useFilterState = < TPersistenceKeyPrefix extends string = string, >( args: IFilterStateArgs & - IFeaturePersistenceArgs + IPersistenceOptions ): IFilterState => { - const { persistTo = "state", persistenceKeyPrefix, isEnabled } = args; + const { persistTo = "state", persistenceKeyPrefix } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 @@ -33,7 +32,6 @@ export const useFilterState = < >({ defaultValue: {}, persistenceKeyPrefix, - isEnabled, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 0491e2cd4b..16ca4fc01f 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -1,5 +1,5 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; -import { IFeaturePersistenceArgs } from "../types"; +import { IPersistenceOptions } from "../types"; export interface IActivePagination { pageNumber: number; @@ -13,19 +13,17 @@ export interface IPaginationState extends IActivePagination { export interface IPaginationStateArgs { initialItemsPerPage?: number; - isEnabled?: boolean; } export const usePaginationState = < TPersistenceKeyPrefix extends string = string, >( - args: IPaginationStateArgs & IFeaturePersistenceArgs + args: IPaginationStateArgs & IPersistenceOptions ): IPaginationState => { const { persistTo = "state", persistenceKeyPrefix, initialItemsPerPage = 10, - isEnabled, } = args; const defaultValue: IActivePagination = { @@ -42,7 +40,6 @@ export const usePaginationState = < >({ defaultValue, persistenceKeyPrefix, - isEnabled, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index 578f237161..d456f1fb75 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -1,4 +1,4 @@ -import { IFeaturePersistenceArgs } from ".."; +import { IPersistenceOptions } from ".."; import { usePersistentState } from "@app/hooks/usePersistentState"; export interface IActiveSort { @@ -14,7 +14,6 @@ export interface ISortState { export type ISortStateArgs = { sortableColumns?: TSortableColumnKey[]; initialSort?: IActiveSort | null; - isEnabled?: boolean; }; export const useSortState = < @@ -22,7 +21,7 @@ export const useSortState = < TPersistenceKeyPrefix extends string = string, >( args: ISortStateArgs & - IFeaturePersistenceArgs + IPersistenceOptions ): ISortState => { const { persistTo = "state", @@ -31,7 +30,6 @@ export const useSortState = < initialSort = sortableColumns[0] ? { columnKey: sortableColumns[0], direction: "asc" } : null, - isEnabled, } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. @@ -43,7 +41,6 @@ export const useSortState = < >({ defaultValue: initialSort, persistenceKeyPrefix, - isEnabled, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index aa8d39bbde..1fc18f3adc 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -19,16 +19,8 @@ import { IPaginationPropsArgs, IPaginationState, } from "./pagination"; -import { - IExpansionDerivedStateArgs, - IExpansionState, - IExpansionStateArgs, -} from "./expansion"; -import { - IActiveRowDerivedStateArgs, - IActiveRowState, - IActiveRowStateArgs, -} from "./active-row"; +import { IExpansionDerivedStateArgs, IExpansionState } from "./expansion"; +import { IActiveRowDerivedStateArgs, IActiveRowState } from "./active-row"; import { useTableControlState } from "./useTableControlState"; // Generic type params used here: @@ -45,14 +37,6 @@ import { useTableControlState } from "./useTableControlState"; // This may be resolved in a newer TypeScript version after https://github.com/microsoft/TypeScript/pull/54047 is merged! // See https://github.com/konveyor/tackle2-ui/issues/1456 -export type TableFeature = - | "filter" - | "sort" - | "pagination" - | "selection" - | "expansion" - | "activeRow"; - export type PersistTarget = | "state" | "urlParams" @@ -62,28 +46,13 @@ export type PersistTarget = // Persistence-specific args // - Extra args needed for useTableControlState and each feature-specific use*State hook for persisting state // - Does not require any state or query data in scope -// Common: -export type ICommonPersistenceArgs< - TPersistenceKeyPrefix extends string = string, -> = { - persistenceKeyPrefix?: DisallowCharacters; -}; -// Feature-level: -export type IFeaturePersistenceArgs< - TPersistenceKeyPrefix extends string = string, -> = ICommonPersistenceArgs & { - persistTo?: PersistTarget; -}; -// Table-level: -export type ITablePersistenceArgs< - TPersistenceKeyPrefix extends string = string, -> = ICommonPersistenceArgs & { - persistTo?: - | PersistTarget - | Partial>; -}; +export type IPersistenceOptions = + { + persistTo?: PersistTarget; + persistenceKeyPrefix?: DisallowCharacters; + }; -// Table-level state args +// State args // - Used by useTableControlState // - Does not require any state or query data in scope export type IUseTableControlStateArgs< @@ -92,20 +61,30 @@ export type IUseTableControlStateArgs< TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, -> = Omit, "isEnabled"> & - Omit, "isEnabled"> & - Omit & - Omit & - Omit & - ITablePersistenceArgs & { - featuresEnabled?: Partial>; +> = IFilterStateArgs & + ISortStateArgs & + IPaginationStateArgs & + // There is no IExpansionStateArgs because the only args there are the IPersistenceOptions + // There is no IActiveRowStateArgs because the only args there are the IPersistenceOptions + Omit, "persistTo"> & { + persistTo?: + | PersistTarget + | { + default?: PersistTarget; + filters?: PersistTarget; + sort?: PersistTarget; + pagination?: PersistTarget; + expansion?: PersistTarget; + activeRow?: PersistTarget; + }; columnNames: Record; // An ordered mapping of unique keys to human-readable column name strings + isSelectable?: boolean; hasPagination?: boolean; // TODO disable pagination state stuff if this is falsy expandableVariant?: "single" | "compound" | null; // TODO disable expandable state stuff if this is falsy // TODO do we want to have a featuresEnabled object instead? }; -// Table-level state object +// State object // - Returned by useTableControlState // - Contains persisted state for all features // - Also includes all of useTableControlState's args for convenience, since useTableControlProps requires them along with the state itself @@ -129,7 +108,7 @@ export type ITableControlState< activeRowState: IActiveRowState; }; -// Table-level local derived state args +// Local derived state args // - Used by getLocalTableControlDerivedState (client-side filtering/sorting/pagination) // - getLocalTableControlDerivedState also requires the return values from useTableControlState // - Also used indirectly by the useLocalTableControls shorthand @@ -142,8 +121,8 @@ export type ITableControlLocalDerivedStateArgs< > = ILocalFilterDerivedStateArgs & ILocalSortDerivedStateArgs & ILocalPaginationDerivedStateArgs; -// There is no ILocalExpansionDerivedStateArgs type because expansion derived state is always local and internal to useTableControlProps -// There is no ILocalActiveRowDerivedStateArgs type because activeRow derived state is always local and internal to useTableControlProps +// There is no ILocalExpansionDerivedStateArgs because expansion derived state is always local an internal to useTableControlProps +// There is no ILocalActiveRowDerivedStateArgs because expansion derived state is always local an internal to useTableControlProps // Table-level derived state object // - Used by useTableControlProps diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 5dca77da1e..bd0c05cba5 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -10,10 +10,7 @@ import { getFilterProps } from "./filtering"; import { getSortProps } from "./sorting"; import { getPaginationProps, usePaginationEffects } from "./pagination"; import { getActiveRowDerivedState, useActiveRowEffects } from "./active-row"; -import { - getFeaturesEnabledWithFallbacks, - handlePropagatedRowClick, -} from "./utils"; +import { handlePropagatedRowClick } from "./utils"; import { getExpansionDerivedState } from "./expansion"; export const useTableControlProps = < @@ -49,24 +46,21 @@ export const useTableControlProps = < }, columnNames, sortableColumns = [], + isSelectable = false, expandableVariant = null, hasActionsColumn = false, variant, idProperty, } = args; - const featuresEnabled = getFeaturesEnabledWithFallbacks(args.featuresEnabled); - // TODO make sure we are not passing stuff for any disabled features - const columnKeys = objectKeys(columnNames); // Some table controls rely on extra columns inserted before or after the ones included in columnNames. // We need to account for those when dealing with props based on column index and colSpan. let numColumnsBeforeData = 0; let numColumnsAfterData = 0; - if (featuresEnabled.selection) numColumnsBeforeData++; - if (featuresEnabled.expansion && expandableVariant === "single") - numColumnsBeforeData++; + if (isSelectable) numColumnsBeforeData++; + if (expandableVariant === "single") numColumnsBeforeData++; if (hasActionsColumn) numColumnsAfterData++; const numRenderedColumns = forceNumRenderedColumns || @@ -126,8 +120,6 @@ export const useTableControlProps = < children: columnNames[columnKey], }); - // TODO move this into a getActiveRowProps helper? - // TODO have the consumer always call getTrProps and only include clickable stuff if the feature is enabled const getClickableTrProps = ({ onRowClick, item, @@ -157,7 +149,6 @@ export const useTableControlProps = < dataLabel: columnNames[columnKey], }); - // TODO move this into a getSelectionProps helper somehow? const getSelectCheckboxTdProps = ({ item, rowIndex, @@ -174,7 +165,6 @@ export const useTableControlProps = < }, }); - // TODO move this into a getExpansionProps helper somehow? const getSingleExpandTdProps = ({ item, rowIndex, @@ -194,7 +184,6 @@ export const useTableControlProps = < }, }); - // TODO move this into a getExpansionProps helper somehow? const getCompoundExpandTdProps = ({ item, rowIndex, @@ -219,7 +208,6 @@ export const useTableControlProps = < }, }); - // TODO move this into a getExpansionProps helper somehow? const getExpandedContentTdProps = ({ item, }: { diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index e5db2925c0..be2db9ea81 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -1,14 +1,13 @@ import { ITableControlState, IUseTableControlStateArgs, - TableFeature, + PersistTarget, } from "./types"; import { useFilterState } from "./filtering"; import { useSortState } from "./sorting"; import { usePaginationState } from "./pagination"; import { useActiveRowState } from "./active-row"; import { useExpansionState } from "./expansion"; -import { getFeaturesEnabledWithFallbacks } from "./utils"; export const useTableControlState = < TItem, @@ -31,41 +30,34 @@ export const useTableControlState = < TFilterCategoryKey, TPersistenceKeyPrefix > => { - const featuresEnabled = getFeaturesEnabledWithFallbacks(args.featuresEnabled); - - const getFeatureArgs = (feature: TableFeature) => ({ - isEnabled: featuresEnabled[feature], - persistTo: - !args.persistTo || typeof args.persistTo === "string" - ? args.persistTo - : args.persistTo[feature] || args.persistTo.default, - }); + const getPersistTo = ( + feature: "filters" | "sort" | "pagination" | "expansion" | "activeRow" + ): PersistTarget | undefined => + !args.persistTo || typeof args.persistTo === "string" + ? args.persistTo + : args.persistTo[feature] || args.persistTo.default; const filterState = useFilterState< TItem, TFilterCategoryKey, TPersistenceKeyPrefix - >({ - ...args, - ...getFeatureArgs("filter"), - }); + >({ ...args, persistTo: getPersistTo("filters") }); const sortState = useSortState({ ...args, - ...getFeatureArgs("sort"), + persistTo: getPersistTo("sort"), }); const paginationState = usePaginationState({ ...args, - ...getFeatureArgs("pagination"), + persistTo: getPersistTo("pagination"), }); const expansionState = useExpansionState({ ...args, - ...getFeatureArgs("expansion"), + persistTo: getPersistTo("expansion"), }); const activeRowState = useActiveRowState({ ...args, - ...getFeatureArgs("activeRow"), + persistTo: getPersistTo("activeRow"), }); - // TODO include selectionState here when we move it from lib-ui return { ...args, filterState, diff --git a/client/src/app/hooks/table-controls/utils.ts b/client/src/app/hooks/table-controls/utils.ts index 22cdd0ee26..60b625f506 100644 --- a/client/src/app/hooks/table-controls/utils.ts +++ b/client/src/app/hooks/table-controls/utils.ts @@ -1,8 +1,7 @@ import React from "react"; -import { TableFeature } from "./types"; export const handlePropagatedRowClick = < - E extends React.KeyboardEvent | React.MouseEvent, + E extends React.KeyboardEvent | React.MouseEvent >( event: E | undefined, onRowClick: (event: E) => void @@ -30,31 +29,3 @@ export const handlePropagatedRowClick = < onRowClick(event); } }; - -export const getFeaturesEnabledWithFallbacks = ( - featuresEnabled?: Partial> -) => { - // Most tables have only filtering, sorting and pagination. - const defaultFeaturesEnabled: Record = { - filter: true, - sort: true, - pagination: true, - selection: false, - expansion: false, - activeRow: false, - }; - if (!featuresEnabled) return defaultFeaturesEnabled; - // Omitting a feature from featuresEnabled shouldn't explicitly disable it, so we need to check for undefined. - const getFeatureEnabled = (feature: TableFeature) => - featuresEnabled[feature] !== undefined - ? (featuresEnabled[feature] as boolean) - : defaultFeaturesEnabled[feature]; - return { - filter: getFeatureEnabled("filter"), - sort: getFeatureEnabled("sort"), - pagination: getFeatureEnabled("pagination"), - selection: getFeatureEnabled("selection"), - expansion: getFeatureEnabled("expansion"), - activeRow: getFeatureEnabled("activeRow"), - } satisfies Record; -}; From b31155eb6eb695bd57378b27698730600a1ed708 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 17:01:00 -0400 Subject: [PATCH 054/102] Add TODOs Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/useTableControlProps.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index bd0c05cba5..02f42d8769 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -120,6 +120,8 @@ export const useTableControlProps = < children: columnNames[columnKey], }); + // TODO move this into a getActiveRowProps helper? + // TODO have the consumer always call getTrProps and only include clickable stuff if the feature is enabled const getClickableTrProps = ({ onRowClick, item, @@ -149,6 +151,7 @@ export const useTableControlProps = < dataLabel: columnNames[columnKey], }); + // TODO move this into a getSelectionProps helper somehow? const getSelectCheckboxTdProps = ({ item, rowIndex, @@ -165,6 +168,7 @@ export const useTableControlProps = < }, }); + // TODO move this into a getExpansionProps helper somehow? const getSingleExpandTdProps = ({ item, rowIndex, @@ -184,6 +188,7 @@ export const useTableControlProps = < }, }); + // TODO move this into a getExpansionProps helper somehow? const getCompoundExpandTdProps = ({ item, rowIndex, @@ -208,6 +213,7 @@ export const useTableControlProps = < }, }); + // TODO move this into a getExpansionProps helper somehow? const getExpandedContentTdProps = ({ item, }: { From 5aa48b0c75096510b919d5aae7f43470714814e7 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 17:19:32 -0400 Subject: [PATCH 055/102] Improve persistence arg types Signed-off-by: Mike Turley --- .../active-row/useActiveRowState.ts | 4 +- .../expansion/useExpansionState.ts | 4 +- .../filtering/useFilterState.ts | 4 +- .../pagination/usePaginationState.ts | 4 +- .../table-controls/sorting/useSortState.ts | 4 +- client/src/app/hooks/table-controls/types.ts | 45 ++++++++++++------- .../table-controls/useTableControlState.ts | 7 ++- 7 files changed, 42 insertions(+), 30 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts index 7b5db1e8e0..b15423703e 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts @@ -1,5 +1,5 @@ import { parseMaybeNumericString } from "@app/utils/utils"; -import { IPersistenceOptions } from "../types"; +import { IFeaturePersistenceArgs } from "../types"; import { usePersistentState } from "@app/hooks/usePersistentState"; export interface IActiveRowState { @@ -10,7 +10,7 @@ export interface IActiveRowState { export const useActiveRowState = < TPersistenceKeyPrefix extends string = string, >( - args: IPersistenceOptions = {} + args: IFeaturePersistenceArgs = {} ): IActiveRowState => { const { persistTo, persistenceKeyPrefix } = args; diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index 3e0b1761fd..bdc755d73b 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -1,6 +1,6 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; import { objectKeys } from "@app/utils/utils"; -import { IPersistenceOptions } from "../types"; +import { IFeaturePersistenceArgs } from "../types"; // TExpandedCells maps item[idProperty] values to either: // - The key of an expanded column in that row, if the table is compound-expandable @@ -21,7 +21,7 @@ export const useExpansionState = < TColumnKey extends string, TPersistenceKeyPrefix extends string = string, >( - args: IPersistenceOptions = {} + args: IFeaturePersistenceArgs = {} ): IExpansionState => { const { persistTo = "state", persistenceKeyPrefix } = args; diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index 937ef1b68f..824a04c6b1 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -1,5 +1,5 @@ import { FilterCategory, IFilterValues } from "@app/components/FilterToolbar"; -import { IPersistenceOptions } from "../types"; +import { IFeaturePersistenceArgs } from "../types"; import { usePersistentState } from "@app/hooks/usePersistentState"; import { serializeFilterUrlParams } from "./helpers"; import { deserializeFilterUrlParams } from "./helpers"; @@ -19,7 +19,7 @@ export const useFilterState = < TPersistenceKeyPrefix extends string = string, >( args: IFilterStateArgs & - IPersistenceOptions + IFeaturePersistenceArgs ): IFilterState => { const { persistTo = "state", persistenceKeyPrefix } = args; diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 16ca4fc01f..3742ab8293 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -1,5 +1,5 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; -import { IPersistenceOptions } from "../types"; +import { IFeaturePersistenceArgs } from "../types"; export interface IActivePagination { pageNumber: number; @@ -18,7 +18,7 @@ export interface IPaginationStateArgs { export const usePaginationState = < TPersistenceKeyPrefix extends string = string, >( - args: IPaginationStateArgs & IPersistenceOptions + args: IPaginationStateArgs & IFeaturePersistenceArgs ): IPaginationState => { const { persistTo = "state", diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index d456f1fb75..4fc1d975c1 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -1,4 +1,4 @@ -import { IPersistenceOptions } from ".."; +import { IFeaturePersistenceArgs } from ".."; import { usePersistentState } from "@app/hooks/usePersistentState"; export interface IActiveSort { @@ -21,7 +21,7 @@ export const useSortState = < TPersistenceKeyPrefix extends string = string, >( args: ISortStateArgs & - IPersistenceOptions + IFeaturePersistenceArgs ): ISortState => { const { persistTo = "state", diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 1fc18f3adc..57a12ecd97 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -37,6 +37,14 @@ import { useTableControlState } from "./useTableControlState"; // This may be resolved in a newer TypeScript version after https://github.com/microsoft/TypeScript/pull/54047 is merged! // See https://github.com/konveyor/tackle2-ui/issues/1456 +export type TableFeature = + | "filter" + | "sort" + | "pagination" + | "selection" + | "expansion" + | "activeRow"; + export type PersistTarget = | "state" | "urlParams" @@ -46,11 +54,26 @@ export type PersistTarget = // Persistence-specific args // - Extra args needed for useTableControlState and each feature-specific use*State hook for persisting state // - Does not require any state or query data in scope -export type IPersistenceOptions = - { - persistTo?: PersistTarget; - persistenceKeyPrefix?: DisallowCharacters; - }; +// Common +export type ICommonPersistenceArgs< + TPersistenceKeyPrefix extends string = string, +> = { + persistenceKeyPrefix?: DisallowCharacters; +}; +// Feature-level +export type IFeaturePersistenceArgs< + TPersistenceKeyPrefix extends string = string, +> = ICommonPersistenceArgs & { + persistTo?: PersistTarget; +}; +// Table-level +export type ITablePersistenceArgs< + TPersistenceKeyPrefix extends string = string, +> = ICommonPersistenceArgs & { + persistTo?: + | PersistTarget + | Partial>; +}; // State args // - Used by useTableControlState @@ -66,17 +89,7 @@ export type IUseTableControlStateArgs< IPaginationStateArgs & // There is no IExpansionStateArgs because the only args there are the IPersistenceOptions // There is no IActiveRowStateArgs because the only args there are the IPersistenceOptions - Omit, "persistTo"> & { - persistTo?: - | PersistTarget - | { - default?: PersistTarget; - filters?: PersistTarget; - sort?: PersistTarget; - pagination?: PersistTarget; - expansion?: PersistTarget; - activeRow?: PersistTarget; - }; + ITablePersistenceArgs & { columnNames: Record; // An ordered mapping of unique keys to human-readable column name strings isSelectable?: boolean; hasPagination?: boolean; // TODO disable pagination state stuff if this is falsy diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index be2db9ea81..7f873571a1 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -2,6 +2,7 @@ import { ITableControlState, IUseTableControlStateArgs, PersistTarget, + TableFeature, } from "./types"; import { useFilterState } from "./filtering"; import { useSortState } from "./sorting"; @@ -30,9 +31,7 @@ export const useTableControlState = < TFilterCategoryKey, TPersistenceKeyPrefix > => { - const getPersistTo = ( - feature: "filters" | "sort" | "pagination" | "expansion" | "activeRow" - ): PersistTarget | undefined => + const getPersistTo = (feature: TableFeature): PersistTarget | undefined => !args.persistTo || typeof args.persistTo === "string" ? args.persistTo : args.persistTo[feature] || args.persistTo.default; @@ -41,7 +40,7 @@ export const useTableControlState = < TItem, TFilterCategoryKey, TPersistenceKeyPrefix - >({ ...args, persistTo: getPersistTo("filters") }); + >({ ...args, persistTo: getPersistTo("filter") }); const sortState = useSortState({ ...args, persistTo: getPersistTo("sort"), From 5dfbd1ec1a0efb68dfccc57396c0efe0704a5929 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 17:51:43 -0400 Subject: [PATCH 056/102] Use discriminated unions for required args per feature Signed-off-by: Mike Turley --- .../TableRowContentWithControls.tsx | 18 ++++++----- .../components/answer-table/answer-table.tsx | 2 -- .../active-row/useActiveRowState.ts | 10 ++++-- .../expansion/useExpansionState.ts | 18 +++++++++-- .../filtering/useFilterState.ts | 18 +++++++++-- .../getLocalTableControlDerivedState.ts | 4 +-- .../pagination/usePaginationState.ts | 3 ++ .../table-controls/sorting/useSortState.ts | 14 +++++++-- client/src/app/hooks/table-controls/types.ts | 31 +++++++++++-------- .../table-controls/useTableControlProps.ts | 10 +++--- .../applications-table-analyze.tsx | 2 +- .../applications-table-assessment.tsx | 2 +- .../app/pages/archetypes/archetypes-page.tsx | 2 +- .../assessment-settings-page.tsx | 5 ++- .../components/questionnaires-table.tsx | 2 +- .../controls/stakeholders/stakeholders.tsx | 2 +- .../src/app/pages/external/jira/trackers.tsx | 2 +- client/src/app/pages/issues/issues-table.tsx | 6 +++- .../components/manage-applications-form.tsx | 7 +++-- .../components/stakeholders-table.tsx | 4 +-- .../components/wave-applications-table.tsx | 2 +- .../components/wave-status-table.tsx | 3 +- .../pages/migration-waves/migration-waves.tsx | 4 +-- 23 files changed, 114 insertions(+), 57 deletions(-) diff --git a/client/src/app/components/TableControls/TableRowContentWithControls.tsx b/client/src/app/components/TableControls/TableRowContentWithControls.tsx index f34f132efd..dc0e4e1fbc 100644 --- a/client/src/app/components/TableControls/TableRowContentWithControls.tsx +++ b/client/src/app/components/TableControls/TableRowContentWithControls.tsx @@ -5,10 +5,11 @@ import { useTableControlProps } from "@app/hooks/table-controls"; export interface ITableRowContentWithControlsProps< TItem, TColumnKey extends string, - TSortableColumnKey extends TColumnKey + TSortableColumnKey extends TColumnKey, > { - expandableVariant?: "single" | "compound" | null; - isSelectable?: boolean; + isExpansionEnabled?: boolean; + expandableVariant?: "single" | "compound"; + isSelectionEnabled?: boolean; propHelpers: ReturnType< typeof useTableControlProps >["propHelpers"]; @@ -20,10 +21,11 @@ export interface ITableRowContentWithControlsProps< export const TableRowContentWithControls = < TItem, TColumnKey extends string, - TSortableColumnKey extends TColumnKey + TSortableColumnKey extends TColumnKey, >({ - expandableVariant = null, - isSelectable = false, + isExpansionEnabled = false, + expandableVariant, + isSelectionEnabled = false, propHelpers: { getSingleExpandTdProps, getSelectCheckboxTdProps }, item, rowIndex, @@ -32,10 +34,10 @@ export const TableRowContentWithControls = < ITableRowContentWithControlsProps >) => ( <> - {expandableVariant === "single" ? ( + {isExpansionEnabled && expandableVariant === "single" ? ( ) : null} - {isSelectable ? ( + {isSelectionEnabled ? ( ) : null} {children} diff --git a/client/src/app/components/answer-table/answer-table.tsx b/client/src/app/components/answer-table/answer-table.tsx index c5d5c76485..82cbc7895f 100644 --- a/client/src/app/components/answer-table/answer-table.tsx +++ b/client/src/app/components/answer-table/answer-table.tsx @@ -37,8 +37,6 @@ const AnswerTable: React.FC = ({ choice: "Answer choice", weight: "Weight", }, - hasActionsColumn: false, - hasPagination: false, variant: "compact", }); const { diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts index b15423703e..56c6feac29 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts @@ -7,12 +7,17 @@ export interface IActiveRowState { setActiveRowId: (id: string | number | null) => void; } +export interface IActiveRowStateArgs { + isActiveRowEnabled?: boolean; +} + export const useActiveRowState = < TPersistenceKeyPrefix extends string = string, >( - args: IFeaturePersistenceArgs = {} + args: IActiveRowStateArgs & + IFeaturePersistenceArgs = {} ): IActiveRowState => { - const { persistTo, persistenceKeyPrefix } = args; + const { isActiveRowEnabled, persistTo, persistenceKeyPrefix } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 @@ -21,6 +26,7 @@ export const useActiveRowState = < TPersistenceKeyPrefix, "activeRow" >({ + isEnabled: isActiveRowEnabled, defaultValue: null, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index bdc755d73b..5c35c68619 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -17,13 +17,26 @@ export interface IExpansionState { ) => void; } +export type IRequiredExpansionStateArgs = { + expandableVariant: "single" | "compound"; +}; + +export type IExpansionStateArgs = + | ({ isExpansionEnabled: true } & IRequiredExpansionStateArgs) + | ({ isExpansionEnabled?: false } & Partial); + export const useExpansionState = < TColumnKey extends string, TPersistenceKeyPrefix extends string = string, >( - args: IFeaturePersistenceArgs = {} + args: IExpansionStateArgs & + IFeaturePersistenceArgs = {} ): IExpansionState => { - const { persistTo = "state", persistenceKeyPrefix } = args; + const { + isExpansionEnabled, + persistTo = "state", + persistenceKeyPrefix, + } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 @@ -39,6 +52,7 @@ export const useExpansionState = < // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. ...(persistTo === "urlParams" ? { + isEnabled: isExpansionEnabled, persistTo, keys: ["expandedCells"], serialize: (expandedCellsObj) => { diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index 824a04c6b1..2165bb0f9d 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -9,10 +9,21 @@ export interface IFilterState { setFilterValues: (values: IFilterValues) => void; } -export type IFilterStateArgs = { - filterCategories?: FilterCategory[]; +export type IRequiredFilterStateArgs< + TItem, + TFilterCategoryKey extends string, +> = { + filterCategories: FilterCategory[]; }; +export type IFilterStateArgs = + | ({ + isFilterEnabled: true; + } & IRequiredFilterStateArgs) + | ({ isFilterEnabled?: false } & Partial< + IRequiredFilterStateArgs + >); + export const useFilterState = < TItem, TFilterCategoryKey extends string, @@ -21,7 +32,7 @@ export const useFilterState = < args: IFilterStateArgs & IFeaturePersistenceArgs ): IFilterState => { - const { persistTo = "state", persistenceKeyPrefix } = args; + const { isFilterEnabled, persistTo = "state", persistenceKeyPrefix } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 @@ -30,6 +41,7 @@ export const useFilterState = < TPersistenceKeyPrefix, "filters" >({ + isEnabled: isFilterEnabled, defaultValue: {}, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index a5a7e1efa3..097c95c0aa 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -28,7 +28,7 @@ export const getLocalTableControlDerivedState = < TPersistenceKeyPrefix > ): ITableControlDerivedState => { - const { items, hasPagination = true } = args; + const { items, isPaginationEnabled = true } = args; const { filteredItems } = getLocalFilterDerivedState({ ...args, items, @@ -43,6 +43,6 @@ export const getLocalTableControlDerivedState = < }); return { totalItemCount: items.length, - currentPageItems: hasPagination ? currentPageItems : sortedItems, + currentPageItems: isPaginationEnabled ? currentPageItems : sortedItems, }; }; diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 3742ab8293..91161fd3ac 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -12,6 +12,7 @@ export interface IPaginationState extends IActivePagination { } export interface IPaginationStateArgs { + isPaginationEnabled?: boolean; initialItemsPerPage?: number; } @@ -21,6 +22,7 @@ export const usePaginationState = < args: IPaginationStateArgs & IFeaturePersistenceArgs ): IPaginationState => { const { + isPaginationEnabled, persistTo = "state", persistenceKeyPrefix, initialItemsPerPage = 10, @@ -38,6 +40,7 @@ export const usePaginationState = < TPersistenceKeyPrefix, "pageNumber" | "itemsPerPage" >({ + isEnabled: isPaginationEnabled, defaultValue, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index 4fc1d975c1..2048a14a7b 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -11,11 +11,19 @@ export interface ISortState { setActiveSort: (sort: IActiveSort) => void; } -export type ISortStateArgs = { - sortableColumns?: TSortableColumnKey[]; +export type IRequiredSortStateArgs = { + sortableColumns: TSortableColumnKey[]; initialSort?: IActiveSort | null; }; +export type ISortStateArgs = + | ({ + isSortEnabled: true; + } & IRequiredSortStateArgs) + | ({ isSortEnabled?: false } & Partial< + IRequiredSortStateArgs + >); + export const useSortState = < TSortableColumnKey extends string, TPersistenceKeyPrefix extends string = string, @@ -24,6 +32,7 @@ export const useSortState = < IFeaturePersistenceArgs ): ISortState => { const { + isSortEnabled, persistTo = "state", persistenceKeyPrefix, sortableColumns = [], @@ -39,6 +48,7 @@ export const useSortState = < TPersistenceKeyPrefix, "sortColumn" | "sortDirection" >({ + isEnabled: isSortEnabled, defaultValue: initialSort, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 57a12ecd97..991f2a2312 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -19,8 +19,16 @@ import { IPaginationPropsArgs, IPaginationState, } from "./pagination"; -import { IExpansionDerivedStateArgs, IExpansionState } from "./expansion"; -import { IActiveRowDerivedStateArgs, IActiveRowState } from "./active-row"; +import { + IExpansionDerivedStateArgs, + IExpansionState, + IExpansionStateArgs, +} from "./expansion"; +import { + IActiveRowDerivedStateArgs, + IActiveRowState, + IActiveRowStateArgs, +} from "./active-row"; import { useTableControlState } from "./useTableControlState"; // Generic type params used here: @@ -84,18 +92,15 @@ export type IUseTableControlStateArgs< TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, -> = IFilterStateArgs & +> = { + columnNames: Record; // An ordered mapping of unique keys to human-readable column name strings +} & IFilterStateArgs & ISortStateArgs & - IPaginationStateArgs & - // There is no IExpansionStateArgs because the only args there are the IPersistenceOptions - // There is no IActiveRowStateArgs because the only args there are the IPersistenceOptions - ITablePersistenceArgs & { - columnNames: Record; // An ordered mapping of unique keys to human-readable column name strings - isSelectable?: boolean; - hasPagination?: boolean; // TODO disable pagination state stuff if this is falsy - expandableVariant?: "single" | "compound" | null; // TODO disable expandable state stuff if this is falsy - // TODO do we want to have a featuresEnabled object instead? - }; + IPaginationStateArgs & { + isSelectionEnabled?: boolean; // TODO move this into useSelectionState when we move it from lib-ui + } & IExpansionStateArgs & + IActiveRowStateArgs & + ITablePersistenceArgs; // State object // - Returned by useTableControlState diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 02f42d8769..b872cb0ef9 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -46,8 +46,9 @@ export const useTableControlProps = < }, columnNames, sortableColumns = [], - isSelectable = false, - expandableVariant = null, + isSelectionEnabled = false, + isExpansionEnabled = false, + expandableVariant, hasActionsColumn = false, variant, idProperty, @@ -59,8 +60,9 @@ export const useTableControlProps = < // We need to account for those when dealing with props based on column index and colSpan. let numColumnsBeforeData = 0; let numColumnsAfterData = 0; - if (isSelectable) numColumnsBeforeData++; - if (expandableVariant === "single") numColumnsBeforeData++; + if (isSelectionEnabled) numColumnsBeforeData++; + if (isExpansionEnabled && expandableVariant === "single") + numColumnsBeforeData++; if (hasActionsColumn) numColumnsAfterData++; const numRenderedColumns = forceNumRenderedColumns || diff --git a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx index ab51251729..9db1edbe8c 100644 --- a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx +++ b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx @@ -324,7 +324,7 @@ export const ApplicationsTableAnalyze: React.FC = () => { ], initialItemsPerPage: 10, hasActionsColumn: true, - isSelectable: true, + isSelectionEnabled: true, }); const { diff --git a/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx b/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx index 339d5a8ce9..d488ae076c 100644 --- a/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx +++ b/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx @@ -371,7 +371,7 @@ export const ApplicationsTable: React.FC = () => { ], initialItemsPerPage: 10, hasActionsColumn: true, - isSelectable: true, + isSelectionEnabled: true, }); const queryClient = useQueryClient(); diff --git a/client/src/app/pages/archetypes/archetypes-page.tsx b/client/src/app/pages/archetypes/archetypes-page.tsx index 3598183ab0..86cb30b97f 100644 --- a/client/src/app/pages/archetypes/archetypes-page.tsx +++ b/client/src/app/pages/archetypes/archetypes-page.tsx @@ -136,7 +136,7 @@ const Archetypes: React.FC = () => { }), initialSort: { columnKey: "name", direction: "asc" }, - hasPagination: true, + isPaginationEnabled: true, }); const { currentPageItems, diff --git a/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx b/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx index 2ceb25cdac..e5c6d02485 100644 --- a/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx +++ b/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx @@ -113,8 +113,7 @@ const AssessmentSettings: React.FC = () => { rating: "Rating", createTime: "Date imported", }, - isSelectable: false, - expandableVariant: null, + isSelectionEnabled: false, hasActionsColumn: true, filterCategories: [ { @@ -136,7 +135,7 @@ const AssessmentSettings: React.FC = () => { createTime: questionnaire.createTime || "", }), initialSort: { columnKey: "name", direction: "asc" }, - hasPagination: true, + isPaginationEnabled: true, isLoading: isFetching, }); const { diff --git a/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx b/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx index f5848175a2..fc1e1aa18d 100644 --- a/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx +++ b/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx @@ -41,7 +41,7 @@ const QuestionnairesTable: React.FC = ({ questionnaires: tableName, }, hasActionsColumn: false, - hasPagination: false, + isPaginationEnabled: false, variant: "compact", }); diff --git a/client/src/app/pages/controls/stakeholders/stakeholders.tsx b/client/src/app/pages/controls/stakeholders/stakeholders.tsx index 4143eb1fd9..ce9a4e8e6e 100644 --- a/client/src/app/pages/controls/stakeholders/stakeholders.tsx +++ b/client/src/app/pages/controls/stakeholders/stakeholders.tsx @@ -165,7 +165,7 @@ export const Stakeholders: React.FC = () => { jobFunction: item.jobFunction?.name || "", }), initialSort: { columnKey: "name", direction: "asc" }, - hasPagination: true, + isPaginationEnabled: true, isLoading: isFetching, }); diff --git a/client/src/app/pages/external/jira/trackers.tsx b/client/src/app/pages/external/jira/trackers.tsx index cdde160ab2..aafdf60bcf 100644 --- a/client/src/app/pages/external/jira/trackers.tsx +++ b/client/src/app/pages/external/jira/trackers.tsx @@ -131,7 +131,7 @@ export const JiraTrackers: React.FC = () => { url: tracker.url || "", }), sortableColumns: ["name", "url"], - hasPagination: true, + isPaginationEnabled: true, isLoading: isFetching, }); const { diff --git a/client/src/app/pages/issues/issues-table.tsx b/client/src/app/pages/issues/issues-table.tsx index 5de31d8fb6..75b30575af 100644 --- a/client/src/app/pages/issues/issues-table.tsx +++ b/client/src/app/pages/issues/issues-table.tsx @@ -107,6 +107,10 @@ export const IssuesTable: React.FC = ({ mode }) => { affected: mode === "singleApp" ? "Affected files" : "Affected applications", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + isExpansionEnabled: true, sortableColumns: ["description", "category", "effort", "affected"], initialSort: { columnKey: "description", direction: "asc" }, filterCategories: [ @@ -165,6 +169,7 @@ export const IssuesTable: React.FC = ({ mode }) => { // }, ], initialItemsPerPage: 10, + expandableVariant: "single", }); const hubRequestParams = getHubRequestParams({ @@ -215,7 +220,6 @@ export const IssuesTable: React.FC = ({ mode }) => { currentPageItems: currentPageReports, totalItemCount: totalReportCount, isLoading, - expandableVariant: "single", // TODO FIXME - we don't need selectionState but it's required by this hook? selectionState: useSelectionState({ items: currentPageReports, diff --git a/client/src/app/pages/migration-waves/components/manage-applications-form.tsx b/client/src/app/pages/migration-waves/components/manage-applications-form.tsx index a1a68587fc..4c49e9cc3a 100644 --- a/client/src/app/pages/migration-waves/components/manage-applications-form.tsx +++ b/client/src/app/pages/migration-waves/components/manage-applications-form.tsx @@ -102,14 +102,15 @@ export const ManageApplicationsForm: React.FC = ({ const tableControls = useLocalTableControls({ idProperty: "name", items: availableApplications, - initialSelected: assignedApplications, columnNames: { name: "Application Name", description: "Description", businessService: "Business service", owner: "Owner", }, - isSelectable: true, + isSelectionEnabled: true, + initialSelected: assignedApplications, + isExpansionEnabled: true, expandableVariant: "compound", hasActionsColumn: true, filterCategories: [ @@ -169,7 +170,7 @@ export const ManageApplicationsForm: React.FC = ({ owner: application.owner?.name || "", }), initialSort: { columnKey: "name", direction: "asc" }, - hasPagination: true, + isPaginationEnabled: true, }); const { currentPageItems, diff --git a/client/src/app/pages/migration-waves/components/stakeholders-table.tsx b/client/src/app/pages/migration-waves/components/stakeholders-table.tsx index 3d76f6243c..0337fc1157 100644 --- a/client/src/app/pages/migration-waves/components/stakeholders-table.tsx +++ b/client/src/app/pages/migration-waves/components/stakeholders-table.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { WaveWithStatus, Role } from "@app/api/models"; +import { WaveWithStatus } from "@app/api/models"; import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core"; import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; import { useLocalTableControls } from "@app/hooks/table-controls"; @@ -35,7 +35,7 @@ export const WaveStakeholdersTable: React.FC = ({ email: stakeholder.email, }), sortableColumns: ["name", "jobFunction", "role", "email"], - hasPagination: true, + isPaginationEnabled: true, variant: "compact", }); const { diff --git a/client/src/app/pages/migration-waves/components/wave-applications-table.tsx b/client/src/app/pages/migration-waves/components/wave-applications-table.tsx index 094d26ca82..161ac0cda4 100644 --- a/client/src/app/pages/migration-waves/components/wave-applications-table.tsx +++ b/client/src/app/pages/migration-waves/components/wave-applications-table.tsx @@ -42,7 +42,7 @@ export const WaveApplicationsTable: React.FC = ({ owner: app.owner?.name || "", }), sortableColumns: ["appName", "businessService", "owner"], - hasPagination: true, + isPaginationEnabled: true, variant: "compact", }); const { diff --git a/client/src/app/pages/migration-waves/components/wave-status-table.tsx b/client/src/app/pages/migration-waves/components/wave-status-table.tsx index 256f2e715e..7649d3cdc5 100644 --- a/client/src/app/pages/migration-waves/components/wave-status-table.tsx +++ b/client/src/app/pages/migration-waves/components/wave-status-table.tsx @@ -54,13 +54,14 @@ export const WaveStatusTable: React.FC = ({ issue: "Issue", }, hasActionsColumn: true, + isSortEnabled: true, getSortValues: (app) => ({ appName: app.name || "", status: app.comments || "", issue: "", }), sortableColumns: ["appName", "status", "issue"], - hasPagination: true, + isPaginationEnabled: true, variant: "compact", }); const { diff --git a/client/src/app/pages/migration-waves/migration-waves.tsx b/client/src/app/pages/migration-waves/migration-waves.tsx index 7370057484..c213dcba6f 100644 --- a/client/src/app/pages/migration-waves/migration-waves.tsx +++ b/client/src/app/pages/migration-waves/migration-waves.tsx @@ -188,7 +188,7 @@ export const MigrationWaves: React.FC = () => { stakeholders: "Stakeholders", status: "Status", }, - isSelectable: true, + isSelectionEnabled: true, expandableVariant: "compound", hasActionsColumn: true, filterCategories: [ @@ -212,7 +212,7 @@ export const MigrationWaves: React.FC = () => { endDate: migrationWave.endDate || "", }), initialSort: { columnKey: "startDate", direction: "asc" }, - hasPagination: true, + isPaginationEnabled: true, isLoading: isFetching, }); const { From 59357d9d7a333adb8083c8388cced83de391b0bb Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 11 Oct 2023 17:55:38 -0400 Subject: [PATCH 057/102] Restore accidentally reverted changes to types Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/types.ts | 30 +++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 991f2a2312..e4c912cd85 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -29,7 +29,6 @@ import { IActiveRowState, IActiveRowStateArgs, } from "./active-row"; -import { useTableControlState } from "./useTableControlState"; // Generic type params used here: // TItem - The actual API objects represented by rows in the table. Can be any object. @@ -83,7 +82,7 @@ export type ITablePersistenceArgs< | Partial>; }; -// State args +// Table-level state args // - Used by useTableControlState // - Does not require any state or query data in scope export type IUseTableControlStateArgs< @@ -102,7 +101,7 @@ export type IUseTableControlStateArgs< IActiveRowStateArgs & ITablePersistenceArgs; -// State object +// Table-level state object // - Returned by useTableControlState // - Contains persisted state for all features // - Also includes all of useTableControlState's args for convenience, since useTableControlProps requires them along with the state itself @@ -126,7 +125,7 @@ export type ITableControlState< activeRowState: IActiveRowState; }; -// Local derived state args +// Table-level local derived state args // - Used by getLocalTableControlDerivedState (client-side filtering/sorting/pagination) // - getLocalTableControlDerivedState also requires the return values from useTableControlState // - Also used indirectly by the useLocalTableControls shorthand @@ -139,8 +138,8 @@ export type ITableControlLocalDerivedStateArgs< > = ILocalFilterDerivedStateArgs & ILocalSortDerivedStateArgs & ILocalPaginationDerivedStateArgs; -// There is no ILocalExpansionDerivedStateArgs because expansion derived state is always local an internal to useTableControlProps -// There is no ILocalActiveRowDerivedStateArgs because expansion derived state is always local an internal to useTableControlProps +// There is no ILocalExpansionDerivedStateArgs type because expansion derived state is always local and internal to useTableControlProps +// There is no ILocalActiveRowDerivedStateArgs type because expansion derived state is always local and internal to useTableControlProps // Table-level derived state object // - Used by useTableControlProps @@ -212,17 +211,14 @@ export type IUseLocalTableControlsArgs< TSortableColumnKey, TFilterCategoryKey >, - | keyof (ITableControlDerivedState & - // TODO make this an explicit type - ReturnType< - typeof useTableControlState< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - > - >) + | keyof ITableControlDerivedState + | keyof ITableControlState< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + > | "selectionState" // TODO this won't be included here when selection is part of useTableControlState > & Pick, "initialSelected" | "isItemSelectable">; // TODO this won't be included here when selection is part of useTableControlState From 9d6ab85e776c1f3104073194e1713eef12f9fe75 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 12 Oct 2023 14:40:14 -0400 Subject: [PATCH 058/102] Enforce both required and disallowed args with discriminated union Signed-off-by: Mike Turley --- .../questions-table/questions-table.tsx | 1 + client/src/app/hooks/table-controls/DOCS.md | 8 +++++++ .../active-row/useActiveRowState.ts | 4 +--- .../expansion/useExpansionState.ts | 6 ++--- .../filtering/getLocalFilterDerivedState.ts | 4 ++-- .../filtering/useFilterState.ts | 8 +++---- .../getLocalTableControlDerivedState.ts | 10 ++++---- .../pagination/usePaginationState.ts | 12 ++++++---- .../table-controls/sorting/useSortState.ts | 24 +++++++------------ client/src/app/hooks/table-controls/types.ts | 1 + .../table-controls/useTableControlProps.ts | 11 +++++---- 11 files changed, 47 insertions(+), 42 deletions(-) diff --git a/client/src/app/components/questions-table/questions-table.tsx b/client/src/app/components/questions-table/questions-table.tsx index a5816d9bbe..48a158138c 100644 --- a/client/src/app/components/questions-table/questions-table.tsx +++ b/client/src/app/components/questions-table/questions-table.tsx @@ -44,6 +44,7 @@ const QuestionsTable: React.FC<{ formulation: "Name", section: "Section", }, + isExpansionEnabled: true, expandableVariant: "single", forceNumRenderedColumns: isAllQuestionsTab ? 3 : 2, // columns+1 for expand control }); diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 892d792178..a39dca46e3 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -26,6 +26,8 @@ For client-paginated tables, the only hook we need is `useLocalTableControls`. A This simple example includes only the filtering, sorting and pagination features and excludes arguments and properties related to the other features (see [Features](#features)). +Features are enabled by passing the `is[Feature]Enabled` boolean argument. Required arguments for the enabled features will be enforced by TypeScript based on which features are enabled. + ```tsx // In a real table, this API data would come from a useQuery call. const isLoading = false; @@ -43,6 +45,9 @@ const tableControls = useLocalTableControls({ name: "Name", description: "Description", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, filterCategories: [ { key: "name", @@ -161,6 +166,9 @@ const tableControlState = useTableControlState({ name: "Name", description: "Description", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, filterCategories: [ { key: "name", diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts index 56c6feac29..9bb8563d8a 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts @@ -7,9 +7,7 @@ export interface IActiveRowState { setActiveRowId: (id: string | number | null) => void; } -export interface IActiveRowStateArgs { - isActiveRowEnabled?: boolean; -} +export type IActiveRowStateArgs = { isActiveRowEnabled?: boolean }; export const useActiveRowState = < TPersistenceKeyPrefix extends string = string, diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index 5c35c68619..a4fd95d648 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -17,13 +17,13 @@ export interface IExpansionState { ) => void; } -export type IRequiredExpansionStateArgs = { +export type IExpansionStateEnabledArgs = { expandableVariant: "single" | "compound"; }; export type IExpansionStateArgs = - | ({ isExpansionEnabled: true } & IRequiredExpansionStateArgs) - | ({ isExpansionEnabled?: false } & Partial); + | ({ isExpansionEnabled: true } & IExpansionStateEnabledArgs) + | { isExpansionEnabled?: false }; export const useExpansionState = < TColumnKey extends string, diff --git a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts index 09ac315d30..cb5a7e9e09 100644 --- a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts +++ b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts @@ -7,7 +7,7 @@ import { IFilterState } from "./useFilterState"; export interface ILocalFilterDerivedStateArgs< TItem, - TFilterCategoryKey extends string + TFilterCategoryKey extends string, > { items: TItem[]; filterCategories?: FilterCategory[]; @@ -15,7 +15,7 @@ export interface ILocalFilterDerivedStateArgs< export const getLocalFilterDerivedState = < TItem, - TFilterCategoryKey extends string + TFilterCategoryKey extends string, >({ items, filterCategories = [], diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index 2165bb0f9d..e96ce2aa08 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -9,7 +9,7 @@ export interface IFilterState { setFilterValues: (values: IFilterValues) => void; } -export type IRequiredFilterStateArgs< +export type IFilterStateEnabledArgs< TItem, TFilterCategoryKey extends string, > = { @@ -19,10 +19,8 @@ export type IRequiredFilterStateArgs< export type IFilterStateArgs = | ({ isFilterEnabled: true; - } & IRequiredFilterStateArgs) - | ({ isFilterEnabled?: false } & Partial< - IRequiredFilterStateArgs - >); + } & IFilterStateEnabledArgs) + | { isFilterEnabled?: false }; export const useFilterState = < TItem, diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index 097c95c0aa..b7a4d256fa 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -14,18 +14,18 @@ export const getLocalTableControlDerivedState = < TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, >( - args: ITableControlLocalDerivedStateArgs< + args: ITableControlState< TItem, TColumnKey, TSortableColumnKey, - TFilterCategoryKey + TFilterCategoryKey, + TPersistenceKeyPrefix > & - ITableControlState< + ITableControlLocalDerivedStateArgs< TItem, TColumnKey, TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix + TFilterCategoryKey > ): ITableControlDerivedState => { const { items, isPaginationEnabled = true } = args; diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 91161fd3ac..691c93fa6d 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -11,10 +11,13 @@ export interface IPaginationState extends IActivePagination { setItemsPerPage: (numItems: number) => void; } -export interface IPaginationStateArgs { - isPaginationEnabled?: boolean; +export type IPaginationStateEnabledArgs = { initialItemsPerPage?: number; -} +}; + +export type IPaginationStateArgs = + | ({ isPaginationEnabled: true } & IPaginationStateEnabledArgs) + | { isPaginationEnabled?: false }; export const usePaginationState = < TPersistenceKeyPrefix extends string = string, @@ -25,8 +28,9 @@ export const usePaginationState = < isPaginationEnabled, persistTo = "state", persistenceKeyPrefix, - initialItemsPerPage = 10, } = args; + const initialItemsPerPage = + (isPaginationEnabled && args.initialItemsPerPage) || 10; const defaultValue: IActivePagination = { pageNumber: 1, diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index 2048a14a7b..aac4e0c06b 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -11,18 +11,14 @@ export interface ISortState { setActiveSort: (sort: IActiveSort) => void; } -export type IRequiredSortStateArgs = { +export type ISortStateEnabledArgs = { sortableColumns: TSortableColumnKey[]; initialSort?: IActiveSort | null; }; export type ISortStateArgs = - | ({ - isSortEnabled: true; - } & IRequiredSortStateArgs) - | ({ isSortEnabled?: false } & Partial< - IRequiredSortStateArgs - >); + | ({ isSortEnabled: true } & ISortStateEnabledArgs) + | { isSortEnabled?: false }; export const useSortState = < TSortableColumnKey extends string, @@ -31,15 +27,11 @@ export const useSortState = < args: ISortStateArgs & IFeaturePersistenceArgs ): ISortState => { - const { - isSortEnabled, - persistTo = "state", - persistenceKeyPrefix, - sortableColumns = [], - initialSort = sortableColumns[0] - ? { columnKey: sortableColumns[0], direction: "asc" } - : null, - } = args; + const { isSortEnabled, persistTo = "state", persistenceKeyPrefix } = args; + const sortableColumns = (isSortEnabled && args.sortableColumns) || []; + const initialSort: IActiveSort | null = sortableColumns[0] + ? { columnKey: sortableColumns[0], direction: "asc" } + : null; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index e4c912cd85..e3d7ec9f3e 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -85,6 +85,7 @@ export type ITablePersistenceArgs< // Table-level state args // - Used by useTableControlState // - Does not require any state or query data in scope +// - Requires/disallows args based on which features are enabled (see individual [Feature]StateArgs types) export type IUseTableControlStateArgs< TItem, TColumnKey extends string, diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index b872cb0ef9..c50df6fb06 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -45,15 +45,18 @@ export const useTableControlProps = < isItemSelected, }, columnNames, - sortableColumns = [], - isSelectionEnabled = false, - isExpansionEnabled = false, - expandableVariant, hasActionsColumn = false, variant, idProperty, + isSortEnabled, + isSelectionEnabled, + isExpansionEnabled, } = args; + const sortableColumns = (isSortEnabled && args.sortableColumns) || []; + const expandableVariant = + (isExpansionEnabled && args.expandableVariant) || undefined; + const columnKeys = objectKeys(columnNames); // Some table controls rely on extra columns inserted before or after the ones included in columnNames. From d436f5feaa86601e3b0218ffb30c36a80d12e56f Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 12 Oct 2023 15:36:24 -0400 Subject: [PATCH 059/102] Attempting to improve disallowing irrelevant args, not working out Signed-off-by: Mike Turley --- .../filtering/getFilterProps.ts | 40 ++++++++++++------- .../filtering/getLocalFilterDerivedState.ts | 36 ++++++++++++----- .../getLocalTableControlDerivedState.ts | 4 +- .../applications-table-analyze.tsx | 4 +- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/getFilterProps.ts b/client/src/app/hooks/table-controls/filtering/getFilterProps.ts index 6eb3d65f58..21e6acf928 100644 --- a/client/src/app/hooks/table-controls/filtering/getFilterProps.ts +++ b/client/src/app/hooks/table-controls/filtering/getFilterProps.ts @@ -4,19 +4,31 @@ import { } from "@app/components/FilterToolbar"; import { IFilterState } from "./useFilterState"; -export interface IFilterPropsArgs { +export type IFilterPropsEnabledArgs< + TItem, + TFilterCategoryKey extends string, +> = { + filterCategories: FilterCategory[]; +}; + +export type IFilterPropsArgs = { filterState: IFilterState; - filterCategories?: FilterCategory[]; -} +} & ( + | ({ isFilterEnabled: true } & IFilterPropsEnabledArgs< + TItem, + TFilterCategoryKey + >) + | { isFilterEnabled?: false } +); -export const getFilterProps = ({ - filterState: { filterValues, setFilterValues }, - filterCategories = [], -}: IFilterPropsArgs): IFilterToolbarProps< - TItem, - TFilterCategoryKey -> => ({ - filterCategories, - filterValues, - setFilterValues, -}); +export const getFilterProps = ( + args: IFilterPropsArgs +): IFilterToolbarProps => { + const { isFilterEnabled } = args; + return { + filterCategories: (isFilterEnabled && args.filterCategories) || [], + filterValues: (isFilterEnabled && args.filterState.filterValues) || {}, + setFilterValues: + (isFilterEnabled && args.filterState.setFilterValues) || (() => {}), + }; +}; diff --git a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts index cb5a7e9e09..aa98aef5ca 100644 --- a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts +++ b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts @@ -5,24 +5,38 @@ import { import { objectKeys } from "@app/utils/utils"; import { IFilterState } from "./useFilterState"; -export interface ILocalFilterDerivedStateArgs< +export type ILocalFilterDerivedStateEnabledArgs< TItem, TFilterCategoryKey extends string, -> { +> = { items: TItem[]; - filterCategories?: FilterCategory[]; -} + filterCategories: FilterCategory[]; +}; + +export type ILocalFilterDerivedStateArgs< + TItem, + TFilterCategoryKey extends string, +> = + | ({ isFilterEnabled: true } & ILocalFilterDerivedStateEnabledArgs< + TItem, + TFilterCategoryKey + >) + | { isFilterEnabled?: false }; export const getLocalFilterDerivedState = < TItem, TFilterCategoryKey extends string, ->({ - items, - filterCategories = [], - filterState: { filterValues }, -}: ILocalFilterDerivedStateArgs & { - filterState: IFilterState; -}) => { +>( + args: ILocalFilterDerivedStateArgs & { + filterState: IFilterState; + } +) => { + const { + isFilterEnabled, + filterState: { filterValues }, + } = args; + const items = (isFilterEnabled && args.items) || []; + const filterCategories = (isFilterEnabled && args.filterCategories) || []; const filteredItems = items.filter((item) => objectKeys(filterValues).every((categoryKey) => { const values = filterValues[categoryKey]; diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index b7a4d256fa..4e196effc8 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -28,10 +28,10 @@ export const getLocalTableControlDerivedState = < TFilterCategoryKey > ): ITableControlDerivedState => { - const { items, isPaginationEnabled = true } = args; + const { items, isPaginationEnabled, isFilterEnabled } = args; const { filteredItems } = getLocalFilterDerivedState({ ...args, - items, + ...(isFilterEnabled ? { items } : {}), }); const { sortedItems } = getLocalSortDerivedState({ ...args, diff --git a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx index 9db1edbe8c..bf30575227 100644 --- a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx +++ b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx @@ -196,6 +196,8 @@ export const ApplicationsTableAnalyze: React.FC = () => { analysis: "Analysis", tags: "Tags", }, + isSortEnabled: true, + isSelectionEnabled: true, sortableColumns: ["name", "description", "businessService", "tags"], initialSort: { columnKey: "name", direction: "asc" }, getSortValues: (app) => ({ @@ -204,6 +206,7 @@ export const ApplicationsTableAnalyze: React.FC = () => { businessService: app.businessService?.name || "", tags: app.tags?.length || 0, }), + // TODO why is there no error for including filterCategories when isFilterEnabled isn't true? filterCategories: [ { key: "name", @@ -324,7 +327,6 @@ export const ApplicationsTableAnalyze: React.FC = () => { ], initialItemsPerPage: 10, hasActionsColumn: true, - isSelectionEnabled: true, }); const { From 61da6ad3e096465b8caf1cf81ef6e40cb178978a Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 12 Oct 2023 15:36:33 -0400 Subject: [PATCH 060/102] Revert "Attempting to improve disallowing irrelevant args, not working out" This reverts commit 9b0f43efee689ee127bd21f2e3c927510deb8983. Signed-off-by: Mike Turley --- .../filtering/getFilterProps.ts | 40 +++++++------------ .../filtering/getLocalFilterDerivedState.ts | 36 +++++------------ .../getLocalTableControlDerivedState.ts | 4 +- .../applications-table-analyze.tsx | 4 +- 4 files changed, 28 insertions(+), 56 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/getFilterProps.ts b/client/src/app/hooks/table-controls/filtering/getFilterProps.ts index 21e6acf928..6eb3d65f58 100644 --- a/client/src/app/hooks/table-controls/filtering/getFilterProps.ts +++ b/client/src/app/hooks/table-controls/filtering/getFilterProps.ts @@ -4,31 +4,19 @@ import { } from "@app/components/FilterToolbar"; import { IFilterState } from "./useFilterState"; -export type IFilterPropsEnabledArgs< - TItem, - TFilterCategoryKey extends string, -> = { - filterCategories: FilterCategory[]; -}; - -export type IFilterPropsArgs = { +export interface IFilterPropsArgs { filterState: IFilterState; -} & ( - | ({ isFilterEnabled: true } & IFilterPropsEnabledArgs< - TItem, - TFilterCategoryKey - >) - | { isFilterEnabled?: false } -); + filterCategories?: FilterCategory[]; +} -export const getFilterProps = ( - args: IFilterPropsArgs -): IFilterToolbarProps => { - const { isFilterEnabled } = args; - return { - filterCategories: (isFilterEnabled && args.filterCategories) || [], - filterValues: (isFilterEnabled && args.filterState.filterValues) || {}, - setFilterValues: - (isFilterEnabled && args.filterState.setFilterValues) || (() => {}), - }; -}; +export const getFilterProps = ({ + filterState: { filterValues, setFilterValues }, + filterCategories = [], +}: IFilterPropsArgs): IFilterToolbarProps< + TItem, + TFilterCategoryKey +> => ({ + filterCategories, + filterValues, + setFilterValues, +}); diff --git a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts index aa98aef5ca..cb5a7e9e09 100644 --- a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts +++ b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts @@ -5,38 +5,24 @@ import { import { objectKeys } from "@app/utils/utils"; import { IFilterState } from "./useFilterState"; -export type ILocalFilterDerivedStateEnabledArgs< +export interface ILocalFilterDerivedStateArgs< TItem, TFilterCategoryKey extends string, -> = { +> { items: TItem[]; - filterCategories: FilterCategory[]; -}; - -export type ILocalFilterDerivedStateArgs< - TItem, - TFilterCategoryKey extends string, -> = - | ({ isFilterEnabled: true } & ILocalFilterDerivedStateEnabledArgs< - TItem, - TFilterCategoryKey - >) - | { isFilterEnabled?: false }; + filterCategories?: FilterCategory[]; +} export const getLocalFilterDerivedState = < TItem, TFilterCategoryKey extends string, ->( - args: ILocalFilterDerivedStateArgs & { - filterState: IFilterState; - } -) => { - const { - isFilterEnabled, - filterState: { filterValues }, - } = args; - const items = (isFilterEnabled && args.items) || []; - const filterCategories = (isFilterEnabled && args.filterCategories) || []; +>({ + items, + filterCategories = [], + filterState: { filterValues }, +}: ILocalFilterDerivedStateArgs & { + filterState: IFilterState; +}) => { const filteredItems = items.filter((item) => objectKeys(filterValues).every((categoryKey) => { const values = filterValues[categoryKey]; diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index 4e196effc8..b7a4d256fa 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -28,10 +28,10 @@ export const getLocalTableControlDerivedState = < TFilterCategoryKey > ): ITableControlDerivedState => { - const { items, isPaginationEnabled, isFilterEnabled } = args; + const { items, isPaginationEnabled = true } = args; const { filteredItems } = getLocalFilterDerivedState({ ...args, - ...(isFilterEnabled ? { items } : {}), + items, }); const { sortedItems } = getLocalSortDerivedState({ ...args, diff --git a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx index bf30575227..9db1edbe8c 100644 --- a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx +++ b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx @@ -196,8 +196,6 @@ export const ApplicationsTableAnalyze: React.FC = () => { analysis: "Analysis", tags: "Tags", }, - isSortEnabled: true, - isSelectionEnabled: true, sortableColumns: ["name", "description", "businessService", "tags"], initialSort: { columnKey: "name", direction: "asc" }, getSortValues: (app) => ({ @@ -206,7 +204,6 @@ export const ApplicationsTableAnalyze: React.FC = () => { businessService: app.businessService?.name || "", tags: app.tags?.length || 0, }), - // TODO why is there no error for including filterCategories when isFilterEnabled isn't true? filterCategories: [ { key: "name", @@ -327,6 +324,7 @@ export const ApplicationsTableAnalyze: React.FC = () => { ], initialItemsPerPage: 10, hasActionsColumn: true, + isSelectionEnabled: true, }); const { From 16d8d30148267f08de69d5f74e0443db8a681d2f Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 12 Oct 2023 15:59:26 -0400 Subject: [PATCH 061/102] Pass new feature flags to every usage Signed-off-by: Mike Turley --- .../applications-table-analyze.tsx | 6 +++++- .../applications-table-assessment.tsx | 4 ++++ client/src/app/pages/archetypes/archetypes-page.tsx | 7 +++++-- .../assessment-settings/assessment-settings-page.tsx | 5 +++-- .../assessment-actions/components/questionnaires-table.tsx | 2 -- .../src/app/pages/controls/stakeholders/stakeholders.tsx | 5 ++++- client/src/app/pages/dependencies/dependencies.tsx | 4 ++++ .../src/app/pages/dependencies/dependency-apps-table.tsx | 3 +++ client/src/app/pages/external/jira/trackers.tsx | 4 +++- .../issues/affected-applications/affected-applications.tsx | 4 ++++ .../file-all-incidents-table.tsx | 2 ++ .../issue-detail-drawer/issue-affected-files-table.tsx | 3 +++ .../components/manage-applications-form.tsx | 6 ++++-- .../migration-waves/components/stakeholders-table.tsx | 3 ++- .../migration-waves/components/wave-applications-table.tsx | 3 ++- .../pages/migration-waves/components/wave-status-table.tsx | 4 ++-- client/src/app/pages/migration-waves/migration-waves.tsx | 5 ++++- 17 files changed, 54 insertions(+), 16 deletions(-) diff --git a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx index 9db1edbe8c..5bf9bfc07e 100644 --- a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx +++ b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx @@ -196,6 +196,11 @@ export const ApplicationsTableAnalyze: React.FC = () => { analysis: "Analysis", tags: "Tags", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + isSelectionEnabled: true, + isActiveRowEnabled: true, sortableColumns: ["name", "description", "businessService", "tags"], initialSort: { columnKey: "name", direction: "asc" }, getSortValues: (app) => ({ @@ -324,7 +329,6 @@ export const ApplicationsTableAnalyze: React.FC = () => { ], initialItemsPerPage: 10, hasActionsColumn: true, - isSelectionEnabled: true, }); const { diff --git a/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx b/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx index d488ae076c..4d17d05887 100644 --- a/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx +++ b/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx @@ -243,6 +243,10 @@ export const ApplicationsTable: React.FC = () => { review: "Review", tags: "Tags", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + isActiveRowEnabled: true, sortableColumns: ["name", "description", "businessService", "tags"], initialSort: { columnKey: "name", direction: "asc" }, getSortValues: (app) => ({ diff --git a/client/src/app/pages/archetypes/archetypes-page.tsx b/client/src/app/pages/archetypes/archetypes-page.tsx index 86cb30b97f..69c013c9b8 100644 --- a/client/src/app/pages/archetypes/archetypes-page.tsx +++ b/client/src/app/pages/archetypes/archetypes-page.tsx @@ -114,6 +114,11 @@ const Archetypes: React.FC = () => { applications: t("terms.applications"), }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + isActiveRowEnabled: true, + filterCategories: [ { key: "name", @@ -135,8 +140,6 @@ const Archetypes: React.FC = () => { name: archetype.name ?? "", }), initialSort: { columnKey: "name", direction: "asc" }, - - isPaginationEnabled: true, }); const { currentPageItems, diff --git a/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx b/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx index e5c6d02485..5689856f04 100644 --- a/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx +++ b/client/src/app/pages/assessment-management/assessment-settings/assessment-settings-page.tsx @@ -113,7 +113,9 @@ const AssessmentSettings: React.FC = () => { rating: "Rating", createTime: "Date imported", }, - isSelectionEnabled: false, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, hasActionsColumn: true, filterCategories: [ { @@ -135,7 +137,6 @@ const AssessmentSettings: React.FC = () => { createTime: questionnaire.createTime || "", }), initialSort: { columnKey: "name", direction: "asc" }, - isPaginationEnabled: true, isLoading: isFetching, }); const { diff --git a/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx b/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx index fc1e1aa18d..f13f5ecd18 100644 --- a/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx +++ b/client/src/app/pages/assessment/components/assessment-actions/components/questionnaires-table.tsx @@ -40,8 +40,6 @@ const QuestionnairesTable: React.FC = ({ columnNames: { questionnaires: tableName, }, - hasActionsColumn: false, - isPaginationEnabled: false, variant: "compact", }); diff --git a/client/src/app/pages/controls/stakeholders/stakeholders.tsx b/client/src/app/pages/controls/stakeholders/stakeholders.tsx index ce9a4e8e6e..48d61a1d9f 100644 --- a/client/src/app/pages/controls/stakeholders/stakeholders.tsx +++ b/client/src/app/pages/controls/stakeholders/stakeholders.tsx @@ -103,6 +103,10 @@ export const Stakeholders: React.FC = () => { jobFunction: "Job function", groupCount: "Group count", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + isExpansionEnabled: true, expandableVariant: "single", hasActionsColumn: true, filterCategories: [ @@ -165,7 +169,6 @@ export const Stakeholders: React.FC = () => { jobFunction: item.jobFunction?.name || "", }), initialSort: { columnKey: "name", direction: "asc" }, - isPaginationEnabled: true, isLoading: isFetching, }); diff --git a/client/src/app/pages/dependencies/dependencies.tsx b/client/src/app/pages/dependencies/dependencies.tsx index e388a09d5e..a873cb0ee9 100644 --- a/client/src/app/pages/dependencies/dependencies.tsx +++ b/client/src/app/pages/dependencies/dependencies.tsx @@ -49,6 +49,10 @@ export const Dependencies: React.FC = () => { sha: "SHA", version: "Version", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + isActiveRowEnabled: true, sortableColumns: ["name", "foundIn", "labels"], initialSort: { columnKey: "name", direction: "asc" }, filterCategories: [ diff --git a/client/src/app/pages/dependencies/dependency-apps-table.tsx b/client/src/app/pages/dependencies/dependency-apps-table.tsx index 35d2630b02..ca3420b1e9 100644 --- a/client/src/app/pages/dependencies/dependency-apps-table.tsx +++ b/client/src/app/pages/dependencies/dependency-apps-table.tsx @@ -42,6 +42,9 @@ export const DependencyAppsTable: React.FC = ({ // management (3rd party or not boolean... parsed from labels) relationship: "Relationship", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, sortableColumns: ["name", "version"], initialSort: { columnKey: "name", direction: "asc" }, filterCategories: [ diff --git a/client/src/app/pages/external/jira/trackers.tsx b/client/src/app/pages/external/jira/trackers.tsx index aafdf60bcf..f8f49dc3de 100644 --- a/client/src/app/pages/external/jira/trackers.tsx +++ b/client/src/app/pages/external/jira/trackers.tsx @@ -100,6 +100,9 @@ export const JiraTrackers: React.FC = () => { kind: `${t("terms.instance")} type`, connection: "Connection", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, filterCategories: [ { key: "name", @@ -131,7 +134,6 @@ export const JiraTrackers: React.FC = () => { url: tracker.url || "", }), sortableColumns: ["name", "url"], - isPaginationEnabled: true, isLoading: isFetching, }); const { diff --git a/client/src/app/pages/issues/affected-applications/affected-applications.tsx b/client/src/app/pages/issues/affected-applications/affected-applications.tsx index 1b9cffea4c..9623255a28 100644 --- a/client/src/app/pages/issues/affected-applications/affected-applications.tsx +++ b/client/src/app/pages/issues/affected-applications/affected-applications.tsx @@ -62,6 +62,10 @@ export const AffectedApplications: React.FC = () => { effort: "Effort", incidents: "Incidents", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + isActiveRowEnabled: true, sortableColumns: ["name", "businessService", "effort", "incidents"], initialSort: { columnKey: "name", direction: "asc" }, filterCategories: useSharedAffectedApplicationFilterCategories(), diff --git a/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx b/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx index b95f9ae670..ce4946edbe 100644 --- a/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx +++ b/client/src/app/pages/issues/issue-detail-drawer/file-incidents-detail-modal/file-all-incidents-table.tsx @@ -32,6 +32,8 @@ export const FileAllIncidentsTable: React.FC< line: "Line #", message: "Message", }, + isSortEnabled: true, + isPaginationEnabled: true, sortableColumns: ["line", "message"], initialSort: { columnKey: "line", direction: "asc" }, initialItemsPerPage: 10, diff --git a/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx b/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx index ebfd8628ef..a4c7c58411 100644 --- a/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx +++ b/client/src/app/pages/issues/issue-detail-drawer/issue-affected-files-table.tsx @@ -43,6 +43,9 @@ export const IssueAffectedFilesTable: React.FC< incidents: "Incidents", effort: "Effort", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, sortableColumns: ["file", "incidents", "effort"], initialSort: { columnKey: "file", direction: "asc" }, filterCategories: [ diff --git a/client/src/app/pages/migration-waves/components/manage-applications-form.tsx b/client/src/app/pages/migration-waves/components/manage-applications-form.tsx index 4c49e9cc3a..c452d67c93 100644 --- a/client/src/app/pages/migration-waves/components/manage-applications-form.tsx +++ b/client/src/app/pages/migration-waves/components/manage-applications-form.tsx @@ -108,9 +108,12 @@ export const ManageApplicationsForm: React.FC = ({ businessService: "Business service", owner: "Owner", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + isExpansionEnabled: true, isSelectionEnabled: true, initialSelected: assignedApplications, - isExpansionEnabled: true, expandableVariant: "compound", hasActionsColumn: true, filterCategories: [ @@ -170,7 +173,6 @@ export const ManageApplicationsForm: React.FC = ({ owner: application.owner?.name || "", }), initialSort: { columnKey: "name", direction: "asc" }, - isPaginationEnabled: true, }); const { currentPageItems, diff --git a/client/src/app/pages/migration-waves/components/stakeholders-table.tsx b/client/src/app/pages/migration-waves/components/stakeholders-table.tsx index 0337fc1157..125245baae 100644 --- a/client/src/app/pages/migration-waves/components/stakeholders-table.tsx +++ b/client/src/app/pages/migration-waves/components/stakeholders-table.tsx @@ -27,6 +27,8 @@ export const WaveStakeholdersTable: React.FC = ({ email: "Email", groups: "Stakeholder groups", }, + isSortEnabled: true, + isPaginationEnabled: true, hasActionsColumn: true, getSortValues: (stakeholder) => ({ name: stakeholder.name || "", @@ -35,7 +37,6 @@ export const WaveStakeholdersTable: React.FC = ({ email: stakeholder.email, }), sortableColumns: ["name", "jobFunction", "role", "email"], - isPaginationEnabled: true, variant: "compact", }); const { diff --git a/client/src/app/pages/migration-waves/components/wave-applications-table.tsx b/client/src/app/pages/migration-waves/components/wave-applications-table.tsx index 161ac0cda4..ed543cbdc0 100644 --- a/client/src/app/pages/migration-waves/components/wave-applications-table.tsx +++ b/client/src/app/pages/migration-waves/components/wave-applications-table.tsx @@ -35,6 +35,8 @@ export const WaveApplicationsTable: React.FC = ({ businessService: "Business service", owner: "Owner", }, + isSortEnabled: true, + isPaginationEnabled: true, hasActionsColumn: true, getSortValues: (app) => ({ appName: app.name || "", @@ -42,7 +44,6 @@ export const WaveApplicationsTable: React.FC = ({ owner: app.owner?.name || "", }), sortableColumns: ["appName", "businessService", "owner"], - isPaginationEnabled: true, variant: "compact", }); const { diff --git a/client/src/app/pages/migration-waves/components/wave-status-table.tsx b/client/src/app/pages/migration-waves/components/wave-status-table.tsx index 7649d3cdc5..1d56e74afe 100644 --- a/client/src/app/pages/migration-waves/components/wave-status-table.tsx +++ b/client/src/app/pages/migration-waves/components/wave-status-table.tsx @@ -53,15 +53,15 @@ export const WaveStatusTable: React.FC = ({ status: "Status", issue: "Issue", }, - hasActionsColumn: true, isSortEnabled: true, + isPaginationEnabled: true, + hasActionsColumn: true, getSortValues: (app) => ({ appName: app.name || "", status: app.comments || "", issue: "", }), sortableColumns: ["appName", "status", "issue"], - isPaginationEnabled: true, variant: "compact", }); const { diff --git a/client/src/app/pages/migration-waves/migration-waves.tsx b/client/src/app/pages/migration-waves/migration-waves.tsx index c213dcba6f..5cc7c0e3da 100644 --- a/client/src/app/pages/migration-waves/migration-waves.tsx +++ b/client/src/app/pages/migration-waves/migration-waves.tsx @@ -188,6 +188,10 @@ export const MigrationWaves: React.FC = () => { stakeholders: "Stakeholders", status: "Status", }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + isExpansionEnabled: true, isSelectionEnabled: true, expandableVariant: "compound", hasActionsColumn: true, @@ -212,7 +216,6 @@ export const MigrationWaves: React.FC = () => { endDate: migrationWave.endDate || "", }), initialSort: { columnKey: "startDate", direction: "asc" }, - isPaginationEnabled: true, isLoading: isFetching, }); const { From b860c6dc27a902d2bf47dd7800800dcce096af39 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 12 Oct 2023 16:01:10 -0400 Subject: [PATCH 062/102] Fix type errors Signed-off-by: Mike Turley --- client/src/app/hooks/useLegacyFilterState.ts | 1 + client/src/app/hooks/useLegacyPaginationState.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/app/hooks/useLegacyFilterState.ts b/client/src/app/hooks/useLegacyFilterState.ts index 5622ccaa64..c11db00212 100644 --- a/client/src/app/hooks/useLegacyFilterState.ts +++ b/client/src/app/hooks/useLegacyFilterState.ts @@ -19,6 +19,7 @@ export const useLegacyFilterState = ( filterCategories: FilterCategory[] ): IFilterStateHook => { const { filterValues, setFilterValues } = useFilterState({ + isFilterEnabled: true, filterCategories, }); const { filteredItems } = getLocalFilterDerivedState({ diff --git a/client/src/app/hooks/useLegacyPaginationState.ts b/client/src/app/hooks/useLegacyPaginationState.ts index d09d02c414..fa89cadbbb 100644 --- a/client/src/app/hooks/useLegacyPaginationState.ts +++ b/client/src/app/hooks/useLegacyPaginationState.ts @@ -25,7 +25,10 @@ export const useLegacyPaginationState = ( items: T[], initialItemsPerPage: number ): ILegacyPaginationStateHook => { - const paginationState = usePaginationState({ initialItemsPerPage }); + const paginationState = usePaginationState({ + isPaginationEnabled: true, + initialItemsPerPage, + }); usePaginationEffects({ paginationState, totalItemCount: items.length }); const { currentPageItems } = getLocalPaginationDerivedState({ items, From ed3bd2b1a593b83e395251e38ad67e2607788cf6 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 13 Oct 2023 12:56:51 -0400 Subject: [PATCH 063/102] Comments Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index e3d7ec9f3e..c35c12e882 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -172,8 +172,8 @@ export type IUseTableControlPropsArgs< IFilterPropsArgs & ISortPropsArgs & IPaginationPropsArgs & - IExpansionDerivedStateArgs & // TODO should this be IExpansionPropsArgs? should we lift that stuff out of useTableControlProps? - IActiveRowDerivedStateArgs & // TODO should this be IActiveRowPropsArgs? should we lift that stuff out of useTableControlProps? + IExpansionDerivedStateArgs & // Derived in useTableControlProps for convenience because it's always derived on the client + IActiveRowDerivedStateArgs & // Derived in useTableControlProps for convenience because it's always derived on the client ITableControlDerivedState & { isLoading?: boolean; forceNumRenderedColumns?: number; From e91e904eabbdf71dd131c6cdf8b19661bd6df443 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 13 Oct 2023 13:18:33 -0400 Subject: [PATCH 064/102] Ensure features are fully disabled when not marked enabled Signed-off-by: Mike Turley --- .../table-controls/active-row/useActiveRowState.ts | 2 +- .../table-controls/expansion/useExpansionState.ts | 2 +- .../hooks/table-controls/filtering/useFilterState.ts | 2 +- .../table-controls/pagination/usePaginationState.ts | 2 +- .../app/hooks/table-controls/sorting/useSortState.ts | 2 +- client/src/app/hooks/usePersistentState.ts | 10 ++++++++-- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts index 9bb8563d8a..4dbcfec409 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts @@ -24,7 +24,7 @@ export const useActiveRowState = < TPersistenceKeyPrefix, "activeRow" >({ - isEnabled: isActiveRowEnabled, + isEnabled: !!isActiveRowEnabled, defaultValue: null, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index a4fd95d648..f31f9711d3 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -45,6 +45,7 @@ export const useExpansionState = < TPersistenceKeyPrefix, "expandedCells" >({ + isEnabled: !!isExpansionEnabled, defaultValue: {}, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused @@ -52,7 +53,6 @@ export const useExpansionState = < // we need to pass persistTo inside each type-narrowed options object instead of outside the ternary. ...(persistTo === "urlParams" ? { - isEnabled: isExpansionEnabled, persistTo, keys: ["expandedCells"], serialize: (expandedCellsObj) => { diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index e96ce2aa08..d3c6b92fb3 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -39,7 +39,7 @@ export const useFilterState = < TPersistenceKeyPrefix, "filters" >({ - isEnabled: isFilterEnabled, + isEnabled: !!isFilterEnabled, defaultValue: {}, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 691c93fa6d..352cbad91f 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -44,7 +44,7 @@ export const usePaginationState = < TPersistenceKeyPrefix, "pageNumber" | "itemsPerPage" >({ - isEnabled: isPaginationEnabled, + isEnabled: !!isPaginationEnabled, defaultValue, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index aac4e0c06b..37d5468c29 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -40,7 +40,7 @@ export const useSortState = < TPersistenceKeyPrefix, "sortColumn" | "sortDirection" >({ - isEnabled: isSortEnabled, + isEnabled: !!isSortEnabled, defaultValue: initialSort, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused diff --git a/client/src/app/hooks/usePersistentState.ts b/client/src/app/hooks/usePersistentState.ts index f7b23354f5..ccd50c452a 100644 --- a/client/src/app/hooks/usePersistentState.ts +++ b/client/src/app/hooks/usePersistentState.ts @@ -46,7 +46,12 @@ export const usePersistentState = < TURLParamKey > ): [TValue, (value: TValue) => void] => { - const { defaultValue, persistTo, persistenceKeyPrefix } = options; + const { + defaultValue, + persistTo, + persistenceKeyPrefix, + isEnabled = true, + } = options; const isUrlParamsOptions = ( o: typeof options @@ -88,5 +93,6 @@ export const usePersistentState = < : { ...options, isEnabled: false, key: "" } ), }; - return persistence[persistTo || "state"]; + const [value, setValue] = persistence[persistTo || "state"]; + return isEnabled ? [value, setValue] : [defaultValue, () => {}]; }; From f9732bf931fe072ab4743a910ec100e21a71960f Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 13 Oct 2023 13:46:47 -0400 Subject: [PATCH 065/102] Use explicit type for table controls object Signed-off-by: Mike Turley --- .../active-row/getActiveRowDerivedState.ts | 8 ++- .../expansion/getExpansionDerivedState.ts | 14 +++- .../pagination/usePaginationEffects.ts | 4 +- client/src/app/hooks/table-controls/types.ts | 67 ++++++++++++++++++- .../table-controls/useTableControlProps.ts | 55 +++++++++------ 5 files changed, 122 insertions(+), 26 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-row/getActiveRowDerivedState.ts b/client/src/app/hooks/table-controls/active-row/getActiveRowDerivedState.ts index 8757c95ae5..5bf0946066 100644 --- a/client/src/app/hooks/table-controls/active-row/getActiveRowDerivedState.ts +++ b/client/src/app/hooks/table-controls/active-row/getActiveRowDerivedState.ts @@ -7,13 +7,19 @@ export interface IActiveRowDerivedStateArgs { activeRowState: IActiveRowState; } +export interface IActiveRowDerivedState { + activeRowItem: TItem | null; + setActiveRowItem: (item: TItem | null) => void; + clearActiveRow: () => void; +} + // Note: This is not named `getLocalActiveRowDerivedState` because it is always local, // and it is still used when working with server-managed tables. export const getActiveRowDerivedState = ({ currentPageItems, idProperty, activeRowState: { activeRowId, setActiveRowId }, -}: IActiveRowDerivedStateArgs) => ({ +}: IActiveRowDerivedStateArgs): IActiveRowDerivedState => ({ activeRowItem: currentPageItems.find((item) => item[idProperty] === activeRowId) || null, setActiveRowItem: (item: TItem | null) => { diff --git a/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts b/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts index d36325e8ef..0be324d296 100644 --- a/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts +++ b/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts @@ -6,12 +6,24 @@ export interface IExpansionDerivedStateArgs { expansionState: IExpansionState; } +export interface IExpansionDerivedState { + isCellExpanded: (item: TItem, columnKey?: TColumnKey) => boolean; + setCellExpanded: (args: { + item: TItem; + isExpanding?: boolean; + columnKey?: TColumnKey; + }) => void; +} + // Note: This is not named `getLocalExpansionDerivedState` because it is always local, // and it is still used when working with server-managed tables. export const getExpansionDerivedState = ({ idProperty, expansionState: { expandedCells, setExpandedCells }, -}: IExpansionDerivedStateArgs) => { +}: IExpansionDerivedStateArgs): IExpansionDerivedState< + TItem, + TColumnKey +> => { // isCellExpanded: // - If called with a columnKey, returns whether that specific cell is expanded // - If called without a columnKey, returns whether the row is expanded at all diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationEffects.ts b/client/src/app/hooks/table-controls/pagination/usePaginationEffects.ts index 38a3bd0d56..a0376a5e8e 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationEffects.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationEffects.ts @@ -2,12 +2,14 @@ import * as React from "react"; import { IPaginationState } from "./usePaginationState"; export interface IUsePaginationEffectsArgs { + isPaginationEnabled?: boolean; paginationState: IPaginationState; totalItemCount: number; isLoading?: boolean; } export const usePaginationEffects = ({ + isPaginationEnabled, paginationState: { itemsPerPage, pageNumber, setPageNumber }, totalItemCount, isLoading = false, @@ -15,7 +17,7 @@ export const usePaginationEffects = ({ // When items are removed, make sure the current page still exists const lastPageNumber = Math.max(Math.ceil(totalItemCount / itemsPerPage), 1); React.useEffect(() => { - if (pageNumber > lastPageNumber && !isLoading) { + if (isPaginationEnabled && pageNumber > lastPageNumber && !isLoading) { setPageNumber(lastPageNumber); } }); diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index c35c12e882..693aaf8649 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -1,4 +1,4 @@ -import { TableProps } from "@patternfly/react-table"; +import { TableProps, TdProps, ThProps, TrProps } from "@patternfly/react-table"; import { ISelectionStateArgs, useSelectionState } from "@migtools/lib-ui"; import { DisallowCharacters } from "@app/utils/type-utils"; import { @@ -20,15 +20,24 @@ import { IPaginationState, } from "./pagination"; import { + IExpansionDerivedState, IExpansionDerivedStateArgs, IExpansionState, IExpansionStateArgs, } from "./expansion"; import { + IActiveRowDerivedState, IActiveRowDerivedStateArgs, IActiveRowState, IActiveRowStateArgs, } from "./active-row"; +import { + PaginationProps, + ToolbarItemProps, + ToolbarProps, +} from "@patternfly/react-core"; +import { IFilterToolbarProps } from "@app/components/FilterToolbar"; +import { IToolbarBulkSelectorProps } from "@app/components/ToolbarBulkSelector"; // Generic type params used here: // TItem - The actual API objects represented by rows in the table. Can be any object. @@ -163,11 +172,13 @@ export type IUseTableControlPropsArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, + TPersistenceKeyPrefix extends string = string, > = IUseTableControlStateArgs< TItem, TColumnKey, TSortableColumnKey, - TFilterCategoryKey + TFilterCategoryKey, + TPersistenceKeyPrefix > & IFilterPropsArgs & ISortPropsArgs & @@ -182,6 +193,58 @@ export type IUseTableControlPropsArgs< selectionState: ReturnType>; // TODO this won't be included here when selection is part of useTableControlState }; +// Table controls object +// - The object used for rendering +// - Returned by useTableControlProps +// - Includes all args and return values from earlier hooks in the chain along with propHelpers +export type ITableControls< + TItem, + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, + TFilterCategoryKey extends string = string, + TPersistenceKeyPrefix extends string = string, +> = IUseTableControlPropsArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix +> & { + numColumnsBeforeData: number; + numColumnsAfterData: number; + numRenderedColumns: number; + expansionDerivedState: IExpansionDerivedState; + activeRowDerivedState: IActiveRowDerivedState; + propHelpers: { + toolbarProps: Omit; + tableProps: Omit; + getThProps: (args: { columnKey: TColumnKey }) => Omit; + getTdProps: (args: { columnKey: TColumnKey }) => Omit; + filterToolbarProps: IFilterToolbarProps; + paginationProps: PaginationProps; + paginationToolbarItemProps: ToolbarItemProps; + toolbarBulkSelectorProps: IToolbarBulkSelectorProps; + getSelectCheckboxTdProps: (args: { + item: TItem; + rowIndex: number; + }) => Omit; + getCompoundExpandTdProps: (args: { + item: TItem; + rowIndex: number; + columnKey: TColumnKey; + }) => Omit; + getSingleExpandTdProps: (args: { + item: TItem; + rowIndex: number; + }) => Omit; + getExpandedContentTdProps: (args: { item: TItem }) => Omit; + getClickableTrProps: (args: { + onRowClick?: TrProps["onRowClick"]; + item?: TItem | undefined; + }) => Omit; + }; +}; + // Combined args for locally-paginated tables // - Used by useLocalTableControls shorthand hook // - Combines args for useTableControlState, getLocalTableControlDerivedState and useTableControlProps, diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index c50df6fb06..ba2f696797 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -5,7 +5,7 @@ import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { IToolbarBulkSelectorProps } from "@app/components/ToolbarBulkSelector"; import { objectKeys } from "@app/utils/utils"; -import { IUseTableControlPropsArgs } from "./types"; +import { ITableControls, IUseTableControlPropsArgs } from "./types"; import { getFilterProps } from "./filtering"; import { getSortProps } from "./sorting"; import { getPaginationProps, usePaginationEffects } from "./pagination"; @@ -18,14 +18,22 @@ export const useTableControlProps = < TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, + TPersistenceKeyPrefix extends string = string, >( args: IUseTableControlPropsArgs< TItem, TColumnKey, TSortableColumnKey, - TFilterCategoryKey + TFilterCategoryKey, + TPersistenceKeyPrefix > -) => { +): ITableControls< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix +> => { const { t } = useTranslation(); // Note: To avoid repetition, not all args are destructured here since the entire @@ -48,9 +56,12 @@ export const useTableControlProps = < hasActionsColumn = false, variant, idProperty, + isFilterEnabled, isSortEnabled, + isPaginationEnabled, isSelectionEnabled, isExpansionEnabled, + isActiveRowEnabled, } = args; const sortableColumns = (isSortEnabled && args.sortableColumns) || []; @@ -81,9 +92,11 @@ export const useTableControlProps = < const toolbarProps: Omit = { className: variant === "compact" ? spacing.pt_0 : "", - collapseListedFiltersBreakpoint: "xl", - clearAllFilters: () => setFilterValues({}), - clearFiltersButtonText: t("actions.clearAllFilters"), + ...(isFilterEnabled && { + collapseListedFiltersBreakpoint: "xl", + clearAllFilters: () => setFilterValues({}), + clearFiltersButtonText: t("actions.clearAllFilters"), + }), }; const filterToolbarProps = getFilterProps(args); @@ -107,7 +120,7 @@ export const useTableControlProps = < const tableProps: Omit = { variant, - isExpandable: !!expandableVariant, + isExpandable: isExpansionEnabled && !!expandableVariant, }; const getThProps = ({ @@ -115,13 +128,13 @@ export const useTableControlProps = < }: { columnKey: TColumnKey; }): Omit => ({ - ...(sortableColumns.includes(columnKey as TSortableColumnKey) - ? getSortProps({ - ...args, - columnKeys, - columnKey: columnKey as TSortableColumnKey, - }) - : {}), + ...(isSortEnabled && + sortableColumns.includes(columnKey as TSortableColumnKey) && + getSortProps({ + ...args, + columnKeys, + columnKey: columnKey as TSortableColumnKey, + })), children: columnNames[columnKey], }); @@ -241,22 +254,22 @@ export const useTableControlProps = < numColumnsBeforeData, numColumnsAfterData, numRenderedColumns, + expansionDerivedState, + activeRowDerivedState, propHelpers: { toolbarProps, - toolbarBulkSelectorProps, - filterToolbarProps, - paginationProps, - paginationToolbarItemProps, tableProps, getThProps, - getClickableTrProps, getTdProps, + filterToolbarProps, + paginationProps, + paginationToolbarItemProps, + toolbarBulkSelectorProps, getSelectCheckboxTdProps, getCompoundExpandTdProps, getSingleExpandTdProps, getExpandedContentTdProps, + getClickableTrProps, }, - expansionDerivedState, - activeRowDerivedState, }; }; From 648f4bba1463c603a40a28d34fa29f789c450387 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 13 Oct 2023 13:58:20 -0400 Subject: [PATCH 066/102] Attempt to conditionally return stuff from useTableControlProps by enabled feature Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/types.ts | 122 ++++++++++++------ .../table-controls/useTableControlProps.ts | 36 ++++-- 2 files changed, 111 insertions(+), 47 deletions(-) diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 693aaf8649..c85d03576e 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -203,47 +203,97 @@ export type ITableControls< TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, -> = IUseTableControlPropsArgs< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix +> = Omit< + IUseTableControlPropsArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + >, + | "isFilterEnabled" + | "isPaginationEnabled" + | "isSelectionEnabled" + | "isExpansionEnabled" + | "isActiveRowEnabled" > & { numColumnsBeforeData: number; numColumnsAfterData: number; numRenderedColumns: number; - expansionDerivedState: IExpansionDerivedState; - activeRowDerivedState: IActiveRowDerivedState; - propHelpers: { - toolbarProps: Omit; - tableProps: Omit; - getThProps: (args: { columnKey: TColumnKey }) => Omit; - getTdProps: (args: { columnKey: TColumnKey }) => Omit; - filterToolbarProps: IFilterToolbarProps; - paginationProps: PaginationProps; - paginationToolbarItemProps: ToolbarItemProps; - toolbarBulkSelectorProps: IToolbarBulkSelectorProps; - getSelectCheckboxTdProps: (args: { - item: TItem; - rowIndex: number; - }) => Omit; - getCompoundExpandTdProps: (args: { - item: TItem; - rowIndex: number; - columnKey: TColumnKey; - }) => Omit; - getSingleExpandTdProps: (args: { - item: TItem; - rowIndex: number; - }) => Omit; - getExpandedContentTdProps: (args: { item: TItem }) => Omit; - getClickableTrProps: (args: { - onRowClick?: TrProps["onRowClick"]; - item?: TItem | undefined; - }) => Omit; +} & ( + | { + isExpansionEnabled: true; + expansionDerivedState: IExpansionDerivedState; + } + | { isExpansionEnabled?: false } + ) & + ( + | { + isActiveRowEnabled: true; + activeRowDerivedState: IActiveRowDerivedState; + } + | { isExpansionEnabled?: false } + ) & { + propHelpers: { + toolbarProps: Omit; + tableProps: Omit; + getThProps: (args: { columnKey: TColumnKey }) => Omit; + getTdProps: (args: { columnKey: TColumnKey }) => Omit; + } & ( + | { + isFilterEnabled: true; + filterToolbarProps: IFilterToolbarProps; + } + | { isFilterEnabled?: false } + ) & + ( + | { + isPaginationEnabled: true; + paginationProps: PaginationProps; + paginationToolbarItemProps: ToolbarItemProps; + } + | { isPaginationEnabled?: false } + ) & + ( + | { + isSelectionEnabled: true; + toolbarBulkSelectorProps: IToolbarBulkSelectorProps; + getSelectCheckboxTdProps: (args: { + item: TItem; + rowIndex: number; + }) => Omit; + } + | { isSelectionEnabled?: false } + ) & + ( + | { + isExpansionEnabled: true; + getCompoundExpandTdProps: (args: { + item: TItem; + rowIndex: number; + columnKey: TColumnKey; + }) => Omit; + getSingleExpandTdProps: (args: { + item: TItem; + rowIndex: number; + }) => Omit; + getExpandedContentTdProps: (args: { + item: TItem; + }) => Omit; + } + | { isExpansionEnabled?: false } + ) & + ( + | { + isActiveRowEnabled: true; + getClickableTrProps: (args: { + onRowClick?: TrProps["onRowClick"]; + item?: TItem | undefined; + }) => Omit; + } + | { isActiveRowEnabled?: false } + ); }; -}; // Combined args for locally-paginated tables // - Used by useLocalTableControls shorthand hook diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index ba2f696797..50abb534fe 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -254,22 +254,36 @@ export const useTableControlProps = < numColumnsBeforeData, numColumnsAfterData, numRenderedColumns, - expansionDerivedState, - activeRowDerivedState, + ...(isExpansionEnabled && { + expansionDerivedState, + }), + ...(isActiveRowEnabled && { + activeRowDerivedState, + }), propHelpers: { toolbarProps, tableProps, getThProps, getTdProps, - filterToolbarProps, - paginationProps, - paginationToolbarItemProps, - toolbarBulkSelectorProps, - getSelectCheckboxTdProps, - getCompoundExpandTdProps, - getSingleExpandTdProps, - getExpandedContentTdProps, - getClickableTrProps, + ...(isFilterEnabled && { + filterToolbarProps, + }), + ...(isPaginationEnabled && { + paginationProps, + paginationToolbarItemProps, + }), + ...(isSelectionEnabled && { + toolbarBulkSelectorProps, + getSelectCheckboxTdProps, + }), + ...(isExpansionEnabled && { + getCompoundExpandTdProps, + getSingleExpandTdProps, + getExpandedContentTdProps, + }), + ...(isActiveRowEnabled && { + getClickableTrProps, + }), }, }; }; From 5778efb8acd7eb08a48c4379b1964eba99d5d9a1 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 13 Oct 2023 13:58:42 -0400 Subject: [PATCH 067/102] Revert "Attempt to conditionally return stuff from useTableControlProps by enabled feature" This reverts commit b2a1f8c69307c16e129ba76735193950a0e96cd3. Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/types.ts | 122 ++++++------------ .../table-controls/useTableControlProps.ts | 36 ++---- 2 files changed, 47 insertions(+), 111 deletions(-) diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index c85d03576e..693aaf8649 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -203,97 +203,47 @@ export type ITableControls< TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, -> = Omit< - IUseTableControlPropsArgs< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - >, - | "isFilterEnabled" - | "isPaginationEnabled" - | "isSelectionEnabled" - | "isExpansionEnabled" - | "isActiveRowEnabled" +> = IUseTableControlPropsArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix > & { numColumnsBeforeData: number; numColumnsAfterData: number; numRenderedColumns: number; -} & ( - | { - isExpansionEnabled: true; - expansionDerivedState: IExpansionDerivedState; - } - | { isExpansionEnabled?: false } - ) & - ( - | { - isActiveRowEnabled: true; - activeRowDerivedState: IActiveRowDerivedState; - } - | { isExpansionEnabled?: false } - ) & { - propHelpers: { - toolbarProps: Omit; - tableProps: Omit; - getThProps: (args: { columnKey: TColumnKey }) => Omit; - getTdProps: (args: { columnKey: TColumnKey }) => Omit; - } & ( - | { - isFilterEnabled: true; - filterToolbarProps: IFilterToolbarProps; - } - | { isFilterEnabled?: false } - ) & - ( - | { - isPaginationEnabled: true; - paginationProps: PaginationProps; - paginationToolbarItemProps: ToolbarItemProps; - } - | { isPaginationEnabled?: false } - ) & - ( - | { - isSelectionEnabled: true; - toolbarBulkSelectorProps: IToolbarBulkSelectorProps; - getSelectCheckboxTdProps: (args: { - item: TItem; - rowIndex: number; - }) => Omit; - } - | { isSelectionEnabled?: false } - ) & - ( - | { - isExpansionEnabled: true; - getCompoundExpandTdProps: (args: { - item: TItem; - rowIndex: number; - columnKey: TColumnKey; - }) => Omit; - getSingleExpandTdProps: (args: { - item: TItem; - rowIndex: number; - }) => Omit; - getExpandedContentTdProps: (args: { - item: TItem; - }) => Omit; - } - | { isExpansionEnabled?: false } - ) & - ( - | { - isActiveRowEnabled: true; - getClickableTrProps: (args: { - onRowClick?: TrProps["onRowClick"]; - item?: TItem | undefined; - }) => Omit; - } - | { isActiveRowEnabled?: false } - ); + expansionDerivedState: IExpansionDerivedState; + activeRowDerivedState: IActiveRowDerivedState; + propHelpers: { + toolbarProps: Omit; + tableProps: Omit; + getThProps: (args: { columnKey: TColumnKey }) => Omit; + getTdProps: (args: { columnKey: TColumnKey }) => Omit; + filterToolbarProps: IFilterToolbarProps; + paginationProps: PaginationProps; + paginationToolbarItemProps: ToolbarItemProps; + toolbarBulkSelectorProps: IToolbarBulkSelectorProps; + getSelectCheckboxTdProps: (args: { + item: TItem; + rowIndex: number; + }) => Omit; + getCompoundExpandTdProps: (args: { + item: TItem; + rowIndex: number; + columnKey: TColumnKey; + }) => Omit; + getSingleExpandTdProps: (args: { + item: TItem; + rowIndex: number; + }) => Omit; + getExpandedContentTdProps: (args: { item: TItem }) => Omit; + getClickableTrProps: (args: { + onRowClick?: TrProps["onRowClick"]; + item?: TItem | undefined; + }) => Omit; }; +}; // Combined args for locally-paginated tables // - Used by useLocalTableControls shorthand hook diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 50abb534fe..ba2f696797 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -254,36 +254,22 @@ export const useTableControlProps = < numColumnsBeforeData, numColumnsAfterData, numRenderedColumns, - ...(isExpansionEnabled && { - expansionDerivedState, - }), - ...(isActiveRowEnabled && { - activeRowDerivedState, - }), + expansionDerivedState, + activeRowDerivedState, propHelpers: { toolbarProps, tableProps, getThProps, getTdProps, - ...(isFilterEnabled && { - filterToolbarProps, - }), - ...(isPaginationEnabled && { - paginationProps, - paginationToolbarItemProps, - }), - ...(isSelectionEnabled && { - toolbarBulkSelectorProps, - getSelectCheckboxTdProps, - }), - ...(isExpansionEnabled && { - getCompoundExpandTdProps, - getSingleExpandTdProps, - getExpandedContentTdProps, - }), - ...(isActiveRowEnabled && { - getClickableTrProps, - }), + filterToolbarProps, + paginationProps, + paginationToolbarItemProps, + toolbarBulkSelectorProps, + getSelectCheckboxTdProps, + getCompoundExpandTdProps, + getSingleExpandTdProps, + getExpandedContentTdProps, + getClickableTrProps, }, }; }; From 96f9543dec6d6e1c150c46a359818626b609af86 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 13 Oct 2023 14:07:13 -0400 Subject: [PATCH 068/102] Avoid duplicating types in useTableControlProps Signed-off-by: Mike Turley --- .../table-controls/useTableControlProps.ts | 75 +++++++------------ 1 file changed, 28 insertions(+), 47 deletions(-) diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index ba2f696797..5f9bfeb585 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -1,9 +1,6 @@ import { useTranslation } from "react-i18next"; -import { ToolbarItemProps, ToolbarProps } from "@patternfly/react-core"; -import { TableProps, TdProps, ThProps, TrProps } from "@patternfly/react-table"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; -import { IToolbarBulkSelectorProps } from "@app/components/ToolbarBulkSelector"; import { objectKeys } from "@app/utils/utils"; import { ITableControls, IUseTableControlPropsArgs } from "./types"; import { getFilterProps } from "./filtering"; @@ -34,6 +31,14 @@ export const useTableControlProps = < TFilterCategoryKey, TPersistenceKeyPrefix > => { + type PropHelpers = ITableControls< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + >["propHelpers"]; + const { t } = useTranslation(); // Note: To avoid repetition, not all args are destructured here since the entire @@ -58,10 +63,8 @@ export const useTableControlProps = < idProperty, isFilterEnabled, isSortEnabled, - isPaginationEnabled, isSelectionEnabled, isExpansionEnabled, - isActiveRowEnabled, } = args; const sortableColumns = (isSortEnabled && args.sortableColumns) || []; @@ -90,7 +93,7 @@ export const useTableControlProps = < const { activeRowItem, setActiveRowItem, clearActiveRow } = activeRowDerivedState; - const toolbarProps: Omit = { + const toolbarProps: PropHelpers["toolbarProps"] = { className: variant === "compact" ? spacing.pt_0 : "", ...(isFilterEnabled && { collapseListedFiltersBreakpoint: "xl", @@ -104,12 +107,13 @@ export const useTableControlProps = < const paginationProps = getPaginationProps(args); usePaginationEffects(args); - const paginationToolbarItemProps: ToolbarItemProps = { - variant: "pagination", - align: { default: "alignRight" }, - }; + const paginationToolbarItemProps: PropHelpers["paginationToolbarItemProps"] = + { + variant: "pagination", + align: { default: "alignRight" }, + }; - const toolbarBulkSelectorProps: IToolbarBulkSelectorProps = { + const toolbarBulkSelectorProps: PropHelpers["toolbarBulkSelectorProps"] = { onSelectAll: selectAll, areAllSelected, selectedRows: selectedItems, @@ -118,16 +122,12 @@ export const useTableControlProps = < onSelectMultiple: selectMultiple, }; - const tableProps: Omit = { + const tableProps: PropHelpers["tableProps"] = { variant, isExpandable: isExpansionEnabled && !!expandableVariant, }; - const getThProps = ({ - columnKey, - }: { - columnKey: TColumnKey; - }): Omit => ({ + const getThProps: PropHelpers["getThProps"] = ({ columnKey }) => ({ ...(isSortEnabled && sortableColumns.includes(columnKey as TSortableColumnKey) && getSortProps({ @@ -140,13 +140,10 @@ export const useTableControlProps = < // TODO move this into a getActiveRowProps helper? // TODO have the consumer always call getTrProps and only include clickable stuff if the feature is enabled - const getClickableTrProps = ({ + const getClickableTrProps: PropHelpers["getClickableTrProps"] = ({ onRowClick, item, - }: { - onRowClick?: TrProps["onRowClick"]; // Extra callback if necessary - setting the active row is built in - item?: TItem; // Can be omitted if using this just for the click handler and not for active rows - }): Omit => ({ + }) => ({ isSelectable: true, isClickable: true, isRowSelected: item && item[idProperty] === activeRowItem?.[idProperty], @@ -161,22 +158,15 @@ export const useTableControlProps = < }), }); - const getTdProps = ({ - columnKey, - }: { - columnKey: TColumnKey; - }): Omit => ({ + const getTdProps: PropHelpers["getTdProps"] = ({ columnKey }) => ({ dataLabel: columnNames[columnKey], }); // TODO move this into a getSelectionProps helper somehow? - const getSelectCheckboxTdProps = ({ + const getSelectCheckboxTdProps: PropHelpers["getSelectCheckboxTdProps"] = ({ item, rowIndex, - }: { - item: TItem; - rowIndex: number; - }): Omit => ({ + }) => ({ select: { rowIndex, onSelect: (_event, isSelecting) => { @@ -187,13 +177,10 @@ export const useTableControlProps = < }); // TODO move this into a getExpansionProps helper somehow? - const getSingleExpandTdProps = ({ + const getSingleExpandTdProps: PropHelpers["getSingleExpandTdProps"] = ({ item, rowIndex, - }: { - item: TItem; - rowIndex: number; - }): Omit => ({ + }) => ({ expand: { rowIndex, isExpanded: isCellExpanded(item), @@ -207,15 +194,11 @@ export const useTableControlProps = < }); // TODO move this into a getExpansionProps helper somehow? - const getCompoundExpandTdProps = ({ + const getCompoundExpandTdProps: PropHelpers["getCompoundExpandTdProps"] = ({ item, rowIndex, columnKey, - }: { - item: TItem; - rowIndex: number; - columnKey: TColumnKey; - }): Omit => ({ + }) => ({ ...getTdProps({ columnKey }), compoundExpand: { isExpanded: isCellExpanded(item, columnKey), @@ -232,11 +215,9 @@ export const useTableControlProps = < }); // TODO move this into a getExpansionProps helper somehow? - const getExpandedContentTdProps = ({ + const getExpandedContentTdProps: PropHelpers["getExpandedContentTdProps"] = ({ item, - }: { - item: TItem; - }): Omit => { + }) => { const expandedColumnKey = expandedCells[String(item[idProperty])]; return { dataLabel: From 336a88bf7a2956cf04d1da7bad6e75dc4cfe8bb1 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 13 Oct 2023 15:50:02 -0400 Subject: [PATCH 069/102] Refactor away getClickableTrProps into getTrProps Signed-off-by: Mike Turley --- .../active-row/getActiveRowDerivedState.ts | 2 + .../active-row/getActiveRowProps.ts | 25 +++++++++++ .../table-controls/sorting/getSortProps.ts | 45 ++++++++++--------- client/src/app/hooks/table-controls/types.ts | 8 ++-- .../table-controls/useTableControlProps.ts | 38 +++++++--------- .../applications-table-analyze.tsx | 7 +-- .../applications-table-assessment.tsx | 4 +- .../app/pages/archetypes/archetypes-page.tsx | 7 +-- .../app/pages/dependencies/dependencies.tsx | 4 +- .../affected-applications.tsx | 7 +-- 10 files changed, 81 insertions(+), 66 deletions(-) create mode 100644 client/src/app/hooks/table-controls/active-row/getActiveRowProps.ts diff --git a/client/src/app/hooks/table-controls/active-row/getActiveRowDerivedState.ts b/client/src/app/hooks/table-controls/active-row/getActiveRowDerivedState.ts index 5bf0946066..a2240c3c8b 100644 --- a/client/src/app/hooks/table-controls/active-row/getActiveRowDerivedState.ts +++ b/client/src/app/hooks/table-controls/active-row/getActiveRowDerivedState.ts @@ -11,6 +11,7 @@ export interface IActiveRowDerivedState { activeRowItem: TItem | null; setActiveRowItem: (item: TItem | null) => void; clearActiveRow: () => void; + isActiveRowItem: (item: TItem) => boolean; } // Note: This is not named `getLocalActiveRowDerivedState` because it is always local, @@ -27,4 +28,5 @@ export const getActiveRowDerivedState = ({ setActiveRowId(itemId); }, clearActiveRow: () => setActiveRowId(null), + isActiveRowItem: (item) => item[idProperty] === activeRowId, }); diff --git a/client/src/app/hooks/table-controls/active-row/getActiveRowProps.ts b/client/src/app/hooks/table-controls/active-row/getActiveRowProps.ts new file mode 100644 index 0000000000..f84dde6318 --- /dev/null +++ b/client/src/app/hooks/table-controls/active-row/getActiveRowProps.ts @@ -0,0 +1,25 @@ +import { TrProps } from "@patternfly/react-table"; +import { IActiveRowDerivedState } from "./getActiveRowDerivedState"; + +export interface IGetActiveRowPropsArgs { + item: TItem; + activeRowDerivedState: IActiveRowDerivedState; +} + +export const getActiveRowProps = ({ + item, + activeRowDerivedState: { setActiveRowItem, clearActiveRow, isActiveRowItem }, +}: IGetActiveRowPropsArgs): { tr: Omit } => ({ + tr: { + isSelectable: true, + isClickable: true, + isRowSelected: item && isActiveRowItem(item), + onRowClick: () => { + if (!isActiveRowItem(item)) { + setActiveRowItem(item); + } else { + clearActiveRow(); + } + }, + }, +}); diff --git a/client/src/app/hooks/table-controls/sorting/getSortProps.ts b/client/src/app/hooks/table-controls/sorting/getSortProps.ts index 5bd14a7477..a9ddff99be 100644 --- a/client/src/app/hooks/table-controls/sorting/getSortProps.ts +++ b/client/src/app/hooks/table-controls/sorting/getSortProps.ts @@ -7,10 +7,11 @@ export interface ISortPropsArgs< TSortableColumnKey extends TColumnKey, > { sortState: ISortState; + sortableColumns: TSortableColumnKey[]; } // Additional args that need to be passed in on a per-column basis -export interface IUseSortPropsArgs< +export interface IGetSortPropsArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, > extends ISortPropsArgs { @@ -23,25 +24,29 @@ export const getSortProps = < TSortableColumnKey extends TColumnKey, >({ sortState: { activeSort, setActiveSort }, + sortableColumns, columnKeys, columnKey, -}: IUseSortPropsArgs): Pick< - ThProps, - "sort" -> => ({ - sort: { - columnIndex: columnKeys.indexOf(columnKey), - sortBy: { - index: activeSort - ? columnKeys.indexOf(activeSort.columnKey as TSortableColumnKey) - : undefined, - direction: activeSort?.direction, - }, - onSort: (event, index, direction) => { - setActiveSort({ - columnKey: columnKeys[index] as TSortableColumnKey, - direction, - }); - }, - }, +}: IGetSortPropsArgs): { + th: Pick; +} => ({ + th: sortableColumns.includes(columnKey as TSortableColumnKey) + ? { + sort: { + columnIndex: columnKeys.indexOf(columnKey), + sortBy: { + index: activeSort + ? columnKeys.indexOf(activeSort.columnKey as TSortableColumnKey) + : undefined, + direction: activeSort?.direction, + }, + onSort: (event, index, direction) => { + setActiveSort({ + columnKey: columnKeys[index] as TSortableColumnKey, + direction, + }); + }, + }, + } + : {}, }); diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 693aaf8649..71e980f5e5 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -219,6 +219,10 @@ export type ITableControls< toolbarProps: Omit; tableProps: Omit; getThProps: (args: { columnKey: TColumnKey }) => Omit; + getTrProps: (args: { + item: TItem; + onRowClick?: TrProps["onRowClick"]; + }) => Omit; getTdProps: (args: { columnKey: TColumnKey }) => Omit; filterToolbarProps: IFilterToolbarProps; paginationProps: PaginationProps; @@ -238,10 +242,6 @@ export type ITableControls< rowIndex: number; }) => Omit; getExpandedContentTdProps: (args: { item: TItem }) => Omit; - getClickableTrProps: (args: { - onRowClick?: TrProps["onRowClick"]; - item?: TItem | undefined; - }) => Omit; }; }; diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 5f9bfeb585..237e054406 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -9,6 +9,7 @@ import { getPaginationProps, usePaginationEffects } from "./pagination"; import { getActiveRowDerivedState, useActiveRowEffects } from "./active-row"; import { handlePropagatedRowClick } from "./utils"; import { getExpansionDerivedState } from "./expansion"; +import { getActiveRowProps } from "./active-row/getActiveRowProps"; export const useTableControlProps = < TItem, @@ -65,9 +66,9 @@ export const useTableControlProps = < isSortEnabled, isSelectionEnabled, isExpansionEnabled, + isActiveRowEnabled, } = args; - const sortableColumns = (isSortEnabled && args.sortableColumns) || []; const expandableVariant = (isExpansionEnabled && args.expandableVariant) || undefined; @@ -129,34 +130,25 @@ export const useTableControlProps = < const getThProps: PropHelpers["getThProps"] = ({ columnKey }) => ({ ...(isSortEnabled && - sortableColumns.includes(columnKey as TSortableColumnKey) && getSortProps({ ...args, columnKeys, columnKey: columnKey as TSortableColumnKey, - })), + }).th), children: columnNames[columnKey], }); - // TODO move this into a getActiveRowProps helper? - // TODO have the consumer always call getTrProps and only include clickable stuff if the feature is enabled - const getClickableTrProps: PropHelpers["getClickableTrProps"] = ({ - onRowClick, - item, - }) => ({ - isSelectable: true, - isClickable: true, - isRowSelected: item && item[idProperty] === activeRowItem?.[idProperty], - onRowClick: (event) => - handlePropagatedRowClick(event, () => { - if (item && activeRowItem?.[idProperty] !== item[idProperty]) { - setActiveRowItem(item); - } else { - clearActiveRow(); - } - onRowClick?.(event); - }), - }); + const getTrProps: PropHelpers["getTrProps"] = ({ item, onRowClick }) => { + const activeRowProps = getActiveRowProps({ item, activeRowDerivedState }); + return { + ...(isActiveRowEnabled && activeRowProps.tr), + onRowClick: (event) => + handlePropagatedRowClick(event, () => { + activeRowProps.tr.onRowClick?.(event); + onRowClick?.(event); + }), + }; + }; const getTdProps: PropHelpers["getTdProps"] = ({ columnKey }) => ({ dataLabel: columnNames[columnKey], @@ -241,6 +233,7 @@ export const useTableControlProps = < toolbarProps, tableProps, getThProps, + getTrProps, getTdProps, filterToolbarProps, paginationProps, @@ -250,7 +243,6 @@ export const useTableControlProps = < getCompoundExpandTdProps, getSingleExpandTdProps, getExpandedContentTdProps, - getClickableTrProps, }, }; }; diff --git a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx index 5bf9bfc07e..0d093aa27d 100644 --- a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx +++ b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx @@ -341,9 +341,9 @@ export const ApplicationsTableAnalyze: React.FC = () => { paginationProps, tableProps, getThProps, + getTrProps, getTdProps, toolbarBulkSelectorProps, - getClickableTrProps, }, activeRowDerivedState: { activeRowItem, clearActiveRow }, @@ -591,10 +591,7 @@ export const ApplicationsTableAnalyze: React.FC = () => { > {currentPageItems.map((application, rowIndex) => ( - + { paginationProps, tableProps, getThProps, + getTrProps, getTdProps, toolbarBulkSelectorProps, - getClickableTrProps, }, activeRowDerivedState: { activeRowItem, clearActiveRow }, @@ -621,7 +621,7 @@ export const ApplicationsTable: React.FC = () => { return ( { paginationToolbarItemProps, paginationProps, tableProps, - getClickableTrProps, getThProps, + getTrProps, getTdProps, }, activeRowDerivedState: { activeRowItem, clearActiveRow }, @@ -276,10 +276,7 @@ const Archetypes: React.FC = () => { > {currentPageItems?.map((archetype, rowIndex) => ( - + { paginationProps, tableProps, getThProps, + getTrProps, getTdProps, - getClickableTrProps, }, activeRowDerivedState: { activeRowItem, clearActiveRow, setActiveRowItem }, } = tableControls; @@ -194,7 +194,7 @@ export const Dependencies: React.FC = () => { {currentPageItems?.map((dependency, rowIndex) => { return ( - + { paginationProps, tableProps, getThProps, + getTrProps, getTdProps, - getClickableTrProps, }, activeRowDerivedState: { activeRowItem, clearActiveRow }, } = tableControls; @@ -193,10 +193,7 @@ export const AffectedApplications: React.FC = () => { > {currentPageAppReports?.map((appReport, rowIndex) => ( - + Date: Fri, 13 Oct 2023 15:54:07 -0400 Subject: [PATCH 070/102] Fix type errors Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/sorting/getSortProps.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/app/hooks/table-controls/sorting/getSortProps.ts b/client/src/app/hooks/table-controls/sorting/getSortProps.ts index a9ddff99be..c6c2bd7240 100644 --- a/client/src/app/hooks/table-controls/sorting/getSortProps.ts +++ b/client/src/app/hooks/table-controls/sorting/getSortProps.ts @@ -7,7 +7,7 @@ export interface ISortPropsArgs< TSortableColumnKey extends TColumnKey, > { sortState: ISortState; - sortableColumns: TSortableColumnKey[]; + sortableColumns?: TSortableColumnKey[]; } // Additional args that need to be passed in on a per-column basis @@ -24,7 +24,7 @@ export const getSortProps = < TSortableColumnKey extends TColumnKey, >({ sortState: { activeSort, setActiveSort }, - sortableColumns, + sortableColumns = [], columnKeys, columnKey, }: IGetSortPropsArgs): { From 009d995b7dee98f4c525c8b581738dac09225352 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 13 Oct 2023 16:02:54 -0400 Subject: [PATCH 071/102] Use getTrProps everywhere for future-proofing Signed-off-by: Mike Turley --- .../src/app/components/answer-table/answer-table.tsx | 4 ++-- .../app/components/questions-table/questions-table.tsx | 3 ++- client/src/app/hooks/table-controls/DOCS.md | 3 ++- .../assessment-settings/assessment-settings-page.tsx | 3 ++- .../components/questionnaires-table.tsx | 7 +++++-- .../app/pages/controls/stakeholders/stakeholders.tsx | 3 ++- .../app/pages/dependencies/dependency-apps-table.tsx | 6 +++++- client/src/app/pages/external/jira/trackers.tsx | 3 ++- .../file-all-incidents-table.tsx | 10 ++++++++-- .../issue-detail-drawer/issue-affected-files-table.tsx | 3 ++- client/src/app/pages/issues/issues-table.tsx | 3 ++- .../components/manage-applications-form.tsx | 3 ++- .../migration-waves/components/stakeholders-table.tsx | 3 ++- .../components/wave-applications-table.tsx | 3 ++- .../migration-waves/components/wave-status-table.tsx | 3 ++- .../src/app/pages/migration-waves/migration-waves.tsx | 3 ++- 16 files changed, 44 insertions(+), 19 deletions(-) diff --git a/client/src/app/components/answer-table/answer-table.tsx b/client/src/app/components/answer-table/answer-table.tsx index 82cbc7895f..0ca99f0382 100644 --- a/client/src/app/components/answer-table/answer-table.tsx +++ b/client/src/app/components/answer-table/answer-table.tsx @@ -42,7 +42,7 @@ const AnswerTable: React.FC = ({ const { currentPageItems, numRenderedColumns, - propHelpers: { tableProps, getThProps, getTdProps }, + propHelpers: { tableProps, getThProps, getTrProps, getTdProps }, } = tableControls; const getIconByRisk = (risk: string): React.ReactElement => { @@ -116,7 +116,7 @@ const AnswerTable: React.FC = ({ {currentPageItems?.map((answer, rowIndex) => { return ( <> - + - + {currentPageItems?.map((thing, rowIndex) => ( - + { paginationProps, tableProps, getThProps, + getTrProps, getTdProps, }, } = tableControls; @@ -264,7 +265,7 @@ const AssessmentSettings: React.FC = () => { return ( - + = ({ const { currentPageItems, numRenderedColumns, - propHelpers: { tableProps, getThProps, getTdProps }, + propHelpers: { tableProps, getThProps, getTrProps, getTdProps }, } = tableControls; return ( <> @@ -86,7 +86,10 @@ const QuestionnairesTable: React.FC = ({ ); return ( - + { paginationProps, tableProps, getThProps, + getTrProps, getTdProps, getExpandedContentTdProps, }, @@ -266,7 +267,7 @@ export const Stakeholders: React.FC = () => { key={stakeholder.id} isExpanded={isCellExpanded(stakeholder)} > - + = ({ paginationProps, tableProps, getThProps, + getTrProps, getTdProps, }, } = tableControls; @@ -171,7 +172,10 @@ export const DependencyAppsTable: React.FC = ({ > {currentPageAppDependencies?.map((appDependency, rowIndex) => ( - + { paginationProps, tableProps, getThProps, + getTrProps, getTdProps, }, } = tableControls; @@ -242,7 +243,7 @@ export const JiraTrackers: React.FC = () => { > {currentPageItems?.map((tracker, rowIndex) => ( - + {currentPageIncidents?.map((incident, rowIndex) => ( - + {currentPageFileReports?.map((fileReport, rowIndex) => ( - + = ({ mode }) => { paginationProps, tableProps, getThProps, + getTrProps, getTdProps, getExpandedContentTdProps, }, @@ -349,7 +350,7 @@ export const IssuesTable: React.FC = ({ mode }) => { key={report._ui_unique_id} isExpanded={isCellExpanded(report)} > - + = ({ paginationProps, tableProps, getThProps, + getTrProps, getTdProps, }, expansionDerivedState: { isCellExpanded }, @@ -272,7 +273,7 @@ export const ManageApplicationsForm: React.FC = ({ key={application.id} isExpanded={isCellExpanded(application)} > - + = ({ paginationProps, tableProps, getThProps, + getTrProps, getTdProps, }, } = tableControls; @@ -86,7 +87,7 @@ export const WaveStakeholdersTable: React.FC = ({ > {currentPageItems?.map((stakeholder, rowIndex) => ( - + = ({ paginationProps, tableProps, getThProps, + getTrProps, getTdProps, }, } = tableControls; @@ -92,7 +93,7 @@ export const WaveApplicationsTable: React.FC = ({ > {currentPageItems?.map((app, rowIndex) => ( - + = ({ paginationProps, tableProps, getThProps, + getTrProps, getTdProps, }, } = tableControls; @@ -130,7 +131,7 @@ export const WaveStatusTable: React.FC = ({ > {currentPageItems?.map((app, rowIndex) => ( - + { paginationProps, tableProps, getThProps, + getTrProps, getTdProps, getExpandedContentTdProps, getCompoundExpandTdProps, @@ -377,7 +378,7 @@ export const MigrationWaves: React.FC = () => { key={migrationWave.id} isExpanded={isCellExpanded(migrationWave)} > - + Date: Fri, 13 Oct 2023 16:05:14 -0400 Subject: [PATCH 072/102] Remove unused vars Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/useTableControlProps.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 237e054406..145feb4794 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -91,8 +91,6 @@ export const useTableControlProps = < const activeRowDerivedState = getActiveRowDerivedState(args); useActiveRowEffects({ ...args, activeRowDerivedState }); - const { activeRowItem, setActiveRowItem, clearActiveRow } = - activeRowDerivedState; const toolbarProps: PropHelpers["toolbarProps"] = { className: variant === "compact" ? spacing.pt_0 : "", From 2e6b0ec3f778f1bda3864021878ec7d38c50bc1f Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 13 Oct 2023 16:24:51 -0400 Subject: [PATCH 073/102] Cleanup Signed-off-by: Mike Turley --- .../TableRowContentWithControls.tsx | 16 +++++--- client/src/app/hooks/table-controls/DOCS.md | 4 +- .../hooks/table-controls/active-row/index.ts | 1 + client/src/app/hooks/table-controls/types.ts | 2 +- .../table-controls/useTableControlProps.ts | 39 ++++++++++--------- 5 files changed, 35 insertions(+), 27 deletions(-) diff --git a/client/src/app/components/TableControls/TableRowContentWithControls.tsx b/client/src/app/components/TableControls/TableRowContentWithControls.tsx index dc0e4e1fbc..9c1ad5fc9b 100644 --- a/client/src/app/components/TableControls/TableRowContentWithControls.tsx +++ b/client/src/app/components/TableControls/TableRowContentWithControls.tsx @@ -1,17 +1,23 @@ import React from "react"; import { Td } from "@patternfly/react-table"; -import { useTableControlProps } from "@app/hooks/table-controls"; +import { ITableControls } from "@app/hooks/table-controls"; export interface ITableRowContentWithControlsProps< TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, + TFilterCategoryKey extends string = string, + TPersistenceKeyPrefix extends string = string, > { isExpansionEnabled?: boolean; expandableVariant?: "single" | "compound"; isSelectionEnabled?: boolean; - propHelpers: ReturnType< - typeof useTableControlProps + propHelpers: ITableControls< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix >["propHelpers"]; item: TItem; rowIndex: number; @@ -26,7 +32,7 @@ export const TableRowContentWithControls = < isExpansionEnabled = false, expandableVariant, isSelectionEnabled = false, - propHelpers: { getSingleExpandTdProps, getSelectCheckboxTdProps }, + propHelpers: { getSingleExpandButtonTdProps, getSelectCheckboxTdProps }, item, rowIndex, children, @@ -35,7 +41,7 @@ export const TableRowContentWithControls = < >) => ( <> {isExpansionEnabled && expandableVariant === "single" ? ( - + ) : null} {isSelectionEnabled ? ( diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 46130368a8..445539c691 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -295,10 +295,10 @@ Item details can be expanded, either with a "single expansion" variant where an - Expansion state is provided by `useExpansionState`. - Expansion shorthand functions are provided by `getExpansionDerivedState`. - Expansion is never managed server-side. -- Expansion-related component props are provided inside `useTableControlProps` in the `getSingleExpandTdProps` and `getCompoundExpandTdProps` functions. +- Expansion-related component props are provided inside `useTableControlProps` in the `getSingleExpandButtonTdProps` and `getCompoundExpandTdProps` functions. - Expansion inputs are rendered by the table's `Td` component and expanded content is managed at the consumer level by conditionally rendering a second row with full colSpan in a `Tbody` component. The `numRenderedColumns` value returned by `useTableControlProps` can be used for the correct colSpan here. -> ⚠️ TECH DEBT NOTE: `getSingleExpandTdProps` and `getCompoundExpandTdProps` should probably be factored out of `useTableControlProps` into a decoupled `getExpansionProps` helper. +> ⚠️ TECH DEBT NOTE: `getSingleExpandButtonTdProps` and `getCompoundExpandTdProps` should probably be factored out of `useTableControlProps` into a decoupled `getExpansionProps` helper. ### Active Row diff --git a/client/src/app/hooks/table-controls/active-row/index.ts b/client/src/app/hooks/table-controls/active-row/index.ts index 73881002b9..4131086485 100644 --- a/client/src/app/hooks/table-controls/active-row/index.ts +++ b/client/src/app/hooks/table-controls/active-row/index.ts @@ -1,3 +1,4 @@ export * from "./useActiveRowState"; export * from "./getActiveRowDerivedState"; +export * from "./getActiveRowProps"; export * from "./useActiveRowEffects"; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 71e980f5e5..f4dd0bc1d4 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -237,7 +237,7 @@ export type ITableControls< rowIndex: number; columnKey: TColumnKey; }) => Omit; - getSingleExpandTdProps: (args: { + getSingleExpandButtonTdProps: (args: { item: TItem; rowIndex: number; }) => Omit; diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 145feb4794..3d21040b33 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -6,10 +6,13 @@ import { ITableControls, IUseTableControlPropsArgs } from "./types"; import { getFilterProps } from "./filtering"; import { getSortProps } from "./sorting"; import { getPaginationProps, usePaginationEffects } from "./pagination"; -import { getActiveRowDerivedState, useActiveRowEffects } from "./active-row"; +import { + getActiveRowDerivedState, + getActiveRowProps, + useActiveRowEffects, +} from "./active-row"; import { handlePropagatedRowClick } from "./utils"; import { getExpansionDerivedState } from "./expansion"; -import { getActiveRowProps } from "./active-row/getActiveRowProps"; export const useTableControlProps = < TItem, @@ -152,7 +155,7 @@ export const useTableControlProps = < dataLabel: columnNames[columnKey], }); - // TODO move this into a getSelectionProps helper somehow? + // TODO move this into a getSelectionProps helper and make it part of getTdProps once we move selection from lib-ui const getSelectCheckboxTdProps: PropHelpers["getSelectCheckboxTdProps"] = ({ item, rowIndex, @@ -167,21 +170,19 @@ export const useTableControlProps = < }); // TODO move this into a getExpansionProps helper somehow? - const getSingleExpandTdProps: PropHelpers["getSingleExpandTdProps"] = ({ - item, - rowIndex, - }) => ({ - expand: { - rowIndex, - isExpanded: isCellExpanded(item), - onToggle: () => - setCellExpanded({ - item, - isExpanding: !isCellExpanded(item), - }), - expandId: `expandable-row-${item[idProperty]}`, - }, - }); + const getSingleExpandButtonTdProps: PropHelpers["getSingleExpandButtonTdProps"] = + ({ item, rowIndex }) => ({ + expand: { + rowIndex, + isExpanded: isCellExpanded(item), + onToggle: () => + setCellExpanded({ + item, + isExpanding: !isCellExpanded(item), + }), + expandId: `expandable-row-${item[idProperty]}`, + }, + }); // TODO move this into a getExpansionProps helper somehow? const getCompoundExpandTdProps: PropHelpers["getCompoundExpandTdProps"] = ({ @@ -239,7 +240,7 @@ export const useTableControlProps = < toolbarBulkSelectorProps, getSelectCheckboxTdProps, getCompoundExpandTdProps, - getSingleExpandTdProps, + getSingleExpandButtonTdProps, getExpandedContentTdProps, }, }; From 97292d366f1b9a899f42eee808c9e75caa30f499 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 13 Oct 2023 16:31:33 -0400 Subject: [PATCH 074/102] Renaming for more flexibility in prop helper files Signed-off-by: Mike Turley --- .../active-row/activeRowPropHelpers.ts | 23 +++++++++++++++++ .../active-row/getActiveRowProps.ts | 25 ------------------- .../hooks/table-controls/active-row/index.ts | 2 +- .../expansion/expansionPropHelpers.ts | 0 ...getFilterProps.ts => filterPropHelpers.ts} | 12 ++++++--- .../hooks/table-controls/filtering/index.ts | 2 +- .../hooks/table-controls/pagination/index.ts | 2 +- ...ationProps.ts => paginationPropHelpers.ts} | 4 +-- .../app/hooks/table-controls/sorting/index.ts | 2 +- .../{getSortProps.ts => sortPropHelpers.ts} | 20 +++++++-------- client/src/app/hooks/table-controls/types.ts | 12 ++++----- .../table-controls/useTableControlProps.ts | 21 +++++++++------- 12 files changed, 66 insertions(+), 59 deletions(-) create mode 100644 client/src/app/hooks/table-controls/active-row/activeRowPropHelpers.ts delete mode 100644 client/src/app/hooks/table-controls/active-row/getActiveRowProps.ts create mode 100644 client/src/app/hooks/table-controls/expansion/expansionPropHelpers.ts rename client/src/app/hooks/table-controls/filtering/{getFilterProps.ts => filterPropHelpers.ts} (62%) rename client/src/app/hooks/table-controls/pagination/{getPaginationProps.ts => paginationPropHelpers.ts} (84%) rename client/src/app/hooks/table-controls/sorting/{getSortProps.ts => sortPropHelpers.ts} (78%) diff --git a/client/src/app/hooks/table-controls/active-row/activeRowPropHelpers.ts b/client/src/app/hooks/table-controls/active-row/activeRowPropHelpers.ts new file mode 100644 index 0000000000..bd576cd23f --- /dev/null +++ b/client/src/app/hooks/table-controls/active-row/activeRowPropHelpers.ts @@ -0,0 +1,23 @@ +import { TrProps } from "@patternfly/react-table"; +import { IActiveRowDerivedState } from "./getActiveRowDerivedState"; + +export interface IGetActiveRowTrPropsArgs { + item: TItem; + activeRowDerivedState: IActiveRowDerivedState; +} + +export const getActiveRowTrProps = ({ + item, + activeRowDerivedState: { setActiveRowItem, clearActiveRow, isActiveRowItem }, +}: IGetActiveRowTrPropsArgs): Omit => ({ + isSelectable: true, + isClickable: true, + isRowSelected: item && isActiveRowItem(item), + onRowClick: () => { + if (!isActiveRowItem(item)) { + setActiveRowItem(item); + } else { + clearActiveRow(); + } + }, +}); diff --git a/client/src/app/hooks/table-controls/active-row/getActiveRowProps.ts b/client/src/app/hooks/table-controls/active-row/getActiveRowProps.ts deleted file mode 100644 index f84dde6318..0000000000 --- a/client/src/app/hooks/table-controls/active-row/getActiveRowProps.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { TrProps } from "@patternfly/react-table"; -import { IActiveRowDerivedState } from "./getActiveRowDerivedState"; - -export interface IGetActiveRowPropsArgs { - item: TItem; - activeRowDerivedState: IActiveRowDerivedState; -} - -export const getActiveRowProps = ({ - item, - activeRowDerivedState: { setActiveRowItem, clearActiveRow, isActiveRowItem }, -}: IGetActiveRowPropsArgs): { tr: Omit } => ({ - tr: { - isSelectable: true, - isClickable: true, - isRowSelected: item && isActiveRowItem(item), - onRowClick: () => { - if (!isActiveRowItem(item)) { - setActiveRowItem(item); - } else { - clearActiveRow(); - } - }, - }, -}); diff --git a/client/src/app/hooks/table-controls/active-row/index.ts b/client/src/app/hooks/table-controls/active-row/index.ts index 4131086485..d0dbacc788 100644 --- a/client/src/app/hooks/table-controls/active-row/index.ts +++ b/client/src/app/hooks/table-controls/active-row/index.ts @@ -1,4 +1,4 @@ export * from "./useActiveRowState"; export * from "./getActiveRowDerivedState"; -export * from "./getActiveRowProps"; +export * from "./activeRowPropHelpers"; export * from "./useActiveRowEffects"; diff --git a/client/src/app/hooks/table-controls/expansion/expansionPropHelpers.ts b/client/src/app/hooks/table-controls/expansion/expansionPropHelpers.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/client/src/app/hooks/table-controls/filtering/getFilterProps.ts b/client/src/app/hooks/table-controls/filtering/filterPropHelpers.ts similarity index 62% rename from client/src/app/hooks/table-controls/filtering/getFilterProps.ts rename to client/src/app/hooks/table-controls/filtering/filterPropHelpers.ts index 6eb3d65f58..1f439f057f 100644 --- a/client/src/app/hooks/table-controls/filtering/getFilterProps.ts +++ b/client/src/app/hooks/table-controls/filtering/filterPropHelpers.ts @@ -4,15 +4,21 @@ import { } from "@app/components/FilterToolbar"; import { IFilterState } from "./useFilterState"; -export interface IFilterPropsArgs { +export interface IFilterPropHelpersArgs< + TItem, + TFilterCategoryKey extends string, +> { filterState: IFilterState; filterCategories?: FilterCategory[]; } -export const getFilterProps = ({ +export const getFilterToolbarProps = < + TItem, + TFilterCategoryKey extends string, +>({ filterState: { filterValues, setFilterValues }, filterCategories = [], -}: IFilterPropsArgs): IFilterToolbarProps< +}: IFilterPropHelpersArgs): IFilterToolbarProps< TItem, TFilterCategoryKey > => ({ diff --git a/client/src/app/hooks/table-controls/filtering/index.ts b/client/src/app/hooks/table-controls/filtering/index.ts index 4628cd70ae..3605584532 100644 --- a/client/src/app/hooks/table-controls/filtering/index.ts +++ b/client/src/app/hooks/table-controls/filtering/index.ts @@ -1,5 +1,5 @@ export * from "./useFilterState"; export * from "./getLocalFilterDerivedState"; -export * from "./getFilterProps"; +export * from "./filterPropHelpers"; export * from "./getFilterHubRequestParams"; export * from "./helpers"; diff --git a/client/src/app/hooks/table-controls/pagination/index.ts b/client/src/app/hooks/table-controls/pagination/index.ts index 0a99dc24f2..0fa16c08b7 100644 --- a/client/src/app/hooks/table-controls/pagination/index.ts +++ b/client/src/app/hooks/table-controls/pagination/index.ts @@ -1,5 +1,5 @@ export * from "./usePaginationState"; export * from "./getLocalPaginationDerivedState"; -export * from "./getPaginationProps"; +export * from "./paginationPropHelpers"; export * from "./usePaginationEffects"; export * from "./getPaginationHubRequestParams"; diff --git a/client/src/app/hooks/table-controls/pagination/getPaginationProps.ts b/client/src/app/hooks/table-controls/pagination/paginationPropHelpers.ts similarity index 84% rename from client/src/app/hooks/table-controls/pagination/getPaginationProps.ts rename to client/src/app/hooks/table-controls/pagination/paginationPropHelpers.ts index 9bbc7c31fd..b9fb0e7b55 100644 --- a/client/src/app/hooks/table-controls/pagination/getPaginationProps.ts +++ b/client/src/app/hooks/table-controls/pagination/paginationPropHelpers.ts @@ -1,7 +1,7 @@ import { PaginationProps } from "@patternfly/react-core"; import { IPaginationState } from "./usePaginationState"; -export interface IPaginationPropsArgs { +export interface IPaginationPropHelpersArgs { paginationState: IPaginationState; totalItemCount: number; } @@ -9,7 +9,7 @@ export interface IPaginationPropsArgs { export const getPaginationProps = ({ paginationState: { pageNumber, setPageNumber, itemsPerPage, setItemsPerPage }, totalItemCount, -}: IPaginationPropsArgs): PaginationProps => ({ +}: IPaginationPropHelpersArgs): PaginationProps => ({ itemCount: totalItemCount, perPage: itemsPerPage, page: pageNumber, diff --git a/client/src/app/hooks/table-controls/sorting/index.ts b/client/src/app/hooks/table-controls/sorting/index.ts index 37cbe2dd5e..a420e8f437 100644 --- a/client/src/app/hooks/table-controls/sorting/index.ts +++ b/client/src/app/hooks/table-controls/sorting/index.ts @@ -1,4 +1,4 @@ export * from "./useSortState"; export * from "./getLocalSortDerivedState"; -export * from "./getSortProps"; +export * from "./sortPropHelpers"; export * from "./getSortHubRequestParams"; diff --git a/client/src/app/hooks/table-controls/sorting/getSortProps.ts b/client/src/app/hooks/table-controls/sorting/sortPropHelpers.ts similarity index 78% rename from client/src/app/hooks/table-controls/sorting/getSortProps.ts rename to client/src/app/hooks/table-controls/sorting/sortPropHelpers.ts index c6c2bd7240..ee10e325c3 100644 --- a/client/src/app/hooks/table-controls/sorting/getSortProps.ts +++ b/client/src/app/hooks/table-controls/sorting/sortPropHelpers.ts @@ -2,7 +2,7 @@ import { ThProps } from "@patternfly/react-table"; import { ISortState } from "./useSortState"; // Args that are part of IUseTableControlPropsArgs (the args for useTableControlProps) -export interface ISortPropsArgs< +export interface ISortPropHelpersArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, > { @@ -11,15 +11,15 @@ export interface ISortPropsArgs< } // Additional args that need to be passed in on a per-column basis -export interface IGetSortPropsArgs< +export interface IGetSortThPropsArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, -> extends ISortPropsArgs { +> extends ISortPropHelpersArgs { columnKeys: TColumnKey[]; columnKey: TSortableColumnKey; } -export const getSortProps = < +export const getSortThProps = < TColumnKey extends string, TSortableColumnKey extends TColumnKey, >({ @@ -27,10 +27,11 @@ export const getSortProps = < sortableColumns = [], columnKeys, columnKey, -}: IGetSortPropsArgs): { - th: Pick; -} => ({ - th: sortableColumns.includes(columnKey as TSortableColumnKey) +}: IGetSortThPropsArgs): Pick< + ThProps, + "sort" +> => + sortableColumns.includes(columnKey as TSortableColumnKey) ? { sort: { columnIndex: columnKeys.indexOf(columnKey), @@ -48,5 +49,4 @@ export const getSortProps = < }, }, } - : {}, -}); + : {}; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index f4dd0bc1d4..23859be48c 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -4,19 +4,19 @@ import { DisallowCharacters } from "@app/utils/type-utils"; import { IFilterStateArgs, ILocalFilterDerivedStateArgs, - IFilterPropsArgs, + IFilterPropHelpersArgs, IFilterState, } from "./filtering"; import { ILocalSortDerivedStateArgs, - ISortPropsArgs, + ISortPropHelpersArgs, ISortState, ISortStateArgs, } from "./sorting"; import { IPaginationStateArgs, ILocalPaginationDerivedStateArgs, - IPaginationPropsArgs, + IPaginationPropHelpersArgs, IPaginationState, } from "./pagination"; import { @@ -180,9 +180,9 @@ export type IUseTableControlPropsArgs< TFilterCategoryKey, TPersistenceKeyPrefix > & - IFilterPropsArgs & - ISortPropsArgs & - IPaginationPropsArgs & + IFilterPropHelpersArgs & + ISortPropHelpersArgs & + IPaginationPropHelpersArgs & IExpansionDerivedStateArgs & // Derived in useTableControlProps for convenience because it's always derived on the client IActiveRowDerivedStateArgs & // Derived in useTableControlProps for convenience because it's always derived on the client ITableControlDerivedState & { diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 3d21040b33..f2c7bc95fd 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -3,12 +3,12 @@ import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { objectKeys } from "@app/utils/utils"; import { ITableControls, IUseTableControlPropsArgs } from "./types"; -import { getFilterProps } from "./filtering"; -import { getSortProps } from "./sorting"; +import { getFilterToolbarProps } from "./filtering"; +import { getSortThProps } from "./sorting"; import { getPaginationProps, usePaginationEffects } from "./pagination"; import { getActiveRowDerivedState, - getActiveRowProps, + getActiveRowTrProps, useActiveRowEffects, } from "./active-row"; import { handlePropagatedRowClick } from "./utils"; @@ -104,7 +104,7 @@ export const useTableControlProps = < }), }; - const filterToolbarProps = getFilterProps(args); + const filterToolbarProps = getFilterToolbarProps(args); const paginationProps = getPaginationProps(args); usePaginationEffects(args); @@ -131,21 +131,24 @@ export const useTableControlProps = < const getThProps: PropHelpers["getThProps"] = ({ columnKey }) => ({ ...(isSortEnabled && - getSortProps({ + getSortThProps({ ...args, columnKeys, columnKey: columnKey as TSortableColumnKey, - }).th), + })), children: columnNames[columnKey], }); const getTrProps: PropHelpers["getTrProps"] = ({ item, onRowClick }) => { - const activeRowProps = getActiveRowProps({ item, activeRowDerivedState }); + const activeRowTrProps = getActiveRowTrProps({ + item, + activeRowDerivedState, + }); return { - ...(isActiveRowEnabled && activeRowProps.tr), + ...(isActiveRowEnabled && activeRowTrProps), onRowClick: (event) => handlePropagatedRowClick(event, () => { - activeRowProps.tr.onRowClick?.(event); + activeRowTrProps.onRowClick?.(event); onRowClick?.(event); }), }; From c996b35f8c33b7cedca2d72f6870b53901419e0d Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 13 Oct 2023 17:18:47 -0400 Subject: [PATCH 075/102] Closure pattern for extracting expansion prop helpers -- maybe follow this for all features? Signed-off-by: Mike Turley --- .../expansion/expansionPropHelpers.ts | 0 .../expansion/getExpansionPropHelpers.ts | 102 ++++++++++++++++++ client/src/app/hooks/table-controls/types.ts | 18 ++-- .../table-controls/useTableControlProps.ts | 84 +++++---------- .../pages/migration-waves/migration-waves.tsx | 26 ++--- 5 files changed, 153 insertions(+), 77 deletions(-) delete mode 100644 client/src/app/hooks/table-controls/expansion/expansionPropHelpers.ts create mode 100644 client/src/app/hooks/table-controls/expansion/getExpansionPropHelpers.ts diff --git a/client/src/app/hooks/table-controls/expansion/expansionPropHelpers.ts b/client/src/app/hooks/table-controls/expansion/expansionPropHelpers.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/src/app/hooks/table-controls/expansion/getExpansionPropHelpers.ts b/client/src/app/hooks/table-controls/expansion/getExpansionPropHelpers.ts new file mode 100644 index 0000000000..ecf2a09ab7 --- /dev/null +++ b/client/src/app/hooks/table-controls/expansion/getExpansionPropHelpers.ts @@ -0,0 +1,102 @@ +import { KeyWithValueType } from "@app/utils/type-utils"; +import { IExpansionDerivedState, IExpansionState } from "."; +import { TdProps } from "@patternfly/react-table"; + +// Args that should be passed into useTableControlProps +export interface IExpansionPropHelpersExternalArgs< + TItem, + TColumnKey extends string, +> { + columnNames: Record; + idProperty: KeyWithValueType; + expansionState: IExpansionState; +} + +// Additional args that come from logic inside useTableControlProps +export interface IExpansionPropHelpersInternalArgs< + TItem, + TColumnKey extends string, +> { + columnKeys: TColumnKey[]; + numRenderedColumns: number; + expansionDerivedState: IExpansionDerivedState; +} + +export const getExpansionPropHelpers = ( + args: IExpansionPropHelpersExternalArgs & + IExpansionPropHelpersInternalArgs +) => { + const { + columnNames, + idProperty, + columnKeys, + numRenderedColumns, + expansionState: { expandedCells }, + expansionDerivedState: { isCellExpanded, setCellExpanded }, + } = args; + + const getSingleExpandButtonTdProps = ({ + item, + rowIndex, + }: { + item: TItem; + rowIndex: number; + }): Omit => ({ + expand: { + rowIndex, + isExpanded: isCellExpanded(item), + onToggle: () => + setCellExpanded({ + item, + isExpanding: !isCellExpanded(item), + }), + expandId: `expandable-row-${item[idProperty]}`, + }, + }); + + const getCompoundExpandTdProps = ({ + columnKey, + item, + rowIndex, + }: { + columnKey: TColumnKey; + item: TItem; + rowIndex: number; + }): Omit => ({ + compoundExpand: { + isExpanded: isCellExpanded(item, columnKey), + onToggle: () => + setCellExpanded({ + item, + isExpanding: !isCellExpanded(item, columnKey), + columnKey, + }), + expandId: `compound-expand-${item[idProperty]}-${columnKey}`, + rowIndex, + columnIndex: columnKeys.indexOf(columnKey), + }, + }); + + const getExpandedContentTdProps = ({ + item, + }: { + item: TItem; + }): Omit => { + const expandedColumnKey = expandedCells[String(item[idProperty])]; + return { + dataLabel: + typeof expandedColumnKey === "string" + ? columnNames[expandedColumnKey] + : undefined, + noPadding: true, + colSpan: numRenderedColumns, + width: 100, + }; + }; + + return { + getSingleExpandButtonTdProps, + getCompoundExpandTdProps, + getExpandedContentTdProps, + }; +}; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 23859be48c..4a9a381c10 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -38,6 +38,7 @@ import { } from "@patternfly/react-core"; import { IFilterToolbarProps } from "@app/components/FilterToolbar"; import { IToolbarBulkSelectorProps } from "@app/components/ToolbarBulkSelector"; +import { IExpansionPropHelpersExternalArgs } from "./expansion/getExpansionPropHelpers"; // Generic type params used here: // TItem - The actual API objects represented by rows in the table. Can be any object. @@ -183,6 +184,7 @@ export type IUseTableControlPropsArgs< IFilterPropHelpersArgs & ISortPropHelpersArgs & IPaginationPropHelpersArgs & + IExpansionPropHelpersExternalArgs & // TODO should this internal/external args pattern be used for all features? IExpansionDerivedStateArgs & // Derived in useTableControlProps for convenience because it's always derived on the client IActiveRowDerivedStateArgs & // Derived in useTableControlProps for convenience because it's always derived on the client ITableControlDerivedState & { @@ -223,7 +225,16 @@ export type ITableControls< item: TItem; onRowClick?: TrProps["onRowClick"]; }) => Omit; - getTdProps: (args: { columnKey: TColumnKey }) => Omit; + getTdProps: ( + args: { columnKey: TColumnKey } & ( + | { + isCompoundExpandToggle: true; + item: TItem; + rowIndex: number; + } + | { isCompoundExpandToggle?: false } + ) + ) => Omit; filterToolbarProps: IFilterToolbarProps; paginationProps: PaginationProps; paginationToolbarItemProps: ToolbarItemProps; @@ -232,11 +243,6 @@ export type ITableControls< item: TItem; rowIndex: number; }) => Omit; - getCompoundExpandTdProps: (args: { - item: TItem; - rowIndex: number; - columnKey: TColumnKey; - }) => Omit; getSingleExpandButtonTdProps: (args: { item: TItem; rowIndex: number; diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index f2c7bc95fd..fdb9a9f31d 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -13,6 +13,7 @@ import { } from "./active-row"; import { handlePropagatedRowClick } from "./utils"; import { getExpansionDerivedState } from "./expansion"; +import { getExpansionPropHelpers } from "./expansion/getExpansionPropHelpers"; export const useTableControlProps = < TItem, @@ -52,7 +53,6 @@ export const useTableControlProps = < currentPageItems, forceNumRenderedColumns, filterState: { setFilterValues }, - expansionState: { expandedCells }, selectionState: { selectAll, areAllSelected, @@ -64,7 +64,6 @@ export const useTableControlProps = < columnNames, hasActionsColumn = false, variant, - idProperty, isFilterEnabled, isSortEnabled, isSelectionEnabled, @@ -90,7 +89,16 @@ export const useTableControlProps = < columnKeys.length + numColumnsBeforeData + numColumnsAfterData; const expansionDerivedState = getExpansionDerivedState(args); - const { isCellExpanded, setCellExpanded } = expansionDerivedState; + const { + getSingleExpandButtonTdProps, + getCompoundExpandTdProps, + getExpandedContentTdProps, + } = getExpansionPropHelpers({ + ...args, + columnKeys, + numRenderedColumns, + expansionDerivedState, + }); const activeRowDerivedState = getActiveRowDerivedState(args); useActiveRowEffects({ ...args, activeRowDerivedState }); @@ -154,9 +162,20 @@ export const useTableControlProps = < }; }; - const getTdProps: PropHelpers["getTdProps"] = ({ columnKey }) => ({ - dataLabel: columnNames[columnKey], - }); + const getTdProps: PropHelpers["getTdProps"] = (args) => { + const { columnKey } = args; + return { + dataLabel: columnNames[columnKey], + ...(isExpansionEnabled && + expandableVariant === "compound" && + args.isCompoundExpandToggle && + getCompoundExpandTdProps({ + columnKey, + item: args.item, + rowIndex: args.rowIndex, + })), + }; + }; // TODO move this into a getSelectionProps helper and make it part of getTdProps once we move selection from lib-ui const getSelectCheckboxTdProps: PropHelpers["getSelectCheckboxTdProps"] = ({ @@ -172,58 +191,6 @@ export const useTableControlProps = < }, }); - // TODO move this into a getExpansionProps helper somehow? - const getSingleExpandButtonTdProps: PropHelpers["getSingleExpandButtonTdProps"] = - ({ item, rowIndex }) => ({ - expand: { - rowIndex, - isExpanded: isCellExpanded(item), - onToggle: () => - setCellExpanded({ - item, - isExpanding: !isCellExpanded(item), - }), - expandId: `expandable-row-${item[idProperty]}`, - }, - }); - - // TODO move this into a getExpansionProps helper somehow? - const getCompoundExpandTdProps: PropHelpers["getCompoundExpandTdProps"] = ({ - item, - rowIndex, - columnKey, - }) => ({ - ...getTdProps({ columnKey }), - compoundExpand: { - isExpanded: isCellExpanded(item, columnKey), - onToggle: () => - setCellExpanded({ - item, - isExpanding: !isCellExpanded(item, columnKey), - columnKey, - }), - expandId: `compound-expand-${item[idProperty]}-${columnKey}`, - rowIndex, - columnIndex: columnKeys.indexOf(columnKey), - }, - }); - - // TODO move this into a getExpansionProps helper somehow? - const getExpandedContentTdProps: PropHelpers["getExpandedContentTdProps"] = ({ - item, - }) => { - const expandedColumnKey = expandedCells[String(item[idProperty])]; - return { - dataLabel: - typeof expandedColumnKey === "string" - ? columnNames[expandedColumnKey] - : undefined, - noPadding: true, - colSpan: numRenderedColumns, - width: 100, - }; - }; - return { ...args, numColumnsBeforeData, @@ -242,7 +209,6 @@ export const useTableControlProps = < paginationToolbarItemProps, toolbarBulkSelectorProps, getSelectCheckboxTdProps, - getCompoundExpandTdProps, getSingleExpandButtonTdProps, getExpandedContentTdProps, }, diff --git a/client/src/app/pages/migration-waves/migration-waves.tsx b/client/src/app/pages/migration-waves/migration-waves.tsx index a91a7f7dcc..a88365a2a2 100644 --- a/client/src/app/pages/migration-waves/migration-waves.tsx +++ b/client/src/app/pages/migration-waves/migration-waves.tsx @@ -233,7 +233,6 @@ export const MigrationWaves: React.FC = () => { getTrProps, getTdProps, getExpandedContentTdProps, - getCompoundExpandTdProps, }, expansionDerivedState: { isCellExpanded }, } = tableControls; @@ -405,33 +404,36 @@ export const MigrationWaves: React.FC = () => { {migrationWave?.applications?.length.toString()} {migrationWave.allStakeholders.length} {migrationWave.applications.length ? migrationWave.status From 8ccd3ca099bb15afc2ef1e14f407e24082a330a6 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 11:59:03 -0400 Subject: [PATCH 076/102] Fix index Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/expansion/index.ts | 1 + client/src/app/hooks/table-controls/useTableControlProps.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/app/hooks/table-controls/expansion/index.ts b/client/src/app/hooks/table-controls/expansion/index.ts index 64c88055c4..e1e101df9c 100644 --- a/client/src/app/hooks/table-controls/expansion/index.ts +++ b/client/src/app/hooks/table-controls/expansion/index.ts @@ -1,2 +1,3 @@ export * from "./useExpansionState"; export * from "./getExpansionDerivedState"; +export * from "./getExpansionPropHelpers"; diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index fdb9a9f31d..d6b9e8dab6 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -12,8 +12,7 @@ import { useActiveRowEffects, } from "./active-row"; import { handlePropagatedRowClick } from "./utils"; -import { getExpansionDerivedState } from "./expansion"; -import { getExpansionPropHelpers } from "./expansion/getExpansionPropHelpers"; +import { getExpansionDerivedState, getExpansionPropHelpers } from "./expansion"; export const useTableControlProps = < TItem, From b14cc4efc5b8c08550fa7c86d0f1ca3456339b83 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 12:14:25 -0400 Subject: [PATCH 077/102] Consistent naming for external args in useTableControlProps Signed-off-by: Mike Turley --- .../filtering/filterPropHelpers.ts | 7 ++++--- .../pagination/paginationPropHelpers.ts | 5 +++-- .../table-controls/sorting/sortPropHelpers.ts | 6 +++--- client/src/app/hooks/table-controls/types.ts | 14 +++++++------- .../table-controls/useTableControlProps.ts | 19 ++++++++----------- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/filterPropHelpers.ts b/client/src/app/hooks/table-controls/filtering/filterPropHelpers.ts index 1f439f057f..c7a9033234 100644 --- a/client/src/app/hooks/table-controls/filtering/filterPropHelpers.ts +++ b/client/src/app/hooks/table-controls/filtering/filterPropHelpers.ts @@ -4,7 +4,8 @@ import { } from "@app/components/FilterToolbar"; import { IFilterState } from "./useFilterState"; -export interface IFilterPropHelpersArgs< +// Args that should be passed into useTableControlProps +export interface IFilterPropHelpersExternalArgs< TItem, TFilterCategoryKey extends string, > { @@ -18,10 +19,10 @@ export const getFilterToolbarProps = < >({ filterState: { filterValues, setFilterValues }, filterCategories = [], -}: IFilterPropHelpersArgs): IFilterToolbarProps< +}: IFilterPropHelpersExternalArgs< TItem, TFilterCategoryKey -> => ({ +>): IFilterToolbarProps => ({ filterCategories, filterValues, setFilterValues, diff --git a/client/src/app/hooks/table-controls/pagination/paginationPropHelpers.ts b/client/src/app/hooks/table-controls/pagination/paginationPropHelpers.ts index b9fb0e7b55..aa43286259 100644 --- a/client/src/app/hooks/table-controls/pagination/paginationPropHelpers.ts +++ b/client/src/app/hooks/table-controls/pagination/paginationPropHelpers.ts @@ -1,7 +1,8 @@ import { PaginationProps } from "@patternfly/react-core"; import { IPaginationState } from "./usePaginationState"; -export interface IPaginationPropHelpersArgs { +// Args that should be passed into useTableControlProps +export interface IPaginationPropHelpersExternalArgs { paginationState: IPaginationState; totalItemCount: number; } @@ -9,7 +10,7 @@ export interface IPaginationPropHelpersArgs { export const getPaginationProps = ({ paginationState: { pageNumber, setPageNumber, itemsPerPage, setItemsPerPage }, totalItemCount, -}: IPaginationPropHelpersArgs): PaginationProps => ({ +}: IPaginationPropHelpersExternalArgs): PaginationProps => ({ itemCount: totalItemCount, perPage: itemsPerPage, page: pageNumber, diff --git a/client/src/app/hooks/table-controls/sorting/sortPropHelpers.ts b/client/src/app/hooks/table-controls/sorting/sortPropHelpers.ts index ee10e325c3..ffa8a29f9a 100644 --- a/client/src/app/hooks/table-controls/sorting/sortPropHelpers.ts +++ b/client/src/app/hooks/table-controls/sorting/sortPropHelpers.ts @@ -1,8 +1,8 @@ import { ThProps } from "@patternfly/react-table"; import { ISortState } from "./useSortState"; -// Args that are part of IUseTableControlPropsArgs (the args for useTableControlProps) -export interface ISortPropHelpersArgs< +// Args that should be passed into useTableControlProps +export interface ISortPropHelpersExternalArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, > { @@ -14,7 +14,7 @@ export interface ISortPropHelpersArgs< export interface IGetSortThPropsArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, -> extends ISortPropHelpersArgs { +> extends ISortPropHelpersExternalArgs { columnKeys: TColumnKey[]; columnKey: TSortableColumnKey; } diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 4a9a381c10..1ddc8407a9 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -4,19 +4,19 @@ import { DisallowCharacters } from "@app/utils/type-utils"; import { IFilterStateArgs, ILocalFilterDerivedStateArgs, - IFilterPropHelpersArgs, + IFilterPropHelpersExternalArgs, IFilterState, } from "./filtering"; import { ILocalSortDerivedStateArgs, - ISortPropHelpersArgs, + ISortPropHelpersExternalArgs, ISortState, ISortStateArgs, } from "./sorting"; import { IPaginationStateArgs, ILocalPaginationDerivedStateArgs, - IPaginationPropHelpersArgs, + IPaginationPropHelpersExternalArgs, IPaginationState, } from "./pagination"; import { @@ -181,10 +181,10 @@ export type IUseTableControlPropsArgs< TFilterCategoryKey, TPersistenceKeyPrefix > & - IFilterPropHelpersArgs & - ISortPropHelpersArgs & - IPaginationPropHelpersArgs & - IExpansionPropHelpersExternalArgs & // TODO should this internal/external args pattern be used for all features? + IFilterPropHelpersExternalArgs & + ISortPropHelpersExternalArgs & + IPaginationPropHelpersExternalArgs & + IExpansionPropHelpersExternalArgs & IExpansionDerivedStateArgs & // Derived in useTableControlProps for convenience because it's always derived on the client IActiveRowDerivedStateArgs & // Derived in useTableControlProps for convenience because it's always derived on the client ITableControlDerivedState & { diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index d6b9e8dab6..3dcfac9d72 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -70,9 +70,6 @@ export const useTableControlProps = < isActiveRowEnabled, } = args; - const expandableVariant = - (isExpansionEnabled && args.expandableVariant) || undefined; - const columnKeys = objectKeys(columnNames); // Some table controls rely on extra columns inserted before or after the ones included in columnNames. @@ -80,7 +77,7 @@ export const useTableControlProps = < let numColumnsBeforeData = 0; let numColumnsAfterData = 0; if (isSelectionEnabled) numColumnsBeforeData++; - if (isExpansionEnabled && expandableVariant === "single") + if (isExpansionEnabled && args.expandableVariant === "single") numColumnsBeforeData++; if (hasActionsColumn) numColumnsAfterData++; const numRenderedColumns = @@ -133,7 +130,7 @@ export const useTableControlProps = < const tableProps: PropHelpers["tableProps"] = { variant, - isExpandable: isExpansionEnabled && !!expandableVariant, + isExpandable: isExpansionEnabled && !!args.expandableVariant, }; const getThProps: PropHelpers["getThProps"] = ({ columnKey }) => ({ @@ -161,17 +158,17 @@ export const useTableControlProps = < }; }; - const getTdProps: PropHelpers["getTdProps"] = (args) => { - const { columnKey } = args; + const getTdProps: PropHelpers["getTdProps"] = (getTdPropsArgs) => { + const { columnKey } = getTdPropsArgs; return { dataLabel: columnNames[columnKey], ...(isExpansionEnabled && - expandableVariant === "compound" && - args.isCompoundExpandToggle && + args.expandableVariant === "compound" && + getTdPropsArgs.isCompoundExpandToggle && getCompoundExpandTdProps({ columnKey, - item: args.item, - rowIndex: args.rowIndex, + item: getTdPropsArgs.item, + rowIndex: getTdPropsArgs.rowIndex, })), }; }; From 2c5ca702cedd2d53be2a61a9caf35fcd8fc5e91a Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 12:52:19 -0400 Subject: [PATCH 078/102] Move all feature-related prop helpers into use[Feature]PropHelpers Signed-off-by: Mike Turley --- .../active-row/activeRowPropHelpers.ts | 23 ------- .../hooks/table-controls/active-row/index.ts | 2 +- .../active-row/useActiveRowEffects.ts | 2 +- .../active-row/useActiveRowPropHelpers.ts | 46 +++++++++++++ .../hooks/table-controls/expansion/index.ts | 2 +- ...pHelpers.ts => useExpansionPropHelpers.ts} | 18 ++--- .../filtering/filterPropHelpers.ts | 29 -------- .../hooks/table-controls/filtering/index.ts | 2 +- .../filtering/useFilterPropHelpers.ts | 43 ++++++++++++ .../hooks/table-controls/pagination/index.ts | 2 +- .../pagination/paginationPropHelpers.ts | 22 ------ .../pagination/usePaginationPropHelpers.ts | 46 +++++++++++++ .../app/hooks/table-controls/sorting/index.ts | 2 +- .../table-controls/sorting/sortPropHelpers.ts | 52 -------------- .../sorting/useSortPropHelpers.ts | 57 ++++++++++++++++ client/src/app/hooks/table-controls/types.ts | 9 ++- .../table-controls/useTableControlProps.ts | 67 ++++++------------- .../src/app/hooks/useLegacyPaginationState.ts | 4 +- 18 files changed, 233 insertions(+), 195 deletions(-) delete mode 100644 client/src/app/hooks/table-controls/active-row/activeRowPropHelpers.ts create mode 100644 client/src/app/hooks/table-controls/active-row/useActiveRowPropHelpers.ts rename client/src/app/hooks/table-controls/expansion/{getExpansionPropHelpers.ts => useExpansionPropHelpers.ts} (82%) delete mode 100644 client/src/app/hooks/table-controls/filtering/filterPropHelpers.ts create mode 100644 client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts delete mode 100644 client/src/app/hooks/table-controls/pagination/paginationPropHelpers.ts create mode 100644 client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts delete mode 100644 client/src/app/hooks/table-controls/sorting/sortPropHelpers.ts create mode 100644 client/src/app/hooks/table-controls/sorting/useSortPropHelpers.ts diff --git a/client/src/app/hooks/table-controls/active-row/activeRowPropHelpers.ts b/client/src/app/hooks/table-controls/active-row/activeRowPropHelpers.ts deleted file mode 100644 index bd576cd23f..0000000000 --- a/client/src/app/hooks/table-controls/active-row/activeRowPropHelpers.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { TrProps } from "@patternfly/react-table"; -import { IActiveRowDerivedState } from "./getActiveRowDerivedState"; - -export interface IGetActiveRowTrPropsArgs { - item: TItem; - activeRowDerivedState: IActiveRowDerivedState; -} - -export const getActiveRowTrProps = ({ - item, - activeRowDerivedState: { setActiveRowItem, clearActiveRow, isActiveRowItem }, -}: IGetActiveRowTrPropsArgs): Omit => ({ - isSelectable: true, - isClickable: true, - isRowSelected: item && isActiveRowItem(item), - onRowClick: () => { - if (!isActiveRowItem(item)) { - setActiveRowItem(item); - } else { - clearActiveRow(); - } - }, -}); diff --git a/client/src/app/hooks/table-controls/active-row/index.ts b/client/src/app/hooks/table-controls/active-row/index.ts index d0dbacc788..c60b50340d 100644 --- a/client/src/app/hooks/table-controls/active-row/index.ts +++ b/client/src/app/hooks/table-controls/active-row/index.ts @@ -1,4 +1,4 @@ export * from "./useActiveRowState"; export * from "./getActiveRowDerivedState"; -export * from "./activeRowPropHelpers"; +export * from "./useActiveRowPropHelpers"; export * from "./useActiveRowEffects"; diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowEffects.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowEffects.ts index c07af0d82e..b9cee730ff 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowEffects.ts +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowEffects.ts @@ -2,7 +2,7 @@ import * as React from "react"; import { getActiveRowDerivedState } from "./getActiveRowDerivedState"; import { IActiveRowState } from "./useActiveRowState"; -interface IUseActiveRowEffectsArgs { +export interface IUseActiveRowEffectsArgs { isLoading?: boolean; activeRowState: IActiveRowState; activeRowDerivedState: ReturnType>; diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowPropHelpers.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowPropHelpers.ts new file mode 100644 index 0000000000..ec0029d256 --- /dev/null +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowPropHelpers.ts @@ -0,0 +1,46 @@ +import { TrProps } from "@patternfly/react-table"; +import { + IActiveRowDerivedStateArgs, + getActiveRowDerivedState, +} from "./getActiveRowDerivedState"; +import { IActiveRowState } from "./useActiveRowState"; +import { + IUseActiveRowEffectsArgs, + useActiveRowEffects, +} from "./useActiveRowEffects"; + +export type IActiveRowPropHelpersExternalArgs = + IActiveRowDerivedStateArgs & + Omit, "activeRowDerivedState"> & { + isLoading?: boolean; + activeRowState: IActiveRowState; + }; + +export const useActiveRowPropHelpers = ( + args: IActiveRowPropHelpersExternalArgs +) => { + const activeRowDerivedState = getActiveRowDerivedState(args); + const { isActiveRowItem, setActiveRowItem, clearActiveRow } = + activeRowDerivedState; + + useActiveRowEffects({ ...args, activeRowDerivedState }); + + const getActiveRowTrProps = ({ + item, + }: { + item: TItem; + }): Omit => ({ + isSelectable: true, + isClickable: true, + isRowSelected: item && isActiveRowItem(item), + onRowClick: () => { + if (!isActiveRowItem(item)) { + setActiveRowItem(item); + } else { + clearActiveRow(); + } + }, + }); + + return { activeRowDerivedState, getActiveRowTrProps }; +}; diff --git a/client/src/app/hooks/table-controls/expansion/index.ts b/client/src/app/hooks/table-controls/expansion/index.ts index e1e101df9c..495c40182a 100644 --- a/client/src/app/hooks/table-controls/expansion/index.ts +++ b/client/src/app/hooks/table-controls/expansion/index.ts @@ -1,3 +1,3 @@ export * from "./useExpansionState"; export * from "./getExpansionDerivedState"; -export * from "./getExpansionPropHelpers"; +export * from "./useExpansionPropHelpers"; diff --git a/client/src/app/hooks/table-controls/expansion/getExpansionPropHelpers.ts b/client/src/app/hooks/table-controls/expansion/useExpansionPropHelpers.ts similarity index 82% rename from client/src/app/hooks/table-controls/expansion/getExpansionPropHelpers.ts rename to client/src/app/hooks/table-controls/expansion/useExpansionPropHelpers.ts index ecf2a09ab7..8dac553f0e 100644 --- a/client/src/app/hooks/table-controls/expansion/getExpansionPropHelpers.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionPropHelpers.ts @@ -1,5 +1,6 @@ import { KeyWithValueType } from "@app/utils/type-utils"; -import { IExpansionDerivedState, IExpansionState } from "."; +import { IExpansionState } from "./useExpansionState"; +import { getExpansionDerivedState } from "./getExpansionDerivedState"; import { TdProps } from "@patternfly/react-table"; // Args that should be passed into useTableControlProps @@ -13,18 +14,14 @@ export interface IExpansionPropHelpersExternalArgs< } // Additional args that come from logic inside useTableControlProps -export interface IExpansionPropHelpersInternalArgs< - TItem, - TColumnKey extends string, -> { +export interface IExpansionPropHelpersInternalArgs { columnKeys: TColumnKey[]; numRenderedColumns: number; - expansionDerivedState: IExpansionDerivedState; } -export const getExpansionPropHelpers = ( +export const useExpansionPropHelpers = ( args: IExpansionPropHelpersExternalArgs & - IExpansionPropHelpersInternalArgs + IExpansionPropHelpersInternalArgs ) => { const { columnNames, @@ -32,9 +29,11 @@ export const getExpansionPropHelpers = ( columnKeys, numRenderedColumns, expansionState: { expandedCells }, - expansionDerivedState: { isCellExpanded, setCellExpanded }, } = args; + const expansionDerivedState = getExpansionDerivedState(args); + const { isCellExpanded, setCellExpanded } = expansionDerivedState; + const getSingleExpandButtonTdProps = ({ item, rowIndex, @@ -95,6 +94,7 @@ export const getExpansionPropHelpers = ( }; return { + expansionDerivedState, getSingleExpandButtonTdProps, getCompoundExpandTdProps, getExpandedContentTdProps, diff --git a/client/src/app/hooks/table-controls/filtering/filterPropHelpers.ts b/client/src/app/hooks/table-controls/filtering/filterPropHelpers.ts deleted file mode 100644 index c7a9033234..0000000000 --- a/client/src/app/hooks/table-controls/filtering/filterPropHelpers.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - FilterCategory, - IFilterToolbarProps, -} from "@app/components/FilterToolbar"; -import { IFilterState } from "./useFilterState"; - -// Args that should be passed into useTableControlProps -export interface IFilterPropHelpersExternalArgs< - TItem, - TFilterCategoryKey extends string, -> { - filterState: IFilterState; - filterCategories?: FilterCategory[]; -} - -export const getFilterToolbarProps = < - TItem, - TFilterCategoryKey extends string, ->({ - filterState: { filterValues, setFilterValues }, - filterCategories = [], -}: IFilterPropHelpersExternalArgs< - TItem, - TFilterCategoryKey ->): IFilterToolbarProps => ({ - filterCategories, - filterValues, - setFilterValues, -}); diff --git a/client/src/app/hooks/table-controls/filtering/index.ts b/client/src/app/hooks/table-controls/filtering/index.ts index 3605584532..545b3aec73 100644 --- a/client/src/app/hooks/table-controls/filtering/index.ts +++ b/client/src/app/hooks/table-controls/filtering/index.ts @@ -1,5 +1,5 @@ export * from "./useFilterState"; export * from "./getLocalFilterDerivedState"; -export * from "./filterPropHelpers"; +export * from "./useFilterPropHelpers"; export * from "./getFilterHubRequestParams"; export * from "./helpers"; diff --git a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts new file mode 100644 index 0000000000..1d45794a22 --- /dev/null +++ b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts @@ -0,0 +1,43 @@ +import { + FilterCategory, + IFilterToolbarProps, +} from "@app/components/FilterToolbar"; +import { IFilterState } from "./useFilterState"; +import { ToolbarProps } from "@patternfly/react-core"; +import { useTranslation } from "react-i18next"; + +// Args that should be passed into useTableControlProps +export interface IFilterPropHelpersExternalArgs< + TItem, + TFilterCategoryKey extends string, +> { + filterState: IFilterState; + filterCategories?: FilterCategory[]; +} + +export const useFilterPropHelpers = ( + args: IFilterPropHelpersExternalArgs +) => { + const { t } = useTranslation(); + + const { + filterState: { filterValues, setFilterValues }, + filterCategories = [], + } = args; + + const filterPropsForToolbar: ToolbarProps = { + collapseListedFiltersBreakpoint: "xl", + clearAllFilters: () => setFilterValues({}), + clearFiltersButtonText: t("actions.clearAllFilters"), + }; + + const propsForFilterToolbar: IFilterToolbarProps = + { + filterCategories, + filterValues, + setFilterValues, + }; + + // TODO fix the confusing naming here... we have FilterToolbar and Toolbar which both have filter-related props + return { filterPropsForToolbar, propsForFilterToolbar }; +}; diff --git a/client/src/app/hooks/table-controls/pagination/index.ts b/client/src/app/hooks/table-controls/pagination/index.ts index 0fa16c08b7..b3d1706939 100644 --- a/client/src/app/hooks/table-controls/pagination/index.ts +++ b/client/src/app/hooks/table-controls/pagination/index.ts @@ -1,5 +1,5 @@ export * from "./usePaginationState"; export * from "./getLocalPaginationDerivedState"; -export * from "./paginationPropHelpers"; +export * from "./usePaginationPropHelpers"; export * from "./usePaginationEffects"; export * from "./getPaginationHubRequestParams"; diff --git a/client/src/app/hooks/table-controls/pagination/paginationPropHelpers.ts b/client/src/app/hooks/table-controls/pagination/paginationPropHelpers.ts deleted file mode 100644 index aa43286259..0000000000 --- a/client/src/app/hooks/table-controls/pagination/paginationPropHelpers.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PaginationProps } from "@patternfly/react-core"; -import { IPaginationState } from "./usePaginationState"; - -// Args that should be passed into useTableControlProps -export interface IPaginationPropHelpersExternalArgs { - paginationState: IPaginationState; - totalItemCount: number; -} - -export const getPaginationProps = ({ - paginationState: { pageNumber, setPageNumber, itemsPerPage, setItemsPerPage }, - totalItemCount, -}: IPaginationPropHelpersExternalArgs): PaginationProps => ({ - itemCount: totalItemCount, - perPage: itemsPerPage, - page: pageNumber, - onSetPage: (event, pageNumber) => setPageNumber(pageNumber), - onPerPageSelect: (event, perPage) => { - setPageNumber(1); - setItemsPerPage(perPage); - }, -}); diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts b/client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts new file mode 100644 index 0000000000..2ec12d3542 --- /dev/null +++ b/client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts @@ -0,0 +1,46 @@ +import { PaginationProps, ToolbarItemProps } from "@patternfly/react-core"; +import { IPaginationState } from "./usePaginationState"; +import { + IUsePaginationEffectsArgs, + usePaginationEffects, +} from "./usePaginationEffects"; + +// Args that should be passed into useTableControlProps +export type IPaginationPropHelpersExternalArgs = IUsePaginationEffectsArgs & { + paginationState: IPaginationState; + totalItemCount: number; +}; + +export const usePaginationPropHelpers = ( + args: IPaginationPropHelpersExternalArgs +) => { + const { + totalItemCount, + paginationState: { + itemsPerPage, + pageNumber, + setPageNumber, + setItemsPerPage, + }, + } = args; + + usePaginationEffects(args); + + const paginationProps: PaginationProps = { + itemCount: totalItemCount, + perPage: itemsPerPage, + page: pageNumber, + onSetPage: (event, pageNumber) => setPageNumber(pageNumber), + onPerPageSelect: (event, perPage) => { + setPageNumber(1); + setItemsPerPage(perPage); + }, + }; + + const paginationToolbarItemProps: ToolbarItemProps = { + variant: "pagination", + align: { default: "alignRight" }, + }; + + return { paginationProps, paginationToolbarItemProps }; +}; diff --git a/client/src/app/hooks/table-controls/sorting/index.ts b/client/src/app/hooks/table-controls/sorting/index.ts index a420e8f437..cf37beb770 100644 --- a/client/src/app/hooks/table-controls/sorting/index.ts +++ b/client/src/app/hooks/table-controls/sorting/index.ts @@ -1,4 +1,4 @@ export * from "./useSortState"; export * from "./getLocalSortDerivedState"; -export * from "./sortPropHelpers"; +export * from "./useSortPropHelpers"; export * from "./getSortHubRequestParams"; diff --git a/client/src/app/hooks/table-controls/sorting/sortPropHelpers.ts b/client/src/app/hooks/table-controls/sorting/sortPropHelpers.ts deleted file mode 100644 index ffa8a29f9a..0000000000 --- a/client/src/app/hooks/table-controls/sorting/sortPropHelpers.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ThProps } from "@patternfly/react-table"; -import { ISortState } from "./useSortState"; - -// Args that should be passed into useTableControlProps -export interface ISortPropHelpersExternalArgs< - TColumnKey extends string, - TSortableColumnKey extends TColumnKey, -> { - sortState: ISortState; - sortableColumns?: TSortableColumnKey[]; -} - -// Additional args that need to be passed in on a per-column basis -export interface IGetSortThPropsArgs< - TColumnKey extends string, - TSortableColumnKey extends TColumnKey, -> extends ISortPropHelpersExternalArgs { - columnKeys: TColumnKey[]; - columnKey: TSortableColumnKey; -} - -export const getSortThProps = < - TColumnKey extends string, - TSortableColumnKey extends TColumnKey, ->({ - sortState: { activeSort, setActiveSort }, - sortableColumns = [], - columnKeys, - columnKey, -}: IGetSortThPropsArgs): Pick< - ThProps, - "sort" -> => - sortableColumns.includes(columnKey as TSortableColumnKey) - ? { - sort: { - columnIndex: columnKeys.indexOf(columnKey), - sortBy: { - index: activeSort - ? columnKeys.indexOf(activeSort.columnKey as TSortableColumnKey) - : undefined, - direction: activeSort?.direction, - }, - onSort: (event, index, direction) => { - setActiveSort({ - columnKey: columnKeys[index] as TSortableColumnKey, - direction, - }); - }, - }, - } - : {}; diff --git a/client/src/app/hooks/table-controls/sorting/useSortPropHelpers.ts b/client/src/app/hooks/table-controls/sorting/useSortPropHelpers.ts new file mode 100644 index 0000000000..0cd9968c1f --- /dev/null +++ b/client/src/app/hooks/table-controls/sorting/useSortPropHelpers.ts @@ -0,0 +1,57 @@ +import { ThProps } from "@patternfly/react-table"; +import { ISortState } from "./useSortState"; + +// Args that should be passed into useTableControlProps +export interface ISortPropHelpersExternalArgs< + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, +> { + sortState: ISortState; + sortableColumns?: TSortableColumnKey[]; +} + +// Additional args that come from logic inside useTableControlProps +export interface ISortPropHelpersInternalArgs { + columnKeys: TColumnKey[]; +} + +export const useSortPropHelpers = < + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, +>( + args: ISortPropHelpersExternalArgs & + ISortPropHelpersInternalArgs +) => { + const { + sortState: { activeSort, setActiveSort }, + sortableColumns = [], + columnKeys, + } = args; + + const getSortThProps = ({ + columnKey, + }: { + columnKey: TSortableColumnKey; + }): Pick => + sortableColumns.includes(columnKey) + ? { + sort: { + columnIndex: columnKeys.indexOf(columnKey), + sortBy: { + index: activeSort + ? columnKeys.indexOf(activeSort.columnKey) + : undefined, + direction: activeSort?.direction, + }, + onSort: (event, index, direction) => { + setActiveSort({ + columnKey: columnKeys[index] as TSortableColumnKey, + direction, + }); + }, + }, + } + : {}; + + return { getSortThProps }; +}; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 1ddc8407a9..73a15d269b 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -21,13 +21,12 @@ import { } from "./pagination"; import { IExpansionDerivedState, - IExpansionDerivedStateArgs, IExpansionState, IExpansionStateArgs, } from "./expansion"; import { IActiveRowDerivedState, - IActiveRowDerivedStateArgs, + IActiveRowPropHelpersExternalArgs, IActiveRowState, IActiveRowStateArgs, } from "./active-row"; @@ -38,7 +37,7 @@ import { } from "@patternfly/react-core"; import { IFilterToolbarProps } from "@app/components/FilterToolbar"; import { IToolbarBulkSelectorProps } from "@app/components/ToolbarBulkSelector"; -import { IExpansionPropHelpersExternalArgs } from "./expansion/getExpansionPropHelpers"; +import { IExpansionPropHelpersExternalArgs } from "./expansion/useExpansionPropHelpers"; // Generic type params used here: // TItem - The actual API objects represented by rows in the table. Can be any object. @@ -184,9 +183,9 @@ export type IUseTableControlPropsArgs< IFilterPropHelpersExternalArgs & ISortPropHelpersExternalArgs & IPaginationPropHelpersExternalArgs & + // ISelectionPropHelpersExternalArgs // TODO when we move selection from lib-ui IExpansionPropHelpersExternalArgs & - IExpansionDerivedStateArgs & // Derived in useTableControlProps for convenience because it's always derived on the client - IActiveRowDerivedStateArgs & // Derived in useTableControlProps for convenience because it's always derived on the client + IActiveRowPropHelpersExternalArgs & ITableControlDerivedState & { isLoading?: boolean; forceNumRenderedColumns?: number; diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 3dcfac9d72..8985f5de90 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -3,16 +3,12 @@ import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { objectKeys } from "@app/utils/utils"; import { ITableControls, IUseTableControlPropsArgs } from "./types"; -import { getFilterToolbarProps } from "./filtering"; -import { getSortThProps } from "./sorting"; -import { getPaginationProps, usePaginationEffects } from "./pagination"; -import { - getActiveRowDerivedState, - getActiveRowTrProps, - useActiveRowEffects, -} from "./active-row"; +import { useFilterPropHelpers } from "./filtering"; +import { useSortPropHelpers } from "./sorting"; +import { usePaginationPropHelpers } from "./pagination"; +import { useActiveRowPropHelpers } from "./active-row"; +import { useExpansionPropHelpers } from "./expansion"; import { handlePropagatedRowClick } from "./utils"; -import { getExpansionDerivedState, getExpansionPropHelpers } from "./expansion"; export const useTableControlProps = < TItem, @@ -51,7 +47,6 @@ export const useTableControlProps = < const { currentPageItems, forceNumRenderedColumns, - filterState: { setFilterValues }, selectionState: { selectAll, areAllSelected, @@ -84,41 +79,26 @@ export const useTableControlProps = < forceNumRenderedColumns || columnKeys.length + numColumnsBeforeData + numColumnsAfterData; - const expansionDerivedState = getExpansionDerivedState(args); + const { filterPropsForToolbar, propsForFilterToolbar } = + useFilterPropHelpers(args); + const { getSortThProps } = useSortPropHelpers({ ...args, columnKeys }); + const { paginationProps, paginationToolbarItemProps } = + usePaginationPropHelpers(args); const { + expansionDerivedState, getSingleExpandButtonTdProps, getCompoundExpandTdProps, getExpandedContentTdProps, - } = getExpansionPropHelpers({ - ...args, - columnKeys, - numRenderedColumns, - expansionDerivedState, - }); - - const activeRowDerivedState = getActiveRowDerivedState(args); - useActiveRowEffects({ ...args, activeRowDerivedState }); + } = useExpansionPropHelpers({ ...args, columnKeys, numRenderedColumns }); + const { activeRowDerivedState, getActiveRowTrProps } = + useActiveRowPropHelpers(args); const toolbarProps: PropHelpers["toolbarProps"] = { className: variant === "compact" ? spacing.pt_0 : "", - ...(isFilterEnabled && { - collapseListedFiltersBreakpoint: "xl", - clearAllFilters: () => setFilterValues({}), - clearFiltersButtonText: t("actions.clearAllFilters"), - }), + ...(isFilterEnabled && filterPropsForToolbar), }; - const filterToolbarProps = getFilterToolbarProps(args); - - const paginationProps = getPaginationProps(args); - usePaginationEffects(args); - - const paginationToolbarItemProps: PropHelpers["paginationToolbarItemProps"] = - { - variant: "pagination", - align: { default: "alignRight" }, - }; - + // TODO move this to a useSelectionPropHelpers when we move selection from lib-ui const toolbarBulkSelectorProps: PropHelpers["toolbarBulkSelectorProps"] = { onSelectAll: selectAll, areAllSelected, @@ -135,19 +115,12 @@ export const useTableControlProps = < const getThProps: PropHelpers["getThProps"] = ({ columnKey }) => ({ ...(isSortEnabled && - getSortThProps({ - ...args, - columnKeys, - columnKey: columnKey as TSortableColumnKey, - })), + getSortThProps({ columnKey: columnKey as TSortableColumnKey })), children: columnNames[columnKey], }); const getTrProps: PropHelpers["getTrProps"] = ({ item, onRowClick }) => { - const activeRowTrProps = getActiveRowTrProps({ - item, - activeRowDerivedState, - }); + const activeRowTrProps = getActiveRowTrProps({ item }); return { ...(isActiveRowEnabled && activeRowTrProps), onRowClick: (event) => @@ -173,7 +146,7 @@ export const useTableControlProps = < }; }; - // TODO move this into a getSelectionProps helper and make it part of getTdProps once we move selection from lib-ui + // TODO move this into a useSelectionPropHelpers and make it part of getTdProps once we move selection from lib-ui const getSelectCheckboxTdProps: PropHelpers["getSelectCheckboxTdProps"] = ({ item, rowIndex, @@ -200,7 +173,7 @@ export const useTableControlProps = < getThProps, getTrProps, getTdProps, - filterToolbarProps, + filterToolbarProps: propsForFilterToolbar, paginationProps, paginationToolbarItemProps, toolbarBulkSelectorProps, diff --git a/client/src/app/hooks/useLegacyPaginationState.ts b/client/src/app/hooks/useLegacyPaginationState.ts index fa89cadbbb..d0760a4cc2 100644 --- a/client/src/app/hooks/useLegacyPaginationState.ts +++ b/client/src/app/hooks/useLegacyPaginationState.ts @@ -1,9 +1,9 @@ import { PaginationProps } from "@patternfly/react-core"; import { getLocalPaginationDerivedState, - getPaginationProps, usePaginationState, usePaginationEffects, + usePaginationPropHelpers, } from "./table-controls"; // NOTE: This was refactored to return generic state data and decouple the client-side-pagination piece to another helper function. @@ -34,7 +34,7 @@ export const useLegacyPaginationState = ( items, paginationState, }); - const paginationProps = getPaginationProps({ + const { paginationProps } = usePaginationPropHelpers({ totalItemCount: items.length, paginationState, }); From bc09d56474b63dcb26de321038d783a2d45f6b55 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 13:12:04 -0400 Subject: [PATCH 079/102] Use DiscriminatedArgs type util Signed-off-by: Mike Turley --- .../expansion/useExpansionState.ts | 12 +++++------- .../table-controls/filtering/useFilterState.ts | 16 ++++++---------- .../pagination/usePaginationState.ts | 12 +++++------- .../hooks/table-controls/sorting/useSortState.ts | 15 ++++++++------- client/src/app/hooks/table-controls/types.ts | 14 +++++--------- client/src/app/utils/type-utils.ts | 6 +++++- 6 files changed, 34 insertions(+), 41 deletions(-) diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index f31f9711d3..4352bb3d4d 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -1,6 +1,7 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; import { objectKeys } from "@app/utils/utils"; import { IFeaturePersistenceArgs } from "../types"; +import { DiscriminatedArgs } from "@app/utils/type-utils"; // TExpandedCells maps item[idProperty] values to either: // - The key of an expanded column in that row, if the table is compound-expandable @@ -17,13 +18,10 @@ export interface IExpansionState { ) => void; } -export type IExpansionStateEnabledArgs = { - expandableVariant: "single" | "compound"; -}; - -export type IExpansionStateArgs = - | ({ isExpansionEnabled: true } & IExpansionStateEnabledArgs) - | { isExpansionEnabled?: false }; +export type IExpansionStateArgs = DiscriminatedArgs< + "isExpansionEnabled", + { expandableVariant: "single" | "compound" } +>; export const useExpansionState = < TColumnKey extends string, diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index d3c6b92fb3..e32ec195ea 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -3,24 +3,20 @@ import { IFeaturePersistenceArgs } from "../types"; import { usePersistentState } from "@app/hooks/usePersistentState"; import { serializeFilterUrlParams } from "./helpers"; import { deserializeFilterUrlParams } from "./helpers"; +import { DiscriminatedArgs } from "@app/utils/type-utils"; export interface IFilterState { filterValues: IFilterValues; setFilterValues: (values: IFilterValues) => void; } -export type IFilterStateEnabledArgs< +export type IFilterStateArgs< TItem, TFilterCategoryKey extends string, -> = { - filterCategories: FilterCategory[]; -}; - -export type IFilterStateArgs = - | ({ - isFilterEnabled: true; - } & IFilterStateEnabledArgs) - | { isFilterEnabled?: false }; +> = DiscriminatedArgs< + "isFilterEnabled", + { filterCategories: FilterCategory[] } +>; export const useFilterState = < TItem, diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 352cbad91f..d1cd7e7f3e 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -1,5 +1,6 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; import { IFeaturePersistenceArgs } from "../types"; +import { DiscriminatedArgs } from "@app/utils/type-utils"; export interface IActivePagination { pageNumber: number; @@ -11,13 +12,10 @@ export interface IPaginationState extends IActivePagination { setItemsPerPage: (numItems: number) => void; } -export type IPaginationStateEnabledArgs = { - initialItemsPerPage?: number; -}; - -export type IPaginationStateArgs = - | ({ isPaginationEnabled: true } & IPaginationStateEnabledArgs) - | { isPaginationEnabled?: false }; +export type IPaginationStateArgs = DiscriminatedArgs< + "isPaginationEnabled", + { initialItemsPerPage?: number } +>; export const usePaginationState = < TPersistenceKeyPrefix extends string = string, diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index 37d5468c29..4d13d85157 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -1,3 +1,4 @@ +import { DiscriminatedArgs } from "@app/utils/type-utils"; import { IFeaturePersistenceArgs } from ".."; import { usePersistentState } from "@app/hooks/usePersistentState"; @@ -11,14 +12,14 @@ export interface ISortState { setActiveSort: (sort: IActiveSort) => void; } -export type ISortStateEnabledArgs = { - sortableColumns: TSortableColumnKey[]; - initialSort?: IActiveSort | null; -}; - export type ISortStateArgs = - | ({ isSortEnabled: true } & ISortStateEnabledArgs) - | { isSortEnabled?: false }; + DiscriminatedArgs< + "isSortEnabled", + { + sortableColumns: TSortableColumnKey[]; + initialSort?: IActiveSort | null; + } + >; export const useSortState = < TSortableColumnKey extends string, diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 73a15d269b..2fc55963f8 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -1,6 +1,6 @@ import { TableProps, TdProps, ThProps, TrProps } from "@patternfly/react-table"; import { ISelectionStateArgs, useSelectionState } from "@migtools/lib-ui"; -import { DisallowCharacters } from "@app/utils/type-utils"; +import { DisallowCharacters, DiscriminatedArgs } from "@app/utils/type-utils"; import { IFilterStateArgs, ILocalFilterDerivedStateArgs, @@ -225,14 +225,10 @@ export type ITableControls< onRowClick?: TrProps["onRowClick"]; }) => Omit; getTdProps: ( - args: { columnKey: TColumnKey } & ( - | { - isCompoundExpandToggle: true; - item: TItem; - rowIndex: number; - } - | { isCompoundExpandToggle?: false } - ) + args: { columnKey: TColumnKey } & DiscriminatedArgs< + "isCompoundExpandToggle", + { item: TItem; rowIndex: number } + > ) => Omit; filterToolbarProps: IFilterToolbarProps; paginationProps: PaginationProps; diff --git a/client/src/app/utils/type-utils.ts b/client/src/app/utils/type-utils.ts index 164c37fafd..5a3692492e 100644 --- a/client/src/app/utils/type-utils.ts +++ b/client/src/app/utils/type-utils.ts @@ -4,5 +4,9 @@ export type KeyWithValueType = { export type DisallowCharacters< T extends string, - TInvalidCharacter extends string + TInvalidCharacter extends string, > = T extends `${string}${TInvalidCharacter}${string}` ? never : T; + +export type DiscriminatedArgs = + | ({ [key in TBoolDiscriminatorKey]: true } & TArgs) + | { [key in TBoolDiscriminatorKey]?: false }; From 4c406422f84d4ff1170e16c78629531dfe1a05d0 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 13:27:31 -0400 Subject: [PATCH 080/102] Simplify arg types for getLocal[Feature]DerivedState by shuffling omits Signed-off-by: Mike Turley --- .../filtering/getLocalFilterDerivedState.ts | 5 ++--- .../getLocalPaginationDerivedState.ts | 5 ++--- .../sorting/getLocalSortDerivedState.ts | 5 ++--- client/src/app/hooks/table-controls/types.ts | 18 +++++++++--------- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts index cb5a7e9e09..38f332ec3f 100644 --- a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts +++ b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts @@ -11,6 +11,7 @@ export interface ILocalFilterDerivedStateArgs< > { items: TItem[]; filterCategories?: FilterCategory[]; + filterState: IFilterState; } export const getLocalFilterDerivedState = < @@ -20,9 +21,7 @@ export const getLocalFilterDerivedState = < items, filterCategories = [], filterState: { filterValues }, -}: ILocalFilterDerivedStateArgs & { - filterState: IFilterState; -}) => { +}: ILocalFilterDerivedStateArgs) => { const filteredItems = items.filter((item) => objectKeys(filterValues).every((categoryKey) => { const values = filterValues[categoryKey]; diff --git a/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts b/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts index 2761b3f7c8..5eaafdaf40 100644 --- a/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts +++ b/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts @@ -2,14 +2,13 @@ import { IPaginationState } from "./usePaginationState"; export interface ILocalPaginationDerivedStateArgs { items: TItem[]; + paginationState: IPaginationState; } export const getLocalPaginationDerivedState = ({ items, paginationState: { pageNumber, itemsPerPage }, -}: ILocalPaginationDerivedStateArgs & { - paginationState: IPaginationState; -}) => { +}: ILocalPaginationDerivedStateArgs) => { const pageStartIndex = (pageNumber - 1) * itemsPerPage; const currentPageItems = items.slice( pageStartIndex, diff --git a/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts b/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts index a9fe8ab1ca..8e654dd5f9 100644 --- a/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts +++ b/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts @@ -9,6 +9,7 @@ export interface ILocalSortDerivedStateArgs< getSortValues?: ( item: TItem ) => Record; + sortState: ISortState; } export const getLocalSortDerivedState = < @@ -18,9 +19,7 @@ export const getLocalSortDerivedState = < items, getSortValues, sortState: { activeSort }, -}: ILocalSortDerivedStateArgs & { - sortState: ISortState; -}) => { +}: ILocalSortDerivedStateArgs) => { if (!getSortValues || !activeSort) { return { sortedItems: items }; } diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 2fc55963f8..5362e9fdd4 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -249,7 +249,7 @@ export type ITableControls< // Combined args for locally-paginated tables // - Used by useLocalTableControls shorthand hook // - Combines args for useTableControlState, getLocalTableControlDerivedState and useTableControlProps, -// omitting args for useTableControlProps that come from return values of useTableControlState and getLocalTableControlDerivedState. +// omitting args for getLocalTableControlDerivedState and useTableControlProps that come from return values of useTableControlState and getLocalTableControlDerivedState. export type IUseLocalTableControlsArgs< TItem, TColumnKey extends string, @@ -263,19 +263,19 @@ export type IUseLocalTableControlsArgs< TFilterCategoryKey, TPersistenceKeyPrefix > & - ITableControlLocalDerivedStateArgs< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey - > & Omit< - IUseTableControlPropsArgs< + ITableControlLocalDerivedStateArgs< TItem, TColumnKey, TSortableColumnKey, TFilterCategoryKey - >, + > & + IUseTableControlPropsArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey + >, | keyof ITableControlDerivedState | keyof ITableControlState< TItem, From 4f1e063495b17201ca9ca14ec68824f2bd796aec Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 13:29:34 -0400 Subject: [PATCH 081/102] Forgot a comment Signed-off-by: Mike Turley --- .../hooks/table-controls/active-row/useActiveRowPropHelpers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowPropHelpers.ts b/client/src/app/hooks/table-controls/active-row/useActiveRowPropHelpers.ts index ec0029d256..7f5729e1e9 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowPropHelpers.ts +++ b/client/src/app/hooks/table-controls/active-row/useActiveRowPropHelpers.ts @@ -9,6 +9,7 @@ import { useActiveRowEffects, } from "./useActiveRowEffects"; +// Args that should be passed into useTableControlProps export type IActiveRowPropHelpersExternalArgs = IActiveRowDerivedStateArgs & Omit, "activeRowDerivedState"> & { From 8e06e467dc87d5857e858b38a4151cbeecc688b5 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 13:29:52 -0400 Subject: [PATCH 082/102] Remove comment covered by issue Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/types.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 5362e9fdd4..7d5e087506 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -46,13 +46,6 @@ import { IExpansionPropHelpersExternalArgs } from "./expansion/useExpansionPropH // TFilterCategoryKey - Union type of unique identifier strings for filters (not necessarily the same as column keys) // TPersistenceKeyPrefix - String (must not include a `:` character) used to distinguish persisted state for multiple tables -// TODO when calling useTableControlState, the TItem type is not inferred and some of the params have it inferred as `unknown`. -// this currently doesn't seem to matter since TItem becomes inferred later when currentPageItems is in scope, -// but we should see if we can fix that (maybe not depend on TItem in the extended types here, or find a way -// to pass TItem while still letting the rest of the generics be inferred. -// This may be resolved in a newer TypeScript version after https://github.com/microsoft/TypeScript/pull/54047 is merged! -// See https://github.com/konveyor/tackle2-ui/issues/1456 - export type TableFeature = | "filter" | "sort" From 11324161862b97b33e49015b8b06146a22aeb08f Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 13:32:37 -0400 Subject: [PATCH 083/102] Run prettier in table-controls Signed-off-by: Mike Turley --- .../table-controls/filtering/getFilterHubRequestParams.ts | 4 ++-- client/src/app/hooks/table-controls/getHubRequestParams.ts | 2 +- client/src/app/hooks/table-controls/utils.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts b/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts index 778e50d9ff..1b27bd4b06 100644 --- a/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts +++ b/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts @@ -48,7 +48,7 @@ const pushOrMergeFilter = ( export interface IGetFilterHubRequestParamsArgs< TItem, - TFilterCategoryKey extends string = string + TFilterCategoryKey extends string = string, > { filterState?: IFilterState; filterCategories?: FilterCategory[]; @@ -57,7 +57,7 @@ export interface IGetFilterHubRequestParamsArgs< export const getFilterHubRequestParams = < TItem, - TFilterCategoryKey extends string = string + TFilterCategoryKey extends string = string, >({ filterState, filterCategories, diff --git a/client/src/app/hooks/table-controls/getHubRequestParams.ts b/client/src/app/hooks/table-controls/getHubRequestParams.ts index 16abb96808..fcd94aa83f 100644 --- a/client/src/app/hooks/table-controls/getHubRequestParams.ts +++ b/client/src/app/hooks/table-controls/getHubRequestParams.ts @@ -21,7 +21,7 @@ import { export const getHubRequestParams = < TItem, TSortableColumnKey extends string, - TFilterCategoryKey extends string = string + TFilterCategoryKey extends string = string, >( args: IGetFilterHubRequestParamsArgs & IGetSortHubRequestParamsArgs & diff --git a/client/src/app/hooks/table-controls/utils.ts b/client/src/app/hooks/table-controls/utils.ts index 60b625f506..b34632a025 100644 --- a/client/src/app/hooks/table-controls/utils.ts +++ b/client/src/app/hooks/table-controls/utils.ts @@ -1,7 +1,7 @@ import React from "react"; export const handlePropagatedRowClick = < - E extends React.KeyboardEvent | React.MouseEvent + E extends React.KeyboardEvent | React.MouseEvent, >( event: E | undefined, onRowClick: (event: E) => void From 862c37bc626ea162adab18ee4599310d643bc5cc Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 13:53:17 -0400 Subject: [PATCH 084/102] Attempting to discriminate args for feature prop helpers Signed-off-by: Mike Turley --- .../filtering/getLocalFilterDerivedState.ts | 38 +++++++++---- .../filtering/useFilterPropHelpers.ts | 54 ++++++++++++------- .../getLocalTableControlDerivedState.ts | 2 +- client/src/app/hooks/useLegacyFilterState.ts | 1 + .../pages/migration-waves/migration-waves.tsx | 2 +- 5 files changed, 66 insertions(+), 31 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts index 38f332ec3f..0fde49c0d3 100644 --- a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts +++ b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts @@ -4,24 +4,40 @@ import { } from "@app/components/FilterToolbar"; import { objectKeys } from "@app/utils/utils"; import { IFilterState } from "./useFilterState"; +import { DiscriminatedArgs } from "@app/utils/type-utils"; -export interface ILocalFilterDerivedStateArgs< +export type ILocalFilterDerivedStateArgs< TItem, TFilterCategoryKey extends string, -> { - items: TItem[]; - filterCategories?: FilterCategory[]; - filterState: IFilterState; -} +> = DiscriminatedArgs< + "isFilterEnabled", + { + items: TItem[]; + filterCategories: FilterCategory[]; + filterState: IFilterState; + } +>; + +export type ILocalFilterDerivedState = { + filteredItems: TItem[]; +}; export const getLocalFilterDerivedState = < TItem, TFilterCategoryKey extends string, ->({ - items, - filterCategories = [], - filterState: { filterValues }, -}: ILocalFilterDerivedStateArgs) => { +>( + args: ILocalFilterDerivedStateArgs +) => { + if (!args.isFilterEnabled) { + return { filteredItems: [] }; + } + + const { + items, + filterCategories, + filterState: { filterValues }, + } = args; + const filteredItems = items.filter((item) => objectKeys(filterValues).every((categoryKey) => { const values = filterValues[categoryKey]; diff --git a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts index 1d45794a22..80de072018 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts @@ -5,39 +5,57 @@ import { import { IFilterState } from "./useFilterState"; import { ToolbarProps } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; +import { DiscriminatedArgs } from "@app/utils/type-utils"; // Args that should be passed into useTableControlProps -export interface IFilterPropHelpersExternalArgs< +export type IFilterPropHelpersExternalArgs< TItem, TFilterCategoryKey extends string, -> { - filterState: IFilterState; - filterCategories?: FilterCategory[]; -} +> = DiscriminatedArgs< + "isFilterEnabled", + { + filterState: IFilterState; + filterCategories: FilterCategory[]; + } +>; + +// TODO fix the confusing naming here... we have FilterToolbar and Toolbar which both have filter-related props +export type IFilterPropHelpers = { + filterPropsForToolbar: ToolbarProps; + propsForFilterToolbar: IFilterToolbarProps; +}; export const useFilterPropHelpers = ( args: IFilterPropHelpersExternalArgs -) => { +): IFilterPropHelpers => { const { t } = useTranslation(); + if (!args.isFilterEnabled) { + return { + filterPropsForToolbar: {}, + propsForFilterToolbar: { + filterCategories: [], + filterValues: {}, + setFilterValues: () => {}, + }, + }; + } + const { filterState: { filterValues, setFilterValues }, filterCategories = [], } = args; - const filterPropsForToolbar: ToolbarProps = { - collapseListedFiltersBreakpoint: "xl", - clearAllFilters: () => setFilterValues({}), - clearFiltersButtonText: t("actions.clearAllFilters"), - }; - - const propsForFilterToolbar: IFilterToolbarProps = - { + return { + filterPropsForToolbar: { + collapseListedFiltersBreakpoint: "xl", + clearAllFilters: () => setFilterValues({}), + clearFiltersButtonText: t("actions.clearAllFilters"), + }, + propsForFilterToolbar: { filterCategories, filterValues, setFilterValues, - }; - - // TODO fix the confusing naming here... we have FilterToolbar and Toolbar which both have filter-related props - return { filterPropsForToolbar, propsForFilterToolbar }; + }, + }; }; diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index b7a4d256fa..7c769f9a51 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -31,7 +31,7 @@ export const getLocalTableControlDerivedState = < const { items, isPaginationEnabled = true } = args; const { filteredItems } = getLocalFilterDerivedState({ ...args, - items, + ...(args.isFilterEnabled && { items }), }); const { sortedItems } = getLocalSortDerivedState({ ...args, diff --git a/client/src/app/hooks/useLegacyFilterState.ts b/client/src/app/hooks/useLegacyFilterState.ts index c11db00212..8e24452576 100644 --- a/client/src/app/hooks/useLegacyFilterState.ts +++ b/client/src/app/hooks/useLegacyFilterState.ts @@ -23,6 +23,7 @@ export const useLegacyFilterState = ( filterCategories, }); const { filteredItems } = getLocalFilterDerivedState({ + isFilterEnabled: true, items, filterCategories, filterState: { filterValues, setFilterValues }, diff --git a/client/src/app/pages/migration-waves/migration-waves.tsx b/client/src/app/pages/migration-waves/migration-waves.tsx index a88365a2a2..f7a6c9f32f 100644 --- a/client/src/app/pages/migration-waves/migration-waves.tsx +++ b/client/src/app/pages/migration-waves/migration-waves.tsx @@ -188,7 +188,7 @@ export const MigrationWaves: React.FC = () => { stakeholders: "Stakeholders", status: "Status", }, - isFilterEnabled: true, + // isFilterEnabled: true, isSortEnabled: true, isPaginationEnabled: true, isExpansionEnabled: true, From d030a3d4b750c75e67a2ba7a973b9c299fd35b22 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 13:53:29 -0400 Subject: [PATCH 085/102] Revert "Attempting to discriminate args for feature prop helpers" This reverts commit 8f4f4a59bb36b7a7ea944ea4e3f72277323f9a58. Signed-off-by: Mike Turley --- .../filtering/getLocalFilterDerivedState.ts | 38 ++++--------- .../filtering/useFilterPropHelpers.ts | 54 +++++++------------ .../getLocalTableControlDerivedState.ts | 2 +- client/src/app/hooks/useLegacyFilterState.ts | 1 - .../pages/migration-waves/migration-waves.tsx | 2 +- 5 files changed, 31 insertions(+), 66 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts index 0fde49c0d3..38f332ec3f 100644 --- a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts +++ b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts @@ -4,40 +4,24 @@ import { } from "@app/components/FilterToolbar"; import { objectKeys } from "@app/utils/utils"; import { IFilterState } from "./useFilterState"; -import { DiscriminatedArgs } from "@app/utils/type-utils"; -export type ILocalFilterDerivedStateArgs< +export interface ILocalFilterDerivedStateArgs< TItem, TFilterCategoryKey extends string, -> = DiscriminatedArgs< - "isFilterEnabled", - { - items: TItem[]; - filterCategories: FilterCategory[]; - filterState: IFilterState; - } ->; - -export type ILocalFilterDerivedState = { - filteredItems: TItem[]; -}; +> { + items: TItem[]; + filterCategories?: FilterCategory[]; + filterState: IFilterState; +} export const getLocalFilterDerivedState = < TItem, TFilterCategoryKey extends string, ->( - args: ILocalFilterDerivedStateArgs -) => { - if (!args.isFilterEnabled) { - return { filteredItems: [] }; - } - - const { - items, - filterCategories, - filterState: { filterValues }, - } = args; - +>({ + items, + filterCategories = [], + filterState: { filterValues }, +}: ILocalFilterDerivedStateArgs) => { const filteredItems = items.filter((item) => objectKeys(filterValues).every((categoryKey) => { const values = filterValues[categoryKey]; diff --git a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts index 80de072018..1d45794a22 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts @@ -5,57 +5,39 @@ import { import { IFilterState } from "./useFilterState"; import { ToolbarProps } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; -import { DiscriminatedArgs } from "@app/utils/type-utils"; // Args that should be passed into useTableControlProps -export type IFilterPropHelpersExternalArgs< +export interface IFilterPropHelpersExternalArgs< TItem, TFilterCategoryKey extends string, -> = DiscriminatedArgs< - "isFilterEnabled", - { - filterState: IFilterState; - filterCategories: FilterCategory[]; - } ->; - -// TODO fix the confusing naming here... we have FilterToolbar and Toolbar which both have filter-related props -export type IFilterPropHelpers = { - filterPropsForToolbar: ToolbarProps; - propsForFilterToolbar: IFilterToolbarProps; -}; +> { + filterState: IFilterState; + filterCategories?: FilterCategory[]; +} export const useFilterPropHelpers = ( args: IFilterPropHelpersExternalArgs -): IFilterPropHelpers => { +) => { const { t } = useTranslation(); - if (!args.isFilterEnabled) { - return { - filterPropsForToolbar: {}, - propsForFilterToolbar: { - filterCategories: [], - filterValues: {}, - setFilterValues: () => {}, - }, - }; - } - const { filterState: { filterValues, setFilterValues }, filterCategories = [], } = args; - return { - filterPropsForToolbar: { - collapseListedFiltersBreakpoint: "xl", - clearAllFilters: () => setFilterValues({}), - clearFiltersButtonText: t("actions.clearAllFilters"), - }, - propsForFilterToolbar: { + const filterPropsForToolbar: ToolbarProps = { + collapseListedFiltersBreakpoint: "xl", + clearAllFilters: () => setFilterValues({}), + clearFiltersButtonText: t("actions.clearAllFilters"), + }; + + const propsForFilterToolbar: IFilterToolbarProps = + { filterCategories, filterValues, setFilterValues, - }, - }; + }; + + // TODO fix the confusing naming here... we have FilterToolbar and Toolbar which both have filter-related props + return { filterPropsForToolbar, propsForFilterToolbar }; }; diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index 7c769f9a51..b7a4d256fa 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -31,7 +31,7 @@ export const getLocalTableControlDerivedState = < const { items, isPaginationEnabled = true } = args; const { filteredItems } = getLocalFilterDerivedState({ ...args, - ...(args.isFilterEnabled && { items }), + items, }); const { sortedItems } = getLocalSortDerivedState({ ...args, diff --git a/client/src/app/hooks/useLegacyFilterState.ts b/client/src/app/hooks/useLegacyFilterState.ts index 8e24452576..c11db00212 100644 --- a/client/src/app/hooks/useLegacyFilterState.ts +++ b/client/src/app/hooks/useLegacyFilterState.ts @@ -23,7 +23,6 @@ export const useLegacyFilterState = ( filterCategories, }); const { filteredItems } = getLocalFilterDerivedState({ - isFilterEnabled: true, items, filterCategories, filterState: { filterValues, setFilterValues }, diff --git a/client/src/app/pages/migration-waves/migration-waves.tsx b/client/src/app/pages/migration-waves/migration-waves.tsx index f7a6c9f32f..a88365a2a2 100644 --- a/client/src/app/pages/migration-waves/migration-waves.tsx +++ b/client/src/app/pages/migration-waves/migration-waves.tsx @@ -188,7 +188,7 @@ export const MigrationWaves: React.FC = () => { stakeholders: "Stakeholders", status: "Status", }, - // isFilterEnabled: true, + isFilterEnabled: true, isSortEnabled: true, isPaginationEnabled: true, isExpansionEnabled: true, From 677b657426103b76e470b87851b9540874139d9d Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 14:02:09 -0400 Subject: [PATCH 086/102] Rename files for active-row to active-item Signed-off-by: Mike Turley --- .../getActiveItemDerivedState.ts} | 2 +- client/src/app/hooks/table-controls/active-item/index.ts | 4 ++++ .../useActiveItemEffects.ts} | 4 ++-- .../useActiveItemPropHelpers.ts} | 6 +++--- .../useActiveItemState.ts} | 0 client/src/app/hooks/table-controls/active-row/index.ts | 4 ---- client/src/app/hooks/table-controls/index.ts | 2 +- client/src/app/hooks/table-controls/types.ts | 2 +- client/src/app/hooks/table-controls/useTableControlProps.ts | 2 +- client/src/app/hooks/table-controls/useTableControlState.ts | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) rename client/src/app/hooks/table-controls/{active-row/getActiveRowDerivedState.ts => active-item/getActiveItemDerivedState.ts} (95%) create mode 100644 client/src/app/hooks/table-controls/active-item/index.ts rename client/src/app/hooks/table-controls/{active-row/useActiveRowEffects.ts => active-item/useActiveItemEffects.ts} (85%) rename client/src/app/hooks/table-controls/{active-row/useActiveRowPropHelpers.ts => active-item/useActiveItemPropHelpers.ts} (90%) rename client/src/app/hooks/table-controls/{active-row/useActiveRowState.ts => active-item/useActiveItemState.ts} (100%) delete mode 100644 client/src/app/hooks/table-controls/active-row/index.ts diff --git a/client/src/app/hooks/table-controls/active-row/getActiveRowDerivedState.ts b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts similarity index 95% rename from client/src/app/hooks/table-controls/active-row/getActiveRowDerivedState.ts rename to client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts index a2240c3c8b..508be877fc 100644 --- a/client/src/app/hooks/table-controls/active-row/getActiveRowDerivedState.ts +++ b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts @@ -1,5 +1,5 @@ import { KeyWithValueType } from "@app/utils/type-utils"; -import { IActiveRowState } from "./useActiveRowState"; +import { IActiveRowState } from "./useActiveItemState"; export interface IActiveRowDerivedStateArgs { currentPageItems: TItem[]; diff --git a/client/src/app/hooks/table-controls/active-item/index.ts b/client/src/app/hooks/table-controls/active-item/index.ts new file mode 100644 index 0000000000..afa4d418ff --- /dev/null +++ b/client/src/app/hooks/table-controls/active-item/index.ts @@ -0,0 +1,4 @@ +export * from "./useActiveItemState"; +export * from "./getActiveItemDerivedState"; +export * from "./useActiveItemPropHelpers"; +export * from "./useActiveItemEffects"; diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowEffects.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts similarity index 85% rename from client/src/app/hooks/table-controls/active-row/useActiveRowEffects.ts rename to client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts index b9cee730ff..d03c2b8f0f 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowEffects.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts @@ -1,6 +1,6 @@ import * as React from "react"; -import { getActiveRowDerivedState } from "./getActiveRowDerivedState"; -import { IActiveRowState } from "./useActiveRowState"; +import { getActiveRowDerivedState } from "./getActiveItemDerivedState"; +import { IActiveRowState } from "./useActiveItemState"; export interface IUseActiveRowEffectsArgs { isLoading?: boolean; diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowPropHelpers.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts similarity index 90% rename from client/src/app/hooks/table-controls/active-row/useActiveRowPropHelpers.ts rename to client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts index 7f5729e1e9..718784f5e2 100644 --- a/client/src/app/hooks/table-controls/active-row/useActiveRowPropHelpers.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts @@ -2,12 +2,12 @@ import { TrProps } from "@patternfly/react-table"; import { IActiveRowDerivedStateArgs, getActiveRowDerivedState, -} from "./getActiveRowDerivedState"; -import { IActiveRowState } from "./useActiveRowState"; +} from "./getActiveItemDerivedState"; +import { IActiveRowState } from "./useActiveItemState"; import { IUseActiveRowEffectsArgs, useActiveRowEffects, -} from "./useActiveRowEffects"; +} from "./useActiveItemEffects"; // Args that should be passed into useTableControlProps export type IActiveRowPropHelpersExternalArgs = diff --git a/client/src/app/hooks/table-controls/active-row/useActiveRowState.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts similarity index 100% rename from client/src/app/hooks/table-controls/active-row/useActiveRowState.ts rename to client/src/app/hooks/table-controls/active-item/useActiveItemState.ts diff --git a/client/src/app/hooks/table-controls/active-row/index.ts b/client/src/app/hooks/table-controls/active-row/index.ts deleted file mode 100644 index c60b50340d..0000000000 --- a/client/src/app/hooks/table-controls/active-row/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./useActiveRowState"; -export * from "./getActiveRowDerivedState"; -export * from "./useActiveRowPropHelpers"; -export * from "./useActiveRowEffects"; diff --git a/client/src/app/hooks/table-controls/index.ts b/client/src/app/hooks/table-controls/index.ts index 4c06b16c9d..9145da1f37 100644 --- a/client/src/app/hooks/table-controls/index.ts +++ b/client/src/app/hooks/table-controls/index.ts @@ -9,4 +9,4 @@ export * from "./filtering"; export * from "./sorting"; export * from "./pagination"; export * from "./expansion"; -export * from "./active-row"; +export * from "./active-item"; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 7d5e087506..2f8f5ce989 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -29,7 +29,7 @@ import { IActiveRowPropHelpersExternalArgs, IActiveRowState, IActiveRowStateArgs, -} from "./active-row"; +} from "./active-item"; import { PaginationProps, ToolbarItemProps, diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 8985f5de90..f6fb2dc0fd 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -6,7 +6,7 @@ import { ITableControls, IUseTableControlPropsArgs } from "./types"; import { useFilterPropHelpers } from "./filtering"; import { useSortPropHelpers } from "./sorting"; import { usePaginationPropHelpers } from "./pagination"; -import { useActiveRowPropHelpers } from "./active-row"; +import { useActiveRowPropHelpers } from "./active-item"; import { useExpansionPropHelpers } from "./expansion"; import { handlePropagatedRowClick } from "./utils"; diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 7f873571a1..1f8554f59f 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -7,7 +7,7 @@ import { import { useFilterState } from "./filtering"; import { useSortState } from "./sorting"; import { usePaginationState } from "./pagination"; -import { useActiveRowState } from "./active-row"; +import { useActiveRowState } from "./active-item"; import { useExpansionState } from "./expansion"; export const useTableControlState = < From e30e46938d33ac869c825cb24bd481328333ec7a Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 14:03:58 -0400 Subject: [PATCH 087/102] Rename code for active-row to active-item Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 14 +++---- .../active-item/getActiveItemDerivedState.ts | 36 ++++++++-------- .../active-item/useActiveItemEffects.ts | 28 ++++++------- .../active-item/useActiveItemPropHelpers.ts | 42 +++++++++---------- .../active-item/useActiveItemState.ts | 34 +++++++-------- client/src/app/hooks/table-controls/types.ts | 20 ++++----- .../table-controls/useTableControlProps.ts | 16 +++---- .../table-controls/useTableControlState.ts | 8 ++-- .../applications-table-analyze.tsx | 12 +++--- .../applications-table-assessment.tsx | 14 +++---- .../app/pages/archetypes/archetypes-page.tsx | 8 ++-- .../app/pages/dependencies/dependencies.tsx | 17 ++++---- .../affected-applications.tsx | 10 ++--- 13 files changed, 127 insertions(+), 132 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 445539c691..4200788479 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -300,16 +300,14 @@ Item details can be expanded, either with a "single expansion" variant where an > ⚠️ TECH DEBT NOTE: `getSingleExpandButtonTdProps` and `getCompoundExpandTdProps` should probably be factored out of `useTableControlProps` into a decoupled `getExpansionProps` helper. -### Active Row +### Active Item -An item can be clicked to mark it as "active", which usually opens a drawer on the page to show more detail. Note that this is distinct from expansion and selection and these features can all be used together. Active row state is simply a single id value (number or string) for the active item, derived from the `idProperty` config argument (see [Unique Identifiers](#unique-identifiers)). +A row can be clicked to mark its item as "active", which usually opens a drawer on the page to show more detail. Note that this is distinct from expansion and selection and these features can all be used together. Active item state is simply a single id value (number or string) for the active item, derived from the `idProperty` config argument (see [Unique Identifiers](#unique-identifiers)). -- The active row feature requires no config arguments. -- Active row state is provided by `useActiveRowState`. -- Active row shorthand functions are provided by `getActiveRowDerivedState`. -- A `useEffect` call which prevents invalid state after an item is deleted is provided by `useActiveRowEffects`. - -> ⚠️ TECH DEBT NOTE: We may want to rename the "active row" feature and code to "active item" to be consistent about using "item" naming rather than "row" naming outside the rendering code (see [Item Objects, Not Row Objects](#item-objects-not-row-objects)). +- The active item feature requires no config arguments. +- Active item state is provided by `useActiveItemState`. +- Active item shorthand functions are provided by `getActiveItemDerivedState`. +- A `useEffect` call which prevents invalid state after an item is deleted is provided by `useActiveItemEffects`. ### Selection diff --git a/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts index 508be877fc..19419db817 100644 --- a/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts +++ b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts @@ -1,32 +1,32 @@ import { KeyWithValueType } from "@app/utils/type-utils"; -import { IActiveRowState } from "./useActiveItemState"; +import { IActiveItemState } from "./useActiveItemState"; -export interface IActiveRowDerivedStateArgs { +export interface IActiveItemDerivedStateArgs { currentPageItems: TItem[]; idProperty: KeyWithValueType; - activeRowState: IActiveRowState; + activeItemState: IActiveItemState; } -export interface IActiveRowDerivedState { - activeRowItem: TItem | null; - setActiveRowItem: (item: TItem | null) => void; - clearActiveRow: () => void; - isActiveRowItem: (item: TItem) => boolean; +export interface IActiveItemDerivedState { + activeItem: TItem | null; + setActiveItem: (item: TItem | null) => void; + clearActiveItem: () => void; + isActiveItem: (item: TItem) => boolean; } -// Note: This is not named `getLocalActiveRowDerivedState` because it is always local, +// Note: This is not named `getLocalActiveItemDerivedState` because it is always local, // and it is still used when working with server-managed tables. -export const getActiveRowDerivedState = ({ +export const getActiveItemDerivedState = ({ currentPageItems, idProperty, - activeRowState: { activeRowId, setActiveRowId }, -}: IActiveRowDerivedStateArgs): IActiveRowDerivedState => ({ - activeRowItem: - currentPageItems.find((item) => item[idProperty] === activeRowId) || null, - setActiveRowItem: (item: TItem | null) => { + activeItemState: { activeItemId, setActiveItemId }, +}: IActiveItemDerivedStateArgs): IActiveItemDerivedState => ({ + activeItem: + currentPageItems.find((item) => item[idProperty] === activeItemId) || null, + setActiveItem: (item: TItem | null) => { const itemId = (item?.[idProperty] ?? null) as string | number | null; // TODO Assertion shouldn't be necessary here but TS isn't fully inferring item[idProperty]? - setActiveRowId(itemId); + setActiveItemId(itemId); }, - clearActiveRow: () => setActiveRowId(null), - isActiveRowItem: (item) => item[idProperty] === activeRowId, + clearActiveItem: () => setActiveItemId(null), + isActiveItem: (item) => item[idProperty] === activeItemId, }); diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts index d03c2b8f0f..2bc75fce67 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts @@ -1,23 +1,23 @@ import * as React from "react"; -import { getActiveRowDerivedState } from "./getActiveItemDerivedState"; -import { IActiveRowState } from "./useActiveItemState"; +import { getActiveItemDerivedState } from "./getActiveItemDerivedState"; +import { IActiveItemState } from "./useActiveItemState"; -export interface IUseActiveRowEffectsArgs { +export interface IUseActiveItemEffectsArgs { isLoading?: boolean; - activeRowState: IActiveRowState; - activeRowDerivedState: ReturnType>; + activeItemState: IActiveItemState; + activeItemDerivedState: ReturnType>; } -export const useActiveRowEffects = ({ +export const useActiveItemEffects = ({ isLoading, - activeRowState: { activeRowId }, - activeRowDerivedState: { activeRowItem, clearActiveRow }, -}: IUseActiveRowEffectsArgs) => { + activeItemState: { activeItemId }, + activeItemDerivedState: { activeItem, clearActiveItem }, +}: IUseActiveItemEffectsArgs) => { React.useEffect(() => { - // If some state change (e.g. refetch, pagination) causes the active row to disappear, - // remove its id from state so the drawer won't automatically reopen if the row comes back. - if (!isLoading && activeRowId && !activeRowItem) { - clearActiveRow(); + // If some state change (e.g. refetch, pagination) causes the active item to disappear, + // remove its id from state so the drawer won't automatically reopen if the item comes back. + if (!isLoading && activeItemId && !activeItem) { + clearActiveItem(); } - }, [isLoading, activeRowId, activeRowItem]); + }, [isLoading, activeItemId, activeItem]); }; diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts index 718784f5e2..a346b92771 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts @@ -1,47 +1,47 @@ import { TrProps } from "@patternfly/react-table"; import { - IActiveRowDerivedStateArgs, - getActiveRowDerivedState, + IActiveItemDerivedStateArgs, + getActiveItemDerivedState, } from "./getActiveItemDerivedState"; -import { IActiveRowState } from "./useActiveItemState"; +import { IActiveItemState } from "./useActiveItemState"; import { - IUseActiveRowEffectsArgs, - useActiveRowEffects, + IUseActiveItemEffectsArgs, + useActiveItemEffects, } from "./useActiveItemEffects"; // Args that should be passed into useTableControlProps -export type IActiveRowPropHelpersExternalArgs = - IActiveRowDerivedStateArgs & - Omit, "activeRowDerivedState"> & { +export type IActiveItemPropHelpersExternalArgs = + IActiveItemDerivedStateArgs & + Omit, "activeItemDerivedState"> & { isLoading?: boolean; - activeRowState: IActiveRowState; + activeItemState: IActiveItemState; }; -export const useActiveRowPropHelpers = ( - args: IActiveRowPropHelpersExternalArgs +export const useActiveItemPropHelpers = ( + args: IActiveItemPropHelpersExternalArgs ) => { - const activeRowDerivedState = getActiveRowDerivedState(args); - const { isActiveRowItem, setActiveRowItem, clearActiveRow } = - activeRowDerivedState; + const activeItemDerivedState = getActiveItemDerivedState(args); + const { isActiveItem, setActiveItem, clearActiveItem } = + activeItemDerivedState; - useActiveRowEffects({ ...args, activeRowDerivedState }); + useActiveItemEffects({ ...args, activeItemDerivedState }); - const getActiveRowTrProps = ({ + const getActiveItemTrProps = ({ item, }: { item: TItem; }): Omit => ({ isSelectable: true, isClickable: true, - isRowSelected: item && isActiveRowItem(item), + isRowSelected: item && isActiveItem(item), onRowClick: () => { - if (!isActiveRowItem(item)) { - setActiveRowItem(item); + if (!isActiveItem(item)) { + setActiveItem(item); } else { - clearActiveRow(); + clearActiveItem(); } }, }); - return { activeRowDerivedState, getActiveRowTrProps }; + return { activeItemDerivedState, getActiveItemTrProps }; }; diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts index 4dbcfec409..dab6bd92f1 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts @@ -2,29 +2,29 @@ import { parseMaybeNumericString } from "@app/utils/utils"; import { IFeaturePersistenceArgs } from "../types"; import { usePersistentState } from "@app/hooks/usePersistentState"; -export interface IActiveRowState { - activeRowId: string | number | null; - setActiveRowId: (id: string | number | null) => void; +export interface IActiveItemState { + activeItemId: string | number | null; + setActiveItemId: (id: string | number | null) => void; } -export type IActiveRowStateArgs = { isActiveRowEnabled?: boolean }; +export type IActiveItemStateArgs = { isActiveItemEnabled?: boolean }; -export const useActiveRowState = < +export const useActiveItemState = < TPersistenceKeyPrefix extends string = string, >( - args: IActiveRowStateArgs & + args: IActiveItemStateArgs & IFeaturePersistenceArgs = {} -): IActiveRowState => { - const { isActiveRowEnabled, persistTo, persistenceKeyPrefix } = args; +): IActiveItemState => { + const { isActiveItemEnabled, persistTo, persistenceKeyPrefix } = args; // We won't need to pass the latter two type params here if TS adds support for partial inference. // See https://github.com/konveyor/tackle2-ui/issues/1456 - const [activeRowId, setActiveRowId] = usePersistentState< + const [activeItemId, setActiveItemId] = usePersistentState< string | number | null, TPersistenceKeyPrefix, - "activeRow" + "activeItem" >({ - isEnabled: !!isActiveRowEnabled, + isEnabled: !!isActiveItemEnabled, defaultValue: null, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused @@ -33,18 +33,18 @@ export const useActiveRowState = < ...(persistTo === "urlParams" ? { persistTo, - keys: ["activeRow"], - serialize: (activeRowId) => ({ - activeRow: activeRowId !== null ? String(activeRowId) : null, + keys: ["activeItem"], + serialize: (activeItemId) => ({ + activeItem: activeItemId !== null ? String(activeItemId) : null, }), - deserialize: ({ activeRow }) => parseMaybeNumericString(activeRow), + deserialize: ({ activeItem }) => parseMaybeNumericString(activeItem), } : persistTo === "localStorage" || persistTo === "sessionStorage" ? { persistTo, - key: "activeRow", + key: "activeItem", } : { persistTo }), }); - return { activeRowId, setActiveRowId }; + return { activeItemId, setActiveItemId }; }; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 2f8f5ce989..2cf0cfd561 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -25,10 +25,10 @@ import { IExpansionStateArgs, } from "./expansion"; import { - IActiveRowDerivedState, - IActiveRowPropHelpersExternalArgs, - IActiveRowState, - IActiveRowStateArgs, + IActiveItemDerivedState, + IActiveItemPropHelpersExternalArgs, + IActiveItemState, + IActiveItemStateArgs, } from "./active-item"; import { PaginationProps, @@ -52,7 +52,7 @@ export type TableFeature = | "pagination" | "selection" | "expansion" - | "activeRow"; + | "activeItem"; export type PersistTarget = | "state" @@ -101,7 +101,7 @@ export type IUseTableControlStateArgs< IPaginationStateArgs & { isSelectionEnabled?: boolean; // TODO move this into useSelectionState when we move it from lib-ui } & IExpansionStateArgs & - IActiveRowStateArgs & + IActiveItemStateArgs & ITablePersistenceArgs; // Table-level state object @@ -125,7 +125,7 @@ export type ITableControlState< sortState: ISortState; paginationState: IPaginationState; expansionState: IExpansionState; - activeRowState: IActiveRowState; + activeItemState: IActiveItemState; }; // Table-level local derived state args @@ -142,7 +142,7 @@ export type ITableControlLocalDerivedStateArgs< ILocalSortDerivedStateArgs & ILocalPaginationDerivedStateArgs; // There is no ILocalExpansionDerivedStateArgs type because expansion derived state is always local and internal to useTableControlProps -// There is no ILocalActiveRowDerivedStateArgs type because expansion derived state is always local and internal to useTableControlProps +// There is no ILocalActiveItemDerivedStateArgs type because expansion derived state is always local and internal to useTableControlProps // Table-level derived state object // - Used by useTableControlProps @@ -178,7 +178,7 @@ export type IUseTableControlPropsArgs< IPaginationPropHelpersExternalArgs & // ISelectionPropHelpersExternalArgs // TODO when we move selection from lib-ui IExpansionPropHelpersExternalArgs & - IActiveRowPropHelpersExternalArgs & + IActiveItemPropHelpersExternalArgs & ITableControlDerivedState & { isLoading?: boolean; forceNumRenderedColumns?: number; @@ -208,7 +208,7 @@ export type ITableControls< numColumnsAfterData: number; numRenderedColumns: number; expansionDerivedState: IExpansionDerivedState; - activeRowDerivedState: IActiveRowDerivedState; + activeItemDerivedState: IActiveItemDerivedState; propHelpers: { toolbarProps: Omit; tableProps: Omit; diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index f6fb2dc0fd..13539d7ca5 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -6,7 +6,7 @@ import { ITableControls, IUseTableControlPropsArgs } from "./types"; import { useFilterPropHelpers } from "./filtering"; import { useSortPropHelpers } from "./sorting"; import { usePaginationPropHelpers } from "./pagination"; -import { useActiveRowPropHelpers } from "./active-item"; +import { useActiveItemPropHelpers } from "./active-item"; import { useExpansionPropHelpers } from "./expansion"; import { handlePropagatedRowClick } from "./utils"; @@ -62,7 +62,7 @@ export const useTableControlProps = < isSortEnabled, isSelectionEnabled, isExpansionEnabled, - isActiveRowEnabled, + isActiveItemEnabled, } = args; const columnKeys = objectKeys(columnNames); @@ -90,8 +90,8 @@ export const useTableControlProps = < getCompoundExpandTdProps, getExpandedContentTdProps, } = useExpansionPropHelpers({ ...args, columnKeys, numRenderedColumns }); - const { activeRowDerivedState, getActiveRowTrProps } = - useActiveRowPropHelpers(args); + const { activeItemDerivedState, getActiveItemTrProps } = + useActiveItemPropHelpers(args); const toolbarProps: PropHelpers["toolbarProps"] = { className: variant === "compact" ? spacing.pt_0 : "", @@ -120,12 +120,12 @@ export const useTableControlProps = < }); const getTrProps: PropHelpers["getTrProps"] = ({ item, onRowClick }) => { - const activeRowTrProps = getActiveRowTrProps({ item }); + const activeItemTrProps = getActiveItemTrProps({ item }); return { - ...(isActiveRowEnabled && activeRowTrProps), + ...(isActiveItemEnabled && activeItemTrProps), onRowClick: (event) => handlePropagatedRowClick(event, () => { - activeRowTrProps.onRowClick?.(event); + activeItemTrProps.onRowClick?.(event); onRowClick?.(event); }), }; @@ -166,7 +166,7 @@ export const useTableControlProps = < numColumnsAfterData, numRenderedColumns, expansionDerivedState, - activeRowDerivedState, + activeItemDerivedState, propHelpers: { toolbarProps, tableProps, diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 1f8554f59f..537fbd7979 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -7,7 +7,7 @@ import { import { useFilterState } from "./filtering"; import { useSortState } from "./sorting"; import { usePaginationState } from "./pagination"; -import { useActiveRowState } from "./active-item"; +import { useActiveItemState } from "./active-item"; import { useExpansionState } from "./expansion"; export const useTableControlState = < @@ -53,9 +53,9 @@ export const useTableControlState = < ...args, persistTo: getPersistTo("expansion"), }); - const activeRowState = useActiveRowState({ + const activeItemState = useActiveItemState({ ...args, - persistTo: getPersistTo("activeRow"), + persistTo: getPersistTo("activeItem"), }); return { ...args, @@ -63,6 +63,6 @@ export const useTableControlState = < sortState, paginationState, expansionState, - activeRowState, + activeItemState, }; }; diff --git a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx index 0d093aa27d..49a6685c59 100644 --- a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx +++ b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx @@ -136,7 +136,7 @@ export const ApplicationsTableAnalyze: React.FC = () => { }), variant: "success", }); - clearActiveRow(); + clearActiveItem(); setApplicationsToDelete([]); }; @@ -200,7 +200,7 @@ export const ApplicationsTableAnalyze: React.FC = () => { isSortEnabled: true, isPaginationEnabled: true, isSelectionEnabled: true, - isActiveRowEnabled: true, + isActiveItemEnabled: true, sortableColumns: ["name", "description", "businessService", "tags"], initialSort: { columnKey: "name", direction: "asc" }, getSortValues: (app) => ({ @@ -345,7 +345,7 @@ export const ApplicationsTableAnalyze: React.FC = () => { getTdProps, toolbarBulkSelectorProps, }, - activeRowDerivedState: { activeRowItem, clearActiveRow }, + activeItemDerivedState: { activeItem, clearActiveItem }, selectionState: { selectedItems: selectedRows }, } = tableControls; @@ -754,10 +754,10 @@ export const ApplicationsTableAnalyze: React.FC = () => { paginationProps={paginationProps} /> { }), variant: "success", }); - clearActiveRow(); + clearActiveItem(); setApplicationsToDelete([]); }; @@ -246,7 +246,7 @@ export const ApplicationsTable: React.FC = () => { isFilterEnabled: true, isSortEnabled: true, isPaginationEnabled: true, - isActiveRowEnabled: true, + isActiveItemEnabled: true, sortableColumns: ["name", "description", "businessService", "tags"], initialSort: { columnKey: "name", direction: "asc" }, getSortValues: (app) => ({ @@ -394,7 +394,7 @@ export const ApplicationsTable: React.FC = () => { getTdProps, toolbarBulkSelectorProps, }, - activeRowDerivedState: { activeRowItem, clearActiveRow }, + activeItemDerivedState: { activeItem, clearActiveItem }, selectionState: { selectedItems: selectedRows }, } = tableControls; @@ -738,9 +738,9 @@ export const ApplicationsTable: React.FC = () => { paginationProps={paginationProps} /> { onConfirm={() => { history.push( formatPath(Paths.applicationAssessmentActions, { - applicationId: activeRowItem?.id, + applicationId: activeItem?.id, }) ); setArchetypeRefsToOverride(null); diff --git a/client/src/app/pages/archetypes/archetypes-page.tsx b/client/src/app/pages/archetypes/archetypes-page.tsx index 2a298f897b..0f78ade644 100644 --- a/client/src/app/pages/archetypes/archetypes-page.tsx +++ b/client/src/app/pages/archetypes/archetypes-page.tsx @@ -117,7 +117,7 @@ const Archetypes: React.FC = () => { isFilterEnabled: true, isSortEnabled: true, isPaginationEnabled: true, - isActiveRowEnabled: true, + isActiveItemEnabled: true, filterCategories: [ { @@ -154,7 +154,7 @@ const Archetypes: React.FC = () => { getTrProps, getTdProps, }, - activeRowDerivedState: { activeRowItem, clearActiveRow }, + activeItemDerivedState: { activeItem, clearActiveItem }, } = tableControls; // TODO: RBAC access checks need to be added. Only Architect (and Administrator) personas @@ -344,8 +344,8 @@ const Archetypes: React.FC = () => { {/* Create modal */} diff --git a/client/src/app/pages/dependencies/dependencies.tsx b/client/src/app/pages/dependencies/dependencies.tsx index 61dce2c656..8add1b5fbe 100644 --- a/client/src/app/pages/dependencies/dependencies.tsx +++ b/client/src/app/pages/dependencies/dependencies.tsx @@ -52,7 +52,7 @@ export const Dependencies: React.FC = () => { isFilterEnabled: true, isSortEnabled: true, isPaginationEnabled: true, - isActiveRowEnabled: true, + isActiveItemEnabled: true, sortableColumns: ["name", "foundIn", "labels"], initialSort: { columnKey: "name", direction: "asc" }, filterCategories: [ @@ -144,7 +144,7 @@ export const Dependencies: React.FC = () => { getTrProps, getTdProps, }, - activeRowDerivedState: { activeRowItem, clearActiveRow, setActiveRowItem }, + activeItemDerivedState: { activeItem, clearActiveItem, setActiveItem }, } = tableControls; return ( @@ -211,13 +211,10 @@ export const Dependencies: React.FC = () => { className={spacing.pl_0} variant="link" onClick={(_) => { - if ( - activeRowItem && - activeRowItem === dependency - ) { - clearActiveRow(); + if (activeItem && activeItem === dependency) { + clearActiveItem(); } else { - setActiveRowItem(dependency); + setActiveItem(dependency); } }} > @@ -261,8 +258,8 @@ export const Dependencies: React.FC = () => { setActiveRowItem(null)} + dependency={activeItem || null} + onCloseClick={() => setActiveItem(null)} > ); diff --git a/client/src/app/pages/issues/affected-applications/affected-applications.tsx b/client/src/app/pages/issues/affected-applications/affected-applications.tsx index 0908662fa2..3ba53b85d9 100644 --- a/client/src/app/pages/issues/affected-applications/affected-applications.tsx +++ b/client/src/app/pages/issues/affected-applications/affected-applications.tsx @@ -65,7 +65,7 @@ export const AffectedApplications: React.FC = () => { isFilterEnabled: true, isSortEnabled: true, isPaginationEnabled: true, - isActiveRowEnabled: true, + isActiveItemEnabled: true, sortableColumns: ["name", "businessService", "effort", "incidents"], initialSort: { columnKey: "name", direction: "asc" }, filterCategories: useSharedAffectedApplicationFilterCategories(), @@ -126,7 +126,7 @@ export const AffectedApplications: React.FC = () => { getTrProps, getTdProps, }, - activeRowDerivedState: { activeRowItem, clearActiveRow }, + activeItemDerivedState: { activeItem, clearActiveItem }, } = tableControls; return ( @@ -238,9 +238,9 @@ export const AffectedApplications: React.FC = () => { ); From f11008462a46786439568c451ee28f194cc1cd26 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 14:06:09 -0400 Subject: [PATCH 088/102] Remove stale comment about no explicit type Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 4200788479..d672e64710 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -235,8 +235,6 @@ In most cases, you'll only need to use these higher-level hooks and helpers to b - Call `useTableControlProps` and pass it an object including all properties from `tableControlState` along with additional config arguments. Some of these arguments will be derived from your API data, such as `currentPageItems`, `totalItemCount` and `isLoading`. Others are simply passed here rather than above because they are used only for rendering and not required for state management. - The return value (the same `tableControls` object returned by `useLocalTableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. -> ⚠️ TECH DEBT NOTE: The `tableControls` object returned by the higher-level hooks here currently has no explicit type. Its type is inferred from the return values of `useTableControlState` and `useTableControlProps`, which was a choice made to ease the original development. However, this makes it difficult to see what properties are available for table rendering without using `console.log` or reading the source. We probably should add an explicit type interface for this object. - If desired, you can use the lower-level feature-specific hooks (see [Features](#features)) on their own (for example, if you really only need pagination and you're not rendering a full table). However, if you are using more than one or two of them you may want to consider using these higher-level hooks even if you don't need all the features. You can omit the config arguments for any features you don't need and then just don't use the relevant `propHelpers`. ## Features From 186b7414334b20ae3f156fce01348d51c81b50ee Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 16 Oct 2023 18:09:26 -0400 Subject: [PATCH 089/102] Add JSDoc comments for main types file Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/types.ts | 239 +++++++++++++++---- 1 file changed, 199 insertions(+), 40 deletions(-) diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 2cf0cfd561..5f9d57b079 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -46,6 +46,9 @@ import { IExpansionPropHelpersExternalArgs } from "./expansion/useExpansionPropH // TFilterCategoryKey - Union type of unique identifier strings for filters (not necessarily the same as column keys) // TPersistenceKeyPrefix - String (must not include a `:` character) used to distinguish persisted state for multiple tables +/** + * A feature of the table. State related to each feature can be persisted in separate places. + */ export type TableFeature = | "filter" | "sort" @@ -54,40 +57,67 @@ export type TableFeature = | "expansion" | "activeItem"; +/** + * Where to persist state related to a table feature or all table features. + */ export type PersistTarget = | "state" | "urlParams" | "localStorage" | "sessionStorage"; -// Persistence-specific args -// - Extra args needed for useTableControlState and each feature-specific use*State hook for persisting state -// - Does not require any state or query data in scope -// Common +/** + * Common persistence-specific args + * - Extra args needed for useTableControlState and each feature-specific use*State hook for persisting state + */ export type ICommonPersistenceArgs< TPersistenceKeyPrefix extends string = string, > = { + /** + * A short string uniquely identifying a specific table. Automatically prepended to any key used in state persistence (e.g. in a URL parameter or localStorage). + * - Optional: Only omit if this table will not be rendered at the same time as any other tables. + * - Allows multiple tables to be used on the same page with the same PersistTarget. + * - Cannot contain a `:` character since this is used as the delimiter in the prefixed key. + */ persistenceKeyPrefix?: DisallowCharacters; }; -// Feature-level +/** + * Feature-level persistence-specific args + * - Extra args needed for each use[Feature]State hook for persisting state + */ export type IFeaturePersistenceArgs< TPersistenceKeyPrefix extends string = string, > = ICommonPersistenceArgs & { + /** + * Where to persist state related to this feature. + */ persistTo?: PersistTarget; }; -// Table-level +/** + * Table-level persistence-specific args + * - Extra args needed for useTableControlState for persisting state + * - Supports specifying a different PersistTarget for each TableFeature + */ export type ITablePersistenceArgs< TPersistenceKeyPrefix extends string = string, > = ICommonPersistenceArgs & { + /** + * Where to persist state for this table. Can either be a single target for all features or an object mapping individual features to different targets. + */ persistTo?: | PersistTarget | Partial>; }; -// Table-level state args -// - Used by useTableControlState -// - Does not require any state or query data in scope -// - Requires/disallows args based on which features are enabled (see individual [Feature]StateArgs types) +/** + * Table-level state configuration arguments + * - Used by useTableControlState + * - Made up of the combined feature-level state configuration argument objects. + * - Does not require any state or query data in scope + * - Requires/disallows args based on which features are enabled (see individual [Feature]StateArgs types) + * - Properties here are included in the `tableControls` object. + * @see ITableControls + */ export type IUseTableControlStateArgs< TItem, TColumnKey extends string, @@ -95,7 +125,12 @@ export type IUseTableControlStateArgs< TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, > = { - columnNames: Record; // An ordered mapping of unique keys to human-readable column name strings + /** + * An ordered mapping of unique keys to human-readable column name strings. + * - Keys of this object are used as unique identifiers for columns (`columnKey`). + * - Values of this object are rendered in the column headers by default (can be overridden by passing children to ) and used as `dataLabel` for cells in the column. + */ + columnNames: Record; } & IFilterStateArgs & ISortStateArgs & IPaginationStateArgs & { @@ -104,10 +139,15 @@ export type IUseTableControlStateArgs< IActiveItemStateArgs & ITablePersistenceArgs; -// Table-level state object -// - Returned by useTableControlState -// - Contains persisted state for all features -// - Also includes all of useTableControlState's args for convenience, since useTableControlProps requires them along with the state itself +/** + * Table-level state object + * - Returned by useTableControlState + * - Contains persisted state for all table features. + * - Also includes all of useTableControlState's arguments for convenience, since useTableControlProps requires them along with the state itself + * - Note that this only contains the "source of truth" state, generally driven by item ids and containing no references to item objects. This is separate from "derived state" which is computed at render time. + * - Properties here are included in the `tableControls` object. + * @see ITableControls + */ export type ITableControlState< TItem, TColumnKey extends string, @@ -121,18 +161,37 @@ export type ITableControlState< TFilterCategoryKey, TPersistenceKeyPrefix > & { + /** + * State for the filter feature. Returned by useFilterState. + */ filterState: IFilterState; + /** + * State for the sort feature. Returned by useSortState. + */ sortState: ISortState; + /** + * State for the pagination feature. Returned by usePaginationState. + */ paginationState: IPaginationState; + /** + * State for the expansion feature. Returned by usePaginationState. + */ expansionState: IExpansionState; + /** + * State for the active item feature. Returned by useActiveItemState. + */ activeItemState: IActiveItemState; }; -// Table-level local derived state args -// - Used by getLocalTableControlDerivedState (client-side filtering/sorting/pagination) -// - getLocalTableControlDerivedState also requires the return values from useTableControlState -// - Also used indirectly by the useLocalTableControls shorthand -// - Requires state and query data in scope +/** + * Table-level local derived state configuration arguments + * - "Local derived state" refers to the results of client-side filtering/sorting/pagination. This is not used for server-paginated tables. + * - Made up of the combined feature-level local derived state argument objects. + * - Used by getLocalTableControlDerivedState. + * - getLocalTableControlDerivedState also requires the return values from useTableControlState. + * - Also used indirectly by the useLocalTableControls shorthand hook. + * - Requires state and query data in scope. + */ export type ITableControlLocalDerivedStateArgs< TItem, TColumnKey extends string, @@ -144,22 +203,37 @@ export type ITableControlLocalDerivedStateArgs< // There is no ILocalExpansionDerivedStateArgs type because expansion derived state is always local and internal to useTableControlProps // There is no ILocalActiveItemDerivedStateArgs type because expansion derived state is always local and internal to useTableControlProps -// Table-level derived state object -// - Used by useTableControlProps -// - Provided by either: -// - Return values of getLocalTableControlDerivedState (client-side filtering/sorting/pagination) -// - The consumer directly (server-side filtering/sorting/pagination) +/** + * Table-level derived state object + * - "Derived state" here refers to the results of filtering/sorting/pagination performed either on the client or the server. + * - Used by useTableControlProps + * - Provided by either: + * - Return values of getLocalTableControlDerivedState (client-side filtering/sorting/pagination) + * - The consumer directly (server-side filtering/sorting/pagination) + * - Properties here are included in the `tableControls` object. + * @see ITableControls + */ export type ITableControlDerivedState = { + /** + * The items to be rendered on the current page of the table. These items have already been filtered, sorted and paginated. + */ currentPageItems: TItem[]; + /** + * The total number of items in the entire un-filtered, un-paginated table. + */ totalItemCount: number; }; -// Rendering args -// - Used by only useTableControlProps -// - Requires state and query values in scope -// - Combines all args above with the return values of useTableControlState, args used only for rendering, and args derived from either: -// - Server-side filtering/sorting/pagination provided by the consumer -// - getLocalTableControlDerivedState (client-side filtering/sorting/pagination) +/** + * Rendering configuration arguments + * - Used by only useTableControlProps + * - Requires state and query values in scope + * - Combines all args for useTableControlState with the return values of useTableControlState, args used only for rendering, and args derived from either: + * - Server-side filtering/sorting/pagination provided by the consumer + * - getLocalTableControlDerivedState (client-side filtering/sorting/pagination) + * - Properties here are included in the `tableControls` object. + * @see ITableControls + */ export type IUseTableControlPropsArgs< TItem, TColumnKey extends string, @@ -180,17 +254,36 @@ export type IUseTableControlPropsArgs< IExpansionPropHelpersExternalArgs & IActiveItemPropHelpersExternalArgs & ITableControlDerivedState & { + /** + * Whether the table data is loading + */ isLoading?: boolean; + /** + * Override the `numRenderedColumns` value used internally. This should be equal to the colSpan of a cell that takes the full width of the table. + * - Optional: when omitted, the value used is based on the number of `columnNames` and whether features are enabled that insert additional columns (like checkboxes for selection, a kebab for actions, etc). + */ forceNumRenderedColumns?: number; + /** + * The variant of the table. Affects some spacing. Gets included in `propHelpers.tableProps`. + */ variant?: TableProps["variant"]; + /** + * Whether there is a separate column for action buttons/menus at the right side of the table + */ hasActionsColumn?: boolean; - selectionState: ReturnType>; // TODO this won't be included here when selection is part of useTableControlState + /** + * Selection state + * @todo this won't be included here when useSelectionState gets moved from lib-ui. It is separated from the other state temporarily and used only at render time. + */ + selectionState: ReturnType>; }; -// Table controls object -// - The object used for rendering -// - Returned by useTableControlProps -// - Includes all args and return values from earlier hooks in the chain along with propHelpers +/** + * Table controls object + * - The object used for rendering. Includes everything you need to return JSX for your table. + * - Returned by useTableControlProps and useLocalTableControls + * - Includes all args and return values from useTableControlState along with derived state and propHelpers. + */ export type ITableControls< TItem, TColumnKey extends string, @@ -204,45 +297,111 @@ export type ITableControls< TFilterCategoryKey, TPersistenceKeyPrefix > & { + /** + * The number of extra non-data columns that appear before the data in each row. Based on whether selection and single-expansion features are enabled. + */ numColumnsBeforeData: number; + /** + * The number of extra non-data columns that appear after the data in each row. Based on `hasActionsColumn`. + */ numColumnsAfterData: number; + /** + * The total number of columns to be rendered including data and non-data columns. + */ numRenderedColumns: number; + /** + * Values derived at render time from the expansion feature state. Includes helper functions for convenience. + */ expansionDerivedState: IExpansionDerivedState; + /** + * Values derived at render time from the active-item feature state. Includes helper functions for convenience. + */ activeItemDerivedState: IActiveItemDerivedState; + /** + * Prop helpers: where it all comes together. + * These objects and functions provide props for specific PatternFly components in your table derived from the state and arguments above. + * As much of the prop passing as possible is abstracted away via these helpers, which are to be used with spread syntax (e.g. ). + * Any props included here can be overridden by simply passing additional props after spreading the helper onto a component. + */ propHelpers: { + /** + * Props for the Toolbar component. + * Includes spacing based on the table variant and props related to filtering. + */ toolbarProps: Omit; + /** + * Props for the Table component. + */ tableProps: Omit; + /** + * Returns props for the Th component for a specific column. + * Includes default children (column name) and props related to sorting. + */ getThProps: (args: { columnKey: TColumnKey }) => Omit; + /** + * Returns props for the Tr component for a specific data item. + * Includes props related to the active-item feature. + */ getTrProps: (args: { item: TItem; onRowClick?: TrProps["onRowClick"]; }) => Omit; + /** + * Returns props for the Td component for a specific column. + * Includes default `dataLabel` (column name) and props related to compound expansion. + * If this cell is a toggle for a compound-expandable row, pass `isCompoundExpandToggle: true`. + * @param args - `columnKey` is always required. If `isCompoundExpandToggle` is passed, `item` and `rowIndex` are also required. + */ getTdProps: ( args: { columnKey: TColumnKey } & DiscriminatedArgs< "isCompoundExpandToggle", { item: TItem; rowIndex: number } > ) => Omit; + /** + * Props for the FilterToolbar component. + */ filterToolbarProps: IFilterToolbarProps; + /** + * Props for the Pagination component. + */ paginationProps: PaginationProps; + /** + * Props for the ToolbarItem component containing the Pagination component above the table. + */ paginationToolbarItemProps: ToolbarItemProps; + /** + * Props for the ToolbarBulkSelector component. + */ toolbarBulkSelectorProps: IToolbarBulkSelectorProps; + /** + * Returns props for the Td component used as the checkbox cell for each row when using the selection feature. + */ getSelectCheckboxTdProps: (args: { item: TItem; rowIndex: number; }) => Omit; + /** + * Returns props for the Td component used as the expand toggle when using the single-expand variant of the expansion feature. + */ getSingleExpandButtonTdProps: (args: { item: TItem; rowIndex: number; }) => Omit; + /** + * Returns props for the Td component used to contain the expanded content when using the expansion feature. + * The Td rendered with these props should be the only child of its Tr, which should be directly after the Tr of the row being expanded. + * The two Trs for the expandable row and expanded content row should be contained in a Tbody with no other Tr components. + */ getExpandedContentTdProps: (args: { item: TItem }) => Omit; }; }; -// Combined args for locally-paginated tables -// - Used by useLocalTableControls shorthand hook -// - Combines args for useTableControlState, getLocalTableControlDerivedState and useTableControlProps, -// omitting args for getLocalTableControlDerivedState and useTableControlProps that come from return values of useTableControlState and getLocalTableControlDerivedState. +/** + * Combined configuration arguments for client-paginated tables + * - Used by useLocalTableControls shorthand hook + * - Combines args for useTableControlState, getLocalTableControlDerivedState and useTableControlProps, omitting args for any of these that come from return values of the others. + */ export type IUseLocalTableControlsArgs< TItem, TColumnKey extends string, From c7e06cc1d3f4b9290e1f447659a9a13c6016bdba Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 17 Oct 2023 15:23:56 -0400 Subject: [PATCH 090/102] Second pass on jsdocs in main types.ts file, add jsdocs for active-item files Signed-off-by: Mike Turley --- .../active-item/getActiveItemDerivedState.ts | 21 +++++++- .../active-item/useActiveItemEffects.ts | 15 ++++-- .../active-item/useActiveItemPropHelpers.ts | 12 ++++- .../active-item/useActiveItemState.ts | 19 +++++++ client/src/app/hooks/table-controls/types.ts | 54 +++++++++++-------- 5 files changed, 94 insertions(+), 27 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts index 19419db817..48041b1d68 100644 --- a/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts +++ b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts @@ -1,12 +1,24 @@ import { KeyWithValueType } from "@app/utils/type-utils"; import { IActiveItemState } from "./useActiveItemState"; +/** + * Args for getActiveItemDerivedState + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) + * @see ITableControlState + * @see IUseTableControlPropsArgs + */ export interface IActiveItemDerivedStateArgs { currentPageItems: TItem[]; idProperty: KeyWithValueType; activeItemState: IActiveItemState; } +/** + * Derived state for the active item feature + * - "Derived state" here refers to values and convenience functions derived at render time based on the "source of truth" state. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + */ export interface IActiveItemDerivedState { activeItem: TItem | null; setActiveItem: (item: TItem | null) => void; @@ -14,8 +26,13 @@ export interface IActiveItemDerivedState { isActiveItem: (item: TItem) => boolean; } -// Note: This is not named `getLocalActiveItemDerivedState` because it is always local, -// and it is still used when working with server-managed tables. +/** + * Given the "source of truth" state for the active item feature and additional arguments, returns "derived state" values and convenience functions. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * + * NOTE: Unlike for the filter, sort and pagination features, this is not named `getLocalActiveItemDerivedState` because it + * is always local/client-computed, and it is still used when working with server-computed tables (it's not local-specific). + */ export const getActiveItemDerivedState = ({ currentPageItems, idProperty, diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts index 2bc75fce67..905a20cf21 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts @@ -2,22 +2,31 @@ import * as React from "react"; import { getActiveItemDerivedState } from "./getActiveItemDerivedState"; import { IActiveItemState } from "./useActiveItemState"; +/** + * Args for useActiveItemEffects + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) + */ export interface IUseActiveItemEffectsArgs { isLoading?: boolean; activeItemState: IActiveItemState; activeItemDerivedState: ReturnType>; } +/** + * Registers side effects necessary to prevent invalid state related to the active item feature. + * - Used internally by by useActiveItemPropHelpers as part of useTableControlProps + * - The effect: If some state change (e.g. refetch, pagination interaction) causes the active item to disappear, + * remove its id from state so the drawer won't automatically reopen if the item comes back. + */ export const useActiveItemEffects = ({ isLoading, activeItemState: { activeItemId }, activeItemDerivedState: { activeItem, clearActiveItem }, }: IUseActiveItemEffectsArgs) => { React.useEffect(() => { - // If some state change (e.g. refetch, pagination) causes the active item to disappear, - // remove its id from state so the drawer won't automatically reopen if the item comes back. if (!isLoading && activeItemId && !activeItem) { clearActiveItem(); } - }, [isLoading, activeItemId, activeItem]); + }, [isLoading, activeItemId, activeItem]); // TODO fix the exhaustive-deps lint warning here without affecting behavior }; diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts index a346b92771..1aec407212 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts @@ -9,7 +9,11 @@ import { useActiveItemEffects, } from "./useActiveItemEffects"; -// Args that should be passed into useTableControlProps +/** + * Args for useActiveItemPropHelpers that come from outside useTableControlProps + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) + */ export type IActiveItemPropHelpersExternalArgs = IActiveItemDerivedStateArgs & Omit, "activeItemDerivedState"> & { @@ -17,6 +21,12 @@ export type IActiveItemPropHelpersExternalArgs = activeItemState: IActiveItemState; }; +/** + * Given "source of truth" state for the active item feature, returns derived state and `propHelpers`. + * - Used internally by useTableControlProps + * - "Derived state" here refers to values and convenience functions derived at render time. + * - Also triggers side effects to prevent invalid state + */ export const useActiveItemPropHelpers = ( args: IActiveItemPropHelpersExternalArgs ) => { diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts index dab6bd92f1..09c8703a9f 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts @@ -2,13 +2,32 @@ import { parseMaybeNumericString } from "@app/utils/utils"; import { IFeaturePersistenceArgs } from "../types"; import { usePersistentState } from "@app/hooks/usePersistentState"; +/** + * The "source of truth" state for the active item feature. + * - Included in the object returned by useTableControlState (ITableControlState) under the `activeItemState` property. + * - Also included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. + * @see ITableControlState + * @see ITableControls + */ export interface IActiveItemState { activeItemId: string | number | null; setActiveItemId: (id: string | number | null) => void; } +/** + * Base args for useActiveItemState + * - Makes up part of the arguments object taken by useTableControlState (IUseTableControlStateArgs) + * - Properties here are included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. + * @see ITableControls + */ export type IActiveItemStateArgs = { isActiveItemEnabled?: boolean }; +/** + * Provides the "source of truth" state for the active item feature. + * - Used internally by useTableControlState + * - Takes base args defined above as well as optional args for persisting state to a configurable storage target. + * @see PersistTarget + */ export const useActiveItemState = < TPersistenceKeyPrefix extends string = string, >( diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 5f9d57b079..18ea39e3bd 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -45,9 +45,10 @@ import { IExpansionPropHelpersExternalArgs } from "./expansion/useExpansionPropH // TSortableColumnKey - A subset of column keys that have sorting enabled // TFilterCategoryKey - Union type of unique identifier strings for filters (not necessarily the same as column keys) // TPersistenceKeyPrefix - String (must not include a `:` character) used to distinguish persisted state for multiple tables +// TODO move this to DOCS.md and reference the paragraph here /** - * A feature of the table. State related to each feature can be persisted in separate places. + * Identifier for a feature of the table. State concerns are separated by feature. */ export type TableFeature = | "filter" @@ -58,7 +59,11 @@ export type TableFeature = | "activeItem"; /** - * Where to persist state related to a table feature or all table features. + * Identifier for where to persist state for a single table feature or for all table features. + * - "state" (default) - Plain React state. Resets on component unmount or page reload + * - "urlParams" (recommended) - URL query parameters. Persists on page reload, browser history buttons (back/forward) or loading a bookmark. Resets on page navigation. + * - "localStorage" - Browser localStorage API. Persists semi-permanently. Resets only on clearing browser data. + * - "sessionStorage" - Browser sessionStorage API. Persists on page/history navigation/reload. Resets when the tab/window is closed. */ export type PersistTarget = | "state" @@ -68,7 +73,9 @@ export type PersistTarget = /** * Common persistence-specific args - * - Extra args needed for useTableControlState and each feature-specific use*State hook for persisting state + * - Makes up part of the arguments object taken by useTableControlState (IUseTableControlStateArgs) + * - Extra args needed for persisting state both at the table level and in each use[Feature]State hook. + * - Not required if using the default "state" PersistTarget */ export type ICommonPersistenceArgs< TPersistenceKeyPrefix extends string = string, @@ -78,25 +85,29 @@ export type ICommonPersistenceArgs< * - Optional: Only omit if this table will not be rendered at the same time as any other tables. * - Allows multiple tables to be used on the same page with the same PersistTarget. * - Cannot contain a `:` character since this is used as the delimiter in the prefixed key. + * - Should be short, especially when using the "urlParams" PersistTarget. */ persistenceKeyPrefix?: DisallowCharacters; }; /** * Feature-level persistence-specific args - * - Extra args needed for each use[Feature]State hook for persisting state + * - Extra args needed for persisting state in each use[Feature]State hook. + * - Not required if using the default "state" PersistTarget. */ export type IFeaturePersistenceArgs< TPersistenceKeyPrefix extends string = string, > = ICommonPersistenceArgs & { /** - * Where to persist state related to this feature. + * Where to persist state for this feature. */ persistTo?: PersistTarget; }; /** * Table-level persistence-specific args - * - Extra args needed for useTableControlState for persisting state - * - Supports specifying a different PersistTarget for each TableFeature + * - Extra args needed for persisting state at the table level. + * - Supports specifying a single PersistTarget for the whole table or a different PersistTarget for each feature. + * - When using multiple PersistTargets, a `default` target can be passed that will be used for any features not configured explicitly. + * - Not required if using the default "state" PersistTarget. */ export type ITablePersistenceArgs< TPersistenceKeyPrefix extends string = string, @@ -111,11 +122,11 @@ export type ITablePersistenceArgs< /** * Table-level state configuration arguments - * - Used by useTableControlState + * - Taken by useTableControlState * - Made up of the combined feature-level state configuration argument objects. - * - Does not require any state or query data in scope - * - Requires/disallows args based on which features are enabled (see individual [Feature]StateArgs types) - * - Properties here are included in the `tableControls` object. + * - Does not require any state or API data in scope (can be called at the top of your component). + * - Requires/disallows feature-specific args based on `is[Feature]Enabled` booleans via discriminated unions (see individual [Feature]StateArgs types) + * - Properties here are included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. * @see ITableControls */ export type IUseTableControlStateArgs< @@ -142,10 +153,11 @@ export type IUseTableControlStateArgs< /** * Table-level state object * - Returned by useTableControlState - * - Contains persisted state for all table features. - * - Also includes all of useTableControlState's arguments for convenience, since useTableControlProps requires them along with the state itself - * - Note that this only contains the "source of truth" state, generally driven by item ids and containing no references to item objects. This is separate from "derived state" which is computed at render time. - * - Properties here are included in the `tableControls` object. + * - Provides persisted "source of truth" state for all table features. + * - Also includes all of useTableControlState's arguments for convenience, since useTableControlProps requires them along with the state itself. + * - Note that this only contains the "source of truth" state and does not include "derived state" which is computed at render time. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - Properties here are included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. * @see ITableControls */ export type ITableControlState< @@ -190,7 +202,7 @@ export type ITableControlState< * - Used by getLocalTableControlDerivedState. * - getLocalTableControlDerivedState also requires the return values from useTableControlState. * - Also used indirectly by the useLocalTableControls shorthand hook. - * - Requires state and query data in scope. + * - Requires state and API data in scope (or just API data if using useLocalTableControls). */ export type ITableControlLocalDerivedStateArgs< TItem, @@ -206,11 +218,11 @@ export type ITableControlLocalDerivedStateArgs< /** * Table-level derived state object * - "Derived state" here refers to the results of filtering/sorting/pagination performed either on the client or the server. - * - Used by useTableControlProps + * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) * - Provided by either: * - Return values of getLocalTableControlDerivedState (client-side filtering/sorting/pagination) * - The consumer directly (server-side filtering/sorting/pagination) - * - Properties here are included in the `tableControls` object. + * - Properties here are included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. * @see ITableControls */ export type ITableControlDerivedState = { @@ -227,11 +239,11 @@ export type ITableControlDerivedState = { /** * Rendering configuration arguments * - Used by only useTableControlProps - * - Requires state and query values in scope + * - Requires state and API data in scope * - Combines all args for useTableControlState with the return values of useTableControlState, args used only for rendering, and args derived from either: * - Server-side filtering/sorting/pagination provided by the consumer * - getLocalTableControlDerivedState (client-side filtering/sorting/pagination) - * - Properties here are included in the `tableControls` object. + * - Properties here are included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. * @see ITableControls */ export type IUseTableControlPropsArgs< @@ -282,7 +294,7 @@ export type IUseTableControlPropsArgs< * Table controls object * - The object used for rendering. Includes everything you need to return JSX for your table. * - Returned by useTableControlProps and useLocalTableControls - * - Includes all args and return values from useTableControlState along with derived state and propHelpers. + * - Includes all args and return values from useTableControlState and useTableControlProps (configuration, state, derived state and propHelpers). */ export type ITableControls< TItem, From 3094ff93563d31a42d6398ffa37ab03e8cb6fbd8 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 17 Oct 2023 15:55:38 -0400 Subject: [PATCH 091/102] Add jsdocs for higher-level hooks and utils Signed-off-by: Mike Turley --- .../hooks/table-controls/getHubRequestParams.ts | 16 ++++++++++++++++ .../getLocalTableControlDerivedState.ts | 6 ++++++ .../table-controls/useLocalTableControls.ts | 16 ++++++++++++++-- .../hooks/table-controls/useTableControlProps.ts | 11 +++++++++++ .../hooks/table-controls/useTableControlState.ts | 9 +++++++++ client/src/app/hooks/table-controls/utils.ts | 9 +++++++-- 6 files changed, 63 insertions(+), 4 deletions(-) diff --git a/client/src/app/hooks/table-controls/getHubRequestParams.ts b/client/src/app/hooks/table-controls/getHubRequestParams.ts index fcd94aa83f..fc7042dfc9 100644 --- a/client/src/app/hooks/table-controls/getHubRequestParams.ts +++ b/client/src/app/hooks/table-controls/getHubRequestParams.ts @@ -18,6 +18,17 @@ import { serializePaginationRequestParamsForHub, } from "./pagination"; +// TODO move this outside this directory as part of decoupling Konveyor-specific code from table-controls. + +/** + * Returns params required to fetch server-filtered/sorted/paginated data from the hub API. + * - NOTE: This is Konveyor-specific. + * - Takes "source of truth" state for all table features (returned by useTableControlState), + * - Call after useTableControlState and before fetching API data and then calling useTableControlProps. + * - Returns a HubRequestParams object which is structured for easier consumption by other code before the fetch is made. + * @see useTableControlState + * @see useTableControlProps + */ export const getHubRequestParams = < TItem, TSortableColumnKey extends string, @@ -32,6 +43,11 @@ export const getHubRequestParams = < ...getPaginationHubRequestParams(args), }); +/** + * Converts the HubRequestParams object created above into URLSearchParams (the browser API object for URL query parameters). + * - NOTE: This is Konveyor-specific. + * - Used internally by the application's useFetch[Resource] hooks + */ export const serializeRequestParamsForHub = ( deserializedParams: HubRequestParams ): URLSearchParams => { diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index b7a4d256fa..9128b1d598 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -7,6 +7,12 @@ import { ITableControlState, } from "./types"; +/** + * Returns table-level "derived state" (the results of local/client-computed filtering/sorting/pagination) + * - Used internally by the shorthand hook useLocalTableControls. + * - Takes "source of truth" state for all features and additional args. + * @see useLocalTableControls + */ export const getLocalTableControlDerivedState = < TItem, TColumnKey extends string, diff --git a/client/src/app/hooks/table-controls/useLocalTableControls.ts b/client/src/app/hooks/table-controls/useLocalTableControls.ts index 18137a27fd..5263561bea 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControls.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControls.ts @@ -1,9 +1,15 @@ import { useTableControlProps } from "./useTableControlProps"; -import { IUseLocalTableControlsArgs } from "./types"; +import { ITableControls, IUseLocalTableControlsArgs } from "./types"; import { getLocalTableControlDerivedState } from "./getLocalTableControlDerivedState"; import { useTableControlState } from "./useTableControlState"; import { useSelectionState } from "@migtools/lib-ui"; +/** + * Provides all state, derived state, side-effects and prop helpers needed to manage a local/client-computed table. + * - Call this and only this if you aren't using server-side filtering/sorting/pagination. + * - "Derived state" here refers to values and convenience functions derived at render time based on the "source of truth" state. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + */ export const useLocalTableControls = < TItem, TColumnKey extends string, @@ -18,7 +24,13 @@ export const useLocalTableControls = < TFilterCategoryKey, TPersistenceKeyPrefix > -) => { +): ITableControls< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix +> => { const state = useTableControlState(args); const derivedState = getLocalTableControlDerivedState({ ...args, ...state }); return useTableControlProps({ diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 13539d7ca5..6169d1f491 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -10,6 +10,17 @@ import { useActiveItemPropHelpers } from "./active-item"; import { useExpansionPropHelpers } from "./expansion"; import { handlePropagatedRowClick } from "./utils"; +/** + * Returns derived state and prop helpers for all features. Used to make rendering the table components easier. + * - Takes "source of truth" state and table-level derived state (derived either on the server or in getLocalTableControlDerivedState) + * along with API data and additional args. + * - Also triggers side-effects for some features to prevent invalid state. + * - If you aren't using server-side filtering/sorting/pagination, call this via the shorthand hook useLocalTableControls. + * - If you are using server-side filtering/sorting/pagination, call this last after calling useTableControlState and fetching your API data. + * @see useLocalTableControls + * @see useTableControlState + * @see getLocalTableControlDerivedState + */ export const useTableControlProps = < TItem, TColumnKey extends string, diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 537fbd7979..936dac2dc5 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -10,6 +10,15 @@ import { usePaginationState } from "./pagination"; import { useActiveItemState } from "./active-item"; import { useExpansionState } from "./expansion"; +/** + * Provides the "source of truth" state for all table features. + * - State can be persisted in one or more configurable storage targets, either the same for the entire table or different targets per feature. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - If you aren't using server-side filtering/sorting/pagination, call this via the shorthand hook useLocalTableControls. + * - If you are using server-side filtering/sorting/pagination, call this first before fetching your API data and then calling useTableControlProps. + * @param args + * @returns + */ export const useTableControlState = < TItem, TColumnKey extends string, diff --git a/client/src/app/hooks/table-controls/utils.ts b/client/src/app/hooks/table-controls/utils.ts index b34632a025..3f1da5994d 100644 --- a/client/src/app/hooks/table-controls/utils.ts +++ b/client/src/app/hooks/table-controls/utils.ts @@ -1,13 +1,18 @@ import React from "react"; +/** + * Works around problems caused by event propagation when handling a clickable element that contains other clickable elements. + * - Used internally by useTableControlProps for the active item feature, but is generic and could be used outside tables. + * - When a click event happens within a row, checks if there is a clickable element in between the target node and the row element. + * (For example: checkboxes, buttons or links). + * - Prevents triggering the row click behavior when inner clickable elements or their children are clicked. + */ export const handlePropagatedRowClick = < E extends React.KeyboardEvent | React.MouseEvent, >( event: E | undefined, onRowClick: (event: E) => void ) => { - // Check if there is a clickable element between the event target and the row such as a - // checkbox, button or link. Don't trigger the row click if those are clicked. // This recursive parent check is necessary because the event target could be, // for example, the SVG icon inside a button rather than the button itself. const isClickableElementInTheWay = (element: Element): boolean => { From c613b046e2cdcc9d8df725b5a1416a2615fe8dab Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 18 Oct 2023 16:24:04 -0400 Subject: [PATCH 092/102] Comment Signed-off-by: Mike Turley --- .../table-controls/active-item/getActiveItemDerivedState.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts index 48041b1d68..5471e02dea 100644 --- a/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts +++ b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts @@ -30,8 +30,9 @@ export interface IActiveItemDerivedState { * Given the "source of truth" state for the active item feature and additional arguments, returns "derived state" values and convenience functions. * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. * - * NOTE: Unlike for the filter, sort and pagination features, this is not named `getLocalActiveItemDerivedState` because it - * is always local/client-computed, and it is still used when working with server-computed tables (it's not local-specific). + * NOTE: Unlike `getLocal[Filter|Sort|Pagination]DerivedState`, this is not named `getLocalActiveItemDerivedState` because it + * is always local/client-computed, and it is still used when working with server-computed tables + * (it's not specific to client-only-computed tables like the other `getLocal*DerivedState` functions are). */ export const getActiveItemDerivedState = ({ currentPageItems, From 26704275da3329f3b2ae4f9314481614248a7f3e Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 19 Oct 2023 10:31:34 -0400 Subject: [PATCH 093/102] Revert "Revert "Attempting to discriminate args for feature prop helpers"" This reverts commit 4f9379682870e65648247040ee8a62240d29a905. Signed-off-by: Mike Turley --- .../filtering/getLocalFilterDerivedState.ts | 38 +++++++++---- .../filtering/useFilterPropHelpers.ts | 54 ++++++++++++------- .../getLocalTableControlDerivedState.ts | 2 +- client/src/app/hooks/useLegacyFilterState.ts | 1 + .../pages/migration-waves/migration-waves.tsx | 2 +- 5 files changed, 66 insertions(+), 31 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts index 38f332ec3f..0fde49c0d3 100644 --- a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts +++ b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts @@ -4,24 +4,40 @@ import { } from "@app/components/FilterToolbar"; import { objectKeys } from "@app/utils/utils"; import { IFilterState } from "./useFilterState"; +import { DiscriminatedArgs } from "@app/utils/type-utils"; -export interface ILocalFilterDerivedStateArgs< +export type ILocalFilterDerivedStateArgs< TItem, TFilterCategoryKey extends string, -> { - items: TItem[]; - filterCategories?: FilterCategory[]; - filterState: IFilterState; -} +> = DiscriminatedArgs< + "isFilterEnabled", + { + items: TItem[]; + filterCategories: FilterCategory[]; + filterState: IFilterState; + } +>; + +export type ILocalFilterDerivedState = { + filteredItems: TItem[]; +}; export const getLocalFilterDerivedState = < TItem, TFilterCategoryKey extends string, ->({ - items, - filterCategories = [], - filterState: { filterValues }, -}: ILocalFilterDerivedStateArgs) => { +>( + args: ILocalFilterDerivedStateArgs +) => { + if (!args.isFilterEnabled) { + return { filteredItems: [] }; + } + + const { + items, + filterCategories, + filterState: { filterValues }, + } = args; + const filteredItems = items.filter((item) => objectKeys(filterValues).every((categoryKey) => { const values = filterValues[categoryKey]; diff --git a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts index 1d45794a22..80de072018 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts @@ -5,39 +5,57 @@ import { import { IFilterState } from "./useFilterState"; import { ToolbarProps } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; +import { DiscriminatedArgs } from "@app/utils/type-utils"; // Args that should be passed into useTableControlProps -export interface IFilterPropHelpersExternalArgs< +export type IFilterPropHelpersExternalArgs< TItem, TFilterCategoryKey extends string, -> { - filterState: IFilterState; - filterCategories?: FilterCategory[]; -} +> = DiscriminatedArgs< + "isFilterEnabled", + { + filterState: IFilterState; + filterCategories: FilterCategory[]; + } +>; + +// TODO fix the confusing naming here... we have FilterToolbar and Toolbar which both have filter-related props +export type IFilterPropHelpers = { + filterPropsForToolbar: ToolbarProps; + propsForFilterToolbar: IFilterToolbarProps; +}; export const useFilterPropHelpers = ( args: IFilterPropHelpersExternalArgs -) => { +): IFilterPropHelpers => { const { t } = useTranslation(); + if (!args.isFilterEnabled) { + return { + filterPropsForToolbar: {}, + propsForFilterToolbar: { + filterCategories: [], + filterValues: {}, + setFilterValues: () => {}, + }, + }; + } + const { filterState: { filterValues, setFilterValues }, filterCategories = [], } = args; - const filterPropsForToolbar: ToolbarProps = { - collapseListedFiltersBreakpoint: "xl", - clearAllFilters: () => setFilterValues({}), - clearFiltersButtonText: t("actions.clearAllFilters"), - }; - - const propsForFilterToolbar: IFilterToolbarProps = - { + return { + filterPropsForToolbar: { + collapseListedFiltersBreakpoint: "xl", + clearAllFilters: () => setFilterValues({}), + clearFiltersButtonText: t("actions.clearAllFilters"), + }, + propsForFilterToolbar: { filterCategories, filterValues, setFilterValues, - }; - - // TODO fix the confusing naming here... we have FilterToolbar and Toolbar which both have filter-related props - return { filterPropsForToolbar, propsForFilterToolbar }; + }, + }; }; diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index 9128b1d598..1cbc41c5b2 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -37,7 +37,7 @@ export const getLocalTableControlDerivedState = < const { items, isPaginationEnabled = true } = args; const { filteredItems } = getLocalFilterDerivedState({ ...args, - items, + ...(args.isFilterEnabled && { items }), }); const { sortedItems } = getLocalSortDerivedState({ ...args, diff --git a/client/src/app/hooks/useLegacyFilterState.ts b/client/src/app/hooks/useLegacyFilterState.ts index c11db00212..8e24452576 100644 --- a/client/src/app/hooks/useLegacyFilterState.ts +++ b/client/src/app/hooks/useLegacyFilterState.ts @@ -23,6 +23,7 @@ export const useLegacyFilterState = ( filterCategories, }); const { filteredItems } = getLocalFilterDerivedState({ + isFilterEnabled: true, items, filterCategories, filterState: { filterValues, setFilterValues }, diff --git a/client/src/app/pages/migration-waves/migration-waves.tsx b/client/src/app/pages/migration-waves/migration-waves.tsx index a88365a2a2..f7a6c9f32f 100644 --- a/client/src/app/pages/migration-waves/migration-waves.tsx +++ b/client/src/app/pages/migration-waves/migration-waves.tsx @@ -188,7 +188,7 @@ export const MigrationWaves: React.FC = () => { stakeholders: "Stakeholders", status: "Status", }, - isFilterEnabled: true, + // isFilterEnabled: true, isSortEnabled: true, isPaginationEnabled: true, isExpansionEnabled: true, From 8c0cd5db19b40528ec68bce483916b66993c4a4b Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 19 Oct 2023 10:49:07 -0400 Subject: [PATCH 094/102] Attempting to use satisfies and NarrowedArgs Signed-off-by: Mike Turley --- .../table-controls/useLocalTableControls.ts | 38 +++++++++++++------ .../table-controls/useTableControlProps.ts | 31 +++++++-------- .../table-controls/useTableControlState.ts | 33 ++++++++-------- 3 files changed, 58 insertions(+), 44 deletions(-) diff --git a/client/src/app/hooks/table-controls/useLocalTableControls.ts b/client/src/app/hooks/table-controls/useLocalTableControls.ts index 5263561bea..ad1cfad472 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControls.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControls.ts @@ -11,28 +11,36 @@ import { useSelectionState } from "@migtools/lib-ui"; * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. */ export const useLocalTableControls = < + TNarrowedArgs extends IUseLocalTableControlsArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + >, TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, >( - args: IUseLocalTableControlsArgs< + args: TNarrowedArgs +) => { + const state = useTableControlState< + TNarrowedArgs, TItem, TColumnKey, TSortableColumnKey, TFilterCategoryKey, TPersistenceKeyPrefix - > -): ITableControls< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix -> => { - const state = useTableControlState(args); - const derivedState = getLocalTableControlDerivedState({ ...args, ...state }); + >(args); + const derivedState = getLocalTableControlDerivedState< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + >({ ...args, ...state }); return useTableControlProps({ ...args, ...state, @@ -42,5 +50,11 @@ export const useLocalTableControls = < ...args, isEqual: (a, b) => a[args.idProperty] === b[args.idProperty], }), - }); + }) satisfies ITableControls< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + >; }; diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 6169d1f491..768f9ff65f 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -22,26 +22,21 @@ import { handlePropagatedRowClick } from "./utils"; * @see getLocalTableControlDerivedState */ export const useTableControlProps = < - TItem, - TColumnKey extends string, - TSortableColumnKey extends TColumnKey, - TFilterCategoryKey extends string = string, - TPersistenceKeyPrefix extends string = string, ->( - args: IUseTableControlPropsArgs< + TNarrowedArgs extends IUseTableControlPropsArgs< TItem, TColumnKey, TSortableColumnKey, TFilterCategoryKey, TPersistenceKeyPrefix - > -): ITableControls< + >, TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix -> => { + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, + TFilterCategoryKey extends string = string, + TPersistenceKeyPrefix extends string = string, +>( + args: TNarrowedArgs +) => { type PropHelpers = ITableControls< TItem, TColumnKey, @@ -192,5 +187,11 @@ export const useTableControlProps = < getSingleExpandButtonTdProps, getExpandedContentTdProps, }, - }; + } satisfies ITableControls< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + >; }; diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 936dac2dc5..53d36c82c3 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -16,30 +16,23 @@ import { useExpansionState } from "./expansion"; * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. * - If you aren't using server-side filtering/sorting/pagination, call this via the shorthand hook useLocalTableControls. * - If you are using server-side filtering/sorting/pagination, call this first before fetching your API data and then calling useTableControlProps. - * @param args - * @returns */ export const useTableControlState = < - TItem, - TColumnKey extends string, - TSortableColumnKey extends TColumnKey, - TFilterCategoryKey extends string = string, - TPersistenceKeyPrefix extends string = string, ->( - args: IUseTableControlStateArgs< + TNarrowedArgs extends IUseTableControlStateArgs< TItem, TColumnKey, TSortableColumnKey, TFilterCategoryKey, TPersistenceKeyPrefix - > -): ITableControlState< + >, TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix -> => { + TColumnKey extends string, + TSortableColumnKey extends TColumnKey, + TFilterCategoryKey extends string = string, + TPersistenceKeyPrefix extends string = string, +>( + args: TNarrowedArgs +) => { const getPersistTo = (feature: TableFeature): PersistTarget | undefined => !args.persistTo || typeof args.persistTo === "string" ? args.persistTo @@ -73,5 +66,11 @@ export const useTableControlState = < paginationState, expansionState, activeItemState, - }; + } satisfies ITableControlState< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + >; }; From 26e89a52004991698a544491471873a7694128ab Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 19 Oct 2023 10:51:57 -0400 Subject: [PATCH 095/102] Revert "Attempting to use satisfies and NarrowedArgs" This reverts commit ba126fa9b0a4bd03466a5610b47faa9604ad11ec. Signed-off-by: Mike Turley --- .../table-controls/useLocalTableControls.ts | 38 ++++++------------- .../table-controls/useTableControlProps.ts | 31 ++++++++------- .../table-controls/useTableControlState.ts | 33 ++++++++-------- 3 files changed, 44 insertions(+), 58 deletions(-) diff --git a/client/src/app/hooks/table-controls/useLocalTableControls.ts b/client/src/app/hooks/table-controls/useLocalTableControls.ts index ad1cfad472..5263561bea 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControls.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControls.ts @@ -11,36 +11,28 @@ import { useSelectionState } from "@migtools/lib-ui"; * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. */ export const useLocalTableControls = < - TNarrowedArgs extends IUseLocalTableControlsArgs< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - >, TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, >( - args: TNarrowedArgs -) => { - const state = useTableControlState< - TNarrowedArgs, - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - >(args); - const derivedState = getLocalTableControlDerivedState< + args: IUseLocalTableControlsArgs< TItem, TColumnKey, TSortableColumnKey, TFilterCategoryKey, TPersistenceKeyPrefix - >({ ...args, ...state }); + > +): ITableControls< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix +> => { + const state = useTableControlState(args); + const derivedState = getLocalTableControlDerivedState({ ...args, ...state }); return useTableControlProps({ ...args, ...state, @@ -50,11 +42,5 @@ export const useLocalTableControls = < ...args, isEqual: (a, b) => a[args.idProperty] === b[args.idProperty], }), - }) satisfies ITableControls< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - >; + }); }; diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 768f9ff65f..6169d1f491 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -22,21 +22,26 @@ import { handlePropagatedRowClick } from "./utils"; * @see getLocalTableControlDerivedState */ export const useTableControlProps = < - TNarrowedArgs extends IUseTableControlPropsArgs< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - >, TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, >( - args: TNarrowedArgs -) => { + args: IUseTableControlPropsArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + > +): ITableControls< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix +> => { type PropHelpers = ITableControls< TItem, TColumnKey, @@ -187,11 +192,5 @@ export const useTableControlProps = < getSingleExpandButtonTdProps, getExpandedContentTdProps, }, - } satisfies ITableControls< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - >; + }; }; diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 53d36c82c3..936dac2dc5 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -16,23 +16,30 @@ import { useExpansionState } from "./expansion"; * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. * - If you aren't using server-side filtering/sorting/pagination, call this via the shorthand hook useLocalTableControls. * - If you are using server-side filtering/sorting/pagination, call this first before fetching your API data and then calling useTableControlProps. + * @param args + * @returns */ export const useTableControlState = < - TNarrowedArgs extends IUseTableControlStateArgs< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - >, TItem, TColumnKey extends string, TSortableColumnKey extends TColumnKey, TFilterCategoryKey extends string = string, TPersistenceKeyPrefix extends string = string, >( - args: TNarrowedArgs -) => { + args: IUseTableControlStateArgs< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix + > +): ITableControlState< + TItem, + TColumnKey, + TSortableColumnKey, + TFilterCategoryKey, + TPersistenceKeyPrefix +> => { const getPersistTo = (feature: TableFeature): PersistTarget | undefined => !args.persistTo || typeof args.persistTo === "string" ? args.persistTo @@ -66,11 +73,5 @@ export const useTableControlState = < paginationState, expansionState, activeItemState, - } satisfies ITableControlState< - TItem, - TColumnKey, - TSortableColumnKey, - TFilterCategoryKey, - TPersistenceKeyPrefix - >; + }; }; From 1b9b16bb3e922ac6c0f9ad1c89b8ce3286aedaac Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 19 Oct 2023 10:52:07 -0400 Subject: [PATCH 096/102] Revert "Revert "Revert "Attempting to discriminate args for feature prop helpers""" This reverts commit bff8e3286b5369f4e60f1b8575a2748a14eae64b. Signed-off-by: Mike Turley --- .../filtering/getLocalFilterDerivedState.ts | 38 ++++--------- .../filtering/useFilterPropHelpers.ts | 54 +++++++------------ .../getLocalTableControlDerivedState.ts | 2 +- client/src/app/hooks/useLegacyFilterState.ts | 1 - .../pages/migration-waves/migration-waves.tsx | 2 +- 5 files changed, 31 insertions(+), 66 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts index 0fde49c0d3..38f332ec3f 100644 --- a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts +++ b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts @@ -4,40 +4,24 @@ import { } from "@app/components/FilterToolbar"; import { objectKeys } from "@app/utils/utils"; import { IFilterState } from "./useFilterState"; -import { DiscriminatedArgs } from "@app/utils/type-utils"; -export type ILocalFilterDerivedStateArgs< +export interface ILocalFilterDerivedStateArgs< TItem, TFilterCategoryKey extends string, -> = DiscriminatedArgs< - "isFilterEnabled", - { - items: TItem[]; - filterCategories: FilterCategory[]; - filterState: IFilterState; - } ->; - -export type ILocalFilterDerivedState = { - filteredItems: TItem[]; -}; +> { + items: TItem[]; + filterCategories?: FilterCategory[]; + filterState: IFilterState; +} export const getLocalFilterDerivedState = < TItem, TFilterCategoryKey extends string, ->( - args: ILocalFilterDerivedStateArgs -) => { - if (!args.isFilterEnabled) { - return { filteredItems: [] }; - } - - const { - items, - filterCategories, - filterState: { filterValues }, - } = args; - +>({ + items, + filterCategories = [], + filterState: { filterValues }, +}: ILocalFilterDerivedStateArgs) => { const filteredItems = items.filter((item) => objectKeys(filterValues).every((categoryKey) => { const values = filterValues[categoryKey]; diff --git a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts index 80de072018..1d45794a22 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts @@ -5,57 +5,39 @@ import { import { IFilterState } from "./useFilterState"; import { ToolbarProps } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; -import { DiscriminatedArgs } from "@app/utils/type-utils"; // Args that should be passed into useTableControlProps -export type IFilterPropHelpersExternalArgs< +export interface IFilterPropHelpersExternalArgs< TItem, TFilterCategoryKey extends string, -> = DiscriminatedArgs< - "isFilterEnabled", - { - filterState: IFilterState; - filterCategories: FilterCategory[]; - } ->; - -// TODO fix the confusing naming here... we have FilterToolbar and Toolbar which both have filter-related props -export type IFilterPropHelpers = { - filterPropsForToolbar: ToolbarProps; - propsForFilterToolbar: IFilterToolbarProps; -}; +> { + filterState: IFilterState; + filterCategories?: FilterCategory[]; +} export const useFilterPropHelpers = ( args: IFilterPropHelpersExternalArgs -): IFilterPropHelpers => { +) => { const { t } = useTranslation(); - if (!args.isFilterEnabled) { - return { - filterPropsForToolbar: {}, - propsForFilterToolbar: { - filterCategories: [], - filterValues: {}, - setFilterValues: () => {}, - }, - }; - } - const { filterState: { filterValues, setFilterValues }, filterCategories = [], } = args; - return { - filterPropsForToolbar: { - collapseListedFiltersBreakpoint: "xl", - clearAllFilters: () => setFilterValues({}), - clearFiltersButtonText: t("actions.clearAllFilters"), - }, - propsForFilterToolbar: { + const filterPropsForToolbar: ToolbarProps = { + collapseListedFiltersBreakpoint: "xl", + clearAllFilters: () => setFilterValues({}), + clearFiltersButtonText: t("actions.clearAllFilters"), + }; + + const propsForFilterToolbar: IFilterToolbarProps = + { filterCategories, filterValues, setFilterValues, - }, - }; + }; + + // TODO fix the confusing naming here... we have FilterToolbar and Toolbar which both have filter-related props + return { filterPropsForToolbar, propsForFilterToolbar }; }; diff --git a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts index 1cbc41c5b2..9128b1d598 100644 --- a/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts +++ b/client/src/app/hooks/table-controls/getLocalTableControlDerivedState.ts @@ -37,7 +37,7 @@ export const getLocalTableControlDerivedState = < const { items, isPaginationEnabled = true } = args; const { filteredItems } = getLocalFilterDerivedState({ ...args, - ...(args.isFilterEnabled && { items }), + items, }); const { sortedItems } = getLocalSortDerivedState({ ...args, diff --git a/client/src/app/hooks/useLegacyFilterState.ts b/client/src/app/hooks/useLegacyFilterState.ts index 8e24452576..c11db00212 100644 --- a/client/src/app/hooks/useLegacyFilterState.ts +++ b/client/src/app/hooks/useLegacyFilterState.ts @@ -23,7 +23,6 @@ export const useLegacyFilterState = ( filterCategories, }); const { filteredItems } = getLocalFilterDerivedState({ - isFilterEnabled: true, items, filterCategories, filterState: { filterValues, setFilterValues }, diff --git a/client/src/app/pages/migration-waves/migration-waves.tsx b/client/src/app/pages/migration-waves/migration-waves.tsx index f7a6c9f32f..a88365a2a2 100644 --- a/client/src/app/pages/migration-waves/migration-waves.tsx +++ b/client/src/app/pages/migration-waves/migration-waves.tsx @@ -188,7 +188,7 @@ export const MigrationWaves: React.FC = () => { stakeholders: "Stakeholders", status: "Status", }, - // isFilterEnabled: true, + isFilterEnabled: true, isSortEnabled: true, isPaginationEnabled: true, isExpansionEnabled: true, From 063ce7394b5c7841200fd771d3a6263d564ce2aa Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 19 Oct 2023 16:56:46 -0400 Subject: [PATCH 097/102] Add JSDoc comments for expansion and filtering features, update active-item JSDocs Signed-off-by: Mike Turley --- .../active-item/getActiveItemDerivedState.ts | 22 ++++++++ .../active-item/useActiveItemEffects.ts | 13 ++++- .../active-item/useActiveItemPropHelpers.ts | 14 ++++- .../active-item/useActiveItemState.ts | 19 +++++-- .../expansion/getExpansionDerivedState.ts | 44 +++++++++++++--- .../expansion/useExpansionPropHelpers.ts | 49 ++++++++++++++++- .../expansion/useExpansionState.ts | 52 ++++++++++++++++--- .../filtering/getFilterHubRequestParams.ts | 34 +++++++++++- .../filtering/getLocalFilterDerivedState.ts | 21 ++++++++ .../hooks/table-controls/filtering/helpers.ts | 9 ++++ .../filtering/useFilterPropHelpers.ts | 26 +++++++++- .../filtering/useFilterState.ts | 37 ++++++++++++- 12 files changed, 313 insertions(+), 27 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts index 5471e02dea..8486f79ad5 100644 --- a/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts +++ b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts @@ -9,8 +9,17 @@ import { IActiveItemState } from "./useActiveItemState"; * @see IUseTableControlPropsArgs */ export interface IActiveItemDerivedStateArgs { + /** + * The current page of API data items after filtering/sorting/pagination + */ currentPageItems: TItem[]; + /** + * The string key/name of a property on the API data item objects that can be used as a unique identifier (string or number) + */ idProperty: KeyWithValueType; + /** + * The "source of truth" state for the active item feature (returned by useActiveItemState) + */ activeItemState: IActiveItemState; } @@ -20,9 +29,22 @@ export interface IActiveItemDerivedStateArgs { * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. */ export interface IActiveItemDerivedState { + /** + * The API data object matching the `activeItemId` in `activeItemState` + */ activeItem: TItem | null; + /** + * Updates the active item (sets `activeItemId` in `activeItemState` to the id of the given item). + * - Pass null to dismiss the active item. + */ setActiveItem: (item: TItem | null) => void; + /** + * Dismisses the active item. Shorthand for setActiveItem(null). + */ clearActiveItem: () => void; + /** + * Returns whether the given item matches the `activeItemId` in `activeItemState`. + */ isActiveItem: (item: TItem) => boolean; } diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts index 905a20cf21..5eef824b97 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts @@ -1,5 +1,5 @@ import * as React from "react"; -import { getActiveItemDerivedState } from "./getActiveItemDerivedState"; +import { IActiveItemDerivedState } from "./getActiveItemDerivedState"; import { IActiveItemState } from "./useActiveItemState"; /** @@ -8,9 +8,18 @@ import { IActiveItemState } from "./useActiveItemState"; * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) */ export interface IUseActiveItemEffectsArgs { + /** + * Whether the table data is loading + */ isLoading?: boolean; + /** + * The "source of truth" state for the active item feature (returned by useActiveItemState) + */ activeItemState: IActiveItemState; - activeItemDerivedState: ReturnType>; + /** + * The "derived state" for the active item feature (returned by getActiveItemDerivedState) + */ + activeItemDerivedState: IActiveItemDerivedState; } /** diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts index 1aec407212..c69776282f 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts @@ -13,19 +13,28 @@ import { * Args for useActiveItemPropHelpers that come from outside useTableControlProps * - Partially satisfied by the object returned by useTableControlState (ITableControlState) * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) + * @see ITableControlState + * @see IUseTableControlPropsArgs */ export type IActiveItemPropHelpersExternalArgs = IActiveItemDerivedStateArgs & Omit, "activeItemDerivedState"> & { + /** + * Whether the table data is loading + */ isLoading?: boolean; + /** + * The "source of truth" state for the active item feature (returned by useActiveItemState) + */ activeItemState: IActiveItemState; }; /** * Given "source of truth" state for the active item feature, returns derived state and `propHelpers`. * - Used internally by useTableControlProps - * - "Derived state" here refers to values and convenience functions derived at render time. * - Also triggers side effects to prevent invalid state + * - "Derived state" here refers to values and convenience functions derived at render time. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. */ export const useActiveItemPropHelpers = ( args: IActiveItemPropHelpersExternalArgs @@ -36,6 +45,9 @@ export const useActiveItemPropHelpers = ( useActiveItemEffects({ ...args, activeItemDerivedState }); + /** + * Returns props for a clickable Tr in a table with the active item feature enabled. Sets or clears the active item when clicked. + */ const getActiveItemTrProps = ({ item, }: { diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts index 09c8703a9f..f0e3f3626f 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts @@ -10,22 +10,35 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; * @see ITableControls */ export interface IActiveItemState { + /** + * The item id (string or number resolved from `item[idProperty]`) of the active item. Null if no item is active. + */ activeItemId: string | number | null; + /** + * Updates the active item by id. Pass null to dismiss the active item. + */ setActiveItemId: (id: string | number | null) => void; } /** - * Base args for useActiveItemState + * Args for useActiveItemState * - Makes up part of the arguments object taken by useTableControlState (IUseTableControlStateArgs) * - Properties here are included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. + * @see IUseTableControlStateArgs * @see ITableControls */ -export type IActiveItemStateArgs = { isActiveItemEnabled?: boolean }; +export type IActiveItemStateArgs = { + /** + * The only arg for this feature is the enabled flag. + * - This does not use DiscriminatedArgs because there are no additional args when the active item feature is enabled. + */ + isActiveItemEnabled?: boolean; +}; /** * Provides the "source of truth" state for the active item feature. * - Used internally by useTableControlState - * - Takes base args defined above as well as optional args for persisting state to a configurable storage target. + * - Takes args defined above as well as optional args for persisting state to a configurable storage target. * @see PersistTarget */ export const useActiveItemState = < diff --git a/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts b/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts index 0be324d296..c55390d2c1 100644 --- a/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts +++ b/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts @@ -1,13 +1,41 @@ import { KeyWithValueType } from "@app/utils/type-utils"; import { IExpansionState } from "./useExpansionState"; +/** + * Args for getExpansionDerivedState + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) + * @see ITableControlState + * @see IUseTableControlPropsArgs + */ export interface IExpansionDerivedStateArgs { + /** + * The string key/name of a property on the API data item objects that can be used as a unique identifier (string or number) + */ idProperty: KeyWithValueType; + /** + * The "source of truth" state for the expansion feature (returned by useExpansionState) + */ expansionState: IExpansionState; } +/** + * Derived state for the expansion feature + * - "Derived state" here refers to values and convenience functions derived at render time based on the "source of truth" state. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + */ export interface IExpansionDerivedState { + /** + * Returns whether a cell or a row is expanded + * - If called with a columnKey, returns whether that column's cell in this row is expanded (for compound-expand) + * - If called without a columnKey, returns whether the entire row or any cell in it is expanded (for both single-expand and compound-expand) + */ isCellExpanded: (item: TItem, columnKey?: TColumnKey) => boolean; + /** + * Set a cell or a row as expanded or collapsed + * - If called with a columnKey, sets that column's cell in this row expanded or collapsed (for compound-expand) + * - If called without a columnKey, sets the entire row as expanded or collapsed (for single-expand) + */ setCellExpanded: (args: { item: TItem; isExpanding?: boolean; @@ -15,8 +43,14 @@ export interface IExpansionDerivedState { }) => void; } -// Note: This is not named `getLocalExpansionDerivedState` because it is always local, -// and it is still used when working with server-managed tables. +/** + * Given the "source of truth" state for the expansion feature and additional arguments, returns "derived state" values and convenience functions. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * + * NOTE: Unlike `getLocal[Filter|Sort|Pagination]DerivedState`, this is not named `getLocalExpansionDerivedState` because it + * is always local/client-computed, and it is still used when working with server-computed tables + * (it's not specific to client-only-computed tables like the other `getLocal*DerivedState` functions are). + */ export const getExpansionDerivedState = ({ idProperty, expansionState: { expandedCells, setExpandedCells }, @@ -24,18 +58,12 @@ export const getExpansionDerivedState = ({ TItem, TColumnKey > => { - // isCellExpanded: - // - If called with a columnKey, returns whether that specific cell is expanded - // - If called without a columnKey, returns whether the row is expanded at all const isCellExpanded = (item: TItem, columnKey?: TColumnKey) => { return columnKey ? expandedCells[String(item[idProperty])] === columnKey : !!expandedCells[String(item[idProperty])]; }; - // setCellExpanded: - // - If called with a columnKey, sets that column expanded or collapsed (use for compound-expand) - // - If called without a columnKey, sets the entire row as expanded or collapsed (use for single-expand) const setCellExpanded = ({ item, isExpanding = true, diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionPropHelpers.ts b/client/src/app/hooks/table-controls/expansion/useExpansionPropHelpers.ts index 8dac553f0e..fc53ad3496 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionPropHelpers.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionPropHelpers.ts @@ -3,22 +3,56 @@ import { IExpansionState } from "./useExpansionState"; import { getExpansionDerivedState } from "./getExpansionDerivedState"; import { TdProps } from "@patternfly/react-table"; -// Args that should be passed into useTableControlProps +/** + * Args for useExpansionPropHelpers that come from outside useTableControlProps + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) + * @see ITableControlState + * @see IUseTableControlPropsArgs + */ export interface IExpansionPropHelpersExternalArgs< TItem, TColumnKey extends string, > { + /** + * An ordered mapping of unique keys to human-readable column name strings. + * - Keys of this object are used as unique identifiers for columns (`columnKey`). + * - Values of this object are rendered in the column headers by default (can be overridden by passing children to ) and used as `dataLabel` for cells in the column. + */ columnNames: Record; + /** + * The string key/name of a property on the API data item objects that can be used as a unique identifier (string or number) + */ idProperty: KeyWithValueType; + /** + * The "source of truth" state for the expansion feature (returned by useExpansionState) + */ expansionState: IExpansionState; } -// Additional args that come from logic inside useTableControlProps +/** + * Additional args for useExpansionPropHelpers that come from logic inside useTableControlProps + * @see useTableControlProps + */ export interface IExpansionPropHelpersInternalArgs { + /** + * The keys of the `columnNames` object (unique keys identifying each column). + */ columnKeys: TColumnKey[]; + /** + * The total number of columns (Td elements that should be rendered in each Tr) + * - Includes data cells (based on the number of `columnKeys`) and non-data cells for enabled features. + * - For use as the colSpan of a cell that spans an entire row. + */ numRenderedColumns: number; } +/** + * Returns derived state and prop helpers for the expansion feature based on given "source of truth" state. + * - Used internally by useTableControlProps + * - "Derived state" here refers to values and convenience functions derived at render time. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + */ export const useExpansionPropHelpers = ( args: IExpansionPropHelpersExternalArgs & IExpansionPropHelpersInternalArgs @@ -34,6 +68,9 @@ export const useExpansionPropHelpers = ( const expansionDerivedState = getExpansionDerivedState(args); const { isCellExpanded, setCellExpanded } = expansionDerivedState; + /** + * Returns props for the Td to the left of the data cells which contains each row's expansion toggle button (only for single-expand). + */ const getSingleExpandButtonTdProps = ({ item, rowIndex, @@ -53,6 +90,9 @@ export const useExpansionPropHelpers = ( }, }); + /** + * Returns props for the Td which is a data cell in an expandable column and functions as an expand toggle (only for compound-expand) + */ const getCompoundExpandTdProps = ({ columnKey, item, @@ -76,6 +116,11 @@ export const useExpansionPropHelpers = ( }, }); + /** + * Returns props for the Td which contains the expanded content below an expandable row (for both single-expand and compound-expand). + * This Td should be rendered as the only cell in a Tr just below the Tr containing the corresponding row. + * The Tr for the row content and the Tr for the expanded content should be the only two children of a Tbody grouping them (one per expandable row). + */ const getExpandedContentTdProps = ({ item, }: { diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index 4352bb3d4d..f22bd3b176 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -3,26 +3,62 @@ import { objectKeys } from "@app/utils/utils"; import { IFeaturePersistenceArgs } from "../types"; import { DiscriminatedArgs } from "@app/utils/type-utils"; -// TExpandedCells maps item[idProperty] values to either: -// - The key of an expanded column in that row, if the table is compound-expandable -// - The `true` literal value (the entire row is expanded), if non-compound-expandable export type TExpandedCells = Record< string, TColumnKey | boolean >; +/** + * The "source of truth" state for the expansion feature. + * - Included in the object returned by useTableControlState (ITableControlState) under the `expansionState` property. + * - Also included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. + * @see ITableControlState + * @see ITableControls + */ export interface IExpansionState { - expandedCells: Record; - setExpandedCells: ( - newExpandedCells: Record - ) => void; + /** + * A map of item ids (strings resolved from `item[idProperty]`) to either: + * - a `columnKey` if that item's row has a compound-expanded cell + * - or a boolean: + * - true if the row is expanded (for single-expand) + * - false if the row and all its cells are collapsed (for both single-expand and compound-expand). + */ + expandedCells: TExpandedCells; + /** + * Updates the `expandedCells` map (replacing the entire map). + * - See `expansionDerivedState` for helper functions to expand/collapse individual cells/rows. + * @see IExpansionDerivedState + */ + setExpandedCells: (newExpandedCells: TExpandedCells) => void; } +/** + * Args for useExpansionState + * - Makes up part of the arguments object taken by useTableControlState (IUseTableControlStateArgs) + * - The properties defined here are only required by useTableControlState if isExpansionEnabled is true (see DiscriminatedArgs) + * - Properties here are included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. + * @see IUseTableControlStateArgs + * @see DiscriminatedArgs + * @see ITableControls + */ export type IExpansionStateArgs = DiscriminatedArgs< "isExpansionEnabled", - { expandableVariant: "single" | "compound" } + { + /** + * Whether to use single-expand or compound-expand behavior + * - "single" for the entire row to be expandable with one toggle. + * - "compound" for multiple cells in a row to be expandable with individual toggles. + */ + expandableVariant: "single" | "compound"; + } >; +/** + * Provides the "source of truth" state for the expansion feature. + * - Used internally by useTableControlState + * - Takes args defined above as well as optional args for persisting state to a configurable storage target. + * @see PersistTarget + */ export const useExpansionState = < TColumnKey extends string, TPersistenceKeyPrefix extends string = string, diff --git a/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts b/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts index 1b27bd4b06..c8a23c919b 100644 --- a/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts +++ b/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts @@ -6,7 +6,11 @@ import { } from "@app/components/FilterToolbar"; import { IFilterState } from "./useFilterState"; -// If we have multiple UI filters using the same hub field, we need to AND them and pass them to the hub as one filter. +/** + * Helper function for getFilterHubRequestParams + * Given a new filter, determines whether there is an existing filter for that hub field and either creates one or merges this filter with the existing one. + * - If we have multiple UI filters using the same hub field, we need to AND them and pass them to the hub as one filter. + */ const pushOrMergeFilter = ( existingFilters: HubFilter[], newFilter: HubFilter @@ -46,15 +50,30 @@ const pushOrMergeFilter = ( } }; +/** + * Args for getFilterHubRequestParams + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + */ export interface IGetFilterHubRequestParamsArgs< TItem, TFilterCategoryKey extends string = string, > { + /** + * The "source of truth" state for the filter feature (returned by useFilterState) + */ filterState?: IFilterState; + /** + * Definitions of the filters to be used (must include `getItemValue` functions for each category when performing filtering locally) + */ filterCategories?: FilterCategory[]; implicitFilters?: HubFilter[]; } +/** + * Given the state for the filter feature and additional arguments, returns params the hub API needs to apply the current filters. + * - Makes up part of the object returned by getHubRequestParams + * @see getHubRequestParams + */ export const getFilterHubRequestParams = < TItem, TFilterCategoryKey extends string = string, @@ -128,9 +147,17 @@ export const getFilterHubRequestParams = < return { filters }; }; +/** + * Helper function for serializeFilterForHub + * - Given a string or number, returns it as a string with quotes (`"`) around it. + * - Adds an escape character before any existing quote (`"`) characters in the string. + */ export const wrapInQuotesAndEscape = (value: string | number): string => `"${String(value).replace('"', '\\"')}"`; +/** + * Converts a single filter object (HubFilter, the higher-level inspectable type) to the query string filter format used by the hub API + */ export const serializeFilterForHub = (filter: HubFilter): string => { const { field, operator, value } = filter; const joinedValue = @@ -144,6 +171,11 @@ export const serializeFilterForHub = (filter: HubFilter): string => { return `${field}${operator}${joinedValue}`; }; +/** + * Converts all HubFilter values to URL query strings and appends them to the given `serializedParams` object for use in the hub API request + * - Constructs part of the object returned by serializeRequestParamsForHub + * @see serializeRequestParamsForHub + */ export const serializeFilterRequestParamsForHub = ( deserializedParams: HubRequestParams, serializedParams: URLSearchParams diff --git a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts index 38f332ec3f..f9e56073c4 100644 --- a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts +++ b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts @@ -5,15 +5,36 @@ import { import { objectKeys } from "@app/utils/utils"; import { IFilterState } from "./useFilterState"; +/** + * Args for getLocalFilterDerivedState + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by getLocalTableControlDerivedState (ITableControlLocalDerivedStateArgs) + * @see ITableControlState + * @see ITableControlLocalDerivedStateArgs + */ export interface ILocalFilterDerivedStateArgs< TItem, TFilterCategoryKey extends string, > { + /** + * The API data items before filtering + */ items: TItem[]; + /** + * Definitions of the filters to be used (must include `getItemValue` functions for each category when performing filtering locally) + */ filterCategories?: FilterCategory[]; + /** + * The "source of truth" state for the filter feature (returned by useFilterState) + */ filterState: IFilterState; } +/** + * Given the "source of truth" state for the filter feature and additional arguments, returns "derived state" values and convenience functions. + * - For local/client-computed tables only. Performs the actual filtering logic, which is done on the server for server-computed tables. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + */ export const getLocalFilterDerivedState = < TItem, TFilterCategoryKey extends string, diff --git a/client/src/app/hooks/table-controls/filtering/helpers.ts b/client/src/app/hooks/table-controls/filtering/helpers.ts index 265da451fa..67811ac629 100644 --- a/client/src/app/hooks/table-controls/filtering/helpers.ts +++ b/client/src/app/hooks/table-controls/filtering/helpers.ts @@ -1,6 +1,10 @@ import { FilterValue, IFilterValues } from "@app/components/FilterToolbar"; import { objectKeys } from "@app/utils/utils"; +/** + * Helper function for useFilterState + * Given a structured filter values object, returns a string to be stored in the feature's PersistTarget (URL params, localStorage, etc). + */ export const serializeFilterUrlParams = ( filterValues: IFilterValues ): { filters?: string | null } => { @@ -21,6 +25,11 @@ export const serializeFilterUrlParams = ( : null, // If there are no filters, remove the filters param from the URL entirely. }; }; + +/** + * Helper function for useFilterState + * Given a string retrieved from the feature's PersistTarget (URL params, localStorage, etc), converts it back to the structured filter values object. + */ export const deserializeFilterUrlParams = < TFilterCategoryKey extends string, >(serializedParams: { diff --git a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts index 1d45794a22..1f01f6a6c3 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts @@ -6,15 +6,33 @@ import { IFilterState } from "./useFilterState"; import { ToolbarProps } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; -// Args that should be passed into useTableControlProps +/** + * Args for useFilterPropHelpers that come from outside useTableControlProps + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) + * @see ITableControlState + * @see IUseTableControlPropsArgs + */ export interface IFilterPropHelpersExternalArgs< TItem, TFilterCategoryKey extends string, > { + /** + * The "source of truth" state for the filter feature (returned by useFilterState) + */ filterState: IFilterState; + /** + * Definitions of the filters to be used (must include `getItemValue` functions for each category when performing filtering locally) + */ filterCategories?: FilterCategory[]; } +/** + * Returns derived state and prop helpers for the filter feature based on given "source of truth" state. + * - Used internally by useTableControlProps + * - "Derived state" here refers to values and convenience functions derived at render time. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + */ export const useFilterPropHelpers = ( args: IFilterPropHelpersExternalArgs ) => { @@ -25,12 +43,18 @@ export const useFilterPropHelpers = ( filterCategories = [], } = args; + /** + * Filter-related props for the PF Toolbar component + */ const filterPropsForToolbar: ToolbarProps = { collapseListedFiltersBreakpoint: "xl", clearAllFilters: () => setFilterValues({}), clearFiltersButtonText: t("actions.clearAllFilters"), }; + /** + * Props for the FilterToolbar component (our component for rendering filters) + */ const propsForFilterToolbar: IFilterToolbarProps = { filterCategories, diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index e32ec195ea..1b2b267d68 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -5,19 +5,54 @@ import { serializeFilterUrlParams } from "./helpers"; import { deserializeFilterUrlParams } from "./helpers"; import { DiscriminatedArgs } from "@app/utils/type-utils"; +/** + * The "source of truth" state for the filter feature. + * - Included in the object returned by useTableControlState (ITableControlState) under the `filterState` property. + * - Also included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. + * @see ITableControlState + * @see ITableControls + */ export interface IFilterState { + /** + * A mapping: + * - from string keys uniquely identifying a filterCategory (inferred from the `key` properties of elements in the `filterCategories` array) + * - to arrays of strings representing the current value(s) of that filter. Single-value filters are stored as an array with one element. + */ filterValues: IFilterValues; + /** + * Updates the `filterValues` mapping. + */ setFilterValues: (values: IFilterValues) => void; } +/** + * Args for useFilterState + * - Makes up part of the arguments object taken by useTableControlState (IUseTableControlStateArgs) + * - The properties defined here are only required by useTableControlState if isFilterEnabled is true (see DiscriminatedArgs) + * - Properties here are included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. + * @see IUseTableControlStateArgs + * @see DiscriminatedArgs + * @see ITableControls + */ export type IFilterStateArgs< TItem, TFilterCategoryKey extends string, > = DiscriminatedArgs< "isFilterEnabled", - { filterCategories: FilterCategory[] } + { + /** + * Definitions of the filters to be used (must include `getItemValue` functions for each category when performing filtering locally) + */ + filterCategories: FilterCategory[]; + } >; +/** + * Provides the "source of truth" state for the filter feature. + * - Used internally by useTableControlState + * - Takes args defined above as well as optional args for persisting state to a configurable storage target. + * @see PersistTarget + */ export const useFilterState = < TItem, TFilterCategoryKey extends string, From d17bcad9ecab496e41ec54ec53092d7b2ff8add6 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 19 Oct 2023 17:39:38 -0400 Subject: [PATCH 098/102] JSDocs for pagination feature Signed-off-by: Mike Turley --- .../active-item/getActiveItemDerivedState.ts | 4 +- .../active-item/useActiveItemEffects.ts | 2 +- .../active-item/useActiveItemPropHelpers.ts | 2 +- .../expansion/getExpansionDerivedState.ts | 4 +- .../expansion/useExpansionPropHelpers.ts | 2 +- .../expansion/useExpansionState.ts | 14 ++++--- .../filtering/getLocalFilterDerivedState.ts | 2 +- .../filtering/useFilterPropHelpers.ts | 2 +- .../getLocalPaginationDerivedState.ts | 18 ++++++++ .../getPaginationHubRequestParams.ts | 12 ++++++ .../pagination/usePaginationEffects.ts | 11 +++++ .../pagination/usePaginationPropHelpers.ts | 20 ++++++++- .../pagination/usePaginationState.ts | 41 ++++++++++++++++++- client/src/app/hooks/table-controls/types.ts | 4 +- .../table-controls/useLocalTableControls.ts | 2 +- .../table-controls/useTableControlState.ts | 2 +- 16 files changed, 122 insertions(+), 20 deletions(-) diff --git a/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts index 8486f79ad5..25747ca141 100644 --- a/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts +++ b/client/src/app/hooks/table-controls/active-item/getActiveItemDerivedState.ts @@ -26,7 +26,7 @@ export interface IActiveItemDerivedStateArgs { /** * Derived state for the active item feature * - "Derived state" here refers to values and convenience functions derived at render time based on the "source of truth" state. - * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. */ export interface IActiveItemDerivedState { /** @@ -50,7 +50,7 @@ export interface IActiveItemDerivedState { /** * Given the "source of truth" state for the active item feature and additional arguments, returns "derived state" values and convenience functions. - * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. * * NOTE: Unlike `getLocal[Filter|Sort|Pagination]DerivedState`, this is not named `getLocalActiveItemDerivedState` because it * is always local/client-computed, and it is still used when working with server-computed tables diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts index 5eef824b97..bbaa2ca8e0 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemEffects.ts @@ -24,7 +24,7 @@ export interface IUseActiveItemEffectsArgs { /** * Registers side effects necessary to prevent invalid state related to the active item feature. - * - Used internally by by useActiveItemPropHelpers as part of useTableControlProps + * - Used internally by useActiveItemPropHelpers as part of useTableControlProps * - The effect: If some state change (e.g. refetch, pagination interaction) causes the active item to disappear, * remove its id from state so the drawer won't automatically reopen if the item comes back. */ diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts index c69776282f..ad7038d936 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemPropHelpers.ts @@ -34,7 +34,7 @@ export type IActiveItemPropHelpersExternalArgs = * - Used internally by useTableControlProps * - Also triggers side effects to prevent invalid state * - "Derived state" here refers to values and convenience functions derived at render time. - * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. */ export const useActiveItemPropHelpers = ( args: IActiveItemPropHelpersExternalArgs diff --git a/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts b/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts index c55390d2c1..5c0511cbd2 100644 --- a/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts +++ b/client/src/app/hooks/table-controls/expansion/getExpansionDerivedState.ts @@ -22,7 +22,7 @@ export interface IExpansionDerivedStateArgs { /** * Derived state for the expansion feature * - "Derived state" here refers to values and convenience functions derived at render time based on the "source of truth" state. - * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. */ export interface IExpansionDerivedState { /** @@ -45,7 +45,7 @@ export interface IExpansionDerivedState { /** * Given the "source of truth" state for the expansion feature and additional arguments, returns "derived state" values and convenience functions. - * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. * * NOTE: Unlike `getLocal[Filter|Sort|Pagination]DerivedState`, this is not named `getLocalExpansionDerivedState` because it * is always local/client-computed, and it is still used when working with server-computed tables diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionPropHelpers.ts b/client/src/app/hooks/table-controls/expansion/useExpansionPropHelpers.ts index fc53ad3496..9292af10fe 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionPropHelpers.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionPropHelpers.ts @@ -51,7 +51,7 @@ export interface IExpansionPropHelpersInternalArgs { * Returns derived state and prop helpers for the expansion feature based on given "source of truth" state. * - Used internally by useTableControlProps * - "Derived state" here refers to values and convenience functions derived at render time. - * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. */ export const useExpansionPropHelpers = ( args: IExpansionPropHelpersExternalArgs & diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index f22bd3b176..ac3a873bbf 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -3,6 +3,13 @@ import { objectKeys } from "@app/utils/utils"; import { IFeaturePersistenceArgs } from "../types"; import { DiscriminatedArgs } from "@app/utils/type-utils"; +/** + * A map of item ids (strings resolved from `item[idProperty]`) to either: + * - a `columnKey` if that item's row has a compound-expanded cell + * - or a boolean: + * - true if the row is expanded (for single-expand) + * - false if the row and all its cells are collapsed (for both single-expand and compound-expand). + */ export type TExpandedCells = Record< string, TColumnKey | boolean @@ -17,11 +24,8 @@ export type TExpandedCells = Record< */ export interface IExpansionState { /** - * A map of item ids (strings resolved from `item[idProperty]`) to either: - * - a `columnKey` if that item's row has a compound-expanded cell - * - or a boolean: - * - true if the row is expanded (for single-expand) - * - false if the row and all its cells are collapsed (for both single-expand and compound-expand). + * A map of item ids to a `columnKey` or boolean for the current expansion state of that cell/row + * @see TExpandedCells */ expandedCells: TExpandedCells; /** diff --git a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts index f9e56073c4..261a4891a0 100644 --- a/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts +++ b/client/src/app/hooks/table-controls/filtering/getLocalFilterDerivedState.ts @@ -33,7 +33,7 @@ export interface ILocalFilterDerivedStateArgs< /** * Given the "source of truth" state for the filter feature and additional arguments, returns "derived state" values and convenience functions. * - For local/client-computed tables only. Performs the actual filtering logic, which is done on the server for server-computed tables. - * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. */ export const getLocalFilterDerivedState = < TItem, diff --git a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts index 1f01f6a6c3..8fff39fcc9 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterPropHelpers.ts @@ -31,7 +31,7 @@ export interface IFilterPropHelpersExternalArgs< * Returns derived state and prop helpers for the filter feature based on given "source of truth" state. * - Used internally by useTableControlProps * - "Derived state" here refers to values and convenience functions derived at render time. - * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. */ export const useFilterPropHelpers = ( args: IFilterPropHelpersExternalArgs diff --git a/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts b/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts index 5eaafdaf40..2aa70536d1 100644 --- a/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts +++ b/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts @@ -1,10 +1,28 @@ import { IPaginationState } from "./usePaginationState"; +/** + * Args for getLocalPaginationDerivedState + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by getLocalTableControlDerivedState (ITableControlLocalDerivedStateArgs) + * @see ITableControlState + * @see ITableControlLocalDerivedStateArgs + */ export interface ILocalPaginationDerivedStateArgs { + /** + * The API data items before pagination + */ items: TItem[]; + /** + * The "source of truth" state for the pagination feature (returned by usePaginationState) + */ paginationState: IPaginationState; } +/** + * Given the "source of truth" state for the pagination feature and additional arguments, returns "derived state" values and convenience functions. + * - For local/client-computed tables only. Performs the actual pagination logic, which is done on the server for server-computed tables. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. + */ export const getLocalPaginationDerivedState = ({ items, paginationState: { pageNumber, itemsPerPage }, diff --git a/client/src/app/hooks/table-controls/pagination/getPaginationHubRequestParams.ts b/client/src/app/hooks/table-controls/pagination/getPaginationHubRequestParams.ts index 168d8813b7..d03bf931c2 100644 --- a/client/src/app/hooks/table-controls/pagination/getPaginationHubRequestParams.ts +++ b/client/src/app/hooks/table-controls/pagination/getPaginationHubRequestParams.ts @@ -1,10 +1,22 @@ import { HubRequestParams } from "@app/api/models"; import { IPaginationState } from "./usePaginationState"; +/** + * Args for getPaginationHubRequestParams + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + */ export interface IGetPaginationHubRequestParamsArgs { + /** + * The "source of truth" state for the pagination feature (returned by usePaginationState) + */ paginationState?: IPaginationState; } +/** + * Given the state for the pagination feature and additional arguments, returns params the hub API needs to apply the current pagination. + * - Makes up part of the object returned by getHubRequestParams + * @see getHubRequestParams + */ export const getPaginationHubRequestParams = ({ paginationState, }: IGetPaginationHubRequestParamsArgs): Partial => { diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationEffects.ts b/client/src/app/hooks/table-controls/pagination/usePaginationEffects.ts index a0376a5e8e..7fe9109c91 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationEffects.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationEffects.ts @@ -1,6 +1,11 @@ import * as React from "react"; import { IPaginationState } from "./usePaginationState"; +/** + * Args for usePaginationEffects + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) + */ export interface IUsePaginationEffectsArgs { isPaginationEnabled?: boolean; paginationState: IPaginationState; @@ -8,6 +13,12 @@ export interface IUsePaginationEffectsArgs { isLoading?: boolean; } +/** + * Registers side effects necessary to prevent invalid state related to the pagination feature. + * - Used internally by usePaginationPropHelpers as part of useTableControlProps + * - The effect: When API data updates, if there are fewer total items and the current page no longer exists + * (e.g. you were on page 11 and now the last page is 10), move to the last page of data. + */ export const usePaginationEffects = ({ isPaginationEnabled, paginationState: { itemsPerPage, pageNumber, setPageNumber }, diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts b/client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts index 2ec12d3542..674861f521 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts @@ -5,12 +5,30 @@ import { usePaginationEffects, } from "./usePaginationEffects"; -// Args that should be passed into useTableControlProps +/** + * Args for usePaginationPropHelpers that come from outside useTableControlProps + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) + * @see ITableControlState + * @see IUseTableControlPropsArgs + */ export type IPaginationPropHelpersExternalArgs = IUsePaginationEffectsArgs & { + /** + * The "source of truth" state for the pagination feature (returned by usePaginationState) + */ paginationState: IPaginationState; + /** + The total number of items in the entire un-filtered, un-paginated table (the size of the entire API collection being tabulated). + */ totalItemCount: number; }; +/** + * Returns derived state and prop helpers for the pagination feature based on given "source of truth" state. + * - Used internally by useTableControlProps + * - "Derived state" here refers to values and convenience functions derived at render time. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. + */ export const usePaginationPropHelpers = ( args: IPaginationPropHelpersExternalArgs ) => { diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index d1cd7e7f3e..0eb9579ef5 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -3,20 +3,59 @@ import { IFeaturePersistenceArgs } from "../types"; import { DiscriminatedArgs } from "@app/utils/type-utils"; export interface IActivePagination { + /** + * The current page number on the user's pagination controls (counting from 1) + */ pageNumber: number; + /** + * The current "items per page" setting on the user's pagination controls (defaults to 10) + */ itemsPerPage: number; } +/** + * The "source of truth" state for the pagination feature. + * - Included in the object returned by useTableControlState (ITableControlState) under the `paginationState` property. + * - Also included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. + * @see ITableControlState + * @see ITableControls + */ export interface IPaginationState extends IActivePagination { + /** + * Updates the current page number on the user's pagination controls (counting from 1) + */ setPageNumber: (pageNumber: number) => void; + /** + * Updates the "items per page" setting on the user's pagination controls (defaults to 10) + */ setItemsPerPage: (numItems: number) => void; } +/** + * Args for usePaginationState + * - Makes up part of the arguments object taken by useTableControlState (IUseTableControlStateArgs) + * - The properties defined here are only required by useTableControlState if isPaginationEnabled is true (see DiscriminatedArgs) + * - Properties here are included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. + * @see IUseTableControlStateArgs + * @see DiscriminatedArgs + * @see ITableControls + */ export type IPaginationStateArgs = DiscriminatedArgs< "isPaginationEnabled", - { initialItemsPerPage?: number } + { + /** + * The initial value of the "items per page" setting on the user's pagination controls (defaults to 10) + */ + initialItemsPerPage?: number; + } >; +/** + * Provides the "source of truth" state for the pagination feature. + * - Used internally by useTableControlState + * - Takes args defined above as well as optional args for persisting state to a configurable storage target. + * @see PersistTarget + */ export const usePaginationState = < TPersistenceKeyPrefix extends string = string, >( diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index 18ea39e3bd..cffa86e0e9 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -156,7 +156,7 @@ export type IUseTableControlStateArgs< * - Provides persisted "source of truth" state for all table features. * - Also includes all of useTableControlState's arguments for convenience, since useTableControlProps requires them along with the state itself. * - Note that this only contains the "source of truth" state and does not include "derived state" which is computed at render time. - * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. * - Properties here are included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. * @see ITableControls */ @@ -231,7 +231,7 @@ export type ITableControlDerivedState = { */ currentPageItems: TItem[]; /** - * The total number of items in the entire un-filtered, un-paginated table. + * The total number of items in the entire un-filtered, un-paginated table (the size of the entire API collection being tabulated). */ totalItemCount: number; }; diff --git a/client/src/app/hooks/table-controls/useLocalTableControls.ts b/client/src/app/hooks/table-controls/useLocalTableControls.ts index 5263561bea..c24d0d70c6 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControls.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControls.ts @@ -8,7 +8,7 @@ import { useSelectionState } from "@migtools/lib-ui"; * Provides all state, derived state, side-effects and prop helpers needed to manage a local/client-computed table. * - Call this and only this if you aren't using server-side filtering/sorting/pagination. * - "Derived state" here refers to values and convenience functions derived at render time based on the "source of truth" state. - * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. */ export const useLocalTableControls = < TItem, diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 936dac2dc5..830a933270 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -13,7 +13,7 @@ import { useExpansionState } from "./expansion"; /** * Provides the "source of truth" state for all table features. * - State can be persisted in one or more configurable storage targets, either the same for the entire table or different targets per feature. - * - "source of truth" (persisted) state and "derived state" are kept separate to prevent state duplication. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. * - If you aren't using server-side filtering/sorting/pagination, call this via the shorthand hook useLocalTableControls. * - If you are using server-side filtering/sorting/pagination, call this first before fetching your API data and then calling useTableControlProps. * @param args From 904acc940067a92fc6a0659f4c48d92057f84b36 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 19 Oct 2023 18:06:40 -0400 Subject: [PATCH 099/102] JSDocs for sort feature Signed-off-by: Mike Turley --- .../filtering/getFilterHubRequestParams.ts | 3 +- .../getLocalPaginationDerivedState.ts | 2 +- .../getPaginationHubRequestParams.ts | 6 +++ .../pagination/usePaginationPropHelpers.ts | 6 +++ .../pagination/usePaginationState.ts | 3 ++ .../sorting/getLocalSortDerivedState.ts | 25 +++++++++++ .../sorting/getSortHubRequestParams.ts | 22 ++++++++++ .../sorting/useSortPropHelpers.ts | 31 ++++++++++++- .../table-controls/sorting/useSortState.ts | 43 +++++++++++++++++++ 9 files changed, 137 insertions(+), 4 deletions(-) diff --git a/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts b/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts index c8a23c919b..78b74fb721 100644 --- a/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts +++ b/client/src/app/hooks/table-controls/filtering/getFilterHubRequestParams.ts @@ -172,7 +172,8 @@ export const serializeFilterForHub = (filter: HubFilter): string => { }; /** - * Converts all HubFilter values to URL query strings and appends them to the given `serializedParams` object for use in the hub API request + * Converts the values returned by getFilterHubRequestParams into the URL query strings expected by the hub API + * - Appends converted URL params to the given `serializedParams` object for use in the hub API request * - Constructs part of the object returned by serializeRequestParamsForHub * @see serializeRequestParamsForHub */ diff --git a/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts b/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts index 2aa70536d1..307155ae3e 100644 --- a/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts +++ b/client/src/app/hooks/table-controls/pagination/getLocalPaginationDerivedState.ts @@ -9,7 +9,7 @@ import { IPaginationState } from "./usePaginationState"; */ export interface ILocalPaginationDerivedStateArgs { /** - * The API data items before pagination + * The API data items before pagination (but after filtering) */ items: TItem[]; /** diff --git a/client/src/app/hooks/table-controls/pagination/getPaginationHubRequestParams.ts b/client/src/app/hooks/table-controls/pagination/getPaginationHubRequestParams.ts index d03bf931c2..8103b2ed93 100644 --- a/client/src/app/hooks/table-controls/pagination/getPaginationHubRequestParams.ts +++ b/client/src/app/hooks/table-controls/pagination/getPaginationHubRequestParams.ts @@ -25,6 +25,12 @@ export const getPaginationHubRequestParams = ({ return { page: { pageNumber, itemsPerPage } }; }; +/** + * Converts the values returned by getPaginationHubRequestParams into the URL query strings expected by the hub API + * - Appends converted URL params to the given `serializedParams` object for use in the hub API request + * - Constructs part of the object returned by serializeRequestParamsForHub + * @see serializeRequestParamsForHub + */ export const serializePaginationRequestParamsForHub = ( deserializedParams: HubRequestParams, serializedParams: URLSearchParams diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts b/client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts index 674861f521..1f1fd178b7 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationPropHelpers.ts @@ -44,6 +44,9 @@ export const usePaginationPropHelpers = ( usePaginationEffects(args); + /** + * Props for the PF Pagination component + */ const paginationProps: PaginationProps = { itemCount: totalItemCount, perPage: itemsPerPage, @@ -55,6 +58,9 @@ export const usePaginationPropHelpers = ( }, }; + /** + * Props for the PF ToolbarItem component which contains the Pagination component + */ const paginationToolbarItemProps: ToolbarItemProps = { variant: "pagination", align: { default: "alignRight" }, diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 0eb9579ef5..6fd87c84b1 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -2,6 +2,9 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; import { IFeaturePersistenceArgs } from "../types"; import { DiscriminatedArgs } from "@app/utils/type-utils"; +/** + * The currently applied pagination parameters + */ export interface IActivePagination { /** * The current page number on the user's pagination controls (counting from 1) diff --git a/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts b/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts index 8e654dd5f9..b63befb5ab 100644 --- a/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts +++ b/client/src/app/hooks/table-controls/sorting/getLocalSortDerivedState.ts @@ -1,17 +1,42 @@ import i18n from "@app/i18n"; import { ISortState } from "./useSortState"; +/** + * Args for getLocalSortDerivedState + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by getLocalTableControlDerivedState (ITableControlLocalDerivedStateArgs) + * @see ITableControlState + * @see ITableControlLocalDerivedStateArgs + */ export interface ILocalSortDerivedStateArgs< TItem, TSortableColumnKey extends string, > { + /** + * The API data items before sorting + */ items: TItem[]; + /** + * A callback function to return, for a given API data item, a record of sortable primitives for that item's sortable columns + * - The record maps: + * - from `columnKey` values (the keys of the `columnNames` object passed to useTableControlState) + * - to easily sorted primitive values (string | number | boolean) for this item's value in that column + */ getSortValues?: ( + // TODO can we require this as non-optional in types that extend this when we know we're configuring a client-computed table? item: TItem ) => Record; + /** + * The "source of truth" state for the sort feature (returned by useSortState) + */ sortState: ISortState; } +/** + * Given the "source of truth" state for the sort feature and additional arguments, returns "derived state" values and convenience functions. + * - For local/client-computed tables only. Performs the actual sorting logic, which is done on the server for server-computed tables. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. + */ export const getLocalSortDerivedState = < TItem, TSortableColumnKey extends string, diff --git a/client/src/app/hooks/table-controls/sorting/getSortHubRequestParams.ts b/client/src/app/hooks/table-controls/sorting/getSortHubRequestParams.ts index ce788b360c..155b053b17 100644 --- a/client/src/app/hooks/table-controls/sorting/getSortHubRequestParams.ts +++ b/client/src/app/hooks/table-controls/sorting/getSortHubRequestParams.ts @@ -1,13 +1,29 @@ import { HubRequestParams } from "@app/api/models"; import { ISortState } from "./useSortState"; +/** + * Args for getSortHubRequestParams + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + */ export interface IGetSortHubRequestParamsArgs< TSortableColumnKey extends string, > { + /** + * The "source of truth" state for the sort feature (returned by usePaginationState) + */ sortState?: ISortState; + /** + * A map of `columnKey` values (keys of the `columnNames` object passed to useTableControlState) to the field keys used by the hub API for sorting on those columns + * - Keys and values in this object will usually be the same, but sometimes we need to present a hub field with a different name/key or have a column that is a composite of multiple hub fields. + */ hubSortFieldKeys?: Record; } +/** + * Given the state for the sort feature and additional arguments, returns params the hub API needs to apply the current sort. + * - Makes up part of the object returned by getHubRequestParams + * @see getHubRequestParams + */ export const getSortHubRequestParams = ({ sortState, hubSortFieldKeys, @@ -22,6 +38,12 @@ export const getSortHubRequestParams = ({ }; }; +/** + * Converts the values returned by getSortHubRequestParams into the URL query strings expected by the hub API + * - Appends converted URL params to the given `serializedParams` object for use in the hub API request + * - Constructs part of the object returned by serializeRequestParamsForHub + * @see serializeRequestParamsForHub + */ export const serializeSortRequestParamsForHub = ( deserializedParams: HubRequestParams, serializedParams: URLSearchParams diff --git a/client/src/app/hooks/table-controls/sorting/useSortPropHelpers.ts b/client/src/app/hooks/table-controls/sorting/useSortPropHelpers.ts index 0cd9968c1f..a8fa132f80 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortPropHelpers.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortPropHelpers.ts @@ -1,20 +1,44 @@ import { ThProps } from "@patternfly/react-table"; import { ISortState } from "./useSortState"; -// Args that should be passed into useTableControlProps +/** + * Args for useSortPropHelpers that come from outside useTableControlProps + * - Partially satisfied by the object returned by useTableControlState (ITableControlState) + * - Makes up part of the arguments object taken by useTableControlProps (IUseTableControlPropsArgs) + * @see ITableControlState + * @see IUseTableControlPropsArgs + */ export interface ISortPropHelpersExternalArgs< TColumnKey extends string, TSortableColumnKey extends TColumnKey, > { + /** + * The "source of truth" state for the sort feature (returned by useSortState) + */ sortState: ISortState; + /** + * The `columnKey` values (keys of the `columnNames` object passed to useTableControlState) corresponding to columns with sorting enabled + */ sortableColumns?: TSortableColumnKey[]; } -// Additional args that come from logic inside useTableControlProps +/** + * Additional args for useSortPropHelpers that come from logic inside useTableControlProps + * @see useTableControlProps + */ export interface ISortPropHelpersInternalArgs { + /** + * The keys of the `columnNames` object passed to useTableControlState (for all columns, not just the sortable ones) + */ columnKeys: TColumnKey[]; } +/** + * Returns derived state and prop helpers for the sort feature based on given "source of truth" state. + * - Used internally by useTableControlProps + * - "Derived state" here refers to values and convenience functions derived at render time. + * - "source of truth" (persisted) state and "derived state" are kept separate to prevent out-of-sync duplicated state. + */ export const useSortPropHelpers = < TColumnKey extends string, TSortableColumnKey extends TColumnKey, @@ -28,6 +52,9 @@ export const useSortPropHelpers = < columnKeys, } = args; + /** + * Returns props for the Th component for a column with sorting enabled. + */ const getSortThProps = ({ columnKey, }: { diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index 4d13d85157..87fc61904c 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -2,25 +2,68 @@ import { DiscriminatedArgs } from "@app/utils/type-utils"; import { IFeaturePersistenceArgs } from ".."; import { usePersistentState } from "@app/hooks/usePersistentState"; +/** + * The currently applied sort parameters + */ export interface IActiveSort { + /** + * The identifier for the currently sorted column (`columnKey` values come from the keys of the `columnNames` object passed to useTableControlState) + */ columnKey: TSortableColumnKey; + /** + * The direction of the currently applied sort (ascending or descending) + */ direction: "asc" | "desc"; } +/** + * The "source of truth" state for the sort feature. + * - Included in the object returned by useTableControlState (ITableControlState) under the `sortState` property. + * - Also included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. + * @see ITableControlState + * @see ITableControls + */ export interface ISortState { + /** + * The currently applied sort column and direction + */ activeSort: IActiveSort | null; + /** + * Updates the currently applied sort column and direction + */ setActiveSort: (sort: IActiveSort) => void; } +/** + * Args for useSortState + * - Makes up part of the arguments object taken by useTableControlState (IUseTableControlStateArgs) + * - The properties defined here are only required by useTableControlState if isSortEnabled is true (see DiscriminatedArgs) + * - Properties here are included in the `ITableControls` object returned by useTableControlProps and useLocalTableControls. + * @see IUseTableControlStateArgs + * @see DiscriminatedArgs + * @see ITableControls + */ export type ISortStateArgs = DiscriminatedArgs< "isSortEnabled", { + /** + * The `columnKey` values (keys of the `columnNames` object passed to useTableControlState) corresponding to columns with sorting enabled + */ sortableColumns: TSortableColumnKey[]; + /** + * The sort column and direction that should be applied by default when the table first loads + */ initialSort?: IActiveSort | null; } >; +/** + * Provides the "source of truth" state for the sort feature. + * - Used internally by useTableControlState + * - Takes args defined above as well as optional args for persisting state to a configurable storage target. + * @see PersistTarget + */ export const useSortState = < TSortableColumnKey extends string, TPersistenceKeyPrefix extends string = string, From 9b471a1df1240cbca9da34da7c5499e015b13103 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 20 Oct 2023 14:23:53 -0400 Subject: [PATCH 100/102] JSDocs for legacy filter/pagination/sort hooks Signed-off-by: Mike Turley --- client/src/app/hooks/useLegacyFilterState.ts | 18 ++++++++++--- .../src/app/hooks/useLegacyPaginationState.ts | 22 +++++++++++++--- client/src/app/hooks/useLegacySortState.ts | 25 ++++++++++++------- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/client/src/app/hooks/useLegacyFilterState.ts b/client/src/app/hooks/useLegacyFilterState.ts index c11db00212..6224418f4a 100644 --- a/client/src/app/hooks/useLegacyFilterState.ts +++ b/client/src/app/hooks/useLegacyFilterState.ts @@ -4,16 +4,26 @@ import { useFilterState, } from "./table-controls/filtering"; -// NOTE: This was refactored to return generic state data and decouple the client-side-filtering piece to another helper function. -// See useFilterState for the new version, which should probably be used instead of this everywhere eventually. -// See useLocalFilterDerivedState and getFilterProps for the pieces that were removed here. - +/** + * @deprecated The return value of useLegacyFilterState which predates table-controls/table-batteries and is deprecated. + * @see useLegacyFilterState + */ export interface IFilterStateHook { filterValues: IFilterValues; setFilterValues: (values: IFilterValues) => void; filteredItems: TItem[]; } +/** + * @deprecated This hook predates table-controls/table-batteries and still hasn't been refactored away from all of our tables. + * This deprecated hook now depends on the table-controls version but wraps it with an API compatible with the legacy usage. + * It was refactored to return generic state data and decouple the client-side-filtering piece to another helper function. + * See useFilterState in table-controls for the new version, which should probably be used instead of this everywhere eventually. + * See getLocalFilterDerivedState and useFilterPropHelpers for the pieces that were removed here. + * @see useFilterState + * @see getLocalFilterDerivedState + * @see getFilterProps + */ export const useLegacyFilterState = ( items: TItem[], filterCategories: FilterCategory[] diff --git a/client/src/app/hooks/useLegacyPaginationState.ts b/client/src/app/hooks/useLegacyPaginationState.ts index d0760a4cc2..90b01d648d 100644 --- a/client/src/app/hooks/useLegacyPaginationState.ts +++ b/client/src/app/hooks/useLegacyPaginationState.ts @@ -6,21 +6,35 @@ import { usePaginationPropHelpers, } from "./table-controls"; -// NOTE: This was refactored to return generic state data and decouple the client-side-pagination piece to another helper function. -// See usePaginationState for the new version, which should probably be used instead of this everywhere eventually. -// See useLocalPaginationDerivedState and getPaginationProps for the pieces that were removed here. - +/** + * @deprecated Args for useLegacyPaginationState which predates table-controls/table-batteries and is deprecated. + * @see useLegacyPaginationState + */ export type PaginationStateProps = Pick< PaginationProps, "itemCount" | "perPage" | "page" | "onSetPage" | "onPerPageSelect" >; +/** + * @deprecated The return value of useLegacyPaginationState which predates table-controls/table-batteries and is deprecated. + * @see useLegacyPaginationState + */ export interface ILegacyPaginationStateHook { currentPageItems: T[]; setPageNumber: (pageNumber: number) => void; paginationProps: PaginationStateProps; } +/** + * @deprecated This hook predates table-controls/table-batteries and still hasn't been refactored away from all of our tables. + * This deprecated hook now depends on the table-controls version but wraps it with an API compatible with the legacy usage. + * It was refactored to return generic state data and decouple the client-side-pagination piece to another helper function. + * See usePaginationState for the new version, which should probably be used instead of this everywhere eventually. + * See getLocalPaginationDerivedState and usePaginationPropHelpers for the pieces that were removed here. + * @see usePaginationState + * @see getLocalPaginationDerivedState + * @see getPaginationProps + */ export const useLegacyPaginationState = ( items: T[], initialItemsPerPage: number diff --git a/client/src/app/hooks/useLegacySortState.ts b/client/src/app/hooks/useLegacySortState.ts index 34909a975f..31a4c8773a 100644 --- a/client/src/app/hooks/useLegacySortState.ts +++ b/client/src/app/hooks/useLegacySortState.ts @@ -2,15 +2,10 @@ import * as React from "react"; import { ISortBy, SortByDirection } from "@patternfly/react-table"; import i18n from "@app/i18n"; -// NOTE: This was refactored to expose an API based on columnKey instead of column index, -// and to return generic state data and decouple the client-side-sorting piece to another helper function. -// See useSortState for the new version, which should probably be used instead of this everywhere eventually. -// See useLocalSortDerivedState and getSortProps for the parts that were removed here. - -// NOTE ALSO: useLegacyFilterState and useLegacyPagination state were able to have their logic factored out -// to reuse the new helpers, but useLegacySortState has to retain its incompatible logic because of -// the switch from using column indexes to columnKeys. - +/** + * @deprecated The return value of useLegacySortState which predates table-controls/table-batteries and is deprecated. + * @see useLegacySortState + */ export interface ILegacySortStateHook { sortBy: ISortBy; onSort: ( @@ -21,6 +16,18 @@ export interface ILegacySortStateHook { sortedItems: T[]; } +/** + * @deprecated This hook predates table-controls/table-batteries and still hasn't been refactored away from all of our tables. + * It was refactored to expose an API based on columnKey instead of column index, and to return generic state data and decouple + * the client-side-sorting piece to another helper function. + * See useSortState in table-controls for the new version, which should probably be used instead of this everywhere eventually. + * NOTE ALSO: useLegacyFilterState and useLegacyPagination state were able to have their logic factored out + * to reuse the new helpers, but useLegacySortState has to retain its incompatible logic because of + * the switch from using column indexes to `columnKey`s. + * @see useSortState + * @see getLocalSortDerivedState + * @see getSortProps + */ export const useLegacySortState = ( items: T[], getSortValues?: (item: T) => (string | number | boolean)[], From ce4a8069f20a1d4aa3684f338e85807a1d8adc83 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 23 Oct 2023 14:17:12 -0400 Subject: [PATCH 101/102] Proofread DOCS.md (up through 'Features' section) and add kitchen-sink example Signed-off-by: Mike Turley --- .../questions-table/questions-table.tsx | 2 +- client/src/app/hooks/table-controls/DOCS.md | 208 ++++++++++++++++-- client/src/app/hooks/table-controls/types.ts | 4 +- .../controls/stakeholders/stakeholders.tsx | 2 +- client/src/app/pages/issues/issues-table.tsx | 2 +- 5 files changed, 198 insertions(+), 20 deletions(-) diff --git a/client/src/app/components/questions-table/questions-table.tsx b/client/src/app/components/questions-table/questions-table.tsx index 3acd2749f1..d1b5495fe1 100644 --- a/client/src/app/components/questions-table/questions-table.tsx +++ b/client/src/app/components/questions-table/questions-table.tsx @@ -65,7 +65,7 @@ const QuestionsTable: React.FC<{ const { t } = useTranslation(); return ( - +
diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index d672e64710..c42bae7f4c 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -4,9 +4,9 @@ Our reusable hooks and components for managing state related to composable Patte ## Why? -These hooks and components are intended as the missing "batteries" for the composable PatternFly Table. When PatternFly moved away from the "batteries included" legacy monolith Table towards the newer composable Table components, the price of the improved flexibility was that the table itself can no longer manage its own state and its usage became more verbose with more required boilerplate code. +These hooks and components are intended as the missing "batteries" for the composable PatternFly Table. When PatternFly deprecated the "batteries included" legacy monolith Table in favor of the newer composable Table components, the price of the improved flexibility was that the table itself can no longer manage its own state and its usage became more verbose with more required boilerplate code. This trade-off was worth it for PatternFly because the composability offered by the new components is so crucial. However, we can have the best of both worlds by encapsulating the boilerplate logic into hooks that feed props into the composable components without abstracting them away. -The table-controls hooks and components provide a pattern where state logic can be encapsulated with simple configuration and JSX for rendering table elements can be shortened via the use of "prop helpers", but the consumer can retain full control over the JSX composition and have access to all the state at any level. With this pattern, tables are simpler to build and maintain but we don't have to sacrifice any of the benefits gained by migrating from the legacy to the composable table. +The table-controls hooks and components provide a pattern where state logic is encapsulated in hooks with simple configuration, JSX for rendering table elements can be shortened via the use of "prop helpers" returned by the hooks, and the consumer retains full control over the JSX and has access to all the state at any level. With this pattern, tables are simpler to build and maintain but we don't have to sacrifice any of the benefits gained by migrating from the deprecated to the composable table. We also gain new easily-enabled features that the deprecated Table never had. ## Goals @@ -14,8 +14,8 @@ The table-controls hooks and components provide a pattern where state logic can - The consumer should be able to override any and all props manually on any element of the table. If there is a future need for table state to be used for rendering in a way we don't currently anticipate, that should not be blocked by this abstraction. - Client-paginated and server-paginated tables should be similar to implement and share reusable code. If a table needs to be converted between client logic and server logic, that should be relatively easy. - There should not be a concept of a "row object" because rows are presentational details and defining them as a separate model from the API data causes unnecessary complexity. See [Item Objects, Not Row Objects](#item-objects-not-row-objects). -- Strict TypeScript types with generics inferred from parameters should be used to provide a safe and convenient development experience without having to repeat type annotations all over the page-level code. -- All features should be optional and fall back to reasonable defaults if their options are omitted. +- Strict TypeScript types with generics inferred from parameters should be used to provide a safe and convenient development experience without having to repeat type annotations all over the page-level code. TypeScript should enforce that we are passing the options required for the features that are enabled. +- All features should be optional in order to support incremental/partial adoption. - Code for each feature should be isolated enough that it could be reasonably used on its own. ## Usage @@ -26,7 +26,7 @@ For client-paginated tables, the only hook we need is `useLocalTableControls`. A This simple example includes only the filtering, sorting and pagination features and excludes arguments and properties related to the other features (see [Features](#features)). -Features are enabled by passing the `is[Feature]Enabled` boolean argument. Required arguments for the enabled features will be enforced by TypeScript based on which features are enabled. +Features are enabled by passing `is[Feature]Enabled` boolean arguments. Required arguments for the enabled features will be enforced by TypeScript based on which features are enabled. All features are disabled by default; for this basic example with filtering, sorting and pagination, we must pass true values for `isFilterEnabled`, `isSortEnabled` and `isPaginationEnabled`. ```tsx // In a real table, this API data would come from a useQuery call. @@ -48,6 +48,7 @@ const tableControls = useLocalTableControls({ isFilterEnabled: true, isSortEnabled: true, isPaginationEnabled: true, + // Because isFilterEnabled is true, TypeScript will require these filterCategories: filterCategories: [ { key: "name", @@ -57,6 +58,7 @@ const tableControls = useLocalTableControls({ getItemValue: (thing) => thing.name || "", }, ], + // Because isSortEnabled, TypeScript will require these sort-related properties: sortableColumns: ["name", "description"], getSortValues: (thing) => ({ name: thing.name || "", @@ -155,14 +157,27 @@ return ( ); ``` -### Example table with server-side filtering/sorting/pagination +### Example table with server-side filtering/sorting/pagination (and state persistence) -The usage is similar here, but some arguments are no longer required (like `getSortValues` and the `getItemValue` property of the filter category) and we break up the arguments object passed to `useLocalTableControls` into two separate objects passed to `useTableControlState` and `useTableControlProps` based on when they are needed. You'll note that the object passed to the latter contains all the properties of the object passed to the former in addition to things derived from the fetched API data. Those arguments are all also included in the `tableControls` object returned by `useTableControlProps` (and `useLocalTableControls` above). This way, we have one big object we can pass around to any components or functions that need any of the configuration, state, derived state, or props present on it. +The usage is similar here, but some client-specific arguments are no longer required (like `getSortValues` and the `getItemValue` property of the filter category) and we break up the arguments object passed to `useLocalTableControls` into two separate objects passed to `useTableControlState` and `useTableControlProps` based on when they are needed. Note that the object passed to the latter contains all the properties of the object returned by the former in addition to things derived from the fetched API data. All of the arguments passed to both `useTableControlState` and `useTableControlProps` as well as the return values from both are included in the `tableControls` object returned by `useTableControlProps` (and by `useLocalTableControls` above). This way, we have one big object we can pass around to any components or functions that need any of the configuration, state, derived state, or props present on it, and we can destructure/reference them from a central place no matter where they came from. -Note also: the destructuring and rendering part of the example code is not included here because **_it is identical to the example above_**. The only difference between client-paginated and server-paginated tables is the hook usage; the `tableControls` object and its usage are the same for both. +Note also: the destructuring of `tableControls` and returned JSX is not included in this example code because **_it is identical to the example above_**. The only differences between client-paginated and server-paginated tables are in the hook calls; the `tableControls` object and its usage are the same for all tables. + +This example also shows a powerful optional capability of these hooks: the `persistTo` argument. This can be passed to either `useTableControlState` or `useLocalTableControls` and it allows us to store the current pagination/sort/filter state in a custom location and use that as the source of truth. The supported `persistTo` values are: + +- `"state"` (default) - Plain React state. Resets on component unmount or page reload. +- `"urlParams"` (recommended) - URL query parameters. Persists on page reload, browser history buttons (back/forward) or loading a bookmark. Resets on page navigation. +- `"localStorage"` - Browser localStorage API. Persists semi-permanently and is shared across all tabs/windows. Resets only when the user clears their browsing data. +- `"sessionStorage"` - Browser sessionStorage API. Persists on page/history navigation/reload. Resets when the tab/window is closed. + +Here we use `persistTo: "urlParams"` which will use URL query params as the source of truth for the state of all this table's features. We also pass an optional `persistenceKeyPrefix` which distinguishes this persisted state from any other state that may be persisted in the URL by other tables on the same page (it can be omitted if there is only one table on the page). It should be a short string because it is included in every URL param name. We'll use `"ex"` which is short for "example". + +Because our state is persisted in the page URL, we can reload the browser or press the Back and Forward buttons without losing our current filter, sort, and pagination selections. We can even bookmark the page and all that state will be restored when loading the bookmark! ```tsx const tableControlState = useTableControlState({ + persistTo: "urlParams", + persistenceKeyPrefix: "ex", columnNames: { name: "Name", description: "Description", @@ -209,9 +224,172 @@ const tableControls = useTableControlProps({ // Everything else (destructuring `tableControls` and returning JSX) is the same as the client-side example! ``` -### Kitchen sink example with all features +### Kitchen sink example with per-feature state persistence and all features enabled + +Here's an example of another server-computed table with all of the table-controls features enabled (see [Features](#features)). Note that if you wanted to make this example client-computed, you would pass all the new feature-specific properties seen here to `useLocalTableControls` instead of `useTableControlState`. + +New features added here in addition to filtering, sorting and pagination are: + +- Expansion - Each row has an expand toggle button to the left of its data (automatically injected by the `TableRowContentWithControls` component), which opens additional detail below the row. (this is the "single" expand variant, compound expansion is also supported). The `expandableVariant` option is required because `isExpansionEnabled` is true. + - This makes the `getExpandedContentTdProps` propHelper and the `expansionDerivedState` object available on the `tableControls` object. + - Each row is now contained in a `` component which pairs the existing `` with another `` containing that row's ``. +- Active item - Rows have hover styles and are clickable (handled automatically by `getTrProps`). Clicking a row marks that row's item as "active", which can be used to open a drawer or whatever else is needed on the page. This is enabled by `isActiveItemEnabled`, which does not require any additional options. + - This makes the `activeItemDerivedState` object available on the `tableControls` object. + +> ⚠️ TECH DEBT NOTE: The selection feature is currently not enabled in this example because it is about to significantly change with a refactor. Currently to use selection you have to use the outdated `useSelectionState` from lib-ui and pass its return values to `useTableControlProps`. Once selection is moved into table-controls, it will be configurable alongside the other features in `useTableControlState` and added to this example. + +> ⚠️ TECH DEBT NOTE: We should also add a compound-expand example, but that can maybe wait for the proper extension-seed docs in table-batteries after the code is moved there. + +Here we'll also show an alternate way of using `persistTo`: separate persistence targets per feature. Let's say that for this table, we want the user's filters to persist in `localStorage` where they will be restored no matter what the user does, but we want the sort, pagination and other state to reset when we leave the page. We can do this by passing an object to `persistTo` instead of a string. We specify the default persistence target as React state with `default: "state"`, and override it for the filters with `filter: "localStorage"`. + +```tsx +const tableControlState = useTableControlState({ + persistTo: { + default: "state", + filter: "localStorage", + }, + persistenceKeyPrefix: "ex", + columnNames: { + name: "Name", + description: "Description", + }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + isExpansionEnabled: true, + isActiveItemEnabled: true, + filterCategories: [ + { + key: "name", + title: "Name", + type: FilterType.search, + placeholderText: "Filter by name...", + }, + ], + sortableColumns: ["name", "description"], + initialSort: { columnKey: "name", direction: "asc" }, + expandableVariant: "single", +}); + +const hubRequestParams = getHubRequestParams({ + ...tableControlState, + hubSortFieldKeys: { + name: "name", + description: "description", + }, +}); -TODO - use all features, plus use an object with different persistTo options here +const { data, totalItemCount, isLoading, isError } = + useFetchThings(hubRequestParams); + +const tableControls = useTableControlProps({ + ...tableControlState, + idProperty: "id", + currentPageItems: data, + totalItemCount, + isLoading, +}); + +const { + currentPageItems, + numRenderedColumns, + propHelpers: { + toolbarProps, + filterToolbarProps, + paginationToolbarItemProps, + paginationProps, + tableProps, + getThProps, + getTrProps, + getTdProps, + getExpandedContentTdProps, + }, + activeItemDerivedState: { activeItem, clearActiveItem }, + expansionDerivedState: { isCellExpanded }, +} = tableControls; + +return ( + <> + + + + + + + + +
+ + + + + + + + + No things available + + + } + numRenderedColumns={numRenderedColumns} + > + + {currentPageItems?.map((thing, rowIndex) => ( + + + + + + + + {isCellExpanded(thing) && ( + + + + )} + + ))} + + +
+ + +
+ {thing.name} + + {thing.description} +
+ + + Some extra detail about thing {thing.name} goes here! + +
+ + {/* Stub example of something custom you might render based on the `activeItem`. Source not included. */} + + <> +); +``` ### Should I Use Client or Server Logic? @@ -223,16 +401,16 @@ If the endpoints do not support these parameters or you need to have the entire In most cases, you'll only need to use these higher-level hooks and helpers to build a table: -- For client-paginated tables: `useLocalTableControls` is all you need. These have the same signature and are interchangeable. - - Internally they use `useTableControlState`, `useTableControlProps` and the `getLocal[Feature]DerivedState` helpers. The config arguments object is a combination of the arguments required by `useTableControlState` and `useTableControlProps`. +- For client-paginated tables: `useLocalTableControls` is all you need. + - Internally it uses `useTableControlState`, `useTableControlProps` and the `getLocalTableControlDerivedState` helper. The config arguments object is a combination of the arguments required by `useTableControlState` and `useTableControlProps`. - The return value (an object we generally name `tableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. - For server-paginated tables: `useTableControlState`, `getHubRequestParams`, and `useTableControlProps`. - Choose whether you want to use React state, URL params or localStorage/sessionStorage as the source of truth, and call `useTableControlState` with the appropriate `persistTo` option and optional `persistenceKeyPrefix` (to namespace persisted state for multiple tables on the same page). - `persistTo` can be `"state" | "urlParams" | "localStorage" | "sessionStorage"`, and defaults to `"state"` if omitted (falls back to regular React state). - - You can also use a different type of storage for the state of each feature by passing an object for `persistTo`. See the [Kitchen sink example with all features](#kitchen-sink-example-with-all-features). - - Take the object returned by that hook (generally named `tableControlState`) and pass it to `getHubRequestParams` function (you may need to spread it and add additional properties like `hubSortFieldKeys`). + - You can also use a different type of storage for the state of each feature by passing an object for `persistTo`. See the [Kitchen sink example](#kitchen-sink-example-with-per-feature-state-persistence-and-all-features-enabled). + - Take the object returned by that hook (generally named `tableControlState`) and pass it to the `getHubRequestParams` function (you may need to spread it and add additional properties like `hubSortFieldKeys`). (⚠️ TECH DEBT NOTE: This is Konveyor-specific) - Call your API query hooks, using the `hubRequestParams` as needed. - - Call `useTableControlProps` and pass it an object including all properties from `tableControlState` along with additional config arguments. Some of these arguments will be derived from your API data, such as `currentPageItems`, `totalItemCount` and `isLoading`. Others are simply passed here rather than above because they are used only for rendering and not required for state management. + - Call `useTableControlProps` and pass it an object spreading all properties from `tableControlState` along with additional config arguments. Some of these arguments will be derived from your API data, such as `currentPageItems`, `totalItemCount` and `isLoading`. Others are simply passed here rather than above because they are used only for rendering and not required for state management. - The return value (the same `tableControls` object returned by `useLocalTableControls`) has everything you need to render your table. Give it a `console.log` to see what is available. If desired, you can use the lower-level feature-specific hooks (see [Features](#features)) on their own (for example, if you really only need pagination and you're not rendering a full table). However, if you are using more than one or two of them you may want to consider using these higher-level hooks even if you don't need all the features. You can omit the config arguments for any features you don't need and then just don't use the relevant `propHelpers`. diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index cffa86e0e9..256166ac2e 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -60,9 +60,9 @@ export type TableFeature = /** * Identifier for where to persist state for a single table feature or for all table features. - * - "state" (default) - Plain React state. Resets on component unmount or page reload + * - "state" (default) - Plain React state. Resets on component unmount or page reload. * - "urlParams" (recommended) - URL query parameters. Persists on page reload, browser history buttons (back/forward) or loading a bookmark. Resets on page navigation. - * - "localStorage" - Browser localStorage API. Persists semi-permanently. Resets only on clearing browser data. + * - "localStorage" - Browser localStorage API. Persists semi-permanently and is shared across all tabs/windows. Resets only when the user clears their browsing data. * - "sessionStorage" - Browser sessionStorage API. Persists on page/history navigation/reload. Resets when the tab/window is closed. */ export type PersistTarget = diff --git a/client/src/app/pages/controls/stakeholders/stakeholders.tsx b/client/src/app/pages/controls/stakeholders/stakeholders.tsx index 7446cf4a0b..762655fc30 100644 --- a/client/src/app/pages/controls/stakeholders/stakeholders.tsx +++ b/client/src/app/pages/controls/stakeholders/stakeholders.tsx @@ -229,7 +229,7 @@ export const Stakeholders: React.FC = () => { - +
diff --git a/client/src/app/pages/issues/issues-table.tsx b/client/src/app/pages/issues/issues-table.tsx index 115015df04..7229f9015c 100644 --- a/client/src/app/pages/issues/issues-table.tsx +++ b/client/src/app/pages/issues/issues-table.tsx @@ -309,7 +309,7 @@ export const IssuesTable: React.FC = ({ mode }) => { -
+
From 06546cb189246e39a9a1c49d9c9f684bdbb683e2 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 23 Oct 2023 15:30:32 -0400 Subject: [PATCH 102/102] Proofread the rest of DOCS.md Signed-off-by: Mike Turley --- client/src/app/hooks/table-controls/DOCS.md | 101 ++++++++------------ 1 file changed, 40 insertions(+), 61 deletions(-) diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index c42bae7f4c..1b409e1b12 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -163,21 +163,16 @@ The usage is similar here, but some client-specific arguments are no longer requ Note also: the destructuring of `tableControls` and returned JSX is not included in this example code because **_it is identical to the example above_**. The only differences between client-paginated and server-paginated tables are in the hook calls; the `tableControls` object and its usage are the same for all tables. -This example also shows a powerful optional capability of these hooks: the `persistTo` argument. This can be passed to either `useTableControlState` or `useLocalTableControls` and it allows us to store the current pagination/sort/filter state in a custom location and use that as the source of truth. The supported `persistTo` values are: +This example also shows a powerful optional capability of these hooks: the `persistTo` argument. This can be passed to either `useTableControlState` or `useLocalTableControls` and it allows us to store the current pagination/sort/filter state in a custom location and use that as the source of truth. The supported `persistTo` values are `"state"` (default), `"urlParams"` (recommended), `"localStorage"` or `"sessionStorage"`. For more on each persistence target see [Features](#features). -- `"state"` (default) - Plain React state. Resets on component unmount or page reload. -- `"urlParams"` (recommended) - URL query parameters. Persists on page reload, browser history buttons (back/forward) or loading a bookmark. Resets on page navigation. -- `"localStorage"` - Browser localStorage API. Persists semi-permanently and is shared across all tabs/windows. Resets only when the user clears their browsing data. -- `"sessionStorage"` - Browser sessionStorage API. Persists on page/history navigation/reload. Resets when the tab/window is closed. - -Here we use `persistTo: "urlParams"` which will use URL query params as the source of truth for the state of all this table's features. We also pass an optional `persistenceKeyPrefix` which distinguishes this persisted state from any other state that may be persisted in the URL by other tables on the same page (it can be omitted if there is only one table on the page). It should be a short string because it is included in every URL param name. We'll use `"ex"` which is short for "example". +Here we use `persistTo: "urlParams"` which will use URL query parameters as the source of truth for the state of all this table's features. We also pass an optional `persistenceKeyPrefix` which distinguishes this persisted state from any other state that may be persisted in the URL by other tables on the same page (it can be omitted if there is only one table on the page). It should be a short string because it is included as a prefix on every URL param name. We'll use `"t"` for the table containing our Thing objects. Because our state is persisted in the page URL, we can reload the browser or press the Back and Forward buttons without losing our current filter, sort, and pagination selections. We can even bookmark the page and all that state will be restored when loading the bookmark! ```tsx const tableControlState = useTableControlState({ persistTo: "urlParams", - persistenceKeyPrefix: "ex", + persistenceKeyPrefix: "t", columnNames: { name: "Name", description: "Description", @@ -248,7 +243,7 @@ const tableControlState = useTableControlState({ default: "state", filter: "localStorage", }, - persistenceKeyPrefix: "ex", + persistenceKeyPrefix: "t", columnNames: { name: "Name", description: "Description", @@ -417,26 +412,31 @@ If desired, you can use the lower-level feature-specific hooks (see [Features](# ## Features -The functionality of the table-controls hooks is broken down into the following features. Most features are defined by operations to be performed on API data before it is displayed in a table. +The functionality and state of the table-controls hooks is broken down into the following features. Each of these features represents a slice of the logical concerns for a table UI. + +Note that the filtering, sorting and pagination features are special because they must be performed in a specific order to work correctly: filter and sort data, then paginate it. Using the higher-level hooks like `useLocalTableControls` or `useTableControlState` + `useTableControlProps` will take care of this for you (see [Usage](#usage)), but if you are handling filtering/sorting/pagination yourself with the lower-level hooks you'll need to be mindful of this order. -Note that filtering, sorting and pagination are special because they must be performed in a specific order to work correctly: filter and sort data, then paginate it. Using the higher-level hooks like `useLocalTableControls` or `useTableControlState` + `useTableControlProps` will take care of this for you (see [Usage](#usage)), but if you are handling pagination yourself with the lower-level hooks you'll need to be mindful of this order (see [Hooks and Helper Functions](#hooks-and-helper-functions)). +The state used by each feature (provided by `use[Feature]State` hooks) can be stored either in React state (default), in the browser's URL query parameters (recommended), or in the browser's `localStorage` or `sessionStorage`. If URL params are used, the user's current filters, sort, pagination state, expanded/active rows and more are preserved when reloading the browser, using the browser Back and Forward buttons, or loading a bookmark. The storage target for each feature is specified with the `persistTo` property. The supported `persistTo` values are: -The state used by these features (provided by `use[Feature]State` hooks) can be stored either in React state, in the browser's URL query parameters, or in the browser's `localStorage` or `sessionStorage`. If URL params are used, the user's current filters, sort, pagination state, expanded/active rows and more are preserved when reloading the browser, using the browser Back and Forward buttons, or loading a bookmark. +- `"state"` (default) - Plain React state. Resets on component unmount or page reload. +- `"urlParams"` (recommended) - URL query parameters. Persists on page reload, browser history buttons (back/forward) or loading a bookmark. Resets on page navigation. +- `"localStorage"` - Browser localStorage API. Persists semi-permanently and is shared across all tabs/windows. Resets only when the user clears their browsing data. +- `"sessionStorage"` - Browser sessionStorage API. Persists on page/history navigation/reload. Resets when the tab/window is closed. -All of the hooks and helpers described in this section are used internally by the higher-level hooks and helpers, and do not need to be used directly (see [Hooks and Helper Functions](#hooks-and-helper-functions) and [Usage](#usage)). +All of the hooks and helpers described in this section are used internally by the higher-level hooks and helpers, and do not need to be used directly (see [Usage](#usage)). ### Filtering Items are filtered according to user-selected filter key/value pairs. -- Keys and filter types (search, select, etc) are defined by the `filterCategories` array config argument. The `key` properties of each of these `FilterCategory` objects are the source of truth for the inferred generic type `TFilterCategoryKeys` (see [Types](#types)). +- Keys and filter types (search, select, etc) are defined by the `filterCategories` array config argument. The `key` properties of each of these `FilterCategory` objects are the source of truth for the inferred generic type `TFilterCategoryKeys` (For more, see the JSDoc comments in the `types.ts` file). - Filter state is provided by `useFilterState`. - For client-side filtering, the filter logic is provided by `getLocalFilterDerivedState` (based on the `getItemValue` callback defined on each `FilterCategory` object, which is not required when using server-side filtering). - For server-side filtering, filter state is serialized for the API by `getFilterHubRequestParams`. -- Filter-related component props are provided by `getFilterProps`. +- Filter-related component props are provided by `useFilterPropHelpers`. - Filter inputs and chips are rendered by the `FilterToolbar` component. -> ⚠️ TECH DEBT NOTE: The `FilterToolbar` component and `FilterCategory` type predate the table-controls pattern and are not located in this directory. The abstraction there may be a little too opaque and it does not take full advantage of TypeScript generics. We may want to adjust that code to better fit these patterns and move it here. +> ⚠️ TECH DEBT NOTE: The `FilterToolbar` component and `FilterCategory` type predate the table-controls pattern (they are tackle2-ui legacy code) and are not located in this directory. The abstraction there may be a little too opaque and it does not take full advantage of TypeScript generics. We may want to adjust that code to better fit these patterns and move it here. ### Sorting @@ -446,8 +446,8 @@ Items are sorted according to the user-selected sort column and direction. - Sort state is provided by `useSortState`. - For client-side sorting, the sort logic is provided by `getLocalSortDerivedState` (based on the `getSortValues` config argument, which is not required when using server-side sorting). - For server-side sorting, sort state is serialized for the API by `getSortHubRequestParams`. -- Sort-related component props are provided by `getSortProps`. -- Sort inputs are rendered by the table's `Th` component. +- Sort-related component props are provided by `useSortPropHelpers`. +- Sort inputs are rendered by the table's `Th` PatternFly component. ### Pagination @@ -457,11 +457,13 @@ Items are paginated according to the user-selected page number and items-per-pag - Pagination state is provided by `usePaginationState`. - For client-side pagination, the pagination logic is provided by `getLocalPaginationDerivedState`. - For server-side pagination, pagination state is serialized for the API by `getPaginationHubRequestParams`. -- Pagination-related component props are provided by `getPaginationProps`. -- A `useEffect` call which prevents invalid state after an item is deleted is provided by `usePaginationEffects`. +- Pagination-related component props are provided by `usePaginationPropHelpers`. +- A `useEffect` call which prevents invalid state after an item is deleted is provided by `usePaginationEffects`. This is called internally by `usePaginationPropHelpers`. - Pagination inputs are rendered by our `SimplePagination` component which is a thin wrapper around the PatternFly `Pagination` component. -> ⚠️ TECH DEBT NOTE: Do we really need `SimplePagination`? +> ⚠️ TECH DEBT NOTE: The `SimplePagination` component also predates the table-controls pattern (legacy tackle2-ui code). We probably don't even need it and should remove it. + +> ⚠️ TECH DEBT NOTE: Is usePaginationPropHelpers the best place to call usePaginationEffects? Should we make the consumer call it explicitly? ### Expansion @@ -471,23 +473,24 @@ Item details can be expanded, either with a "single expansion" variant where an - Expansion state is provided by `useExpansionState`. - Expansion shorthand functions are provided by `getExpansionDerivedState`. - Expansion is never managed server-side. -- Expansion-related component props are provided inside `useTableControlProps` in the `getSingleExpandButtonTdProps` and `getCompoundExpandTdProps` functions. -- Expansion inputs are rendered by the table's `Td` component and expanded content is managed at the consumer level by conditionally rendering a second row with full colSpan in a `Tbody` component. The `numRenderedColumns` value returned by `useTableControlProps` can be used for the correct colSpan here. - -> ⚠️ TECH DEBT NOTE: `getSingleExpandButtonTdProps` and `getCompoundExpandTdProps` should probably be factored out of `useTableControlProps` into a decoupled `getExpansionProps` helper. +- Expansion-related component props are provided by `useExpansionPropHelpers`. +- Expansion inputs are rendered by the table's `Td` PatternFly component and expanded content is managed at the consumer level by conditionally rendering a second row with full colSpan inside a PatternFly `Tbody` component. The `numRenderedColumns` value returned by `useTableControlProps` can be used for the correct colSpan here. ### Active Item -A row can be clicked to mark its item as "active", which usually opens a drawer on the page to show more detail. Note that this is distinct from expansion and selection and these features can all be used together. Active item state is simply a single id value (number or string) for the active item, derived from the `idProperty` config argument (see [Unique Identifiers](#unique-identifiers)). +A row can be clicked to mark its item as "active", which usually opens a drawer on the page to show more detail. Note that this is distinct from expansion (toggle arrows) and selection (checkboxes) and these features can all be used together. Active item state is simply a single id value (number or string) for the active item, derived from the `idProperty` config argument (see [Unique Identifiers](#unique-identifiers)). - The active item feature requires no config arguments. - Active item state is provided by `useActiveItemState`. - Active item shorthand functions are provided by `getActiveItemDerivedState`. -- A `useEffect` call which prevents invalid state after an item is deleted is provided by `useActiveItemEffects`. +- Active-item-related component props are provided by `useActiveItemPropHelpers`. +- A `useEffect` call which prevents invalid state after an item is deleted is provided by `useActiveItemEffects`. This is called internally in `useActiveItemPropHelpers`. + +> ⚠️ TECH DEBT NOTE: Is useActiveItemPropHelpers the best place to call useActiveItemEffects? Should we make the consumer call it explicitly? ### Selection -Items can be selected with checkboxes on each row or with a bulk select control that provides actions like "select all", "select none" and "select page". The list of selected item ids in state can be used to perform bulk actions. +Items can be selected with checkboxes on each row or with a bulk select control that provides actions like "select all", "select none" and "select page". The list of selected item ids in state can be used to perform bulk actions like Delete. > ⚠️ TECH DEBT NOTE: Currently, selection state has not yet been refactored to be a part of the table-controls pattern and we are still relying on [the old `useSelectionState` from lib-ui](https://migtools.github.io/lib-ui/?path=/docs/hooks-useselectionstate--checkboxes) which dates back to older migtools projects. The return value of this legacy `useSelectionState` is required by `useTableControlProps`. Mike is working on a refactor to bring selection state hooks into this directory. @@ -495,54 +498,30 @@ Items can be selected with checkboxes on each row or with a bulk select control ### Item Objects, Not Row Objects -None of the code here treats "rows" as their own data structure. The content and style of a row is a presentational detail that should be limited to the JSX where rows are rendered. When an array of row objects is used, those objects tend to duplicate API data with a different structure and the code must reason about two different representations of the data. Instead, this code works directly with arrays of "items" (the API data objects themselves) and makes all of an item's properties available where they might be needed without extra lookups. The consumer maps over item objects and derives row components from them only at render time. +None of the code here treats "rows" as their own data structure. The content and style of a row is a presentational detail that should be limited to the JSX where rows are rendered. In implementations which use arrays of row objects (like the deprecated PatternFly table) those objects tend to duplicate API data with a different structure and the code must reason about two different representations of the data. Instead, this code works directly with arrays of "items" (the API data objects themselves) and makes all of an item's API object properties available where they might be needed without extra lookups. The consumer maps over item objects and derives row components from them only at render time. -An item object has the generic type `TItem`, which is inferred either from the type of the `items` array passed into `useLocalTableControls` (for client-paginated tables) or from the `currentPageItems` array passed into `useTableControlProps` (for server-paginated tables). See [Types](#types). +An item object has the generic type `TItem`, which is inferred by TypeScript either from the type of the `items` array passed into `useLocalTableControls` (for client-paginated tables) or from the `currentPageItems` array passed into `useTableControlProps` (for server-paginated tables). For more, see the JSDoc comments in the `types.ts` file. > ℹ️ CAVEAT: For server-paginated tables the item data is not in scope until after the API query hook is called, but the `useTableControlState` hook must be called _before_ API queries because its return values are needed to serialize filter/sort/pagination params for the API. This means the inferred `TItem` type is not available when passing arguments to `useTableControlState`. `TItem` resolves to `unknown` in this scope, which is usually fine since the arguments there don't need to know what type of items they are working with. If the item type is needed for any of these arguments it can be explicitly passed as a type param. However... > -> ⚠️ TECH DEBT NOTE: Since TypeScript generic type param lists are all-or-nothing (you must either omit the list and infer all generics for a function or pass them all explicitly), this means all other type params which are normally inferred must be explicitly passed (including all of the `TColumnKey`s and `TFilterCategoryKey`s). This makes for some redundant code, although TypeScript will still enforce that it is all consistent. There is a possible upcoming TypeScript language feature which would allow partial inference in type param lists and may alleviate this in the future. See TypeScript pull requests [#26349](https://github.com/microsoft/TypeScript/pull/26349) and [#54047](https://github.com/microsoft/TypeScript/pull/54047), and our issue [#1456](https://github.com/konveyor/tackle2-ui/issues/1456). +> ⚠️ TECH DEBT NOTE: TypeScript generic type param lists (example: `fn(args);`) are all-or-nothing (you must either omit the list and infer all generics for a function or pass them all explicitly). This means if you need to pass an explicit type for `TItem`, all other type params which are normally inferred must also be explicitly passed (including all of the `TColumnKey`s and `TFilterCategoryKey`s). This makes for some redundant code, although TypeScript will still enforce that it is all consistent. There is a possible upcoming TypeScript language feature which would allow partial inference in type param lists and may alleviate this in the future. See TypeScript pull requests [#26349](https://github.com/microsoft/TypeScript/pull/26349) and [#54047](https://github.com/microsoft/TypeScript/pull/54047), and our issue [#1456](https://github.com/konveyor/tackle2-ui/issues/1456). ### Unique Identifiers -Table columns are identified by unique keys which are statically inferred from the keys of the `columnNames` object (used in many places via the inferred generic type `TColumnKey`. See [Types](#types)). Any state which keeps track of something by column (such as which columns are sorted, and which columns are expanded in a compound-expandable row) uses these column keys as identifiers, and the user-facing column names can be looked up from the `columnNames` object anywhere a `columnKey` is present. Valid column keys are enforced via TypeScript generics; if a `columnKey` value is used that is not present in `columnNames`, you should get a type error. +#### Column keys -Item objects must contain some unique identifier which is either a string or number. The property key of this identifier is a required config argument called `idProperty`, which will usually be `"id"`. If no unique identifier is present in the API data, an artificial one can be injected before passing the data into these hooks, which can be done in the useQuery `select` callback (see instances where we have used `"_ui_unique_id"`). Any state which keeps track of something by item (i.e. by row) makes use of `item[idProperty]` as an identifier. Examples of this include selected rows, expanded rows and active rows. Valid `idProperty` values are also enforced by TypeScript generics; if an `idProperty` is provided that is not a property on the `TItem` type, you should get a type error. +Table columns are identified by unique keys which are statically inferred from the keys of the `columnNames` object (used in many places via the inferred generic type `TColumnKey`. See the JSDoc comments in the `types.ts` file). Any state which keeps track of something by column (such as which columns are sorted and which columns are expanded in a compound-expandable row) uses these column keys as identifiers, and the user-facing column names can be looked up from the `columnNames` object anywhere a `columnKey` is present. Valid column keys are enforced via TypeScript generics; if a `columnKey` value is used that is not present in `columnNames`, you should get a type error. ---- - -


+#### Item IDs -# NOTE: Sections below this line are WIP. Ask Mike for clarification if you need it before he finishes writing this. +Item objects must contain some unique identifier which is either a string or number. The property key of this identifier is a required config argument called `idProperty`, which will usually be `"id"`. If no unique identifier is present in the API data, an artificial one can be injected before passing the data into these hooks, which can be done in the useQuery `select` callback (see instances where we have used `"_ui_unique_id"`). Any state which keeps track of something by item (i.e. by row) makes use of `item[idProperty]` as an identifier. Examples of this include selected rows, expanded rows and active rows. Valid `idProperty` values are also enforced by TypeScript generics; if an `idProperty` is provided that is not a property on the `TItem` type, you should get a type error. -


+> ⚠️ TECH DEBT NOTE: Things specific to `useQuery` and `_ui_unique_id` here are Konveyor-specific notes that should be removed after moving this to table-batteries. --- -## Types - -TODO cover the stuff in types.ts export by export referencing the usage in specific hooks and components - -## Hooks and Helper Functions - -TODO maybe this section isn't necessary anymore if we go into enough detail in features and usage -TODO if we remove this remember to remove/change anchor links above - -### Higher-level hooks (handle all state with combined options and return values) - -TODO list all the hooks used above and their signatures and implementation overview - -### Lower-level hooks (used internally by composite hooks but also usable standalone) - -TODO list all the hooks for each concern and how they flow into each other in the composite hooks - -## Components - -TODO maybe this section isn't necessary anymore if we go into enough detail in features and usage -TODO summarize why it is still useful to have some component abstractions even though the goal is to preserve all composability / leave all control of JSX to the consumer -TODO list all the components exported from this directory, their signatures and why they are useful - ## Future Features and Improvements - Tech debt notes above should be addressed. +- We should add full API docs for all the hooks, helpers and components generated from the JSDoc comments. - It would be nice to support inline editable rows with a clean abstraction that fits into this pattern.