diff --git a/CLAUDE.md b/CLAUDE.md index 883ee7f4..a7ad68f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,3 +90,7 @@ DEBUG # Debug namespaces (e.g., umami:*) - Node.js 18.18+ - PostgreSQL 12.14+ - pnpm (package manager) + +## Git Workflow + +Always ask for confirmation before running `git commit` or `git push`. diff --git a/package.json b/package.json index dea505fe..326f1720 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "stylelint-config-css-modules": "^4.5.1", "stylelint-config-prettier": "^9.0.3", "stylelint-config-recommended": "^14.0.0", - "tar": "^6.1.2", + "tar": "^7.5.3", "ts-jest": "^29.4.6", "ts-node": "^10.9.1", "tsup": "^8.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6292443..daefaf06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -316,8 +316,8 @@ importers: specifier: ^14.0.0 version: 14.0.1(stylelint@15.11.0(typescript@5.9.3)) tar: - specifier: ^6.1.2 - version: 6.2.1 + specifier: ^7.5.3 + version: 7.5.3 ts-jest: 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) @@ -1479,6 +1479,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 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': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -3279,9 +3283,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} @@ -4066,10 +4070,6 @@ packages: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -5180,21 +5180,13 @@ packages: minimist@1.2.8: 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: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} @@ -6781,9 +6773,9 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} + tar@7.5.3: + resolution: {integrity: sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==} + engines: {node: '>=18'} terser@5.43.1: resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} @@ -7190,6 +7182,10 @@ packages: yallist@4.0.0: 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: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -8224,6 +8220,10 @@ snapshots: wrap-ansi: 8.1.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': dependencies: camelcase: 5.3.1 @@ -10609,7 +10609,7 @@ snapshots: dependencies: readdirp: 4.1.2 - chownr@2.0.0: {} + chownr@3.0.0: {} ci-info@3.9.0: {} @@ -11576,10 +11576,6 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 - fs-minipass@2.1.0: - dependencies: - minipass: 3.3.6 - fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -12882,18 +12878,11 @@ snapshots: minimist@1.2.8: {} - minipass@3.3.6: - dependencies: - yallist: 4.0.0 - - minipass@5.0.0: {} - minipass@7.1.2: {} - minizlib@2.1.2: + minizlib@3.1.0: dependencies: - minipass: 3.3.6 - yallist: 4.0.0 + minipass: 7.1.2 mkdirp@1.0.4: {} @@ -14706,14 +14695,13 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tar@6.2.1: + tar@7.5.3: dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 terser@5.43.1: dependencies: @@ -15132,6 +15120,8 @@ snapshots: yallist@4.0.0: {} + yallist@5.0.0: {} + yaml@1.10.2: {} yaml@2.8.1: {} diff --git a/scripts/check-db.js b/scripts/check-db.js index 68374f6f..8c806cd8 100644 --- a/scripts/check-db.js +++ b/scripts/check-db.js @@ -48,7 +48,7 @@ async function checkConnection() { success('Database connection successful.'); } catch (e) { - throw new Error('Unable to connect to the database: ' + e.message); + throw new Error(`Unable to connect to the database: ${e.message}`); } } diff --git a/src/app/(main)/boards/BoardProvider.tsx b/src/app/(main)/boards/BoardProvider.tsx index fa9c7c90..783f92ef 100644 --- a/src/app/(main)/boards/BoardProvider.tsx +++ b/src/app/(main)/boards/BoardProvider.tsx @@ -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; @@ -17,6 +17,7 @@ export const BoardContext = createContext(null); const defaultBoard: Partial = { name: '', description: '', + parameters: { rows: [] }, }; export function BoardProvider({ boardId, children }: { boardId?: string; children: ReactNode }) { diff --git a/src/app/(main)/boards/[boardId]/BoardBody.tsx b/src/app/(main)/boards/[boardId]/BoardBody.tsx index 64715872..0c05486f 100644 --- a/src/app/(main)/boards/[boardId]/BoardBody.tsx +++ b/src/app/(main)/boards/[boardId]/BoardBody.tsx @@ -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 ( + <> + + {rows.map((row, index) => ( + + + + + {index < rows.length - 1 && } + + ))} + + + + + Add 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 ( - - {board?.parameters?.rows?.map((row, rowIndex) => { - return ; - })} - - - - + + + Remove row + + + ); } -function BoardComponent() { - return hi; -} - -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 ( - - {components?.map(component => { - return ; - })} + - + ); } diff --git a/src/app/api/boards/[boardId]/route.ts b/src/app/api/boards/[boardId]/route.ts index 2f94e527..24bae4dc 100644 --- a/src/app/api/boards/[boardId]/route.ts +++ b/src/app/api/boards/[boardId]/route.ts @@ -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) { diff --git a/src/lib/types.ts b/src/lib/types.ts index 458f899d..2da6fe35 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -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 { + parameters: BoardParameters; }