From bf5d41bd6c7863e7eac5544d9ea5a3967422abea Mon Sep 17 00:00:00 2001 From: Ian Bolton Date: Wed, 13 Mar 2024 16:57:35 -0400 Subject: [PATCH] Integrate col mgmt modal with table hooks --- .../table-controls/column/useColumnState.ts | 60 ++++++ client/src/app/hooks/table-controls/types.ts | 24 ++- .../table-controls/useLocalTableControls.ts | 3 + .../table-controls/useTableControlProps.ts | 13 ++ .../table-controls/useTableControlState.ts | 19 ++ .../applications-table/applications-table.tsx | 195 ++++++++++-------- .../components/manage-columns-modal.tsx | 104 ++++------ .../components/manage-columns-toolbar.tsx | 26 +-- 8 files changed, 277 insertions(+), 167 deletions(-) create mode 100644 client/src/app/hooks/table-controls/column/useColumnState.ts 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..27aa935334 --- /dev/null +++ b/client/src/app/hooks/table-controls/column/useColumnState.ts @@ -0,0 +1,60 @@ +import { usePersistentState } from "@app/hooks/usePersistentState"; +import { IFeaturePersistenceArgs } from "../types"; + +export interface ColumnState { + id: TColumnKey; + label: string; + isVisible: boolean; + order: number; + width?: number; +} + +export interface IColumnState { + columns: ColumnState[]; + setColumns: (newColumns: ColumnState[]) => void; +} + +interface IColumnStateArgs { + initialColumns: ColumnState[]; +} + +export const useColumnState = < + TColumnKey extends string, + TPersistenceKeyPrefix extends string = string, +>( + args: IColumnStateArgs & + IFeaturePersistenceArgs +): IColumnState => { + const { persistTo = "state", persistenceKeyPrefix, initialColumns } = args; + + const [columns, setColumns] = usePersistentState< + ColumnState[], + TPersistenceKeyPrefix, + "columns" + >({ + defaultValue: initialColumns, + persistenceKeyPrefix, + ...(persistTo === "urlParams" + ? { + persistTo, + keys: ["columns"], + serialize: (columnsObj) => { + return { columns: JSON.stringify(columnsObj) }; + }, + deserialize: ({ columns: columnsStr }) => { + try { + return JSON.parse(columnsStr || "[]"); + } catch (e) { + return initialColumns; // Fallback to initial columns on parse failure + } + }, + } + : persistTo === "localStorage" || persistTo === "sessionStorage" + ? { + persistTo, + key: "columns", + } + : { persistTo }), + }); + 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..55e991b54b 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. 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..e55ffc7376 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,18 @@ export const useTableControlProps = < }, }); + const getColumnProps = (columnKey: string) => { + const column = columns.find((c) => c.id === columnKey); + return { + isVisible: column?.isVisible, + order: column?.order, + // Any additional props or handlers for managing columns + }; + }; + const getColumnVisibility = (columnKey: TColumnKey) => { + return columns.find((column) => column.id === columnKey)?.isVisible ?? true; + }; + return { ...args, numColumnsBeforeData, diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 830a933270..0e5ac51dde 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,23 @@ 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, + order: index, + }) + ); + + const columnState = useColumnState({ + ...restArgs, + persistTo: getPersistTo("columns"), + initialColumns, + }); return { ...args, filterState, @@ -73,5 +91,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 6e4af77571..c1c7f3a615 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -16,7 +16,7 @@ import { Modal, Tooltip, } from "@patternfly/react-core"; -import { PencilAltIcon, TagIcon } from "@patternfly/react-icons"; +import { PencilAltIcon } from "@patternfly/react-icons"; import { Table, Thead, @@ -216,7 +216,7 @@ export const ApplicationsTable: React.FC = () => { refetch: fetchApplications, } = useFetchApplications(!hasActiveTasks); - const { assessments, isFetching: isFetchingAssesments } = + const { assessments, isFetching: isFetchingAssessments } = useFetchAssessments(); const { archetypes, isFetching: isFetchingArchetypes } = useFetchArchetypes(); @@ -311,20 +311,18 @@ export const ApplicationsTable: React.FC = () => { const deserializedFilterValues = deserializeFilterUrlParams({ filters }); - const [columnNames, setColumnNames] = React.useState>({ - name: "Name", - businessService: "Business Service", - assessment: "Assessment", - review: "Review", - analysis: "Analysis", - tags: "Tags", - effort: "Effort", - }); - const tableControls = useLocalTableControls({ idProperty: "id", items: applications || [], - columnNames, + columnNames: { + name: "Name", + businessService: "Business Service", + assessment: "Assessment", + review: "Review", + analysis: "Analysis", + tags: "Tags", + effort: "Effort", + }, isFilterEnabled: true, isSortEnabled: true, isPaginationEnabled: true, @@ -522,8 +520,8 @@ export const ApplicationsTable: React.FC = () => { toolbarBulkSelectorProps, }, activeItemDerivedState: { activeItem, clearActiveItem }, - selectionState: { selectedItems: selectedRows }, + columnState, } = tableControls; const clearFilters = () => { @@ -736,9 +734,93 @@ export const ApplicationsTable: React.FC = () => { const applicationName = assessmentToDiscard?.name || "Application name"; - ///TEMP HOME FOR manage cols modal + //TEST AREA + //TODO fix this. not needed. Should configure width on col name definition for table state + const columnBaseConfig = { + name: { label: "Name", width: 20 }, + businessService: { label: "Business Service", width: 25 }, + assessment: { label: "Assessment", width: 15 }, + review: { label: "Review", width: 15 }, + analysis: { label: "Analysis", width: 15 }, + tags: { label: "Tags", width: 15 }, + effort: { label: "Effort", width: 15 }, + }; + + const renderHeaders = () => { + return columnState.columns + .filter((column) => column.isVisible) + .sort((a, b) => a.order - b.order) + .map((column) => ( + + {column.label} + + )); + }; + + const renderRowCells = (application: Application) => { + return columnState.columns + .filter((column) => column.isVisible) + .sort((a, b) => a.order - b.order) + .map((column) => { + let cellContent; + switch (column.id) { + case "name": + cellContent = application.name || ""; + break; + case "businessService": + cellContent = ( + + ); + break; + case "assessment": + cellContent = ( + + ); + break; + case "review": + cellContent = ; + break; + case "analysis": + cellContent = ( + + ); + break; + case "tags": + cellContent = application.tags?.length || 0; + break; + case "effort": + cellContent = application.effort || "-"; + break; + + default: + cellContent = null; + } - /// + return ( + + {cellContent} + + ); + }); + }; + // return ( { <> )} @@ -837,7 +919,8 @@ export const ApplicationsTable: React.FC = () => { - + {renderHeaders()} + {/* { - + */} @@ -874,7 +957,7 @@ export const ApplicationsTable: React.FC = () => { return ( { item={application} rowIndex={rowIndex} > - - {application.name} - - - {application.businessService && ( - - )} - - - - - - - - - - - - - {application.tags ? application.tags.length : 0} - - - {application?.effort ?? "-"} - + {renderRowCells(application)} {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 index 8f7cd074e7..76b9769e74 100644 --- 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 @@ -17,13 +17,13 @@ import { TextContent, TextVariants, } from "@patternfly/react-core"; +import { ColumnState } from "@app/hooks/table-controls/column/useColumnState"; -export interface ManagedColumnsProps { +export interface ManagedColumnsProps { showModal: boolean; onClose(): void; - onChange(columnNames: Record): void; - columnNames: Record; - setColumnNames(columnNames: Record): void; + columns: ColumnState[]; // Updated to accept columns directly + setColumns: (newColumns: ColumnState[]) => void; // Accept setColumns function description?: string; saveLabel?: string; cancelLabel?: string; @@ -32,68 +32,51 @@ export interface ManagedColumnsProps { title?: string; } -export const ManageColumnsModal = ({ +export const ManageColumnsModal = ({ showModal, description = "Selected columns will be displayed in the table.", onClose, - onChange, - columnNames, + 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( - Object.entries(columnNames).map(([id, label]) => ({ - id, - label, - isVisible: true, // Initially set all columns as visible - })) - ); +}: ManagedColumnsProps) => { + const [editedColumns, setEditedColumns] = + useState[]>(columns); useEffect(() => { - // Reset editedColumns when columnNames prop changes - setEditedColumns( - Object.entries(columnNames).map(([id, label]) => ({ - id, - label, - isVisible: true, - })) - ); - }, [columnNames]); - - const restoreDefaults = () => { - // Assuming you want to reset to the initial state passed through props - setEditedColumns( - Object.entries(columnNames).map(([id, label]) => ({ - id, - label, - isVisible: true, - })) - ); - }; - - const onDrop = (sourceIndex: any, destinationIndex: any): boolean => { - if ( - typeof sourceIndex !== "number" || - typeof destinationIndex !== "number" - ) { - return false; - } + // Reset editedColumns when columns prop changes + setEditedColumns(columns); + }, [columns]); + const onDrop = (sourceIndex: number, destinationIndex: number): boolean => { const result = Array.from(editedColumns); const [removed] = result.splice(sourceIndex, 1); result.splice(destinationIndex, 0, removed); setEditedColumns(result); - - // Assuming the operation is successful return true; }; + // const onDrop = (sourceIndex: any, destinationIndex: any): boolean => { + // if ( + // typeof sourceIndex !== "number" || + // typeof destinationIndex !== "number" + // ) { + // return false; + // } + + // const result = Array.from(editedColumns); + // const [removed] = result.splice(sourceIndex, 1); + // result.splice(destinationIndex, 0, removed); + // setEditedColumns(result); - const onSelect = (id: any, isVisible: any) => { - console.log(`Toggling visibility for ${id}: ${isVisible}`); + // // Assuming the operation is successful + // return true; + // }; + const onSelect = (id: TColumnKey, isVisible: boolean): void => { setEditedColumns( editedColumns.map((col) => ({ ...col, @@ -103,16 +86,15 @@ export const ManageColumnsModal = ({ }; const onSave = () => { - const newColumnNames = editedColumns.reduce>( - (acc, { id, label, isVisible }) => { - if (isVisible) { - acc[id] = label; - } - return acc; - }, - {} + // Call the provided setColumns function with the updated columns + setColumns( + editedColumns.map((column, index) => ({ + ...column, + order: index, // Update the order based on the new position + })) ); - onChange(newColumnNames); + console.log("update cols", editedColumns); + onClose(); }; @@ -134,16 +116,16 @@ export const ManageColumnsModal = ({ , - , + // Restore button might be removed or adjusted based on how you handle default columns ]} > - onDrop(source.index, dest?.index)}> + onDrop(source.index, dest?.index || 0)} + > {editedColumns.map(({ id, label, isVisible }, index) => ( - + 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 index 966ef9d4e4..4444b3b8a4 100644 --- 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 @@ -7,22 +7,19 @@ import { } 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"; -export interface ManageColumnsToolbarProps { - /** Read only. The defaults used for initialization.*/ - columnNames: Record; - /** Setter to modify state in the parent.*/ - setColumnNames(columnNames: Record): void; +// Define props to accept columns and setColumns directly +interface ManageColumnsToolbarProps { + columns: ColumnState[]; + setColumns: (newColumns: ColumnState[]) => void; } -/** - * Toggles a modal dialog for managing resourceFields visibility and order. - */ -export const ManageColumnsToolbar = ({ - setColumnNames, - columnNames, -}: ManageColumnsToolbarProps) => { +export const ManageColumnsToolbar = ({ + columns, + setColumns, +}: ManageColumnsToolbarProps) => { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); return ( @@ -43,9 +40,8 @@ export const ManageColumnsToolbar = ({ showModal={isOpen} onClose={() => setIsOpen(false)} description={t("Selected columns will be displayed in the table.")} - onChange={setColumnNames} - setColumnNames={setColumnNames} - columnNames={columnNames} + setColumns={setColumns} + columns={columns} saveLabel={t("Save")} cancelLabel={t("Cancel")} reorderLabel={t("Reorder")}