mirror of
https://github.com/umami-software/umami.git
synced 2026-02-10 07:37:11 +01:00
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:
parent
5f404f62d8
commit
18702e130e
8 changed files with 311 additions and 310 deletions
|
|
@ -74,9 +74,7 @@ export function BoardProvider({
|
||||||
|
|
||||||
// Get current layout sizes from BoardBody if registered
|
// Get current layout sizes from BoardBody if registered
|
||||||
const layoutData = layoutGetterRef.current?.();
|
const layoutData = layoutGetterRef.current?.();
|
||||||
console.log('layoutData from getter:', layoutData);
|
|
||||||
const parameters = layoutData ? { ...board.parameters, ...layoutData } : board.parameters;
|
const parameters = layoutData ? { ...board.parameters, ...layoutData } : board.parameters;
|
||||||
console.log('parameters to save:', parameters);
|
|
||||||
|
|
||||||
const result = await mutateAsync({
|
const result = await mutateAsync({
|
||||||
...board,
|
...board,
|
||||||
|
|
|
||||||
|
|
@ -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 { 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 { 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 { Plus } from '@/components/icons';
|
||||||
import type { BoardColumn as BoardColumnType } from '@/lib/types';
|
import { BoardRow } from './BoardRow';
|
||||||
|
import { BUTTON_ROW_HEIGHT, MAX_ROW_HEIGHT, MIN_ROW_HEIGHT } from './boardConstants';
|
||||||
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;
|
|
||||||
|
|
||||||
export function BoardBody() {
|
export function BoardBody() {
|
||||||
const { board, editing, updateBoard, saveBoard, isPending, registerLayoutGetter } = useBoard();
|
const { board, editing, updateBoard, saveBoard, isPending, registerLayoutGetter } = useBoard();
|
||||||
|
|
@ -29,19 +17,14 @@ export function BoardBody() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerLayoutGetter(() => {
|
registerLayoutGetter(() => {
|
||||||
const rows = board?.parameters?.rows;
|
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;
|
if (!rows?.length) return null;
|
||||||
|
|
||||||
const rowLayout = rowGroupRef.current?.getLayout();
|
const rowLayout = rowGroupRef.current?.getLayout();
|
||||||
console.log('rowLayout:', rowLayout);
|
|
||||||
|
|
||||||
const updatedRows = rows.map(row => {
|
const updatedRows = rows.map(row => {
|
||||||
const columnGroupRef = columnGroupRefs.current.get(row.id);
|
const columnGroupRef = columnGroupRefs.current.get(row.id);
|
||||||
const columnLayout = columnGroupRef?.getLayout();
|
const columnLayout = columnGroupRef?.getLayout();
|
||||||
console.log(`Row ${row.id} columnLayout:`, columnLayout);
|
|
||||||
|
|
||||||
const updatedColumns = row.columns.map(col => ({
|
const updatedColumns = row.columns.map(col => ({
|
||||||
...col,
|
...col,
|
||||||
|
|
@ -55,7 +38,6 @@ export function BoardBody() {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('updatedRows:', updatedRows);
|
|
||||||
return { rows: updatedRows };
|
return { rows: updatedRows };
|
||||||
});
|
});
|
||||||
}, [registerLayoutGetter, board?.parameters?.rows]);
|
}, [registerLayoutGetter, board?.parameters?.rows]);
|
||||||
|
|
@ -68,8 +50,6 @@ export function BoardBody() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log({ board });
|
|
||||||
|
|
||||||
const handleAddRow = () => {
|
const handleAddRow = () => {
|
||||||
updateBoard({
|
updateBoard({
|
||||||
parameters: produce(board.parameters, draft => {
|
parameters: produce(board.parameters, draft => {
|
||||||
|
|
@ -168,167 +148,3 @@ export function BoardBody() {
|
||||||
</Group>
|
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
52
src/app/(main)/boards/[boardId]/BoardColumn.tsx
Normal file
52
src/app/(main)/boards/[boardId]/BoardColumn.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
src/app/(main)/boards/[boardId]/BoardEditHeader.tsx
Normal file
99
src/app/(main)/boards/[boardId]/BoardEditHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,6 @@
|
||||||
import {
|
import { useBoard } from '@/components/hooks';
|
||||||
Button,
|
import { BoardEditHeader } from './BoardEditHeader';
|
||||||
Column,
|
import { BoardViewHeader } from './BoardViewHeader';
|
||||||
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';
|
|
||||||
|
|
||||||
export function BoardHeader() {
|
export function BoardHeader() {
|
||||||
const { board, editing } = useBoard();
|
const { board, editing } = useBoard();
|
||||||
|
|
@ -24,106 +11,3 @@ export function BoardHeader() {
|
||||||
|
|
||||||
return <BoardViewHeader />;
|
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
125
src/app/(main)/boards/[boardId]/BoardRow.tsx
Normal file
125
src/app/(main)/boards/[boardId]/BoardRow.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/app/(main)/boards/[boardId]/BoardViewHeader.tsx
Normal file
22
src/app/(main)/boards/[boardId]/BoardViewHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/app/(main)/boards/[boardId]/boardConstants.ts
Normal file
5
src/app/(main)/boards/[boardId]/boardConstants.ts
Normal 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;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue