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 (
+
+ );
+}
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 (
-
-
-
-
-
- );
-}
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;
+}