Skip to content

feat: add column visibility management to nodes table #751

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/web/public/i18n/locales/en/nodes.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@
"never": "Never"
}
},

"columnSettings": {
"title": "Column Settings",
"description": "Choose which columns to display",
"reset": "Reset to Default"
},
"actions": {
"added": "Added",
"removed": "Removed",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useColumnManager } from "@core/hooks/useColumnManager.ts";
import { useAppStore } from "@core/stores/appStore.ts";
import { useTranslation } from "react-i18next";
import { GenericColumnVisibilityControl } from "./GenericColumnVisibilityControl.tsx";

export const ColumnVisibilityControl = () => {
const { t } = useTranslation("nodes");
const { nodesTableColumns, updateColumnVisibility, resetColumnsToDefault } =
useAppStore();

const columnManager = useColumnManager({
columns: nodesTableColumns,
onUpdateColumn: (columnId, updates) => {
if ("visible" in updates && updates.visible !== undefined) {
updateColumnVisibility(columnId, updates.visible);
}
},
onResetColumns: resetColumnsToDefault,
});

return (
<GenericColumnVisibilityControl
columnManager={columnManager}
title={t("columnSettings.title", "Column Settings")}
resetLabel={t("columnSettings.reset", "Reset to Default")}
translateColumnTitle={(title) => (title.includes(".") ? t(title) : title)}
isColumnDisabled={(column) => column.id === "avatar"}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Button } from "@components/UI/Button.tsx";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@components/UI/DropdownMenu.tsx";
import type {
TableColumn,
UseColumnManagerReturn,
} from "@core/hooks/useColumnManager.ts";
import { SettingsIcon } from "lucide-react";
import type { ReactNode } from "react";

export interface GenericColumnVisibilityControlProps<
T extends TableColumn = TableColumn,
> {
columnManager: UseColumnManagerReturn<T>;
title?: string;
resetLabel?: string;
trigger?: ReactNode;
className?: string;
translateColumnTitle?: (title: string) => string;
isColumnDisabled?: (column: T) => boolean;
}

export function GenericColumnVisibilityControl<
T extends TableColumn = TableColumn,
>({
columnManager,
title = "Column Settings",
resetLabel = "Reset to Default",
trigger,
className,
translateColumnTitle = (title) => title,
isColumnDisabled = () => false,
}: GenericColumnVisibilityControlProps<T>) {
const {
allColumns,
visibleCount,
totalCount,
updateColumnVisibility,
resetColumns,
} = columnManager;

const defaultTrigger = (
<Button
variant="outline"
size="sm"
className="ml-2 h-8 px-2 text-xs"
title={title}
>
<SettingsIcon size={14} />
<span className="ml-1 hidden sm:inline">
Columns ({visibleCount}/{totalCount})
</span>
</Button>
);

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{trigger || defaultTrigger}
</DropdownMenuTrigger>
<DropdownMenuContent className={`w-56 ${className || ""}`} align="end">
<DropdownMenuLabel>{title}</DropdownMenuLabel>
<DropdownMenuSeparator />

{allColumns.map((column) => (
<DropdownMenuCheckboxItem
key={column.id}
checked={column.visible}
onCheckedChange={(checked) =>
updateColumnVisibility(column.id, checked ?? false)
}
disabled={isColumnDisabled(column)}
>
{translateColumnTitle(column.title)}
</DropdownMenuCheckboxItem>
))}

<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
checked={false}
onCheckedChange={() => resetColumns()}
>
{resetLabel}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
183 changes: 183 additions & 0 deletions packages/web/src/components/generic/ColumnVisibilityControl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Generic Table Column Management System

This system provides reusable components and hooks for managing table column visibility and state across the application.

## Components

### 1. `useColumnManager` Hook

A generic hook for managing table column state.

```tsx
import { useColumnManager } from "@core/hooks/useColumnManager.ts";

const columnManager = useColumnManager({
columns: myColumns,
onUpdateColumn: (columnId, updates) => {
// Handle column updates
},
onResetColumns: () => {
// Reset to default columns
},
});

// Access column data
const { visibleColumns, hiddenColumns, visibleCount } = columnManager;

// Update column visibility
columnManager.updateColumnVisibility("columnId", true);
```

### 2. `GenericColumnVisibilityControl` Component

A reusable dropdown component for managing column visibility.

```tsx
import { GenericColumnVisibilityControl } from "@components/generic/ColumnVisibilityControl";

<GenericColumnVisibilityControl
columnManager={columnManager}
title="Column Settings"
resetLabel="Reset to Default"
translateColumnTitle={(title) => t(title)}
isColumnDisabled={(column) => column.id === "required"}
/>
```

### 3. `createTableColumnStore` Factory

A factory function for creating column management state within Zustand stores.

```tsx
import { createTableColumnStore } from "@core/stores/createTableColumnStore";

// Define your column type
interface MyTableColumn extends TableColumn {
customProperty?: string;
}

// Create the column store
const myTableColumnStore = createTableColumnStore({
defaultColumns: myDefaultColumns,
storeName: "myTable",
});

// Use in your Zustand store
const useMyStore = create<MyState>()(
persist(
(set, get) => {
const setWrapper = (fn: (state: any) => void) => {
set(produce<MyState>(fn));
};

const columnActions = myTableColumnStore.createActions(setWrapper, get);

return {
...myTableColumnStore.initialState,
...columnActions,
// ... other state and actions
};
}
)
);
```

## Usage Examples

### Example 1: Simple Table with Column Management

```tsx
import { useColumnManager } from "@core/hooks/useColumnManager.ts";
import { GenericColumnVisibilityControl } from "@components/generic/ColumnVisibilityControl";

const MyTableComponent = () => {
const [columns, setColumns] = useState(defaultColumns);

const columnManager = useColumnManager({
columns,
onUpdateColumn: (columnId, updates) => {
setColumns(prev => prev.map(col =>
col.id === columnId ? { ...col, ...updates } : col
));
},
onResetColumns: () => setColumns(defaultColumns),
});

return (
<div>
<div className="flex justify-end mb-4">
<GenericColumnVisibilityControl
columnManager={columnManager}
title="Manage Columns"
/>
</div>

<Table
headings={columnManager.visibleColumns.map(col => ({
title: col.title,
sortable: col.sortable
}))}
rows={data.map(row => ({
id: row.id,
cells: columnManager.visibleColumns.map(col =>
getCellData(row, col.key)
)
}))}
/>
</div>
);
};
```

### Example 2: Custom Column Types

```tsx
interface CustomTableColumn extends TableColumn {
width?: number;
align?: 'left' | 'center' | 'right';
format?: 'currency' | 'date' | 'text';
}

const customColumnStore = createTableColumnStore<CustomTableColumn>({
defaultColumns: [
{
id: "name",
key: "name",
title: "Name",
visible: true,
sortable: true,
width: 200,
align: 'left'
},
{
id: "amount",
key: "amount",
title: "Amount",
visible: true,
sortable: true,
width: 120,
align: 'right',
format: 'currency'
},
],
});
```

## Benefits

1. **Reusable**: Can be used across multiple tables in the application
2. **Type-safe**: Full TypeScript support with generic types
3. **Flexible**: Supports custom column properties and behaviors
4. **Consistent**: Uses existing UI components and patterns
5. **Persistent**: Column preferences can be saved to localStorage
6. **Accessible**: Proper ARIA labels and keyboard navigation

## Migration from Existing Systems

If you have existing column management code, you can migrate it step by step:

1. Replace custom column state with `useColumnManager`
2. Replace custom UI components with `GenericColumnVisibilityControl`
3. Optionally refactor stores to use `createTableColumnStore`

The existing `ColumnVisibilityControl` component has been updated to use this new system while maintaining backward compatibility.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ColumnVisibilityControl } from "./ColumnVisibilityControl.tsx";
export { GenericColumnVisibilityControl } from "./GenericColumnVisibilityControl.tsx";
Loading