Browse Source
- Add generic column management system with TypeScript support - Implement ColumnVisibilityControl component with dropdown interface - Add persistent column visibility settings via Zustand store - Extend nodes table with additional columns (role, battery, uptime, etc.) - Refactor table cell rendering to use helper functions - Add comprehensive documentation and examples - Support internationalization for column settings Features: - Toggle visibility of table columns individually - Reset columns to default configuration - Persistent settings across sessions - Generic system reusable for other tables - Proper TypeScript types throughout - Existing UI component patterns maintainedpull/751/head
11 changed files with 1048 additions and 227 deletions
@ -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"} |
|||
/> |
|||
); |
|||
}; |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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. |
|||
@ -0,0 +1,2 @@ |
|||
export { ColumnVisibilityControl } from "./ColumnVisibilityControl.tsx"; |
|||
export { GenericColumnVisibilityControl } from "./GenericColumnVisibilityControl.tsx"; |
|||
@ -0,0 +1,252 @@ |
|||
import { Mono } from "@components/generic/Mono.tsx"; |
|||
import { TimeAgo } from "@components/generic/TimeAgo.tsx"; |
|||
import { Avatar } from "@components/UI/Avatar.tsx"; |
|||
import { Protobuf } from "@meshtastic/core"; |
|||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; |
|||
import { LockIcon, LockOpenIcon } from "lucide-react"; |
|||
import type { JSX } from "react"; |
|||
import { base16 } from "rfc4648"; |
|||
|
|||
// Helper function to format position
|
|||
const formatPosition = (position?: Protobuf.Mesh.Position) => { |
|||
if (!position || (!position.latitudeI && !position.longitudeI)) { |
|||
return "Unknown"; |
|||
} |
|||
const lat = position.latitudeI ? (position.latitudeI * 1e-7).toFixed(6) : "0"; |
|||
const lng = position.longitudeI |
|||
? (position.longitudeI * 1e-7).toFixed(6) |
|||
: "0"; |
|||
return `${lat}, ${lng}`; |
|||
}; |
|||
|
|||
// Helper function to format battery level
|
|||
const formatBatteryLevel = ( |
|||
deviceMetrics?: Protobuf.Telemetry.DeviceMetrics, |
|||
) => { |
|||
if (!deviceMetrics?.batteryLevel) { |
|||
return "Unknown"; |
|||
} |
|||
return `${deviceMetrics.batteryLevel}%`; |
|||
}; |
|||
|
|||
// Helper function to format uptime
|
|||
const formatUptime = (deviceMetrics?: Protobuf.Telemetry.DeviceMetrics) => { |
|||
if (!deviceMetrics?.uptimeSeconds) { |
|||
return "Unknown"; |
|||
} |
|||
const seconds = deviceMetrics.uptimeSeconds; |
|||
const days = Math.floor(seconds / 86400); |
|||
const hours = Math.floor((seconds % 86400) / 3600); |
|||
const minutes = Math.floor((seconds % 3600) / 60); |
|||
|
|||
if (days > 0) { |
|||
return `${days}d ${hours}h`; |
|||
} |
|||
if (hours > 0) { |
|||
return `${hours}h ${minutes}m`; |
|||
} |
|||
return `${minutes}m`; |
|||
}; |
|||
|
|||
export const getNodeCellData = ( |
|||
node: Protobuf.Mesh.NodeInfo, |
|||
columnKey: string, |
|||
t: (key: string) => string, |
|||
currentLanguage?: { code: string }, |
|||
hasNodeError?: (num: number) => boolean, |
|||
handleNodeInfoDialog?: (num: number) => void, |
|||
): { content: JSX.Element; sortValue: string | number } => { |
|||
switch (columnKey) { |
|||
case "avatar": |
|||
return { |
|||
content: ( |
|||
<Avatar |
|||
text={node.user?.shortName ?? t("unknown.shortName")} |
|||
showFavorite={node.isFavorite} |
|||
showError={hasNodeError?.(node.num) ?? false} |
|||
/> |
|||
), |
|||
sortValue: node.user?.shortName ?? "", |
|||
}; |
|||
|
|||
case "longName": |
|||
return { |
|||
content: ( |
|||
<h1 |
|||
onMouseDown={() => handleNodeInfoDialog?.(node.num)} |
|||
onKeyUp={(evt) => { |
|||
evt.key === "Enter" && handleNodeInfoDialog?.(node.num); |
|||
}} |
|||
className="cursor-pointer underline ml-2 whitespace-break-spaces" |
|||
> |
|||
{node.user?.longName ?? numberToHexUnpadded(node.num)} |
|||
</h1> |
|||
), |
|||
sortValue: node.user?.longName ?? numberToHexUnpadded(node.num), |
|||
}; |
|||
|
|||
case "shortName": |
|||
return { |
|||
content: <Mono>{node.user?.shortName ?? t("unknown.shortName")}</Mono>, |
|||
sortValue: node.user?.shortName ?? "", |
|||
}; |
|||
|
|||
case "nodeId": |
|||
return { |
|||
content: <Mono>{numberToHexUnpadded(node.num)}</Mono>, |
|||
sortValue: node.num, |
|||
}; |
|||
|
|||
case "connection": { |
|||
const connectionText = |
|||
node.hopsAway !== undefined |
|||
? node?.viaMqtt === false && node.hopsAway === 0 |
|||
? t("nodesTable.connectionStatus.direct") |
|||
: `${node.hopsAway?.toString()} ${ |
|||
(node.hopsAway ?? 0 > 1) |
|||
? t("unit.hop.plural") |
|||
: t("unit.hops_one") |
|||
} ${t("nodesTable.connectionStatus.away")}` |
|||
: t("nodesTable.connectionStatus.unknown"); |
|||
|
|||
const mqttText = |
|||
node?.viaMqtt === true ? t("nodesTable.connectionStatus.viaMqtt") : ""; |
|||
|
|||
return { |
|||
content: ( |
|||
<Mono className="w-16"> |
|||
{connectionText} |
|||
{mqttText} |
|||
</Mono> |
|||
), |
|||
sortValue: node.hopsAway ?? Number.MAX_SAFE_INTEGER, |
|||
}; |
|||
} |
|||
|
|||
case "lastHeard": |
|||
return { |
|||
content: ( |
|||
<Mono> |
|||
{node.lastHeard === 0 ? ( |
|||
<p>{t("nodesTable.lastHeardStatus.never")}</p> |
|||
) : ( |
|||
<TimeAgo |
|||
timestamp={node.lastHeard * 1000} |
|||
locale={currentLanguage?.code} |
|||
/> |
|||
)} |
|||
</Mono> |
|||
), |
|||
sortValue: node.lastHeard, |
|||
}; |
|||
|
|||
case "encryption": |
|||
return { |
|||
content: ( |
|||
<Mono> |
|||
{node.user?.publicKey && node.user?.publicKey.length > 0 ? ( |
|||
<LockIcon className="text-green-600 mx-auto" /> |
|||
) : ( |
|||
<LockOpenIcon className="text-yellow-300 mx-auto" /> |
|||
)} |
|||
</Mono> |
|||
), |
|||
sortValue: "", |
|||
}; |
|||
|
|||
case "snr": |
|||
return { |
|||
content: ( |
|||
<Mono> |
|||
{node.snr} |
|||
{t("unit.dbm")}/{Math.min(Math.max((node.snr + 10) * 5, 0), 100)} |
|||
%/{(node.snr + 10) * 5} |
|||
{t("unit.raw")} |
|||
</Mono> |
|||
), |
|||
sortValue: node.snr, |
|||
}; |
|||
|
|||
case "model": { |
|||
const modelName = |
|||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ?? "Unknown"; |
|||
return { |
|||
content: <Mono>{modelName}</Mono>, |
|||
sortValue: modelName, |
|||
}; |
|||
} |
|||
|
|||
case "macAddress": { |
|||
const macAddress = |
|||
base16 |
|||
.stringify(node.user?.macaddr ?? []) |
|||
.match(/.{1,2}/g) |
|||
?.join(":") ?? t("unknown.shortName"); |
|||
|
|||
return { |
|||
content: <Mono>{macAddress}</Mono>, |
|||
sortValue: macAddress, |
|||
}; |
|||
} |
|||
|
|||
case "role": { |
|||
const roleName = |
|||
Protobuf.Config.Config_DeviceConfig_Role[node.user?.role ?? 0] ?? |
|||
"Unknown"; |
|||
return { |
|||
content: <Mono>{roleName.replace(/_/g, " ")}</Mono>, |
|||
sortValue: roleName, |
|||
}; |
|||
} |
|||
|
|||
case "batteryLevel": |
|||
return { |
|||
content: <Mono>{formatBatteryLevel(node.deviceMetrics)}</Mono>, |
|||
sortValue: node.deviceMetrics?.batteryLevel ?? 0, |
|||
}; |
|||
|
|||
case "channelUtilization": |
|||
return { |
|||
content: ( |
|||
<Mono> |
|||
{node.deviceMetrics?.channelUtilization |
|||
? `${node.deviceMetrics.channelUtilization.toFixed(1)}%` |
|||
: "Unknown"} |
|||
</Mono> |
|||
), |
|||
sortValue: node.deviceMetrics?.channelUtilization ?? 0, |
|||
}; |
|||
|
|||
case "airtimeUtilization": |
|||
return { |
|||
content: ( |
|||
<Mono> |
|||
{node.deviceMetrics?.airUtilTx |
|||
? `${node.deviceMetrics.airUtilTx.toFixed(1)}%` |
|||
: "Unknown"} |
|||
</Mono> |
|||
), |
|||
sortValue: node.deviceMetrics?.airUtilTx ?? 0, |
|||
}; |
|||
|
|||
case "uptime": |
|||
return { |
|||
content: <Mono>{formatUptime(node.deviceMetrics)}</Mono>, |
|||
sortValue: node.deviceMetrics?.uptimeSeconds ?? 0, |
|||
}; |
|||
|
|||
case "position": |
|||
return { |
|||
content: ( |
|||
<Mono className="text-xs">{formatPosition(node.position)}</Mono> |
|||
), |
|||
sortValue: formatPosition(node.position), |
|||
}; |
|||
|
|||
default: |
|||
return { |
|||
content: <Mono>Unknown</Mono>, |
|||
sortValue: "", |
|||
}; |
|||
} |
|||
}; |
|||
@ -0,0 +1,92 @@ |
|||
import { useCallback, useMemo } from "react"; |
|||
|
|||
export interface TableColumn { |
|||
id: string; |
|||
key: string; |
|||
title: string; |
|||
visible: boolean; |
|||
sortable: boolean; |
|||
} |
|||
|
|||
export interface ColumnManagerConfig<T extends TableColumn = TableColumn> { |
|||
columns: T[]; |
|||
onUpdateColumn: (columnId: string, updates: Partial<T>) => void; |
|||
onResetColumns: () => void; |
|||
} |
|||
|
|||
export interface UseColumnManagerReturn<T extends TableColumn = TableColumn> { |
|||
allColumns: T[]; |
|||
visibleColumns: T[]; |
|||
hiddenColumns: T[]; |
|||
visibleCount: number; |
|||
totalCount: number; |
|||
updateColumnVisibility: (columnId: string, visible: boolean) => void; |
|||
toggleColumnVisibility: (columnId: string) => void; |
|||
resetColumns: () => void; |
|||
isColumnVisible: (columnId: string) => boolean; |
|||
getColumn: (columnId: string) => T | undefined; |
|||
} |
|||
|
|||
/** |
|||
* Generic hook for managing table column visibility and state |
|||
* Can be used with any table that needs column management |
|||
*/ |
|||
export function useColumnManager<T extends TableColumn = TableColumn>({ |
|||
columns, |
|||
onUpdateColumn, |
|||
onResetColumns, |
|||
}: ColumnManagerConfig<T>): UseColumnManagerReturn<T> { |
|||
const visibleColumns = useMemo( |
|||
() => columns.filter((col) => col.visible), |
|||
[columns], |
|||
); |
|||
|
|||
const hiddenColumns = useMemo( |
|||
() => columns.filter((col) => !col.visible), |
|||
[columns], |
|||
); |
|||
|
|||
const updateColumnVisibility = useCallback( |
|||
(columnId: string, visible: boolean) => { |
|||
onUpdateColumn(columnId, { visible } as Partial<T>); |
|||
}, |
|||
[onUpdateColumn], |
|||
); |
|||
|
|||
const toggleColumnVisibility = useCallback( |
|||
(columnId: string) => { |
|||
const column = columns.find((col) => col.id === columnId); |
|||
if (column) { |
|||
updateColumnVisibility(columnId, !column.visible); |
|||
} |
|||
}, |
|||
[columns, updateColumnVisibility], |
|||
); |
|||
|
|||
const isColumnVisible = useCallback( |
|||
(columnId: string) => { |
|||
return columns.find((col) => col.id === columnId)?.visible ?? false; |
|||
}, |
|||
[columns], |
|||
); |
|||
|
|||
const getColumn = useCallback( |
|||
(columnId: string) => { |
|||
return columns.find((col) => col.id === columnId); |
|||
}, |
|||
[columns], |
|||
); |
|||
|
|||
return { |
|||
allColumns: columns, |
|||
visibleColumns, |
|||
hiddenColumns, |
|||
visibleCount: visibleColumns.length, |
|||
totalCount: columns.length, |
|||
updateColumnVisibility, |
|||
toggleColumnVisibility, |
|||
resetColumns: onResetColumns, |
|||
isColumnVisible, |
|||
getColumn, |
|||
}; |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
import type { TableColumn } from "@core/hooks/useColumnManager.ts"; |
|||
import { produce } from "immer"; |
|||
|
|||
export interface TableStoreConfig<T extends TableColumn> { |
|||
defaultColumns: T[]; |
|||
storeName?: string; |
|||
} |
|||
|
|||
export interface TableStoreActions<T extends TableColumn> { |
|||
updateColumnVisibility: (columnId: string, visible: boolean) => void; |
|||
updateColumn: (columnId: string, updates: Partial<T>) => void; |
|||
resetColumnsToDefault: () => void; |
|||
setColumns: (columns: T[]) => void; |
|||
} |
|||
|
|||
export interface TableStoreState<T extends TableColumn> { |
|||
columns: T[]; |
|||
} |
|||
|
|||
/** |
|||
* Factory function to create column management state and actions |
|||
* Can be used within any Zustand store or as a standalone solution |
|||
*/ |
|||
export function createTableColumnStore<T extends TableColumn>( |
|||
config: TableStoreConfig<T>, |
|||
) { |
|||
const { defaultColumns } = config; |
|||
|
|||
// Initial state
|
|||
const initialState: TableStoreState<T> = { |
|||
columns: defaultColumns, |
|||
}; |
|||
|
|||
// Actions factory
|
|||
const createActions = ( |
|||
set: (fn: (state: any) => void) => void, |
|||
_get?: () => any, |
|||
): TableStoreActions<T> => ({ |
|||
updateColumnVisibility: (columnId: string, visible: boolean) => { |
|||
set( |
|||
produce((draft: any) => { |
|||
// Access the nodesTableColumns property instead of columns
|
|||
const columns = draft.nodesTableColumns; |
|||
if (columns) { |
|||
const column = columns.find((col: T) => col.id === columnId); |
|||
if (column) { |
|||
column.visible = visible; |
|||
} |
|||
} |
|||
}), |
|||
); |
|||
}, |
|||
|
|||
updateColumn: (columnId: string, updates: Partial<T>) => { |
|||
set( |
|||
produce((draft: any) => { |
|||
const columns = draft.nodesTableColumns; |
|||
if (columns) { |
|||
const column = columns.find((col: T) => col.id === columnId); |
|||
if (column) { |
|||
Object.assign(column, updates); |
|||
} |
|||
} |
|||
}), |
|||
); |
|||
}, |
|||
|
|||
resetColumnsToDefault: () => { |
|||
set( |
|||
produce((draft: any) => { |
|||
draft.nodesTableColumns = defaultColumns; |
|||
}), |
|||
); |
|||
}, |
|||
|
|||
setColumns: (columns: T[]) => { |
|||
set( |
|||
produce((draft: any) => { |
|||
draft.nodesTableColumns = columns; |
|||
}), |
|||
); |
|||
}, |
|||
}); |
|||
|
|||
return { |
|||
initialState, |
|||
createActions, |
|||
defaultColumns, |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Utility type for extracting column store slice from a larger store |
|||
*/ |
|||
export type TableColumnStoreSlice<T extends TableColumn> = TableStoreState<T> & |
|||
TableStoreActions<T>; |
|||
Loading…
Reference in new issue