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

@ -3,7 +3,7 @@ import { Loading, useToast } from '@umami/react-zen';
import { createContext, type ReactNode, useCallback, useEffect, useState } from 'react';
import { useApi, useMessages, useModified, useNavigation } from '@/components/hooks';
import { useBoardQuery } from '@/components/hooks/queries/useBoardQuery';
import type { Board } from '@/generated/prisma/client';
import type { Board } from '@/lib/types';
export interface BoardContextValue {
board: Partial<Board>;
@ -17,6 +17,7 @@ export const BoardContext = createContext<BoardContextValue>(null);
const defaultBoard: Partial<Board> = {
name: '',
description: '',
parameters: { rows: [] },
};
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 { Fragment, type ReactElement } from 'react';
import { Group, Panel, Separator } from 'react-resizable-panels';
import { v4 as uuid } from 'uuid';
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() {
const { board, updateBoard, saveBoard, isPending } = useBoard();
@ -15,55 +25,126 @@ export function BoardBody() {
if (!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 (
<Column>
{board?.parameters?.rows?.map((row, rowIndex) => {
return <BoardRow key={row.id} rowIndex={rowIndex} components={row.components} />;
})}
<Row>
<Button variant="outline" onPress={handleAddRow}>
<Group style={{ height: '100%' }}>
{columns?.map((column, index) => (
<Fragment key={column.id}>
<Panel minSize={300}>
<BoardColumn {...column} />
</Panel>
{index < columns.length - 1 && <Separator />}
</Fragment>
))}
<Box alignSelf="center" padding="3">
<Button variant="outline" onPress={handleAddColumn}>
<Icon>
<Plus />
</Icon>
</Button>
</Row>
</Column>
<TooltipTrigger delay={0}>
<Button variant="outline" onPress={() => onRemove?.(rowId)}>
<Icon>
<Minus />
</Icon>
</Button>
<Tooltip placement="bottom">Remove row</Tooltip>
</TooltipTrigger>
</Box>
</Group>
);
}
function BoardComponent() {
return <Column>hi</Column>;
}
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: '' });
}),
});
};
function BoardColumn({ id, component }: { id: string; component?: ReactElement }) {
const handleAddComponent = () => {};
return (
<Row>
{components?.map(component => {
return <BoardComponent key={component.id} />;
})}
<Column
marginTop="3"
marginLeft="3"
width="100%"
height="100%"
alignItems="center"
justifyContent="center"
backgroundColor="3"
>
<Button variant="outline" onPress={handleAddComponent}>
<Icon>
<Plus />
</Icon>
</Button>
</Row>
</Column>
);
}

View file

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

View file

@ -1,4 +1,6 @@
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 { TIME_UNIT } from './date';
@ -142,6 +144,26 @@ export interface ApiError extends Error {
message: string;
}
export interface BoardData {
rows: { id: string; name: string; value: number }[];
export interface BoardComponent {
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;
}