From 076f9a2fac2eaaff62679917afb02c6c11f8fef8 Mon Sep 17 00:00:00 2001 From: Ian Bolton Date: Tue, 12 Mar 2024 22:08:08 -0400 Subject: [PATCH] :sparkles: Integrate col mgmt modal feature with table hooks --- .../table-controls/column/useColumnState.ts | 25 +++ client/src/app/hooks/table-controls/types.ts | 30 ++- .../table-controls/useLocalTableControls.ts | 3 + .../table-controls/useTableControlProps.ts | 6 + .../table-controls/useTableControlState.ts | 17 ++ .../applications-table/applications-table.tsx | 194 +++++++++++------- .../components/manage-columns-modal.tsx | 115 +++++++++++ .../components/manage-columns-toolbar.tsx | 53 +++++ 8 files changed, 363 insertions(+), 80 deletions(-) create mode 100644 client/src/app/hooks/table-controls/column/useColumnState.ts create mode 100644 client/src/app/pages/applications/applications-table/components/manage-columns-modal.tsx create mode 100644 client/src/app/pages/applications/applications-table/components/manage-columns-toolbar.tsx diff --git a/client/src/app/hooks/table-controls/column/useColumnState.ts b/client/src/app/hooks/table-controls/column/useColumnState.ts new file mode 100644 index 0000000000..73769fae1e --- /dev/null +++ b/client/src/app/hooks/table-controls/column/useColumnState.ts @@ -0,0 +1,25 @@ +import { useState } from "react"; + +export interface ColumnState { + id: TColumnKey; + label: string; + isVisible: boolean; +} + +export interface IColumnState { + columns: ColumnState[]; + setColumns: (newColumns: ColumnState[]) => void; +} + +interface IColumnStateArgs { + initialColumns: ColumnState[]; +} + +export const useColumnState = ( + args: IColumnStateArgs +): IColumnState => { + const [columns, setColumns] = useState[]>( + args.initialColumns + ); + return { columns, setColumns }; +}; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index a96829c6a1..5ae62caedc 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -38,6 +38,7 @@ import { import { IFilterToolbarProps } from "@app/components/FilterToolbar"; import { IToolbarBulkSelectorProps } from "@app/components/ToolbarBulkSelector"; import { IExpansionPropHelpersExternalArgs } from "./expansion/useExpansionPropHelpers"; +import { IColumnState } from "./column/useColumnState"; // Generic type params used here: // TItem - The actual API objects represented by rows in the table. Can be any object. @@ -56,7 +57,8 @@ export type TableFeature = | "pagination" | "selection" | "expansion" - | "activeItem"; + | "activeItem" + | "columns"; /** * Identifier for where to persist state for a single table feature or for all table features. @@ -142,6 +144,9 @@ export type IUseTableControlStateArgs< * - 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; + /** + * Initial state for the columns feature. If omitted, all columns are enabled by default. + */ } & IFilterStateArgs & ISortStateArgs & IPaginationStateArgs & { @@ -193,6 +198,10 @@ export type ITableControlState< * State for the active item feature. Returned by useActiveItemState. */ activeItemState: IActiveItemState; + /** + * State for the columns feature. Returned by useColumnState. + */ + columnState: IColumnState; }; /** @@ -288,6 +297,10 @@ export type IUseTableControlPropsArgs< * @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>; + /** + * The state for the columns feature. Returned by useColumnState. + */ + columnState: IColumnState; }; /** @@ -325,9 +338,18 @@ export type ITableControls< * Values derived at render time from the expansion feature state. Includes helper functions for convenience. */ expansionDerivedState: IExpansionDerivedState; + /** + * Values derived at render time from the column feature state. Includes helper functions for convenience. + * + * + * + * + */ + columnState: IColumnState; /** * 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. @@ -406,6 +428,12 @@ export type ITableControls< * 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; + + /** + * Returns the visibility of a column + */ + + getColumnVisibility: (columnKey: TColumnKey) => boolean; }; }; diff --git a/client/src/app/hooks/table-controls/useLocalTableControls.ts b/client/src/app/hooks/table-controls/useLocalTableControls.ts index c24d0d70c6..4ddc694a3b 100644 --- a/client/src/app/hooks/table-controls/useLocalTableControls.ts +++ b/client/src/app/hooks/table-controls/useLocalTableControls.ts @@ -33,6 +33,8 @@ export const useLocalTableControls = < > => { const state = useTableControlState(args); const derivedState = getLocalTableControlDerivedState({ ...args, ...state }); + const { columnState } = state; + console.log("columnState", columnState); return useTableControlProps({ ...args, ...state, @@ -42,5 +44,6 @@ export const useLocalTableControls = < ...args, isEqual: (a, b) => a[args.idProperty] === b[args.idProperty], }), + ...columnState, }); }; diff --git a/client/src/app/hooks/table-controls/useTableControlProps.ts b/client/src/app/hooks/table-controls/useTableControlProps.ts index 6169d1f491..b78b082cba 100644 --- a/client/src/app/hooks/table-controls/useTableControlProps.ts +++ b/client/src/app/hooks/table-controls/useTableControlProps.ts @@ -74,6 +74,7 @@ export const useTableControlProps = < isSelectionEnabled, isExpansionEnabled, isActiveItemEnabled, + columnState: { columns, setColumns }, } = args; const columnKeys = objectKeys(columnNames); @@ -171,6 +172,10 @@ export const useTableControlProps = < }, }); + const getColumnVisibility = (columnKey: TColumnKey) => { + return columns.find((column) => column.id === columnKey)?.isVisible ?? true; + }; + return { ...args, numColumnsBeforeData, @@ -191,6 +196,7 @@ export const useTableControlProps = < getSelectCheckboxTdProps, getSingleExpandButtonTdProps, getExpandedContentTdProps, + getColumnVisibility, }, }; }; diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 830a933270..a6c0c1b169 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -9,6 +9,7 @@ import { useSortState } from "./sorting"; import { usePaginationState } from "./pagination"; import { useActiveItemState } from "./active-item"; import { useExpansionState } from "./expansion"; +import { useColumnState } from "./column/useColumnState"; /** * Provides the "source of truth" state for all table features. @@ -66,6 +67,21 @@ export const useTableControlState = < ...args, persistTo: getPersistTo("activeItem"), }); + + const { columnNames, ...restArgs } = args; + + const initialColumns = Object.entries(columnNames).map( + ([id, label], index) => ({ + id: id as TColumnKey, + label: label as string, + isVisible: true, + }) + ); + + const columnState = useColumnState({ + ...restArgs, + initialColumns, + }); return { ...args, filterState, @@ -73,5 +89,6 @@ export const useTableControlState = < paginationState, expansionState, activeItemState, + columnState, }; }; diff --git a/client/src/app/pages/applications/applications-table/applications-table.tsx b/client/src/app/pages/applications/applications-table/applications-table.tsx index 40024f3a50..cc96c70d8d 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -108,6 +108,7 @@ import { ApplicationIdentityForm } from "../components/application-identity-form import { ApplicationReviewStatus } from "../components/application-review-status/application-review-status"; import { KebabDropdown } from "@app/components/KebabDropdown"; import { useFetchArchetypes } from "@app/queries/archetypes"; +import { ManageColumnsToolbar } from "./components/manage-columns-toolbar"; export const ApplicationsTable: React.FC = () => { const { t } = useTranslation(); @@ -214,7 +215,7 @@ export const ApplicationsTable: React.FC = () => { refetch: fetchApplications, } = useFetchApplications(!hasActiveTasks); - const { assessments, isFetching: isFetchingAssesments } = + const { assessments, isFetching: isFetchingAssessments } = useFetchAssessments(); const { archetypes, isFetching: isFetchingArchetypes } = useFetchArchetypes(); @@ -517,10 +518,12 @@ export const ApplicationsTable: React.FC = () => { getTrProps, getTdProps, toolbarBulkSelectorProps, + getColumnVisibility, }, activeItemDerivedState: { activeItem, clearActiveItem }, selectionState: { selectedItems: selectedRows }, + columnState, } = tableControls; const clearFilters = () => { @@ -811,6 +814,10 @@ export const ApplicationsTable: React.FC = () => { ) : ( <> )} + @@ -826,16 +833,31 @@ export const ApplicationsTable: React.FC = () => { - - - - - - - + {/* {columnState.columns. isVisible then render this column} */} + {getColumnVisibility("name") && ( + + )} + {getColumnVisibility("businessService") && ( + + )} + {getColumnVisibility("assessment") && ( + + )} + {getColumnVisibility("review") && ( + + )} + {getColumnVisibility("analysis") && ( + + )} + {getColumnVisibility("tags") && ( + + )} + {getColumnVisibility("effort") && ( + + )} @@ -871,75 +893,89 @@ export const ApplicationsTable: React.FC = () => { item={application} rowIndex={rowIndex} > - - {application.name} - - - {application.businessService && ( - + {application.name} + + )} + {getColumnVisibility("businessService") && ( + + {application.businessService && ( + + )} + + )} + {getColumnVisibility("assessment") && ( + + - )} - - - - - - - - - - - - - {application.tags ? application.tags.length : 0} - - - {application?.effort ?? "-"} - + + )} + {getColumnVisibility("review") && ( + + + + )} + {getColumnVisibility("analysis") && ( + + + + )} + {getColumnVisibility("tags") && ( + + + {application.tags ? application.tags.length : 0} + + )} + {getColumnVisibility("effort") && ( + + {application?.effort ?? "-"} + + )} {applicationWriteAccess && ( diff --git a/client/src/app/pages/applications/applications-table/components/manage-columns-modal.tsx b/client/src/app/pages/applications/applications-table/components/manage-columns-modal.tsx new file mode 100644 index 0000000000..d01a645371 --- /dev/null +++ b/client/src/app/pages/applications/applications-table/components/manage-columns-modal.tsx @@ -0,0 +1,115 @@ +import React, { useEffect, useState } from "react"; +import { + Button, + DataList, + DataListCell, + DataListCheck, + DataListControl, + DataListItem, + DataListItemCells, + DataListItemRow, + Modal, + Text, + TextContent, + TextVariants, +} from "@patternfly/react-core"; +import { ColumnState } from "@app/hooks/table-controls/column/useColumnState"; + +export interface ManagedColumnsProps { + showModal: boolean; + onClose(): void; + columns: ColumnState[]; // Updated to accept columns directly + setColumns: (newColumns: ColumnState[]) => void; // Accept setColumns function + description?: string; + saveLabel?: string; + cancelLabel?: string; + reorderLabel?: string; + restoreLabel?: string; + title?: string; +} + +export const ManageColumnsModal = ({ + showModal, + description = "Selected columns will be displayed in the table.", + onClose, + columns, // Use the direct columns prop + setColumns, // Use the direct setColumns function + saveLabel = "Save", + cancelLabel = "Cancel", + reorderLabel = "Reorder", + restoreLabel = "Restore default columns", + title = "Manage Columns", +}: ManagedColumnsProps) => { + const [editedColumns, setEditedColumns] = + useState[]>(columns); + + useEffect(() => { + setEditedColumns(columns); + }, [columns]); + + const onSelect = (id: TColumnKey, isVisible: boolean): void => { + setEditedColumns( + editedColumns.map((col) => ({ + ...col, + isVisible: col.id === id ? isVisible : col.isVisible, + })) + ); + }; + + const onSave = () => { + //TODO: Update this if ordering is implemented + setColumns( + editedColumns.map((column, index) => ({ + ...column, + // order: index, + })) + ); + + onClose(); + }; + + return ( + + {description} + + } + onClose={onClose} + actions={[ + , + , + ]} + > + + {editedColumns.map(({ id, label, isVisible }, index) => ( + + + + onSelect(id, checked)} + /> + + + {label} + , + ]} + /> + + + ))} + + + ); +}; diff --git a/client/src/app/pages/applications/applications-table/components/manage-columns-toolbar.tsx b/client/src/app/pages/applications/applications-table/components/manage-columns-toolbar.tsx new file mode 100644 index 0000000000..4444b3b8a4 --- /dev/null +++ b/client/src/app/pages/applications/applications-table/components/manage-columns-toolbar.tsx @@ -0,0 +1,53 @@ +import { + Button, + OverflowMenu, + OverflowMenuGroup, + OverflowMenuItem, + ToolbarItem, +} from "@patternfly/react-core"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { ColumnState } from "@app/hooks/table-controls/column/useColumnState"; +import { ManageColumnsModal } from "./manage-columns-modal"; + +// Define props to accept columns and setColumns directly +interface ManageColumnsToolbarProps { + columns: ColumnState[]; + setColumns: (newColumns: ColumnState[]) => void; +} + +export const ManageColumnsToolbar = ({ + columns, + setColumns, +}: ManageColumnsToolbarProps) => { + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + return ( + <> + + + + + + + + + + + setIsOpen(false)} + description={t("Selected columns will be displayed in the table.")} + setColumns={setColumns} + columns={columns} + saveLabel={t("Save")} + cancelLabel={t("Cancel")} + reorderLabel={t("Reorder")} + restoreLabel={t("Restore default columns")} + title={t("Manage Columns")} + /> + + ); +};