mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Add panel size persistence on board save.
- Add registerLayoutGetter to BoardContext for collecting sizes on save - Use GroupImperativeHandle and groupRef prop for react-resizable-panels - Add id props to Panels for layout mapping by panel id - Collect row and column sizes via getLayout() only when saving - Restore saved sizes via defaultSize prop on Panels Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
30e48e3aaa
commit
385bdd6734
2 changed files with 89 additions and 11 deletions
|
|
@ -1,15 +1,18 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Loading, useToast } from '@umami/react-zen';
|
import { Loading, useToast } from '@umami/react-zen';
|
||||||
import { createContext, type ReactNode, useCallback, useEffect, useState } from 'react';
|
import { createContext, type ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useApi, useMessages, useModified, useNavigation } from '@/components/hooks';
|
import { useApi, useMessages, useModified, useNavigation } from '@/components/hooks';
|
||||||
import { useBoardQuery } from '@/components/hooks/queries/useBoardQuery';
|
import { useBoardQuery } from '@/components/hooks/queries/useBoardQuery';
|
||||||
import type { Board } from '@/lib/types';
|
import type { Board, BoardParameters } from '@/lib/types';
|
||||||
|
|
||||||
|
export type LayoutGetter = () => Partial<BoardParameters> | null;
|
||||||
|
|
||||||
export interface BoardContextValue {
|
export interface BoardContextValue {
|
||||||
board: Partial<Board>;
|
board: Partial<Board>;
|
||||||
updateBoard: (data: Partial<Board>) => void;
|
updateBoard: (data: Partial<Board>) => void;
|
||||||
saveBoard: () => Promise<Board>;
|
saveBoard: () => Promise<Board>;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
|
registerLayoutGetter: (getter: LayoutGetter) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BoardContext = createContext<BoardContextValue>(null);
|
export const BoardContext = createContext<BoardContextValue>(null);
|
||||||
|
|
@ -29,6 +32,11 @@ export function BoardProvider({ boardId, children }: { boardId?: string; childre
|
||||||
const { router, renderUrl } = useNavigation();
|
const { router, renderUrl } = useNavigation();
|
||||||
|
|
||||||
const [board, setBoard] = useState<Partial<Board>>(data ?? defaultBoard);
|
const [board, setBoard] = useState<Partial<Board>>(data ?? defaultBoard);
|
||||||
|
const layoutGetterRef = useRef<LayoutGetter | null>(null);
|
||||||
|
|
||||||
|
const registerLayoutGetter = useCallback((getter: LayoutGetter) => {
|
||||||
|
layoutGetterRef.current = getter;
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
|
@ -51,7 +59,18 @@ export function BoardProvider({ boardId, children }: { boardId?: string; childre
|
||||||
|
|
||||||
const saveBoard = useCallback(async () => {
|
const saveBoard = useCallback(async () => {
|
||||||
const defaultName = formatMessage(labels.untitled);
|
const defaultName = formatMessage(labels.untitled);
|
||||||
const result = await mutateAsync({ ...board, name: board.name || defaultName });
|
|
||||||
|
// Get current layout sizes from BoardBody if registered
|
||||||
|
const layoutData = layoutGetterRef.current?.();
|
||||||
|
console.log('layoutData from getter:', layoutData);
|
||||||
|
const parameters = layoutData ? { ...board.parameters, ...layoutData } : board.parameters;
|
||||||
|
console.log('parameters to save:', parameters);
|
||||||
|
|
||||||
|
const result = await mutateAsync({
|
||||||
|
...board,
|
||||||
|
name: board.name || defaultName,
|
||||||
|
parameters,
|
||||||
|
});
|
||||||
|
|
||||||
toast(formatMessage(messages.saved));
|
toast(formatMessage(messages.saved));
|
||||||
touch('boards');
|
touch('boards');
|
||||||
|
|
@ -80,7 +99,9 @@ export function BoardProvider({ boardId, children }: { boardId?: string; childre
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoardContext.Provider value={{ board, updateBoard, saveBoard, isPending }}>
|
<BoardContext.Provider
|
||||||
|
value={{ board, updateBoard, saveBoard, isPending, registerLayoutGetter }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</BoardContext.Provider>
|
</BoardContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Box, Button, Column, Icon, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
import { Box, Button, Column, Icon, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
import { Fragment, type ReactElement } from 'react';
|
import { Fragment, type ReactElement, useEffect, useRef } from 'react';
|
||||||
import { Group, Panel, Separator } from 'react-resizable-panels';
|
import { Group, type GroupImperativeHandle, Panel, Separator } from 'react-resizable-panels';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { useBoard } from '@/components/hooks';
|
import { useBoard } from '@/components/hooks';
|
||||||
import { ChevronDown, Minus, Plus, X } from '@/components/icons';
|
import { ChevronDown, Minus, Plus, X } from '@/components/icons';
|
||||||
|
|
@ -21,7 +21,52 @@ const BUTTON_ROW_HEIGHT = 60;
|
||||||
const MAX_COLUMNS = 4;
|
const MAX_COLUMNS = 4;
|
||||||
|
|
||||||
export function BoardBody() {
|
export function BoardBody() {
|
||||||
const { board, updateBoard, saveBoard, isPending } = useBoard();
|
const { board, updateBoard, saveBoard, isPending, registerLayoutGetter } = useBoard();
|
||||||
|
const rowGroupRef = useRef<GroupImperativeHandle>(null);
|
||||||
|
const columnGroupRefs = useRef<Map<string, GroupImperativeHandle>>(new Map());
|
||||||
|
|
||||||
|
// Register a function to get current layout sizes on save
|
||||||
|
useEffect(() => {
|
||||||
|
registerLayoutGetter(() => {
|
||||||
|
const rows = board?.parameters?.rows;
|
||||||
|
console.log('Layout getter called, rows:', rows);
|
||||||
|
console.log('rowGroupRef.current:', rowGroupRef.current);
|
||||||
|
console.log('columnGroupRefs.current:', columnGroupRefs.current);
|
||||||
|
|
||||||
|
if (!rows?.length) return null;
|
||||||
|
|
||||||
|
const rowLayout = rowGroupRef.current?.getLayout();
|
||||||
|
console.log('rowLayout:', rowLayout);
|
||||||
|
|
||||||
|
const updatedRows = rows.map(row => {
|
||||||
|
const columnGroupRef = columnGroupRefs.current.get(row.id);
|
||||||
|
const columnLayout = columnGroupRef?.getLayout();
|
||||||
|
console.log(`Row ${row.id} columnLayout:`, columnLayout);
|
||||||
|
|
||||||
|
const updatedColumns = row.columns.map(col => ({
|
||||||
|
...col,
|
||||||
|
size: columnLayout?.[col.id],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
size: rowLayout?.[row.id],
|
||||||
|
columns: updatedColumns,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('updatedRows:', updatedRows);
|
||||||
|
return { rows: updatedRows };
|
||||||
|
});
|
||||||
|
}, [registerLayoutGetter, board?.parameters?.rows]);
|
||||||
|
|
||||||
|
const registerColumnGroupRef = (rowId: string, ref: GroupImperativeHandle | null) => {
|
||||||
|
if (ref) {
|
||||||
|
columnGroupRefs.current.set(rowId, ref);
|
||||||
|
} else {
|
||||||
|
columnGroupRefs.current.delete(rowId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
console.log({ board });
|
console.log({ board });
|
||||||
|
|
||||||
|
|
@ -82,10 +127,15 @@ export function BoardBody() {
|
||||||
const minHeight = (rows?.length || 1) * MAX_ROW_HEIGHT + BUTTON_ROW_HEIGHT;
|
const minHeight = (rows?.length || 1) * MAX_ROW_HEIGHT + BUTTON_ROW_HEIGHT;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group orientation="vertical" style={{ minHeight }}>
|
<Group groupRef={rowGroupRef} orientation="vertical" style={{ minHeight }}>
|
||||||
{rows.map((row, index) => (
|
{rows.map((row, index) => (
|
||||||
<Fragment key={row.id}>
|
<Fragment key={row.id}>
|
||||||
<Panel minSize={MIN_ROW_HEIGHT} maxSize={MAX_ROW_HEIGHT}>
|
<Panel
|
||||||
|
id={row.id}
|
||||||
|
minSize={MIN_ROW_HEIGHT}
|
||||||
|
maxSize={MAX_ROW_HEIGHT}
|
||||||
|
defaultSize={row.size}
|
||||||
|
>
|
||||||
<BoardRow
|
<BoardRow
|
||||||
{...row}
|
{...row}
|
||||||
rowId={row.id}
|
rowId={row.id}
|
||||||
|
|
@ -94,6 +144,7 @@ export function BoardBody() {
|
||||||
onRemove={handleRemoveRow}
|
onRemove={handleRemoveRow}
|
||||||
onMoveUp={handleMoveRowUp}
|
onMoveUp={handleMoveRowUp}
|
||||||
onMoveDown={handleMoveRowDown}
|
onMoveDown={handleMoveRowDown}
|
||||||
|
onRegisterRef={registerColumnGroupRef}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
{index < rows?.length - 1 && <Separator />}
|
{index < rows?.length - 1 && <Separator />}
|
||||||
|
|
@ -123,6 +174,7 @@ function BoardRow({
|
||||||
onRemove,
|
onRemove,
|
||||||
onMoveUp,
|
onMoveUp,
|
||||||
onMoveDown,
|
onMoveDown,
|
||||||
|
onRegisterRef,
|
||||||
}: {
|
}: {
|
||||||
rowId: string;
|
rowId: string;
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
|
|
@ -131,9 +183,14 @@ function BoardRow({
|
||||||
onRemove?: (id: string) => void;
|
onRemove?: (id: string) => void;
|
||||||
onMoveUp?: (id: string) => void;
|
onMoveUp?: (id: string) => void;
|
||||||
onMoveDown?: (id: string) => void;
|
onMoveDown?: (id: string) => void;
|
||||||
|
onRegisterRef?: (rowId: string, ref: GroupImperativeHandle | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const { board, updateBoard } = useBoard();
|
const { board, updateBoard } = useBoard();
|
||||||
|
|
||||||
|
const handleGroupRef = (ref: GroupImperativeHandle | null) => {
|
||||||
|
onRegisterRef?.(rowId, ref);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddColumn = () => {
|
const handleAddColumn = () => {
|
||||||
updateBoard({
|
updateBoard({
|
||||||
parameters: produce(board.parameters, draft => {
|
parameters: produce(board.parameters, draft => {
|
||||||
|
|
@ -160,10 +217,10 @@ function BoardRow({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group style={{ height: '100%' }}>
|
<Group groupRef={handleGroupRef} style={{ height: '100%' }}>
|
||||||
{columns?.map((column, index) => (
|
{columns?.map((column, index) => (
|
||||||
<Fragment key={column.id}>
|
<Fragment key={column.id}>
|
||||||
<Panel minSize={MIN_COLUMN_WIDTH}>
|
<Panel id={column.id} minSize={MIN_COLUMN_WIDTH} defaultSize={column.size}>
|
||||||
<BoardColumn {...column} onRemove={handleRemoveColumn} />
|
<BoardColumn {...column} onRemove={handleRemoveColumn} />
|
||||||
</Panel>
|
</Panel>
|
||||||
{index < columns?.length - 1 && <Separator />}
|
{index < columns?.length - 1 && <Separator />}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue