mirror of
https://github.com/umami-software/umami.git
synced 2026-02-19 20:15:41 +01:00
Add personal dashboard flow and per-component website selection
Some checks are pending
Node.js CI / build (push) Waiting to run
Some checks are pending
Node.js CI / build (push) Waiting to run
This introduces a user-scoped dashboard with board-style view/edit pages while keeping it unavailable in team context, and moves website targeting to component config so dashboard components can each select their own website.
This commit is contained in:
parent
2633697585
commit
631cc46f7f
73 changed files with 418 additions and 88 deletions
|
|
@ -14,16 +14,33 @@ import { SettingsNav } from '@/app/(main)/settings/SettingsNav';
|
|||
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||
import { IconLabel } from '@/components/common/IconLabel';
|
||||
import { useGlobalState, useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Globe, Grid2x2, LayoutDashboard, LinkIcon, PanelLeft } from '@/components/icons';
|
||||
import {
|
||||
Globe,
|
||||
Grid2x2,
|
||||
LayoutDashboard,
|
||||
LayoutGrid,
|
||||
LinkIcon,
|
||||
PanelLeft,
|
||||
} from '@/components/icons';
|
||||
import { UserButton } from '@/components/input/UserButton';
|
||||
import { Logo } from '@/components/svg';
|
||||
|
||||
export function SideNav(props: any) {
|
||||
const { t, labels } = useMessages();
|
||||
const { pathname, renderUrl, websiteId } = useNavigation();
|
||||
const { pathname, renderUrl, websiteId, teamId } = useNavigation();
|
||||
const [isCollapsed] = useGlobalState('sidenav-collapsed', false);
|
||||
|
||||
const links = [
|
||||
...(!teamId
|
||||
? [
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: t(labels.dashboard),
|
||||
path: '/dashboard',
|
||||
icon: <LayoutGrid />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'boards',
|
||||
label: t(labels.boards),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function BoardComponentRendererComponent({
|
|||
websiteId,
|
||||
}: {
|
||||
config: BoardComponentConfig;
|
||||
websiteId: string;
|
||||
websiteId?: string;
|
||||
}) {
|
||||
const definition = getComponentDefinition(config.type);
|
||||
|
||||
|
|
@ -22,6 +22,14 @@ function BoardComponentRendererComponent({
|
|||
|
||||
const Component = definition.component;
|
||||
|
||||
if (!websiteId) {
|
||||
return (
|
||||
<Column alignItems="center" justifyContent="center" width="100%" height="100%">
|
||||
<Text color="muted">Select a website</Text>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return <Component websiteId={websiteId} {...config.props} />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
||||
import type { BoardComponentConfig } from '@/lib/types';
|
||||
import {
|
||||
CATEGORIES,
|
||||
|
|
@ -21,12 +22,16 @@ import {
|
|||
import { BoardComponentRenderer } from './BoardComponentRenderer';
|
||||
|
||||
export function BoardComponentSelect({
|
||||
teamId,
|
||||
websiteId,
|
||||
defaultWebsiteId,
|
||||
initialConfig,
|
||||
onSelect,
|
||||
onClose,
|
||||
}: {
|
||||
websiteId: string;
|
||||
teamId?: string;
|
||||
websiteId?: string;
|
||||
defaultWebsiteId?: string;
|
||||
initialConfig?: BoardComponentConfig;
|
||||
onSelect: (config: BoardComponentConfig) => void;
|
||||
onClose: () => void;
|
||||
|
|
@ -34,6 +39,9 @@ export function BoardComponentSelect({
|
|||
const { t, labels, messages } = useMessages();
|
||||
const [selectedDef, setSelectedDef] = useState<ComponentDefinition | null>(null);
|
||||
const [configValues, setConfigValues] = useState<Record<string, any>>({});
|
||||
const [selectedWebsiteId, setSelectedWebsiteId] = useState(
|
||||
initialConfig?.websiteId || websiteId || defaultWebsiteId,
|
||||
);
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
|
||||
|
|
@ -73,9 +81,10 @@ export function BoardComponentSelect({
|
|||
|
||||
setSelectedDef(definition);
|
||||
setConfigValues(getDefaultConfigValues(definition, initialConfig));
|
||||
setSelectedWebsiteId(initialConfig.websiteId || websiteId || defaultWebsiteId);
|
||||
setTitle(initialConfig.title ?? '');
|
||||
setDescription(initialConfig.description || '');
|
||||
}, [initialConfig, allDefinitions]);
|
||||
}, [initialConfig, allDefinitions, websiteId, defaultWebsiteId]);
|
||||
|
||||
const handleSelectComponent = (def: ComponentDefinition) => {
|
||||
setSelectedDef(def);
|
||||
|
|
@ -107,6 +116,7 @@ export function BoardComponentSelect({
|
|||
|
||||
const config: BoardComponentConfig = {
|
||||
type: selectedDef.type,
|
||||
websiteId: selectedWebsiteId,
|
||||
title,
|
||||
description,
|
||||
};
|
||||
|
|
@ -172,12 +182,14 @@ export function BoardComponentSelect({
|
|||
|
||||
<Column gap="3" flexGrow={1} style={{ minWidth: 0 }}>
|
||||
<Panel maxHeight="100%">
|
||||
{previewConfig && websiteId ? (
|
||||
<BoardComponentRenderer config={previewConfig} websiteId={websiteId} />
|
||||
{previewConfig && selectedWebsiteId ? (
|
||||
<BoardComponentRenderer config={previewConfig} websiteId={selectedWebsiteId} />
|
||||
) : (
|
||||
<Column alignItems="center" justifyContent="center" height="100%">
|
||||
<Text color="muted">
|
||||
{websiteId ? t(messages.selectComponentPreview) : t(messages.selectWebsiteFirst)}
|
||||
{selectedWebsiteId
|
||||
? t(messages.selectComponentPreview)
|
||||
: t(messages.selectWebsiteFirst)}
|
||||
</Text>
|
||||
</Column>
|
||||
)}
|
||||
|
|
@ -187,6 +199,18 @@ export function BoardComponentSelect({
|
|||
<Column gap="3" style={{ width: 320, flexShrink: 0, overflowY: 'auto' }}>
|
||||
<Text weight="bold">{t(labels.properties)}</Text>
|
||||
|
||||
<Column gap="2">
|
||||
<Text size="sm" color="muted">
|
||||
{t(labels.website)}
|
||||
</Text>
|
||||
<WebsiteSelect
|
||||
websiteId={selectedWebsiteId}
|
||||
teamId={teamId}
|
||||
placeholder={t(labels.selectWebsite)}
|
||||
onChange={setSelectedWebsiteId}
|
||||
/>
|
||||
</Column>
|
||||
|
||||
<Column gap="2">
|
||||
<Text size="sm" color="muted">
|
||||
{t(labels.title)}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { GripHorizontal, Plus } from '@/components/icons';
|
|||
import { BoardEditRow } from './BoardEditRow';
|
||||
import { BUTTON_ROW_HEIGHT, MAX_ROW_HEIGHT, MIN_ROW_HEIGHT } from './boardConstants';
|
||||
|
||||
export function BoardEditBody() {
|
||||
export function BoardEditBody({ requiresBoardWebsite = true }: { requiresBoardWebsite?: boolean }) {
|
||||
const { board, updateBoard, registerLayoutGetter } = useBoard();
|
||||
const rowGroupRef = useRef<GroupImperativeHandle>(null);
|
||||
const columnGroupRefs = useRef<Map<string, GroupImperativeHandle>>(new Map());
|
||||
|
|
@ -103,6 +103,7 @@ export function BoardEditBody() {
|
|||
};
|
||||
|
||||
const websiteId = board?.parameters?.websiteId;
|
||||
const canEdit = requiresBoardWebsite ? !!websiteId : true;
|
||||
const rows = board?.parameters?.rows ?? [];
|
||||
const minHeight = (rows.length || 1) * MAX_ROW_HEIGHT + BUTTON_ROW_HEIGHT;
|
||||
|
||||
|
|
@ -122,7 +123,7 @@ export function BoardEditBody() {
|
|||
rowId={row.id}
|
||||
rowIndex={index}
|
||||
rowCount={rows.length}
|
||||
canEdit={!!websiteId}
|
||||
canEdit={canEdit}
|
||||
onRemove={handleRemoveRow}
|
||||
onMoveUp={handleMoveRowUp}
|
||||
onMoveDown={handleMoveRowDown}
|
||||
|
|
@ -157,7 +158,7 @@ export function BoardEditBody() {
|
|||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
{!!websiteId && (
|
||||
{canEdit && (
|
||||
<Panel minSize={BUTTON_ROW_HEIGHT}>
|
||||
<Row padding="3">
|
||||
<TooltipTrigger delay={0}>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from '@umami/react-zen';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useBoard, useMessages } from '@/components/hooks';
|
||||
import { useBoard, useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Pencil, Plus, X } from '@/components/icons';
|
||||
import type { BoardComponentConfig } from '@/lib/types';
|
||||
import { BoardComponentRenderer } from './BoardComponentRenderer';
|
||||
|
|
@ -36,7 +36,9 @@ export function BoardEditColumn({
|
|||
const [showActions, setShowActions] = useState(false);
|
||||
const { board } = useBoard();
|
||||
const { t, labels } = useMessages();
|
||||
const websiteId = board?.parameters?.websiteId;
|
||||
const { teamId } = useNavigation();
|
||||
const boardWebsiteId = board?.parameters?.websiteId;
|
||||
const websiteId = component?.websiteId || boardWebsiteId;
|
||||
const renderedComponent = useMemo(() => {
|
||||
if (!component || !websiteId) {
|
||||
return null;
|
||||
|
|
@ -126,7 +128,9 @@ export function BoardEditColumn({
|
|||
>
|
||||
{() => (
|
||||
<BoardComponentSelect
|
||||
teamId={teamId}
|
||||
websiteId={websiteId}
|
||||
defaultWebsiteId={boardWebsiteId}
|
||||
initialConfig={component}
|
||||
onSelect={handleSelect}
|
||||
onClose={() => setShowSelect(false)}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { BoardComponentRenderer } from './BoardComponentRenderer';
|
|||
|
||||
export function BoardViewColumn({ component }: { component?: BoardComponentConfig }) {
|
||||
const { board } = useBoard();
|
||||
const websiteId = board?.parameters?.websiteId;
|
||||
const websiteId = component?.websiteId || board?.parameters?.websiteId;
|
||||
|
||||
if (!component || !websiteId) {
|
||||
return null;
|
||||
|
|
|
|||
27
src/app/(main)/dashboard/DashboardEditHeader.tsx
Normal file
27
src/app/(main)/dashboard/DashboardEditHeader.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Button, LoadingButton, Row } from '@umami/react-zen';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { useBoard, useMessages, useNavigation } from '@/components/hooks';
|
||||
|
||||
export function DashboardEditHeader() {
|
||||
const { saveBoard, isPending } = useBoard();
|
||||
const { t, labels } = useMessages();
|
||||
const { router, renderUrl } = useNavigation();
|
||||
|
||||
const handleSave = async () => {
|
||||
await saveBoard();
|
||||
router.push(renderUrl('/dashboard', false));
|
||||
};
|
||||
|
||||
return (
|
||||
<PageHeader title={t(labels.dashboard)}>
|
||||
<Row gap="3">
|
||||
<Button variant="quiet" onPress={() => router.push(renderUrl('/dashboard', false))}>
|
||||
{t(labels.cancel)}
|
||||
</Button>
|
||||
<LoadingButton variant="primary" onPress={handleSave} isLoading={isPending}>
|
||||
{t(labels.save)}
|
||||
</LoadingButton>
|
||||
</Row>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
33
src/app/(main)/dashboard/DashboardEditPage.tsx
Normal file
33
src/app/(main)/dashboard/DashboardEditPage.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { useEffect } from 'react';
|
||||
import { BoardEditBody } from '@/app/(main)/boards/[boardId]/BoardEditBody';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { useNavigation } from '@/components/hooks';
|
||||
import { DashboardEditHeader } from './DashboardEditHeader';
|
||||
import { DashboardProvider } from './DashboardProvider';
|
||||
|
||||
export function DashboardEditPage() {
|
||||
const { teamId, router } = useNavigation();
|
||||
|
||||
useEffect(() => {
|
||||
if (teamId) {
|
||||
router.replace('/dashboard/edit');
|
||||
}
|
||||
}, [teamId, router]);
|
||||
|
||||
if (teamId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardProvider editing>
|
||||
<PageBody>
|
||||
<Column>
|
||||
<DashboardEditHeader />
|
||||
<BoardEditBody requiresBoardWebsite={false} />
|
||||
</Column>
|
||||
</PageBody>
|
||||
</DashboardProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export function DashboardPage() {
|
||||
const { t, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<PageBody>
|
||||
<Column margin="2">
|
||||
<PageHeader title={t(labels.dashboard)}></PageHeader>
|
||||
</Column>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
111
src/app/(main)/dashboard/DashboardProvider.tsx
Normal file
111
src/app/(main)/dashboard/DashboardProvider.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
'use client';
|
||||
import { Loading, useToast } from '@umami/react-zen';
|
||||
import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { BoardContext, type LayoutGetter } from '@/app/(main)/boards/BoardProvider';
|
||||
import { getComponentDefinition } from '@/app/(main)/boards/boardComponentRegistry';
|
||||
import { useApi, useDashboardQuery, useMessages, useModified } from '@/components/hooks';
|
||||
import type { Board, BoardParameters } from '@/lib/types';
|
||||
|
||||
const createDefaultBoard = (): Partial<Board> => ({
|
||||
name: '',
|
||||
description: '',
|
||||
parameters: {
|
||||
rows: [{ id: uuid(), columns: [{ id: uuid(), component: null }] }],
|
||||
},
|
||||
});
|
||||
|
||||
function sanitizeBoardParameters(parameters?: BoardParameters): BoardParameters | undefined {
|
||||
if (!parameters?.rows) {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
return {
|
||||
...parameters,
|
||||
rows: parameters.rows.map(row => ({
|
||||
...row,
|
||||
columns: row.columns.map(column => {
|
||||
if (column.component && !getComponentDefinition(column.component.type)) {
|
||||
return {
|
||||
...column,
|
||||
component: null,
|
||||
};
|
||||
}
|
||||
|
||||
return column;
|
||||
}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function DashboardProvider({
|
||||
editing = false,
|
||||
children,
|
||||
}: {
|
||||
editing?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { data, isFetching, isLoading } = useDashboardQuery();
|
||||
const { post, useMutation } = useApi();
|
||||
const { touch } = useModified();
|
||||
const { toast } = useToast();
|
||||
const { t, labels, messages } = useMessages();
|
||||
const [board, setBoard] = useState<Partial<Board>>(data ?? createDefaultBoard());
|
||||
const layoutGetterRef = useRef<LayoutGetter | null>(null);
|
||||
|
||||
const registerLayoutGetter = useCallback((getter: LayoutGetter) => {
|
||||
layoutGetterRef.current = getter;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setBoard({
|
||||
...data,
|
||||
parameters: sanitizeBoardParameters(data.parameters),
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const { mutateAsync, isPending } = useMutation({
|
||||
mutationFn: (boardData: Partial<Board>) => {
|
||||
return post('/dashboard', boardData);
|
||||
},
|
||||
});
|
||||
|
||||
const updateBoard = useCallback((data: Partial<Board>) => {
|
||||
setBoard(current => ({ ...current, ...data }));
|
||||
}, []);
|
||||
|
||||
const saveBoard = useCallback(async () => {
|
||||
const dashboardName = t(labels.dashboard);
|
||||
const layoutData = layoutGetterRef.current?.();
|
||||
const parameters = sanitizeBoardParameters(
|
||||
layoutData ? { ...board.parameters, ...layoutData } : board.parameters,
|
||||
);
|
||||
|
||||
const result = await mutateAsync({
|
||||
...board,
|
||||
name: dashboardName,
|
||||
description: '',
|
||||
parameters,
|
||||
});
|
||||
|
||||
toast(t(messages.saved));
|
||||
touch('dashboard');
|
||||
touch('boards');
|
||||
|
||||
return result;
|
||||
}, [board, labels.dashboard, messages.saved, mutateAsync, t, toast, touch]);
|
||||
|
||||
if (isFetching && isLoading) {
|
||||
return <Loading placement="absolute" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BoardContext.Provider
|
||||
value={{ board, editing, updateBoard, saveBoard, isPending, registerLayoutGetter }}
|
||||
>
|
||||
{children}
|
||||
</BoardContext.Provider>
|
||||
);
|
||||
}
|
||||
18
src/app/(main)/dashboard/DashboardViewHeader.tsx
Normal file
18
src/app/(main)/dashboard/DashboardViewHeader.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { IconLabel } from '@/components/common/IconLabel';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Edit } from '@/components/icons';
|
||||
|
||||
export function DashboardViewHeader() {
|
||||
const { t, labels } = useMessages();
|
||||
const { renderUrl } = useNavigation();
|
||||
|
||||
return (
|
||||
<PageHeader title={t(labels.dashboard)}>
|
||||
<LinkButton href={renderUrl('/dashboard/edit', false)}>
|
||||
<IconLabel icon={<Edit />}>{t(labels.edit)}</IconLabel>
|
||||
</LinkButton>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
49
src/app/(main)/dashboard/DashboardViewPage.tsx
Normal file
49
src/app/(main)/dashboard/DashboardViewPage.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { useEffect } from 'react';
|
||||
import { BoardViewBody } from '@/app/(main)/boards/[boardId]/BoardViewBody';
|
||||
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { useBoard, useMessages, useNavigation } from '@/components/hooks';
|
||||
import { DashboardProvider } from './DashboardProvider';
|
||||
import { DashboardViewHeader } from './DashboardViewHeader';
|
||||
|
||||
function DashboardContent() {
|
||||
const { board } = useBoard();
|
||||
const { t, labels, messages } = useMessages();
|
||||
const rows = board?.parameters?.rows ?? [];
|
||||
const hasComponents = rows.some(row => row.columns?.some(column => !!column.component));
|
||||
|
||||
if (!hasComponents) {
|
||||
return (
|
||||
<EmptyPlaceholder title={t(labels.dashboard)} description={t(messages.emptyDashboard)} />
|
||||
);
|
||||
}
|
||||
|
||||
return <BoardViewBody />;
|
||||
}
|
||||
|
||||
export function DashboardViewPage() {
|
||||
const { teamId, router } = useNavigation();
|
||||
|
||||
useEffect(() => {
|
||||
if (teamId) {
|
||||
router.replace('/dashboard');
|
||||
}
|
||||
}, [teamId, router]);
|
||||
|
||||
if (teamId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardProvider>
|
||||
<PageBody>
|
||||
<Column>
|
||||
<DashboardViewHeader />
|
||||
<DashboardContent />
|
||||
</Column>
|
||||
</PageBody>
|
||||
</DashboardProvider>
|
||||
);
|
||||
}
|
||||
10
src/app/(main)/dashboard/edit/page.tsx
Normal file
10
src/app/(main)/dashboard/edit/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { DashboardEditPage } from '../DashboardEditPage';
|
||||
|
||||
export default function () {
|
||||
return <DashboardEditPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Edit Dashboard',
|
||||
};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { DashboardPage } from './DashboardPage';
|
||||
import { DashboardViewPage } from './DashboardViewPage';
|
||||
|
||||
export default async function () {
|
||||
return <DashboardPage />;
|
||||
return <DashboardViewPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
|
|||
64
src/app/api/dashboard/route.ts
Normal file
64
src/app/api/dashboard/route.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { z } from 'zod';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { createBoard, getBoard, updateBoard } from '@/queries/prisma';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { auth, error } = await parseRequest(request);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const board = await getBoard(auth.user.id);
|
||||
|
||||
if (board && board.userId !== auth.user.id) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
return json(board);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
parameters: z.object({}).passthrough().optional(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const userId = auth.user.id;
|
||||
const existing = await getBoard(userId);
|
||||
|
||||
if (existing && existing.userId !== userId) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = {
|
||||
name: body.name,
|
||||
description: body.description,
|
||||
parameters: body.parameters ?? {},
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
const result = await updateBoard(userId, data);
|
||||
|
||||
return json(result);
|
||||
}
|
||||
|
||||
const result = await createBoard({
|
||||
id: userId,
|
||||
type: 'dashboard',
|
||||
slug: uuid(),
|
||||
userId,
|
||||
...data,
|
||||
});
|
||||
|
||||
return json(result);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue