From 7451fc0d483809d277ef63ab5c256e3d05c56cb9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc=20Gu=CC=88ell=20Segarra?= <marc@ondori.dev>
Date: Fri, 26 Jul 2024 11:38:56 +0200
Subject: [PATCH] feat: add status column behaviour to infinitetable

---
 .../InfiniteTable/InfiniteTable.tsx           | 101 +++++++++++++-----
 .../InfiniteTable/useAutoFitColumns.ts        |  15 ++-
 src/stories/InfiniteTable.stories.tsx         |   4 +
 3 files changed, 88 insertions(+), 32 deletions(-)

diff --git a/src/components/InfiniteTable/InfiniteTable.tsx b/src/components/InfiniteTable/InfiniteTable.tsx
index 753099f..113533c 100644
--- a/src/components/InfiniteTable/InfiniteTable.tsx
+++ b/src/components/InfiniteTable/InfiniteTable.tsx
@@ -47,6 +47,9 @@ export type InfiniteTableProps = Omit<
   onAllRowSelectedModeChange?: (allRowSelectedMode: boolean) => void;
   footer?: React.ReactNode;
   footerHeight?: number;
+  hasStatusColumn?: boolean;
+  onRowStatus?: (item: any) => any;
+  statusComponent?: (status: any) => React.ReactNode;
 };
 
 export type InfiniteTableRef = {
@@ -73,6 +76,9 @@ const InfiniteTableComp = forwardRef<InfiniteTableRef, InfiniteTableProps>(
       allRowSelectedMode: allRowSelectedModeProps,
       footer,
       footerHeight = 50,
+      onRowStatus,
+      statusComponent,
+      hasStatusColumn = false,
     } = props;
 
     const gridRef = useRef<AgGridReact>(null);
@@ -88,6 +94,7 @@ const InfiniteTableComp = forwardRef<InfiniteTableRef, InfiniteTableProps>(
       gridRef,
       containerRef,
       columnsPersistedStateRef,
+      hasStatusColumn,
     });
 
     // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -144,41 +151,60 @@ const InfiniteTableComp = forwardRef<InfiniteTableRef, InfiniteTableProps>(
     const defaultColDef = useMemo<ColDef>(() => ({}), []);
 
     const colDefs = useMemo((): ColDef[] => {
+      const checkboxColumn = {
+        checkboxSelection: true,
+        suppressMovable: true,
+        sortable: false,
+        lockPosition: true,
+        pinned: "left",
+        maxWidth: 50,
+        resizable: false,
+        headerComponent: () => (
+          <HeaderCheckbox
+            totalRows={totalRows}
+            selectedRowKeysLength={internalSelectedRowKeys.length}
+            allRowSelected={
+              totalRows === internalSelectedRowKeys.length && totalRows > 0
+            }
+            allRowSelectedMode={allRowSelectedMode}
+            onHeaderCheckboxChange={onHeaderCheckboxChange}
+          />
+        ),
+      } as ColDef;
+
+      const restOfColumns = columns.map((column) => ({
+        field: column.key,
+        sortable: false,
+        headerName: column.title,
+        cellRenderer: column.render
+          ? (cell: any) => column.render(cell.value)
+          : undefined,
+      }));
+
+      const statusColumn = {
+        field: "$status",
+        suppressMovable: true,
+        sortable: false,
+        lockPosition: true,
+        maxWidth: 50,
+        pinned: "left",
+        resizable: false,
+        headerComponent: () => null,
+        cellRenderer: (cell: any) => statusComponent?.(cell.value),
+      } as ColDef;
+
       return [
-        {
-          checkboxSelection: true,
-          suppressMovable: true,
-          sortable: false,
-          lockPosition: true,
-          pinned: "left",
-          maxWidth: 50,
-          resizable: false,
-          headerComponent: () => (
-            <HeaderCheckbox
-              totalRows={totalRows}
-              selectedRowKeysLength={internalSelectedRowKeys.length}
-              allRowSelected={
-                totalRows === internalSelectedRowKeys.length && totalRows > 0
-              }
-              allRowSelectedMode={allRowSelectedMode}
-              onHeaderCheckboxChange={onHeaderCheckboxChange}
-            />
-          ),
-        },
-        ...columns.map((column) => ({
-          field: column.key,
-          sortable: false,
-          headerName: column.title,
-          cellRenderer: column.render
-            ? (cell: any) => column.render(cell.value)
-            : undefined,
-        })),
+        checkboxColumn,
+        ...(hasStatusColumn ? [statusColumn] : []),
+        ...restOfColumns,
       ];
     }, [
       allRowSelectedMode,
       columns,
+      hasStatusColumn,
       internalSelectedRowKeys.length,
       onHeaderCheckboxChange,
+      statusComponent,
       totalRows,
     ]);
 
@@ -191,7 +217,22 @@ const InfiniteTableComp = forwardRef<InfiniteTableRef, InfiniteTableProps>(
         if (data.length < endRow - startRow) {
           lastRow = startRow + data.length;
         }
-        params.successCallback(data, lastRow);
+
+        // We must call onRowStatus for each item of the data array and merge the result
+        // with the data array
+        const finalData = hasStatusColumn
+          ? await Promise.all(
+              data.map(async (item) => {
+                const status = await onRowStatus?.(item);
+                return {
+                  ...item,
+                  $status: status,
+                };
+              }),
+            )
+          : data;
+
+        params.successCallback(finalData, lastRow);
         if (allRowSelectedModeRef.current) {
           gridRef?.current?.api.forEachNode((node) => {
             node.setSelected(true);
@@ -219,8 +260,10 @@ const InfiniteTableComp = forwardRef<InfiniteTableRef, InfiniteTableProps>(
       },
       [
         autoSizeColumnsIfNecessary,
+        hasStatusColumn,
         onGetSelectedRowKeys,
         onRequestData,
+        onRowStatus,
         selectedRowKeysPendingToRender,
         setSelectedRowKeysPendingToRender,
       ],
diff --git a/src/components/InfiniteTable/useAutoFitColumns.ts b/src/components/InfiniteTable/useAutoFitColumns.ts
index 2d9666b..40ea020 100644
--- a/src/components/InfiniteTable/useAutoFitColumns.ts
+++ b/src/components/InfiniteTable/useAutoFitColumns.ts
@@ -6,13 +6,20 @@ export const useAutoFitColumns = ({
   gridRef,
   containerRef,
   columnsPersistedStateRef,
+  hasStatusColumn,
 }: {
   gridRef: RefObject<AgGridReact>;
   containerRef: RefObject<HTMLDivElement>;
   columnsPersistedStateRef: RefObject<any>;
+  hasStatusColumn: boolean;
 }) => {
   const firstTimeResized = useRef(false);
 
+  const columnsToIgnore = ["0"]; // 0 is for header checkbox column
+  if (hasStatusColumn) {
+    columnsToIgnore.push("$status");
+  }
+
   const remainingBlankSpace = useCallback(
     (allColumns: Array<Column<any>>) => {
       const totalColumnWidth = allColumns?.reduce(
@@ -36,12 +43,14 @@ export const useAutoFitColumns = ({
         if (!allColumns) return;
         const blankSpace = remainingBlankSpace(allColumns);
         if (blankSpace > 0) {
-          const spacePerColumn = blankSpace / (allColumns.length - 1); // we skip the first checkbox column, since it's not resizable
+          const spacePerColumn =
+            blankSpace / (allColumns.length - columnsToIgnore.length);
           const state = gridRef?.current?.api.getColumnState()!;
           const newState = state.map((col: any) => ({
             ...col,
-            // colId 0 is the checkbox column
-            width: col.colId !== "0" ? col.width + spacePerColumn : col.width,
+            width: columnsToIgnore.includes(col.colId)
+              ? col.width
+              : col.width + spacePerColumn,
           }));
           gridRef?.current?.api.applyColumnState({ state: newState });
         }
diff --git a/src/stories/InfiniteTable.stories.tsx b/src/stories/InfiniteTable.stories.tsx
index d84170a..5d6a87f 100644
--- a/src/stories/InfiniteTable.stories.tsx
+++ b/src/stories/InfiniteTable.stories.tsx
@@ -88,6 +88,10 @@ export const HeavyTable = (): React.ReactElement => {
           return columnsState ? JSON.parse(columnsState) : undefined;
         }}
         footer={<p>This is a footer</p>}
+        onRowStatus={(record: any) => {
+          return record.id;
+        }}
+        statusComponent={(status: any) => <strong>{status}</strong>}
       />
     </>
   );