From a8534a9d4d122d5b1cb43f5f23a7b513b2d77ca0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 5 Feb 2026 19:42:50 -0800 Subject: [PATCH] Add board view/edit mode separation with cancel button. /boards/[id] is now view mode (read-only) with an edit button. /boards/[id]/edit is edit mode with save and cancel buttons. Save navigates back to view, cancel discards changes. Co-Authored-By: Claude Opus 4.6 --- src/app/(main)/boards/BoardProvider.tsx | 13 +- src/app/(main)/boards/[boardId]/BoardBody.tsx | 136 ++++++++++-------- .../(main)/boards/[boardId]/BoardHeader.tsx | 59 +++++++- src/app/(main)/boards/[boardId]/BoardPage.tsx | 4 +- src/app/(main)/boards/[boardId]/edit/page.tsx | 12 ++ src/app/(main)/boards/[boardId]/page.tsx | 3 +- src/components/common/IconLabel.tsx | 0 7 files changed, 153 insertions(+), 74 deletions(-) create mode 100644 src/app/(main)/boards/[boardId]/edit/page.tsx create mode 100644 src/components/common/IconLabel.tsx diff --git a/src/app/(main)/boards/BoardProvider.tsx b/src/app/(main)/boards/BoardProvider.tsx index 5dc52d629..1b248e7e8 100644 --- a/src/app/(main)/boards/BoardProvider.tsx +++ b/src/app/(main)/boards/BoardProvider.tsx @@ -10,6 +10,7 @@ export type LayoutGetter = () => Partial | null; export interface BoardContextValue { board: Partial; + editing: boolean; updateBoard: (data: Partial) => void; saveBoard: () => Promise; isPending: boolean; @@ -26,7 +27,15 @@ const createDefaultBoard = (): Partial => ({ }, }); -export function BoardProvider({ boardId, children }: { boardId?: string; children: ReactNode }) { +export function BoardProvider({ + boardId, + editing = false, + children, +}: { + boardId?: string; + editing?: boolean; + children: ReactNode; +}) { const { data, isFetching, isLoading } = useBoardQuery(boardId); const { post, useMutation } = useApi(); const { touch } = useModified(); @@ -103,7 +112,7 @@ export function BoardProvider({ boardId, children }: { boardId?: string; childre return ( {children} diff --git a/src/app/(main)/boards/[boardId]/BoardBody.tsx b/src/app/(main)/boards/[boardId]/BoardBody.tsx index 03034fbd6..d9133b8fc 100644 --- a/src/app/(main)/boards/[boardId]/BoardBody.tsx +++ b/src/app/(main)/boards/[boardId]/BoardBody.tsx @@ -21,7 +21,7 @@ const BUTTON_ROW_HEIGHT = 60; const MAX_COLUMNS = 4; export function BoardBody() { - const { board, updateBoard, saveBoard, isPending, registerLayoutGetter } = useBoard(); + const { board, editing, updateBoard, saveBoard, isPending, registerLayoutGetter } = useBoard(); const rowGroupRef = useRef(null); const columnGroupRefs = useRef>(new Map()); @@ -141,6 +141,7 @@ export function BoardBody() { rowId={row.id} rowIndex={index} rowCount={rows?.length} + editing={editing} onRemove={handleRemoveRow} onMoveUp={handleMoveRowUp} onMoveDown={handleMoveRowDown} @@ -150,18 +151,20 @@ export function BoardBody() { {index < rows?.length - 1 && } ))} - - - - - Add row - - - + {editing && ( + + + + + Add row + + + + )} ); } @@ -171,6 +174,7 @@ function BoardRow({ rowIndex, rowCount, columns, + editing = false, onRemove, onMoveUp, onMoveDown, @@ -180,6 +184,7 @@ function BoardRow({ rowIndex: number; rowCount: number; columns: BoardColumnType[]; + editing?: boolean; onRemove?: (id: string) => void; onMoveUp?: (id: string) => void; onMoveDown?: (id: string) => void; @@ -223,6 +228,7 @@ function BoardRow({ 1} /> @@ -230,48 +236,50 @@ function BoardRow({ {index < columns?.length - 1 && } ))} - - - - Move row up - - - - Add column - - - - Remove row - - - - Move row down - - + {editing && ( + + + + Move row up + + + + Add column + + + + Remove row + + + + Move row down + + + )} ); } @@ -279,11 +287,13 @@ function BoardRow({ function BoardColumn({ id, component, + editing = false, onRemove, canRemove = true, }: { id: string; component?: ReactElement; + editing?: boolean; onRemove?: (id: string) => void; canRemove?: boolean; }) { @@ -297,10 +307,10 @@ function BoardColumn({ height="100%" alignItems="center" justifyContent="center" - backgroundColor="3" + backgroundColor="surface-sunken" position="relative" > - {canRemove && ( + {editing && canRemove && ( + {editing && ( + + )} ); } diff --git a/src/app/(main)/boards/[boardId]/BoardHeader.tsx b/src/app/(main)/boards/[boardId]/BoardHeader.tsx index 18492e04a..718e8b036 100644 --- a/src/app/(main)/boards/[boardId]/BoardHeader.tsx +++ b/src/app/(main)/boards/[boardId]/BoardHeader.tsx @@ -1,9 +1,38 @@ -import { Column, Grid, Heading, LoadingButton, Row, TextField } from '@umami/react-zen'; -import { useBoard, useMessages } from '@/components/hooks'; +import { Button, Column, Grid, Heading, LoadingButton, Row, 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 } from '@/components/hooks'; +import { Edit } from '@/components/icons'; export function BoardHeader() { + const { board, editing } = useBoard(); + + if (editing) { + return ; + } + + return ; +} + +function BoardViewHeader() { + const { board } = useBoard(); + const { renderUrl } = useNavigation(); + const { formatMessage, labels } = useMessages(); + + return ( + + + }>{formatMessage(labels.edit)} + + + ); +} + +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) => { @@ -14,8 +43,19 @@ export function BoardHeader() { updateBoard({ description: value }); }; - const handleSave = () => { - saveBoard(); + 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 ( @@ -55,9 +95,14 @@ export function BoardHeader() { - - {formatMessage(labels.save)} - + + + + {formatMessage(labels.save)} + + ); diff --git a/src/app/(main)/boards/[boardId]/BoardPage.tsx b/src/app/(main)/boards/[boardId]/BoardPage.tsx index 20bdd72ca..e8f55d4bc 100644 --- a/src/app/(main)/boards/[boardId]/BoardPage.tsx +++ b/src/app/(main)/boards/[boardId]/BoardPage.tsx @@ -5,9 +5,9 @@ import { BoardHeader } from '@/app/(main)/boards/[boardId]/BoardHeader'; import { BoardProvider } from '@/app/(main)/boards/BoardProvider'; import { PageBody } from '@/components/common/PageBody'; -export function BoardPage({ boardId }: { boardId: string }) { +export function BoardPage({ boardId, editing = false }: { boardId?: string; editing?: boolean }) { return ( - + diff --git a/src/app/(main)/boards/[boardId]/edit/page.tsx b/src/app/(main)/boards/[boardId]/edit/page.tsx new file mode 100644 index 000000000..e953af57f --- /dev/null +++ b/src/app/(main)/boards/[boardId]/edit/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from 'next'; +import { BoardPage } from '../BoardPage'; + +export default async function ({ params }: { params: Promise<{ boardId: string }> }) { + const { boardId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Edit Board', +}; diff --git a/src/app/(main)/boards/[boardId]/page.tsx b/src/app/(main)/boards/[boardId]/page.tsx index 0eebdb2f1..3c394dcdd 100644 --- a/src/app/(main)/boards/[boardId]/page.tsx +++ b/src/app/(main)/boards/[boardId]/page.tsx @@ -3,8 +3,9 @@ import { BoardPage } from './BoardPage'; export default async function ({ params }: { params: Promise<{ boardId: string }> }) { const { boardId } = await params; + const isCreate = boardId === 'create'; - return ; + return ; } export const metadata: Metadata = { diff --git a/src/components/common/IconLabel.tsx b/src/components/common/IconLabel.tsx new file mode 100644 index 000000000..e69de29bb