mirror of
https://github.com/umami-software/umami.git
synced 2026-05-30 06:47:25 +00:00
Split board details and design flows
This commit is contained in:
@@ -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 (
|
||||
<DialogButton icon={<Plus />} label={t(labels.addBoard)} variant="primary" width="600px">
|
||||
{({ close }) => <BoardEditForm teamId={teamId} onSave={handleSave} onClose={close} />}
|
||||
</DialogButton>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<LinkButton href={renderUrl(`/boards/${boardId}/design`)} aria-label="Design" variant="quiet">
|
||||
<Icon>
|
||||
<LayoutDashboard />
|
||||
</Icon>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,6 @@ export function BoardEditButton({ boardId }: { boardId: string }) {
|
||||
return (
|
||||
<LinkButton
|
||||
href={renderUrl(`/boards/${boardId}/edit`)}
|
||||
title={t(labels.edit)}
|
||||
aria-label={t(labels.edit)}
|
||||
variant="quiet"
|
||||
>
|
||||
|
||||
@@ -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<Board>): 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<void>;
|
||||
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<BoardFormValues>(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 <Loading placement="absolute" />;
|
||||
}
|
||||
|
||||
const entityLabel =
|
||||
values.type === BOARD_TYPES.pixel
|
||||
? t(labels.pixel)
|
||||
: values.type === BOARD_TYPES.link
|
||||
? t(labels.link)
|
||||
: t(labels.website);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} values={values}>
|
||||
<FormField name="name" label={t(labels.name)} rules={{ required: t(labels.required) }}>
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
autoFocus={!boardId}
|
||||
value={values.name}
|
||||
placeholder={t(labels.untitled)}
|
||||
onChange={handleNameChange}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField name="description" label={t(labels.description)}>
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
asTextArea
|
||||
resize="vertical"
|
||||
value={values.description}
|
||||
placeholder={t(labels.addDescription)}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
name="type"
|
||||
label={t(labels.boardType)}
|
||||
rules={{ required: t(labels.required) }}
|
||||
>
|
||||
<Box width="100%" maxWidth="360px">
|
||||
<Select value={values.type} onChange={handleTypeChange}>
|
||||
<ListItem id={BOARD_TYPES.mixed}>{t(labels.open)}</ListItem>
|
||||
<ListItem id={BOARD_TYPES.website}>{t(labels.website)}</ListItem>
|
||||
<ListItem id={BOARD_TYPES.pixel}>{t(labels.pixel)}</ListItem>
|
||||
<ListItem id={BOARD_TYPES.link}>{t(labels.link)}</ListItem>
|
||||
</Select>
|
||||
</Box>
|
||||
</FormField>
|
||||
{requiresBoardEntity(values.type) && (
|
||||
<FormField
|
||||
name="entityId"
|
||||
label={entityLabel}
|
||||
rules={{ required: t(labels.required) }}
|
||||
>
|
||||
<Box width="100%" maxWidth="360px">
|
||||
{values.type === BOARD_TYPES.website ? (
|
||||
<WebsiteSelect
|
||||
websiteId={values.entityId}
|
||||
teamId={resolvedTeamId}
|
||||
onChange={handleEntityChange}
|
||||
/>
|
||||
) : values.type === BOARD_TYPES.pixel ? (
|
||||
<PixelSelect
|
||||
pixelId={values.entityId}
|
||||
teamId={resolvedTeamId}
|
||||
placeholder={t(labels.selectPixel)}
|
||||
onChange={handleEntityChange}
|
||||
/>
|
||||
) : (
|
||||
<LinkSelect
|
||||
linkId={values.entityId}
|
||||
teamId={resolvedTeamId}
|
||||
placeholder={t(labels.selectLink)}
|
||||
onChange={handleEntityChange}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</FormField>
|
||||
)}
|
||||
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||
{onClose && (
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{t(labels.cancel)}
|
||||
</Button>
|
||||
)}
|
||||
<FormSubmitButton isDisabled={isPending}>{t(labels.save)}</FormSubmitButton>
|
||||
</Row>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -70,6 +70,7 @@ export function BoardProvider({
|
||||
const { router, renderUrl, teamId } = useNavigation();
|
||||
|
||||
const [board, setBoard] = useState<Partial<Board>>(data ?? createDefaultBoard());
|
||||
const boardRef = useRef<Partial<Board>>(data ?? createDefaultBoard());
|
||||
const layoutGetterRef = useRef<LayoutGetter | null>(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<Board>) => {
|
||||
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 <Loading placement="absolute" />;
|
||||
|
||||
@@ -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 (
|
||||
<PageBody>
|
||||
<Column margin="2">
|
||||
<PageHeader title={t(labels.boards)}>
|
||||
<LinkButton href={renderUrl('/boards/create')} variant="primary">
|
||||
<IconLabel icon={<Plus />} label={t(labels.addBoard)} />
|
||||
</LinkButton>
|
||||
<BoardAddButton />
|
||||
</PageHeader>
|
||||
<Panel>
|
||||
<BoardsDataTable />
|
||||
|
||||
@@ -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 (
|
||||
<Row>
|
||||
<BoardEditButton boardId={id} />
|
||||
<BoardDesignButton boardId={id} />
|
||||
<BoardDeleteButton boardId={id} name={name} />
|
||||
</Row>
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
<Column gap="1" style={{ width: 280, flexShrink: 0, overflowY: 'auto' }}>
|
||||
{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) {
|
||||
|
||||
@@ -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 (
|
||||
<WebsiteSelect
|
||||
websiteId={entityId}
|
||||
teamId={teamId}
|
||||
onChange={handleEntityChange}
|
||||
width="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (boardType === BOARD_TYPES.pixel) {
|
||||
return (
|
||||
<PixelSelect
|
||||
pixelId={entityId}
|
||||
teamId={teamId}
|
||||
placeholder={t(labels.selectPixel)}
|
||||
onChange={handleEntityChange}
|
||||
width="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (boardType === BOARD_TYPES.link) {
|
||||
return (
|
||||
<LinkSelect
|
||||
linkId={entityId}
|
||||
teamId={teamId}
|
||||
placeholder={t(labels.selectLink)}
|
||||
onChange={handleEntityChange}
|
||||
width="100%"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const entityLabel =
|
||||
boardType === BOARD_TYPES.pixel
|
||||
? t(labels.pixel)
|
||||
: boardType === BOARD_TYPES.link
|
||||
? t(labels.link)
|
||||
: t(labels.website);
|
||||
|
||||
return (
|
||||
<Row width="100%" justifyContent="center">
|
||||
<Panel width="100%" maxWidth="600px" marginBottom="6">
|
||||
<Form
|
||||
onSubmit={saveBoard}
|
||||
values={{
|
||||
name: board?.name ?? '',
|
||||
description: board?.description ?? '',
|
||||
type: boardType,
|
||||
entityId: entityId ?? '',
|
||||
}}
|
||||
>
|
||||
<FormField name="name" label={t(labels.name)} rules={{ required: t(labels.required) }}>
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
autoFocus={!board?.id}
|
||||
value={board?.name ?? ''}
|
||||
placeholder={t(labels.untitled)}
|
||||
onChange={handleNameChange}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField name="description" label={t(labels.description)}>
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
asTextArea
|
||||
resize="vertical"
|
||||
value={board?.description ?? ''}
|
||||
placeholder={t(labels.addDescription)}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
name="type"
|
||||
label={t(labels.boardType)}
|
||||
rules={{ required: t(labels.required) }}
|
||||
>
|
||||
<Box width="100%" maxWidth="360px">
|
||||
<Select value={boardType} onChange={handleTypeChange} width="100%">
|
||||
<ListItem id={BOARD_TYPES.mixed}>{t(labels.open)}</ListItem>
|
||||
<ListItem id={BOARD_TYPES.website}>{t(labels.website)}</ListItem>
|
||||
<ListItem id={BOARD_TYPES.pixel}>{t(labels.pixel)}</ListItem>
|
||||
<ListItem id={BOARD_TYPES.link}>{t(labels.link)}</ListItem>
|
||||
</Select>
|
||||
</Box>
|
||||
</FormField>
|
||||
{requiresBoardEntity(boardType) && (
|
||||
<FormField
|
||||
name="entityId"
|
||||
label={entityLabel}
|
||||
rules={{ required: t(labels.required) }}
|
||||
>
|
||||
<Box width="100%" maxWidth="360px">
|
||||
{renderEntitySelect()}
|
||||
</Box>
|
||||
</FormField>
|
||||
)}
|
||||
</Form>
|
||||
</Panel>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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 (
|
||||
<BoardProvider boardId={boardId} editing>
|
||||
<PageBody>
|
||||
<Column>
|
||||
<BoardEditHeader />
|
||||
<BoardEditForm />
|
||||
<BoardDesignHeader />
|
||||
<BoardControls />
|
||||
<BoardEditBody />
|
||||
</Column>
|
||||
|
||||
@@ -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 && <BoardEntityBadge {...entityBadge} />}
|
||||
{showActions && board?.id && (
|
||||
<>
|
||||
<BoardShareButton boardId={board.id} />
|
||||
<LinkButton href={renderUrl(`/boards/${board.id}/edit`, false)}>
|
||||
<IconLabel icon={<Edit />}>{t(labels.edit)}</IconLabel>
|
||||
</LinkButton>
|
||||
<LinkButton href={renderUrl(`/boards/${board.id}/design`, false)}>
|
||||
<IconLabel icon={<LayoutDashboard />}>Design</IconLabel>
|
||||
</LinkButton>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
@@ -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 <BoardDesignPage boardId={boardId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Design Board',
|
||||
};
|
||||
@@ -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 (
|
||||
<PageBody>
|
||||
<Column
|
||||
margin="2"
|
||||
width="100%"
|
||||
style={{ minWidth: 'min(760px, 100%)', marginInline: 'auto' }}
|
||||
>
|
||||
<>
|
||||
<Column marginTop="6">
|
||||
<Link href={renderUrl(`/boards/${boardId}`)}>
|
||||
<IconLabel icon={<ArrowLeft />} label={t(labels.boards)} />
|
||||
</Link>
|
||||
</Column>
|
||||
<PageHeader
|
||||
title={board?.name || t(labels.untitled)}
|
||||
description={board?.description}
|
||||
icon={<LayoutDashboard />}
|
||||
/>
|
||||
</>
|
||||
<Column gap="6">
|
||||
<Panel>
|
||||
<BoardEditForm boardId={boardId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<BoardShareDialog boardId={boardId} />
|
||||
</Panel>
|
||||
</Column>
|
||||
</Column>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <BoardEditPage />;
|
||||
if (boardId === 'create') {
|
||||
redirect('/boards');
|
||||
}
|
||||
|
||||
return <BoardViewPage boardId={boardId} />;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user