Board editing.

This commit is contained in:
Mike Cao 2026-01-18 04:20:36 -08:00
parent 68c56060b3
commit d9f698ca42
8 changed files with 183 additions and 86 deletions

View file

@ -90,3 +90,7 @@ DEBUG # Debug namespaces (e.g., umami:*)
- Node.js 18.18+ - Node.js 18.18+
- PostgreSQL 12.14+ - PostgreSQL 12.14+
- pnpm (package manager) - pnpm (package manager)
## Git Workflow
Always ask for confirmation before running `git commit` or `git push`.

View file

@ -165,7 +165,7 @@
"stylelint-config-css-modules": "^4.5.1", "stylelint-config-css-modules": "^4.5.1",
"stylelint-config-prettier": "^9.0.3", "stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^14.0.0", "stylelint-config-recommended": "^14.0.0",
"tar": "^6.1.2", "tar": "^7.5.3",
"ts-jest": "^29.4.6", "ts-jest": "^29.4.6",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsup": "^8.5.0", "tsup": "^8.5.0",

78
pnpm-lock.yaml generated
View file

@ -316,8 +316,8 @@ importers:
specifier: ^14.0.0 specifier: ^14.0.0
version: 14.0.1(stylelint@15.11.0(typescript@5.9.3)) version: 14.0.1(stylelint@15.11.0(typescript@5.9.3))
tar: tar:
specifier: ^6.1.2 specifier: ^7.5.3
version: 6.2.1 version: 7.5.3
ts-jest: ts-jest:
specifier: ^29.4.6 specifier: ^29.4.6
version: 29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.8)(ts-node@10.9.2(@types/node@24.10.8)(typescript@5.9.3)))(typescript@5.9.3) version: 29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.8)(ts-node@10.9.2(@types/node@24.10.8)(typescript@5.9.3)))(typescript@5.9.3)
@ -1479,6 +1479,10 @@ packages:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'} engines: {node: '>=12'}
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
'@istanbuljs/load-nyc-config@1.1.0': '@istanbuljs/load-nyc-config@1.1.0':
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -3279,9 +3283,9 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'} engines: {node: '>= 14.16.0'}
chownr@2.0.0: chownr@3.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=10'} engines: {node: '>=18'}
ci-info@3.9.0: ci-info@3.9.0:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
@ -4066,10 +4070,6 @@ packages:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
fs-minipass@2.1.0:
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
engines: {node: '>= 8'}
fs.realpath@1.0.0: fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@ -5180,21 +5180,13 @@ packages:
minimist@1.2.8: minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
minipass@3.3.6:
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
engines: {node: '>=8'}
minipass@5.0.0:
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
engines: {node: '>=8'}
minipass@7.1.2: minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
minizlib@2.1.2: minizlib@3.1.0:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 8'} engines: {node: '>= 18'}
mkdirp@1.0.4: mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
@ -6781,9 +6773,9 @@ packages:
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
tar@6.2.1: tar@7.5.3:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} resolution: {integrity: sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==}
engines: {node: '>=10'} engines: {node: '>=18'}
terser@5.43.1: terser@5.43.1:
resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==}
@ -7190,6 +7182,10 @@ packages:
yallist@4.0.0: yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
yaml@1.10.2: yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -8224,6 +8220,10 @@ snapshots:
wrap-ansi: 8.1.0 wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0 wrap-ansi-cjs: wrap-ansi@7.0.0
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
'@istanbuljs/load-nyc-config@1.1.0': '@istanbuljs/load-nyc-config@1.1.0':
dependencies: dependencies:
camelcase: 5.3.1 camelcase: 5.3.1
@ -10609,7 +10609,7 @@ snapshots:
dependencies: dependencies:
readdirp: 4.1.2 readdirp: 4.1.2
chownr@2.0.0: {} chownr@3.0.0: {}
ci-info@3.9.0: {} ci-info@3.9.0: {}
@ -11576,10 +11576,6 @@ snapshots:
jsonfile: 6.2.0 jsonfile: 6.2.0
universalify: 2.0.1 universalify: 2.0.1
fs-minipass@2.1.0:
dependencies:
minipass: 3.3.6
fs.realpath@1.0.0: {} fs.realpath@1.0.0: {}
fsevents@2.3.3: fsevents@2.3.3:
@ -12882,18 +12878,11 @@ snapshots:
minimist@1.2.8: {} minimist@1.2.8: {}
minipass@3.3.6:
dependencies:
yallist: 4.0.0
minipass@5.0.0: {}
minipass@7.1.2: {} minipass@7.1.2: {}
minizlib@2.1.2: minizlib@3.1.0:
dependencies: dependencies:
minipass: 3.3.6 minipass: 7.1.2
yallist: 4.0.0
mkdirp@1.0.4: {} mkdirp@1.0.4: {}
@ -14706,14 +14695,13 @@ snapshots:
string-width: 4.2.3 string-width: 4.2.3
strip-ansi: 6.0.1 strip-ansi: 6.0.1
tar@6.2.1: tar@7.5.3:
dependencies: dependencies:
chownr: 2.0.0 '@isaacs/fs-minipass': 4.0.1
fs-minipass: 2.1.0 chownr: 3.0.0
minipass: 5.0.0 minipass: 7.1.2
minizlib: 2.1.2 minizlib: 3.1.0
mkdirp: 1.0.4 yallist: 5.0.0
yallist: 4.0.0
terser@5.43.1: terser@5.43.1:
dependencies: dependencies:
@ -15132,6 +15120,8 @@ snapshots:
yallist@4.0.0: {} yallist@4.0.0: {}
yallist@5.0.0: {}
yaml@1.10.2: {} yaml@1.10.2: {}
yaml@2.8.1: {} yaml@2.8.1: {}

View file

@ -48,7 +48,7 @@ async function checkConnection() {
success('Database connection successful.'); success('Database connection successful.');
} catch (e) { } catch (e) {
throw new Error('Unable to connect to the database: ' + e.message); throw new Error(`Unable to connect to the database: ${e.message}`);
} }
} }

View file

@ -3,7 +3,7 @@ import { Loading, useToast } from '@umami/react-zen';
import { createContext, type ReactNode, useCallback, useEffect, useState } from 'react'; import { createContext, type ReactNode, useCallback, useEffect, 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 '@/generated/prisma/client'; import type { Board } from '@/lib/types';
export interface BoardContextValue { export interface BoardContextValue {
board: Partial<Board>; board: Partial<Board>;
@ -17,6 +17,7 @@ export const BoardContext = createContext<BoardContextValue>(null);
const defaultBoard: Partial<Board> = { const defaultBoard: Partial<Board> = {
name: '', name: '',
description: '', description: '',
parameters: { rows: [] },
}; };
export function BoardProvider({ boardId, children }: { boardId?: string; children: ReactNode }) { export function BoardProvider({ boardId, children }: { boardId?: string; children: ReactNode }) {

View file

@ -1,8 +1,18 @@
import { Button, Column, Icon, Row } 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 { Group, 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 { Plus } from '@/components/icons'; import { Minus, Plus } from '@/components/icons';
import type { BoardColumn as BoardColumnType } from '@/lib/types';
const CATALOG = {
text: {
label: 'Text',
component: BoardColumn,
},
};
export function BoardBody() { export function BoardBody() {
const { board, updateBoard, saveBoard, isPending } = useBoard(); const { board, updateBoard, saveBoard, isPending } = useBoard();
@ -15,55 +25,126 @@ export function BoardBody() {
if (!draft.rows) { if (!draft.rows) {
draft.rows = []; draft.rows = [];
} }
draft.rows.push({ id: uuid(), components: [] }); draft.rows.push({ id: uuid(), columns: [{ id: uuid(), component: null }] });
}),
});
};
const handleRemoveRow = (id: string) => {
console.log('Removing row', id);
updateBoard({
parameters: produce(board.parameters, draft => {
if (!draft.rows) {
return;
}
draft.rows = draft.rows.filter(row => row?.id !== id);
}),
});
};
const rows = board?.parameters?.rows ?? [];
const minHeight = 300 * (rows.length || 1);
return (
<>
<Group orientation="vertical" style={{ minHeight }}>
{rows.map((row, index) => (
<Fragment key={row.id}>
<Panel minSize={200}>
<BoardRow {...row} rowId={row.id} onRemove={handleRemoveRow} />
</Panel>
{index < rows.length - 1 && <Separator />}
</Fragment>
))}
</Group>
<Row>
<TooltipTrigger delay={0}>
<Button variant="outline" onPress={handleAddRow}>
<Icon>
<Plus />
</Icon>
</Button>
<Tooltip placement="bottom">Add row</Tooltip>
</TooltipTrigger>
</Row>
</>
);
}
function BoardRow({
rowId,
columns,
onRemove,
}: {
rowId: string;
columns: BoardColumnType[];
onAddComponent?: () => void;
onRemove?: (id: string) => void;
}) {
const { board, updateBoard } = useBoard();
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 });
}), }),
}); });
}; };
return ( return (
<Column> <Group style={{ height: '100%' }}>
{board?.parameters?.rows?.map((row, rowIndex) => { {columns?.map((column, index) => (
return <BoardRow key={row.id} rowIndex={rowIndex} components={row.components} />; <Fragment key={column.id}>
})} <Panel minSize={300}>
<Row> <BoardColumn {...column} />
<Button variant="outline" onPress={handleAddRow}> </Panel>
{index < columns.length - 1 && <Separator />}
</Fragment>
))}
<Box alignSelf="center" padding="3">
<Button variant="outline" onPress={handleAddColumn}>
<Icon> <Icon>
<Plus /> <Plus />
</Icon> </Icon>
</Button> </Button>
</Row> <TooltipTrigger delay={0}>
</Column> <Button variant="outline" onPress={() => onRemove?.(rowId)}>
<Icon>
<Minus />
</Icon>
</Button>
<Tooltip placement="bottom">Remove row</Tooltip>
</TooltipTrigger>
</Box>
</Group>
); );
} }
function BoardComponent() { function BoardColumn({ id, component }: { id: string; component?: ReactElement }) {
return <Column>hi</Column>; const handleAddComponent = () => {};
}
function BoardRow({ rowIndex, components }: { rowIndex: number; components: any[] }) {
const { board, updateBoard } = useBoard();
const handleAddComponent = () => {
updateBoard({
parameters: produce(board.parameters, draft => {
if (!draft.rows[rowIndex]) {
draft.rows[rowIndex] = { id: uuid(), components: [] };
}
draft.rows[rowIndex].components.push({ id: uuid(), type: 'text', value: '' });
}),
});
};
return ( return (
<Row> <Column
{components?.map(component => { marginTop="3"
return <BoardComponent key={component.id} />; marginLeft="3"
})} width="100%"
height="100%"
alignItems="center"
justifyContent="center"
backgroundColor="3"
>
<Button variant="outline" onPress={handleAddComponent}> <Button variant="outline" onPress={handleAddComponent}>
<Icon> <Icon>
<Plus /> <Plus />
</Icon> </Icon>
</Button> </Button>
</Row> </Column>
); );
} }

View file

@ -1,5 +1,4 @@
import { z } from 'zod'; import { z } from 'zod';
import { SHARE_ID_REGEX } from '@/lib/constants';
import { parseRequest } from '@/lib/request'; import { parseRequest } from '@/lib/request';
import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response'; import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
import { canDeleteBoard, canUpdateBoard, canViewBoard } from '@/permissions'; import { canDeleteBoard, canUpdateBoard, canViewBoard } from '@/permissions';
@ -27,7 +26,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ boa
const schema = z.object({ const schema = z.object({
name: z.string().optional(), name: z.string().optional(),
description: z.string().optional(), description: z.string().optional(),
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(), parameters: z.object({}).passthrough().optional(),
}); });
const { auth, body, error } = await parseRequest(request, schema); const { auth, body, error } = await parseRequest(request, schema);
@ -37,14 +36,14 @@ export async function POST(request: Request, { params }: { params: Promise<{ boa
} }
const { boardId } = await params; const { boardId } = await params;
const { name, description, shareId } = body; const { name, description, parameters } = body;
if (!(await canUpdateBoard(auth, boardId))) { if (!(await canUpdateBoard(auth, boardId))) {
return unauthorized(); return unauthorized();
} }
try { try {
const board = await updateBoard(boardId, { name, description, shareId }); const board = await updateBoard(boardId, { name, description, parameters });
return Response.json(board); return Response.json(board);
} catch (e: any) { } catch (e: any) {

View file

@ -1,4 +1,6 @@
import type { UseQueryOptions } from '@tanstack/react-query'; import type { UseQueryOptions } from '@tanstack/react-query';
import type { ReactElement } from 'react';
import type { Board as PrismaBoard } from '@/generated/prisma/client';
import type { DATA_TYPE, OPERATORS, ROLES } from './constants'; import type { DATA_TYPE, OPERATORS, ROLES } from './constants';
import type { TIME_UNIT } from './date'; import type { TIME_UNIT } from './date';
@ -142,6 +144,26 @@ export interface ApiError extends Error {
message: string; message: string;
} }
export interface BoardData { export interface BoardComponent {
rows: { id: string; name: string; value: number }[]; id: string;
type: string;
value: string;
}
export interface BoardColumn {
id: string;
component?: ReactElement;
}
export interface BoardRow {
id: string;
columns: BoardColumn[];
}
export interface BoardParameters {
rows?: BoardRow[];
}
export interface Board extends Omit<PrismaBoard, 'parameters'> {
parameters: BoardParameters;
} }