refactor: update Kanban components for improved layout management and persistence

- Removed the teamName prop from KanbanGridLayout to streamline component usage.
- Renamed GRID_SCOPE_PREFIX to GRID_SCOPE_KEY for clarity in layout management.
- Introduced LoadedKanbanGridLayout for better separation of concerns in rendering.
- Enhanced the usePersistedGridLayout hook to improve layout persistence handling.
- Updated KanbanBoard to utilize the new LoadedKanbanGridLayout, improving task organization.
This commit is contained in:
iliya 2026-03-11 18:32:08 +02:00
parent 1ccc1432fc
commit a216e73ede
3 changed files with 57 additions and 30 deletions

View file

@ -544,7 +544,6 @@ export const KanbanBoard = ({
{viewMode === 'grid' ? (
<KanbanGridLayout
teamName={teamName}
allColumnIds={COLUMNS.map((column) => column.id)}
columns={visibleColumns.map((column) => {
const columnTasks = groupedOrdered.get(column.id) ?? [];

View file

@ -1,5 +1,5 @@
/* eslint-disable tailwindcss/no-custom-classname -- this adapter needs stable non-Tailwind class hooks for react-grid-layout handles. */
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import ReactGridLayout, { WidthProvider } from 'react-grid-layout/legacy';
import { usePersistedGridLayout } from '@renderer/hooks/usePersistedGridLayout';
@ -24,7 +24,7 @@ const DEFAULT_ITEM_HEIGHT = Math.max(
);
const DEFAULT_MIN_HEIGHT = 10;
const DEFAULT_MIN_WIDTH = 3;
const GRID_SCOPE_PREFIX = 'kanban-grid-layout:v2';
const GRID_SCOPE_KEY = 'kanban-grid-layout:global';
const RESIZE_HANDLES: ResizeHandleAxis[] = ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'];
const WidthAwareGridLayout = WidthProvider(ReactGridLayout);
@ -39,11 +39,16 @@ export interface KanbanGridColumn {
}
interface KanbanGridLayoutProps {
teamName: string;
columns: KanbanGridColumn[];
allColumnIds: KanbanColumnId[];
}
interface LoadedKanbanGridLayoutProps {
readonly columns: KanbanGridColumn[];
readonly visibleItems: PersistedGridLayoutItem[];
readonly onPersistLayout: (layout: Layout, options?: { persist?: boolean }) => void;
}
function buildDefaultItems(itemIds: string[]): PersistedGridLayoutItem[] {
return itemIds.map((id, index) => ({
id,
@ -94,49 +99,31 @@ function renderResizeHandle(axis: ResizeHandleAxis, ref: Ref<HTMLElement>): Reac
);
}
export const KanbanGridLayout = ({
teamName,
const LoadedKanbanGridLayout = ({
columns,
allColumnIds,
}: KanbanGridLayoutProps): React.JSX.Element => {
visibleItems,
onPersistLayout,
}: Readonly<LoadedKanbanGridLayoutProps>): ReactElement => {
const columnMap = useMemo(() => new Map(columns.map((column) => [column.id, column])), [columns]);
const visibleColumnIds = useMemo(() => columns.map((column) => column.id), [columns]);
const { visibleItems, applyVisibleItems, isLoaded } = usePersistedGridLayout({
scopeKey: `${GRID_SCOPE_PREFIX}:${teamName}`,
allItemIds: allColumnIds,
visibleItemIds: visibleColumnIds,
cols: GRID_COLS,
repository: browserGridLayoutRepository,
buildDefaultItems,
});
const [renderLayout, setRenderLayout] = useState<Layout>(() =>
visibleItems.map(toReactGridLayoutItem)
);
useEffect(() => {
if (!isLoaded) return;
setRenderLayout(visibleItems.map(toReactGridLayoutItem));
}, [isLoaded, visibleItems]);
const applyReactGridLayout = useCallback(
(layout: Layout, options?: { persist?: boolean }) => {
setRenderLayout(layout);
if (options?.persist) {
applyVisibleItems(fromReactGridLayout(layout), options);
onPersistLayout(layout, options);
}
},
[applyVisibleItems]
[onPersistLayout]
);
if (!isLoaded) {
return <div className="min-h-[640px] p-1.5" />;
}
return (
<div className="p-1.5">
<WidthAwareGridLayout
className="kanban-grid-layout"
measureBeforeMount
layout={renderLayout}
cols={GRID_COLS}
rowHeight={GRID_ROW_HEIGHT}
@ -187,4 +174,45 @@ export const KanbanGridLayout = ({
</div>
);
};
export const KanbanGridLayout = ({
columns,
allColumnIds,
}: KanbanGridLayoutProps): React.JSX.Element => {
const visibleColumnIds = useMemo(() => columns.map((column) => column.id), [columns]);
const { visibleItems, applyVisibleItems, isLoaded } = usePersistedGridLayout({
scopeKey: GRID_SCOPE_KEY,
allItemIds: allColumnIds,
visibleItemIds: visibleColumnIds,
cols: GRID_COLS,
repository: browserGridLayoutRepository,
buildDefaultItems,
});
const applyReactGridLayout = useCallback(
(layout: Layout, options?: { persist?: boolean }) => {
if (options?.persist) {
applyVisibleItems(fromReactGridLayout(layout), options);
}
},
[applyVisibleItems]
);
if (!isLoaded) {
return <div className="min-h-[640px] p-1.5" />;
}
const gridKey = visibleItems
.map((item) => `${item.id}:${item.x}:${item.y}:${item.w}:${item.h}`)
.join('|');
return (
<LoadedKanbanGridLayout
key={gridKey}
columns={columns}
visibleItems={visibleItems}
onPersistLayout={applyReactGridLayout}
/>
);
};
/* eslint-enable tailwindcss/no-custom-classname -- stable class hooks remain scoped to this file. */

View file

@ -38,7 +38,7 @@ function removeLocalStorage(key: string): void {
}
function pickNewestState(
...states: Array<PersistedGridLayoutState | null | undefined>
...states: (PersistedGridLayoutState | null | undefined)[]
): PersistedGridLayoutState | null {
return states.reduce<PersistedGridLayoutState | null>((latest, current) => {
if (!current) return latest;