From cdcb1f379ba0b4f2f9120ad4ecc794f30fca057c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 10 Mar 2026 02:08:24 -0700 Subject: [PATCH] Split board details and design flows --- src/app/(main)/boards/BoardAddButton.tsx | 20 ++ src/app/(main)/boards/BoardDesignButton.tsx | 16 ++ src/app/(main)/boards/BoardEditButton.tsx | 1 - src/app/(main)/boards/BoardEditForm.tsx | 199 ++++++++++++++++++ src/app/(main)/boards/BoardProvider.tsx | 28 ++- src/app/(main)/boards/BoardsPage.tsx | 11 +- src/app/(main)/boards/BoardsTable.tsx | 2 + .../boards/[boardId]/BoardComponentSelect.tsx | 21 +- .../(main)/boards/[boardId]/BoardEditForm.tsx | 150 ------------- .../boards/[boardId]/BoardEditHeader.tsx | 5 +- .../(main)/boards/[boardId]/BoardEditPage.tsx | 8 +- .../boards/[boardId]/BoardViewHeader.tsx | 7 +- .../(main)/boards/[boardId]/design/page.tsx | 12 ++ .../boards/[boardId]/edit/BoardEditPage.tsx | 48 +++++ src/app/(main)/boards/[boardId]/edit/page.tsx | 2 +- src/app/(main)/boards/[boardId]/page.tsx | 7 +- src/app/(main)/boards/create/page.tsx | 13 ++ 17 files changed, 348 insertions(+), 202 deletions(-) create mode 100644 src/app/(main)/boards/BoardAddButton.tsx create mode 100644 src/app/(main)/boards/BoardDesignButton.tsx create mode 100644 src/app/(main)/boards/BoardEditForm.tsx delete mode 100644 src/app/(main)/boards/[boardId]/BoardEditForm.tsx create mode 100644 src/app/(main)/boards/[boardId]/design/page.tsx create mode 100644 src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx create mode 100644 src/app/(main)/boards/create/page.tsx diff --git a/src/app/(main)/boards/BoardAddButton.tsx b/src/app/(main)/boards/BoardAddButton.tsx new file mode 100644 index 000000000..54e89f354 --- /dev/null +++ b/src/app/(main)/boards/BoardAddButton.tsx @@ -0,0 +1,20 @@ +import { useMessages, useNavigation } from '@/components/hooks'; +import { Plus } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import type { Board } from '@/lib/types'; +import { BoardEditForm } from './BoardEditForm'; + +export function BoardAddButton() { + const { t, labels } = useMessages(); + const { teamId, router, renderUrl } = useNavigation(); + + const handleSave = (board: Board) => { + router.push(renderUrl(`/boards/${board.id}/design`, false)); + }; + + return ( + } label={t(labels.addBoard)} variant="primary" width="600px"> + {({ close }) => } + + ); +} diff --git a/src/app/(main)/boards/BoardDesignButton.tsx b/src/app/(main)/boards/BoardDesignButton.tsx new file mode 100644 index 000000000..20a06f62c --- /dev/null +++ b/src/app/(main)/boards/BoardDesignButton.tsx @@ -0,0 +1,16 @@ +import { Icon } from '@umami/react-zen'; +import { LinkButton } from '@/components/common/LinkButton'; +import { useNavigation } from '@/components/hooks'; +import { LayoutDashboard } from '@/components/icons'; + +export function BoardDesignButton({ boardId }: { boardId: string }) { + const { renderUrl } = useNavigation(); + + return ( + + + + + + ); +} diff --git a/src/app/(main)/boards/BoardEditButton.tsx b/src/app/(main)/boards/BoardEditButton.tsx index a4bdd2863..f8de69cc1 100644 --- a/src/app/(main)/boards/BoardEditButton.tsx +++ b/src/app/(main)/boards/BoardEditButton.tsx @@ -10,7 +10,6 @@ export function BoardEditButton({ boardId }: { boardId: string }) { return ( diff --git a/src/app/(main)/boards/BoardEditForm.tsx b/src/app/(main)/boards/BoardEditForm.tsx new file mode 100644 index 000000000..9360c036d --- /dev/null +++ b/src/app/(main)/boards/BoardEditForm.tsx @@ -0,0 +1,199 @@ +import { + Box, + Button, + Form, + FormField, + FormSubmitButton, + ListItem, + Loading, + Row, + Select, + TextField, +} from '@umami/react-zen'; +import { useEffect, useState } from 'react'; +import { useBoardQuery, useMessages, useNavigation, useUpdateQuery } from '@/components/hooks'; +import { LinkSelect } from '@/components/input/LinkSelect'; +import { PixelSelect } from '@/components/input/PixelSelect'; +import { WebsiteSelect } from '@/components/input/WebsiteSelect'; +import { + BOARD_TYPES, + type BoardType, + getBoardEntity, + getBoardType, + requiresBoardEntity, + setBoardEntity, +} from '@/lib/boards'; +import type { Board } from '@/lib/types'; + +interface BoardFormValues { + name: string; + description: string; + type: BoardType; + entityId: string; +} + +function getDefaultValues(board?: Partial): BoardFormValues { + const boardType = getBoardType(board, { coerceDashboard: true }); + const { entityId } = getBoardEntity(board); + + return { + name: board?.name ?? '', + description: board?.description ?? '', + type: boardType, + entityId: entityId ?? '', + }; +} + +export function BoardEditForm({ + boardId, + teamId, + onSave, + onClose, +}: { + boardId?: string; + teamId?: string; + onSave?: (board: Board) => void | Promise; + onClose?: () => void; +}) { + const { t, labels, messages, getErrorMessage } = useMessages(); + const { teamId: navigationTeamId } = useNavigation(); + const resolvedTeamId = teamId ?? navigationTeamId; + const { data, isLoading } = useBoardQuery(boardId || ''); + const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery( + boardId ? `/boards/${boardId}` : '/boards', + { + id: boardId, + teamId: resolvedTeamId, + }, + ); + const [values, setValues] = useState(getDefaultValues()); + + useEffect(() => { + if (data) { + setValues(getDefaultValues(data)); + } + }, [data]); + + const handleSubmit = async () => { + const result = await mutateAsync({ + name: values.name, + description: values.description, + type: values.type, + parameters: setBoardEntity({}, values.type, values.entityId || undefined), + }); + + toast(t(messages.saved)); + touch('boards'); + touch(`board:${result.id}`); + await onSave?.(result); + onClose?.(); + }; + + const handleNameChange = (name: string) => { + setValues(current => ({ ...current, name })); + }; + + const handleDescriptionChange = (description: string) => { + setValues(current => ({ ...current, description })); + }; + + const handleTypeChange = (type: string) => { + setValues(current => ({ + ...current, + type: type as BoardType, + entityId: '', + })); + }; + + const handleEntityChange = (entityId: string) => { + setValues(current => ({ ...current, entityId })); + }; + + if (boardId && isLoading) { + return ; + } + + const entityLabel = + values.type === BOARD_TYPES.pixel + ? t(labels.pixel) + : values.type === BOARD_TYPES.link + ? t(labels.link) + : t(labels.website); + + return ( +
+ + + + + + + + + + + + {requiresBoardEntity(values.type) && ( + + + {values.type === BOARD_TYPES.website ? ( + + ) : values.type === BOARD_TYPES.pixel ? ( + + ) : ( + + )} + + + )} + + {onClose && ( + + )} + {t(labels.save)} + +
+ ); +} diff --git a/src/app/(main)/boards/BoardProvider.tsx b/src/app/(main)/boards/BoardProvider.tsx index 01b957a08..dc5168424 100644 --- a/src/app/(main)/boards/BoardProvider.tsx +++ b/src/app/(main)/boards/BoardProvider.tsx @@ -70,6 +70,7 @@ export function BoardProvider({ const { router, renderUrl, teamId } = useNavigation(); const [board, setBoard] = useState>(data ?? createDefaultBoard()); + const boardRef = useRef>(data ?? createDefaultBoard()); const layoutGetterRef = useRef(null); const registerLayoutGetter = useCallback((getter: LayoutGetter) => { @@ -78,11 +79,14 @@ export function BoardProvider({ useEffect(() => { if (data) { - setBoard({ + const nextBoard = { ...data, type: getBoardType(data, { coerceDashboard: true }), parameters: sanitizeBoardParameters(data.parameters), - }); + }; + + boardRef.current = nextBoard; + setBoard(nextBoard); } }, [data]); @@ -101,35 +105,41 @@ export function BoardProvider({ }); const updateBoard = useCallback((data: Partial) => { - setBoard(current => ({ ...current, ...data })); + setBoard(current => { + const nextBoard = { ...current, ...data }; + boardRef.current = nextBoard; + + return nextBoard; + }); }, []); const saveBoard = useCallback(async () => { + const currentBoard = boardRef.current; const defaultName = t(labels.untitled); // Get current layout sizes from BoardEditBody if registered const layoutData = layoutGetterRef.current?.(); const parameters = sanitizeBoardParameters( - layoutData ? { ...board.parameters, ...layoutData } : board.parameters, + layoutData ? { ...currentBoard.parameters, ...layoutData } : currentBoard.parameters, ); const result = await mutateAsync({ - ...board, - name: board.name || defaultName, + ...currentBoard, + name: currentBoard.name || defaultName, parameters, }); toast(t(messages.saved)); touch('boards'); - if (board.id) { - touch(`board:${board.id}`); + if (currentBoard.id) { + touch(`board:${currentBoard.id}`); } else if (result?.id) { router.push(renderUrl(`/boards/${result.id}`)); } return result; - }, [board, mutateAsync, toast, t, labels.untitled, messages.saved, touch, router, renderUrl]); + }, [mutateAsync, toast, t, labels.untitled, messages.saved, touch, router, renderUrl]); if (boardId && isFetching && isLoading) { return ; diff --git a/src/app/(main)/boards/BoardsPage.tsx b/src/app/(main)/boards/BoardsPage.tsx index 7be1dbec8..8a3126531 100644 --- a/src/app/(main)/boards/BoardsPage.tsx +++ b/src/app/(main)/boards/BoardsPage.tsx @@ -1,25 +1,20 @@ 'use client'; import { Column } from '@umami/react-zen'; -import { IconLabel } from '@/components/common/IconLabel'; -import { LinkButton } from '@/components/common/LinkButton'; import { PageBody } from '@/components/common/PageBody'; import { PageHeader } from '@/components/common/PageHeader'; import { Panel } from '@/components/common/Panel'; -import { useMessages, useNavigation } from '@/components/hooks'; -import { Plus } from '@/components/icons'; +import { useMessages } from '@/components/hooks'; +import { BoardAddButton } from './BoardAddButton'; import { BoardsDataTable } from './BoardsDataTable'; export function BoardsPage() { const { t, labels } = useMessages(); - const { renderUrl } = useNavigation(); return ( - - } label={t(labels.addBoard)} /> - + diff --git a/src/app/(main)/boards/BoardsTable.tsx b/src/app/(main)/boards/BoardsTable.tsx index c68aab544..e1db5ff1f 100644 --- a/src/app/(main)/boards/BoardsTable.tsx +++ b/src/app/(main)/boards/BoardsTable.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; import { DateDistance } from '@/components/common/DateDistance'; import { useMessages, useNavigation } from '@/components/hooks'; import { BoardDeleteButton } from './BoardDeleteButton'; +import { BoardDesignButton } from './BoardDesignButton'; import { BoardEditButton } from './BoardEditButton'; export function BoardsTable(props: DataTableProps) { @@ -25,6 +26,7 @@ export function BoardsTable(props: DataTableProps) { return ( + ); diff --git a/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx b/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx index 08650b9cc..60b1715b5 100644 --- a/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx +++ b/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx @@ -95,11 +95,7 @@ export function BoardComponentSelect({ const definition = allDefinitions.find(def => def.type === initialConfig.type); - if (!definition || !isBoardComponentSupported(definition.type, activeEntityType)) { - setSelectedDef(null); - setConfigValues({}); - setTitle(''); - setDescription(''); + if (!definition) { return; } @@ -114,24 +110,12 @@ export function BoardComponentSelect({ }, [ initialConfig, allDefinitions, - activeEntityType, boardEntityId, boardEntityType, initialEntity.entityId, initialEntity.entityType, ]); - useEffect(() => { - if (!selectedDef || isSelectedDefSupported) { - return; - } - - setSelectedDef(null); - setConfigValues({}); - setTitle(''); - setDescription(''); - }, [isSelectedDefSupported, selectedDef]); - const handleSelectComponent = (def: ComponentDefinition) => { setSelectedDef(def); setConfigValues(getDefaultConfigValues(def)); @@ -211,7 +195,8 @@ export function BoardComponentSelect({ {CATEGORIES.map(category => { const components = getComponentsByCategory(category.key).filter(def => - isBoardComponentSupported(def.type, activeEntityType), + isBoardComponentSupported(def.type, activeEntityType) || + def.type === selectedDef?.type, ); if (!components.length) { diff --git a/src/app/(main)/boards/[boardId]/BoardEditForm.tsx b/src/app/(main)/boards/[boardId]/BoardEditForm.tsx deleted file mode 100644 index b5843d615..000000000 --- a/src/app/(main)/boards/[boardId]/BoardEditForm.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { Box, Form, FormField, ListItem, Row, Select, TextField } from '@umami/react-zen'; -import { Panel } from '@/components/common/Panel'; -import { useBoard, useMessages, useNavigation } from '@/components/hooks'; -import { LinkSelect } from '@/components/input/LinkSelect'; -import { PixelSelect } from '@/components/input/PixelSelect'; -import { WebsiteSelect } from '@/components/input/WebsiteSelect'; -import { - BOARD_TYPES, - type BoardType, - getBoardEntity, - getBoardType, - requiresBoardEntity, - setBoardEntity, -} from '@/lib/boards'; - -export function BoardEditForm() { - const { board, updateBoard, saveBoard } = useBoard(); - const { t, labels } = useMessages(); - const { teamId } = useNavigation(); - const boardType = getBoardType(board, { coerceDashboard: true }); - const { entityId } = getBoardEntity(board); - - const handleNameChange = (name: string) => { - updateBoard({ name }); - }; - - const handleDescriptionChange = (description: string) => { - updateBoard({ description }); - }; - - const handleTypeChange = (type: string) => { - updateBoard({ - type, - parameters: setBoardEntity(board.parameters, type as BoardType), - }); - }; - - const handleEntityChange = (nextEntityId: string) => { - updateBoard({ - parameters: setBoardEntity(board.parameters, boardType, nextEntityId), - }); - }; - - const renderEntitySelect = () => { - if (boardType === BOARD_TYPES.website) { - return ( - - ); - } - - if (boardType === BOARD_TYPES.pixel) { - return ( - - ); - } - - if (boardType === BOARD_TYPES.link) { - return ( - - ); - } - - return null; - }; - - const entityLabel = - boardType === BOARD_TYPES.pixel - ? t(labels.pixel) - : boardType === BOARD_TYPES.link - ? t(labels.link) - : t(labels.website); - - return ( - - -
- - - - - - - - - - - - {requiresBoardEntity(boardType) && ( - - - {renderEntitySelect()} - - - )} -
-
-
- ); -} diff --git a/src/app/(main)/boards/[boardId]/BoardEditHeader.tsx b/src/app/(main)/boards/[boardId]/BoardEditHeader.tsx index 9b0e7d7d7..2dfa719b6 100644 --- a/src/app/(main)/boards/[boardId]/BoardEditHeader.tsx +++ b/src/app/(main)/boards/[boardId]/BoardEditHeader.tsx @@ -2,12 +2,11 @@ import { Button, LoadingButton, Row } from '@umami/react-zen'; import { PageHeader } from '@/components/common/PageHeader'; import { useBoard, useMessages, useNavigation } from '@/components/hooks'; -export function BoardEditHeader() { +export function BoardDesignHeader() { const { board, saveBoard, isPending } = useBoard(); const { t, labels } = useMessages(); const { router, renderUrl } = useNavigation(); - const defaultName = t(labels.untitled); - const title = board?.id ? board?.name || defaultName : t(labels.addBoard); + const title = board?.name || t(labels.untitled); const handleSave = async () => { await saveBoard(); diff --git a/src/app/(main)/boards/[boardId]/BoardEditPage.tsx b/src/app/(main)/boards/[boardId]/BoardEditPage.tsx index af736bd0b..6ae3fcb65 100644 --- a/src/app/(main)/boards/[boardId]/BoardEditPage.tsx +++ b/src/app/(main)/boards/[boardId]/BoardEditPage.tsx @@ -4,16 +4,14 @@ import { BoardProvider } from '@/app/(main)/boards/BoardProvider'; import { PageBody } from '@/components/common/PageBody'; import { BoardControls } from './BoardControls'; import { BoardEditBody } from './BoardEditBody'; -import { BoardEditForm } from './BoardEditForm'; -import { BoardEditHeader } from './BoardEditHeader'; +import { BoardDesignHeader } from './BoardEditHeader'; -export function BoardEditPage({ boardId }: { boardId?: string }) { +export function BoardDesignPage({ boardId }: { boardId: string }) { return ( - - + diff --git a/src/app/(main)/boards/[boardId]/BoardViewHeader.tsx b/src/app/(main)/boards/[boardId]/BoardViewHeader.tsx index f3832f6a8..d20a67d95 100644 --- a/src/app/(main)/boards/[boardId]/BoardViewHeader.tsx +++ b/src/app/(main)/boards/[boardId]/BoardViewHeader.tsx @@ -4,10 +4,9 @@ import { LinkButton } from '@/components/common/LinkButton'; import { PageHeader } from '@/components/common/PageHeader'; import { useBoard, useMessages, useNavigation } from '@/components/hooks'; import { getBoardEntity } from '@/lib/boards'; -import { Edit } from '@/components/icons'; +import { Edit, LayoutDashboard } from '@/components/icons'; import { BoardEntityBadge } from '../BoardEntityBadge'; import { useBoardEntityBadgeProps } from '../useBoardEntityBadgeProps'; -import { BoardShareButton } from './BoardShareButton'; export function BoardViewHeader({ showActions = true, @@ -28,10 +27,12 @@ export function BoardViewHeader({ {showEntityBadge && entityBadge && } {showActions && board?.id && ( <> - }>{t(labels.edit)} + + }>Design + )} diff --git a/src/app/(main)/boards/[boardId]/design/page.tsx b/src/app/(main)/boards/[boardId]/design/page.tsx new file mode 100644 index 000000000..ec55c213d --- /dev/null +++ b/src/app/(main)/boards/[boardId]/design/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from 'next'; +import { BoardDesignPage } from '../BoardEditPage'; + +export default async function ({ params }: { params: Promise<{ boardId: string }> }) { + const { boardId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Design Board', +}; diff --git a/src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx b/src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx new file mode 100644 index 000000000..7c6568185 --- /dev/null +++ b/src/app/(main)/boards/[boardId]/edit/BoardEditPage.tsx @@ -0,0 +1,48 @@ +'use client'; +import { Column } from '@umami/react-zen'; +import Link from 'next/link'; +import { BoardEditForm } from '@/app/(main)/boards/BoardEditForm'; +import { BoardShareDialog } from '@/app/(main)/boards/[boardId]/BoardShareDialog'; +import { IconLabel } from '@/components/common/IconLabel'; +import { Panel } from '@/components/common/Panel'; +import { PageBody } from '@/components/common/PageBody'; +import { PageHeader } from '@/components/common/PageHeader'; +import { useBoardQuery, useMessages, useNavigation } from '@/components/hooks'; +import { ArrowLeft, LayoutDashboard } from '@/components/icons'; + +export function BoardEditPage({ boardId }: { boardId: string }) { + const { data: board } = useBoardQuery(boardId); + const { t, labels } = useMessages(); + const { renderUrl } = useNavigation(); + + return ( + + + <> + + + } label={t(labels.boards)} /> + + + } + /> + + + + + + + + + + + + ); +} diff --git a/src/app/(main)/boards/[boardId]/edit/page.tsx b/src/app/(main)/boards/[boardId]/edit/page.tsx index 33dc19802..9a5562dd9 100644 --- a/src/app/(main)/boards/[boardId]/edit/page.tsx +++ b/src/app/(main)/boards/[boardId]/edit/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from 'next'; -import { BoardEditPage } from '../BoardEditPage'; +import { BoardEditPage } from './BoardEditPage'; export default async function ({ params }: { params: Promise<{ boardId: string }> }) { const { boardId } = await params; diff --git a/src/app/(main)/boards/[boardId]/page.tsx b/src/app/(main)/boards/[boardId]/page.tsx index ff3aaf25d..4758f9fb0 100644 --- a/src/app/(main)/boards/[boardId]/page.tsx +++ b/src/app/(main)/boards/[boardId]/page.tsx @@ -1,13 +1,12 @@ import type { Metadata } from 'next'; -import { BoardEditPage } from './BoardEditPage'; +import { redirect } from 'next/navigation'; import { BoardViewPage } from './BoardViewPage'; export default async function ({ params }: { params: Promise<{ boardId: string }> }) { const { boardId } = await params; - const isCreate = boardId === 'create'; - if (isCreate) { - return ; + if (boardId === 'create') { + redirect('/boards'); } return ; diff --git a/src/app/(main)/boards/create/page.tsx b/src/app/(main)/boards/create/page.tsx new file mode 100644 index 000000000..f8ec472c4 --- /dev/null +++ b/src/app/(main)/boards/create/page.tsx @@ -0,0 +1,13 @@ +'use client'; +import { useEffect } from 'react'; +import { useNavigation } from '@/components/hooks'; + +export default function CreateBoardPage() { + const { router, renderUrl } = useNavigation(); + + useEffect(() => { + router.replace(renderUrl('/boards', false)); + }, [router, renderUrl]); + + return null; +}