Decompose BoardPage into individual components and remove debug logging.

Extract BoardRow, BoardColumn, BoardViewHeader, BoardEditHeader, and
boardConstants into separate files. Remove 9 console.log statements
from BoardBody and BoardProvider.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mike Cao 2026-02-06 04:47:23 -08:00
parent 5f404f62d8
commit 18702e130e
8 changed files with 311 additions and 310 deletions

View file

@ -74,9 +74,7 @@ export function BoardProvider({
// 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,

View file

@ -1,24 +1,12 @@
import { Box, Button, Column, Icon, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { Button, Icon, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { produce } from 'immer';
import { Fragment, type ReactElement, useEffect, useRef } from 'react';
import { Fragment, useEffect, useRef } from 'react';
import { Group, type GroupImperativeHandle, Panel, Separator } from 'react-resizable-panels';
import { v4 as uuid } from 'uuid';
import { useBoard } from '@/components/hooks';
import { ChevronDown, Minus, Plus, X } from '@/components/icons';
import type { BoardColumn as BoardColumnType } from '@/lib/types';
const CATALOG = {
text: {
label: 'Text',
component: BoardColumn,
},
};
const MIN_ROW_HEIGHT = 300;
const MAX_ROW_HEIGHT = 600;
const MIN_COLUMN_WIDTH = 300;
const BUTTON_ROW_HEIGHT = 60;
const MAX_COLUMNS = 4;
import { Plus } from '@/components/icons';
import { BoardRow } from './BoardRow';
import { BUTTON_ROW_HEIGHT, MAX_ROW_HEIGHT, MIN_ROW_HEIGHT } from './boardConstants';
export function BoardBody() {
const { board, editing, updateBoard, saveBoard, isPending, registerLayoutGetter } = useBoard();
@ -29,19 +17,14 @@ export function BoardBody() {
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,
@ -55,7 +38,6 @@ export function BoardBody() {
};
});
console.log('updatedRows:', updatedRows);
return { rows: updatedRows };
});
}, [registerLayoutGetter, board?.parameters?.rows]);
@ -68,8 +50,6 @@ export function BoardBody() {
}
};
console.log({ board });
const handleAddRow = () => {
updateBoard({
parameters: produce(board.parameters, draft => {
@ -168,167 +148,3 @@ export function BoardBody() {
</Group>
);
}
function BoardRow({
rowId,
rowIndex,
rowCount,
columns,
editing = false,
onRemove,
onMoveUp,
onMoveDown,
onRegisterRef,
}: {
rowId: string;
rowIndex: number;
rowCount: number;
columns: BoardColumnType[];
editing?: boolean;
onRemove?: (id: string) => void;
onMoveUp?: (id: string) => void;
onMoveDown?: (id: string) => void;
onRegisterRef?: (rowId: string, ref: GroupImperativeHandle | null) => void;
}) {
const { board, updateBoard } = useBoard();
const handleGroupRef = (ref: GroupImperativeHandle | null) => {
onRegisterRef?.(rowId, ref);
};
const handleAddColumn = () => {
updateBoard({
parameters: produce(board.parameters, draft => {
const rowIndex = draft.rows.findIndex(row => row.id === rowId);
const row = draft.rows[rowIndex];
if (!row) {
draft.rows[rowIndex] = { id: uuid(), columns: [] };
}
row.columns.push({ id: uuid(), component: null });
}),
});
};
const handleRemoveColumn = (columnId: string) => {
updateBoard({
parameters: produce(board.parameters, draft => {
const row = draft.rows.find(row => row.id === rowId);
if (row) {
row.columns = row.columns.filter(col => col.id !== columnId);
}
}),
});
};
return (
<Group groupRef={handleGroupRef} style={{ height: '100%' }}>
{columns?.map((column, index) => (
<Fragment key={column.id}>
<Panel id={column.id} minSize={MIN_COLUMN_WIDTH} defaultSize={column.size}>
<BoardColumn
{...column}
editing={editing}
onRemove={handleRemoveColumn}
canRemove={columns?.length > 1}
/>
</Panel>
{index < columns?.length - 1 && <Separator />}
</Fragment>
))}
{editing && (
<Column alignSelf="center" padding="3" gap="1">
<TooltipTrigger delay={0}>
<Button variant="outline" onPress={() => onMoveUp?.(rowId)} isDisabled={rowIndex === 0}>
<Icon rotate={180}>
<ChevronDown />
</Icon>
</Button>
<Tooltip placement="top">Move row up</Tooltip>
</TooltipTrigger>
<TooltipTrigger delay={0}>
<Button
variant="outline"
onPress={handleAddColumn}
isDisabled={columns?.length >= MAX_COLUMNS}
>
<Icon>
<Plus />
</Icon>
</Button>
<Tooltip placement="left">Add column</Tooltip>
</TooltipTrigger>
<TooltipTrigger delay={0}>
<Button variant="outline" onPress={() => onRemove?.(rowId)}>
<Icon>
<Minus />
</Icon>
</Button>
<Tooltip placement="left">Remove row</Tooltip>
</TooltipTrigger>
<TooltipTrigger delay={0}>
<Button
variant="outline"
onPress={() => onMoveDown?.(rowId)}
isDisabled={rowIndex === rowCount - 1}
>
<Icon>
<ChevronDown />
</Icon>
</Button>
<Tooltip placement="bottom">Move row down</Tooltip>
</TooltipTrigger>
</Column>
)}
</Group>
);
}
function BoardColumn({
id,
component,
editing = false,
onRemove,
canRemove = true,
}: {
id: string;
component?: ReactElement;
editing?: boolean;
onRemove?: (id: string) => void;
canRemove?: boolean;
}) {
const handleAddComponent = () => {};
return (
<Column
marginTop="3"
marginLeft="3"
width="100%"
height="100%"
alignItems="center"
justifyContent="center"
backgroundColor="surface-sunken"
position="relative"
>
{editing && canRemove && (
<Box position="absolute" top="10px" right="20px" zIndex={100}>
<TooltipTrigger delay={0}>
<Button variant="quiet" onPress={() => onRemove?.(id)}>
<Icon size="sm">
<X />
</Icon>
</Button>
<Tooltip>Remove column</Tooltip>
</TooltipTrigger>
</Box>
)}
{editing && (
<Button variant="outline" onPress={handleAddComponent}>
<Icon>
<Plus />
</Icon>
</Button>
)}
</Column>
);
}

View file

@ -0,0 +1,52 @@
import { Box, Button, Column, Icon, Tooltip, TooltipTrigger } from '@umami/react-zen';
import type { ReactElement } from 'react';
import { Plus, X } from '@/components/icons';
export function BoardColumn({
id,
component,
editing = false,
onRemove,
canRemove = true,
}: {
id: string;
component?: ReactElement;
editing?: boolean;
onRemove?: (id: string) => void;
canRemove?: boolean;
}) {
const handleAddComponent = () => {};
return (
<Column
marginTop="3"
marginLeft="3"
width="100%"
height="100%"
alignItems="center"
justifyContent="center"
backgroundColor="surface-sunken"
position="relative"
>
{editing && canRemove && (
<Box position="absolute" top="10px" right="20px" zIndex={100}>
<TooltipTrigger delay={0}>
<Button variant="quiet" onPress={() => onRemove?.(id)}>
<Icon size="sm">
<X />
</Icon>
</Button>
<Tooltip>Remove column</Tooltip>
</TooltipTrigger>
</Box>
)}
{editing && (
<Button variant="outline" onPress={handleAddComponent}>
<Icon>
<Plus />
</Icon>
</Button>
)}
</Column>
);
}

View file

@ -0,0 +1,99 @@
import {
Button,
Column,
Grid,
Heading,
LoadingButton,
Row,
Text,
TextField,
} from '@umami/react-zen';
import { useBoard, useMessages, useNavigation } from '@/components/hooks';
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
export function BoardEditHeader() {
const { board, updateBoard, saveBoard, isPending } = useBoard();
const { formatMessage, labels } = useMessages();
const { router, renderUrl } = useNavigation();
const defaultName = formatMessage(labels.untitled);
const handleNameChange = (value: string) => {
updateBoard({ name: value });
};
const handleDescriptionChange = (value: string) => {
updateBoard({ description: value });
};
const handleWebsiteChange = (websiteId: string) => {
updateBoard({ parameters: { ...board.parameters, websiteId } });
};
const handleSave = async () => {
await saveBoard();
if (board.id) {
router.push(renderUrl(`/boards/${board.id}`));
}
};
const handleCancel = () => {
if (board.id) {
router.push(renderUrl(`/boards/${board.id}`));
} else {
router.push(renderUrl('/boards'));
}
};
return (
<Grid
columns={{ base: '1fr', md: '1fr 1fr' }}
paddingY="4"
marginBottom="6"
border="bottom"
gapX="6"
>
<Column>
<Row>
<TextField
variant="quiet"
name="name"
value={board?.name ?? ''}
placeholder={defaultName}
onChange={handleNameChange}
autoComplete="off"
style={{ fontSize: '2rem', fontWeight: 700, width: '100%' }}
>
<Heading size="xl">{board?.name}</Heading>
</TextField>
</Row>
<Row>
<TextField
variant="quiet"
name="description"
value={board?.description ?? ''}
placeholder={`+ ${formatMessage(labels.addDescription)}`}
autoComplete="off"
onChange={handleDescriptionChange}
style={{ width: '100%' }}
>
{board?.description}
</TextField>
</Row>
<Row alignItems="center" gap="3">
<Text>{formatMessage(labels.website)}</Text>
<WebsiteSelect websiteId={board?.parameters?.websiteId} onChange={handleWebsiteChange} />
</Row>
</Column>
<Column justifyContent="center" alignItems="flex-end">
<Row gap="3">
<Button variant="quiet" onPress={handleCancel}>
{formatMessage(labels.cancel)}
</Button>
<LoadingButton variant="primary" onPress={handleSave} isLoading={isPending}>
{formatMessage(labels.save)}
</LoadingButton>
</Row>
</Column>
</Grid>
);
}

View file

@ -1,19 +1,6 @@
import {
Button,
Column,
Grid,
Heading,
LoadingButton,
Row,
Text,
TextField,
} from '@umami/react-zen';
import { IconLabel } from '@/components/common/IconLabel';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useBoard, useMessages, useNavigation, useWebsiteQuery } from '@/components/hooks';
import { Edit } from '@/components/icons';
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
import { useBoard } from '@/components/hooks';
import { BoardEditHeader } from './BoardEditHeader';
import { BoardViewHeader } from './BoardViewHeader';
export function BoardHeader() {
const { board, editing } = useBoard();
@ -24,106 +11,3 @@ export function BoardHeader() {
return <BoardViewHeader />;
}
function BoardViewHeader() {
const { board } = useBoard();
const { renderUrl } = useNavigation();
const { formatMessage, labels } = useMessages();
const { data: website } = useWebsiteQuery(board?.parameters?.websiteId);
return (
<PageHeader title={board?.name} description={board?.description}>
{website?.name && <Text>{website.name}</Text>}
<LinkButton href={renderUrl(`/boards/${board?.id}/edit`, false)}>
<IconLabel icon={<Edit />}>{formatMessage(labels.edit)}</IconLabel>
</LinkButton>
</PageHeader>
);
}
function BoardEditHeader() {
const { board, updateBoard, saveBoard, isPending } = useBoard();
const { formatMessage, labels } = useMessages();
const { router, renderUrl } = useNavigation();
const defaultName = formatMessage(labels.untitled);
const handleNameChange = (value: string) => {
updateBoard({ name: value });
};
const handleDescriptionChange = (value: string) => {
updateBoard({ description: value });
};
const handleWebsiteChange = (websiteId: string) => {
updateBoard({ parameters: { ...board.parameters, websiteId } });
};
const handleSave = async () => {
await saveBoard();
if (board.id) {
router.push(renderUrl(`/boards/${board.id}`));
}
};
const handleCancel = () => {
if (board.id) {
router.push(renderUrl(`/boards/${board.id}`));
} else {
router.push(renderUrl('/boards'));
}
};
return (
<Grid
columns={{ base: '1fr', md: '1fr 1fr' }}
paddingY="4"
marginBottom="6"
border="bottom"
gapX="6"
>
<Column>
<Row>
<TextField
variant="quiet"
name="name"
value={board?.name ?? ''}
placeholder={defaultName}
onChange={handleNameChange}
autoComplete="off"
style={{ fontSize: '2rem', fontWeight: 700, width: '100%' }}
>
<Heading size="xl">{board?.name}</Heading>
</TextField>
</Row>
<Row>
<TextField
variant="quiet"
name="description"
value={board?.description ?? ''}
placeholder={`+ ${formatMessage(labels.addDescription)}`}
autoComplete="off"
onChange={handleDescriptionChange}
style={{ width: '100%' }}
>
{board?.description}
</TextField>
</Row>
<Row alignItems="center" gap="3">
<Text>{formatMessage(labels.website)}</Text>
<WebsiteSelect websiteId={board?.parameters?.websiteId} onChange={handleWebsiteChange} />
</Row>
</Column>
<Column justifyContent="center" alignItems="flex-end">
<Row gap="3">
<Button variant="quiet" onPress={handleCancel}>
{formatMessage(labels.cancel)}
</Button>
<LoadingButton variant="primary" onPress={handleSave} isLoading={isPending}>
{formatMessage(labels.save)}
</LoadingButton>
</Row>
</Column>
</Grid>
);
}

View file

@ -0,0 +1,125 @@
import { Button, Column, Icon, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { produce } from 'immer';
import { Fragment } from 'react';
import { Group, type GroupImperativeHandle, Panel, Separator } from 'react-resizable-panels';
import { v4 as uuid } from 'uuid';
import { useBoard } from '@/components/hooks';
import { ChevronDown, Minus, Plus } from '@/components/icons';
import type { BoardColumn as BoardColumnType } from '@/lib/types';
import { BoardColumn } from './BoardColumn';
import { MAX_COLUMNS, MIN_COLUMN_WIDTH } from './boardConstants';
export function BoardRow({
rowId,
rowIndex,
rowCount,
columns,
editing = false,
onRemove,
onMoveUp,
onMoveDown,
onRegisterRef,
}: {
rowId: string;
rowIndex: number;
rowCount: number;
columns: BoardColumnType[];
editing?: boolean;
onRemove?: (id: string) => void;
onMoveUp?: (id: string) => void;
onMoveDown?: (id: string) => void;
onRegisterRef?: (rowId: string, ref: GroupImperativeHandle | null) => void;
}) {
const { board, updateBoard } = useBoard();
const handleGroupRef = (ref: GroupImperativeHandle | null) => {
onRegisterRef?.(rowId, ref);
};
const handleAddColumn = () => {
updateBoard({
parameters: produce(board.parameters, draft => {
const rowIndex = draft.rows.findIndex(row => row.id === rowId);
const row = draft.rows[rowIndex];
if (!row) {
draft.rows[rowIndex] = { id: uuid(), columns: [] };
}
row.columns.push({ id: uuid(), component: null });
}),
});
};
const handleRemoveColumn = (columnId: string) => {
updateBoard({
parameters: produce(board.parameters, draft => {
const row = draft.rows.find(row => row.id === rowId);
if (row) {
row.columns = row.columns.filter(col => col.id !== columnId);
}
}),
});
};
return (
<Group groupRef={handleGroupRef} style={{ height: '100%' }}>
{columns?.map((column, index) => (
<Fragment key={column.id}>
<Panel id={column.id} minSize={MIN_COLUMN_WIDTH} defaultSize={column.size}>
<BoardColumn
{...column}
editing={editing}
onRemove={handleRemoveColumn}
canRemove={columns?.length > 1}
/>
</Panel>
{index < columns?.length - 1 && <Separator />}
</Fragment>
))}
{editing && (
<Column alignSelf="center" padding="3" gap="1">
<TooltipTrigger delay={0}>
<Button variant="outline" onPress={() => onMoveUp?.(rowId)} isDisabled={rowIndex === 0}>
<Icon rotate={180}>
<ChevronDown />
</Icon>
</Button>
<Tooltip placement="top">Move row up</Tooltip>
</TooltipTrigger>
<TooltipTrigger delay={0}>
<Button
variant="outline"
onPress={handleAddColumn}
isDisabled={columns?.length >= MAX_COLUMNS}
>
<Icon>
<Plus />
</Icon>
</Button>
<Tooltip placement="left">Add column</Tooltip>
</TooltipTrigger>
<TooltipTrigger delay={0}>
<Button variant="outline" onPress={() => onRemove?.(rowId)}>
<Icon>
<Minus />
</Icon>
</Button>
<Tooltip placement="left">Remove row</Tooltip>
</TooltipTrigger>
<TooltipTrigger delay={0}>
<Button
variant="outline"
onPress={() => onMoveDown?.(rowId)}
isDisabled={rowIndex === rowCount - 1}
>
<Icon>
<ChevronDown />
</Icon>
</Button>
<Tooltip placement="bottom">Move row down</Tooltip>
</TooltipTrigger>
</Column>
)}
</Group>
);
}

View file

@ -0,0 +1,22 @@
import { Text } from '@umami/react-zen';
import { IconLabel } from '@/components/common/IconLabel';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useBoard, useMessages, useNavigation, useWebsiteQuery } from '@/components/hooks';
import { Edit } from '@/components/icons';
export function BoardViewHeader() {
const { board } = useBoard();
const { renderUrl } = useNavigation();
const { formatMessage, labels } = useMessages();
const { data: website } = useWebsiteQuery(board?.parameters?.websiteId);
return (
<PageHeader title={board?.name} description={board?.description}>
{website?.name && <Text>{website.name}</Text>}
<LinkButton href={renderUrl(`/boards/${board?.id}/edit`, false)}>
<IconLabel icon={<Edit />}>{formatMessage(labels.edit)}</IconLabel>
</LinkButton>
</PageHeader>
);
}

View file

@ -0,0 +1,5 @@
export const MIN_ROW_HEIGHT = 300;
export const MAX_ROW_HEIGHT = 600;
export const MIN_COLUMN_WIDTH = 300;
export const BUTTON_ROW_HEIGHT = 60;
export const MAX_COLUMNS = 4;