mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Add board state management with updateBoard and saveBoard methods.
BoardProvider now manages local board state and exposes updateBoard for editing and saveBoard for persisting to the database. Supports both create and edit modes with proper redirect after creation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e08907d998
commit
68c56060b3
7 changed files with 176 additions and 48 deletions
|
|
@ -113,6 +113,7 @@
|
|||
"react-dom": "^19.2.3",
|
||||
"react-error-boundary": "^4.0.4",
|
||||
"react-intl": "^7.1.14",
|
||||
"react-resizable-panels": "^4.4.1",
|
||||
"react-simple-maps": "^2.3.0",
|
||||
"react-use-measure": "^2.0.4",
|
||||
"react-window": "^1.8.6",
|
||||
|
|
|
|||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
|
|
@ -164,6 +164,9 @@ importers:
|
|||
react-intl:
|
||||
specifier: ^7.1.14
|
||||
version: 7.1.14(react@19.2.3)(typescript@5.9.3)
|
||||
react-resizable-panels:
|
||||
specifier: ^4.4.1
|
||||
version: 4.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react-simple-maps:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(prop-types@15.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
|
|
@ -6181,6 +6184,12 @@ packages:
|
|||
redux:
|
||||
optional: true
|
||||
|
||||
react-resizable-panels@4.4.1:
|
||||
resolution: {integrity: sha512-dpM9oI6rGlAq7VYDeafSRA1JmkJv8aNuKySR+tZLQQLfaeqTnQLSM52EcoI/QdowzsjVUCk6jViKS0xHWITVRQ==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
|
||||
react-simple-maps@2.3.0:
|
||||
resolution: {integrity: sha512-IZVeiPSRZKwD6I/2NvXpQ2uENYGDGZp8DvZjkapcxuJ/LQHTfl+Byb+KNgY7s+iatRA2ad8LnZ3AgqcjziCCsw==}
|
||||
peerDependencies:
|
||||
|
|
@ -13909,6 +13918,11 @@ snapshots:
|
|||
'@types/react': 19.2.8
|
||||
redux: 5.0.1
|
||||
|
||||
react-resizable-panels@4.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
react-simple-maps@2.3.0(prop-types@15.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
d3-geo: 2.0.2
|
||||
|
|
|
|||
|
|
@ -1,17 +1,86 @@
|
|||
'use client';
|
||||
import { Loading } from '@umami/react-zen';
|
||||
import { createContext, type ReactNode } from 'react';
|
||||
import { Loading, useToast } from '@umami/react-zen';
|
||||
import { createContext, type ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import { useApi, useMessages, useModified, useNavigation } from '@/components/hooks';
|
||||
import { useBoardQuery } from '@/components/hooks/queries/useBoardQuery';
|
||||
import type { Board } from '@/generated/prisma/client';
|
||||
|
||||
export const BoardContext = createContext<Board>(null);
|
||||
export interface BoardContextValue {
|
||||
board: Partial<Board>;
|
||||
updateBoard: (data: Partial<Board>) => void;
|
||||
saveBoard: () => Promise<Board>;
|
||||
isPending: boolean;
|
||||
}
|
||||
|
||||
export function BoardProvider({ boardId, children }: { boardId: string; children: ReactNode }) {
|
||||
const { data: board, isFetching, isLoading } = useBoardQuery(boardId);
|
||||
export const BoardContext = createContext<BoardContextValue>(null);
|
||||
|
||||
if (isFetching && isLoading) {
|
||||
const defaultBoard: Partial<Board> = {
|
||||
name: '',
|
||||
description: '',
|
||||
};
|
||||
|
||||
export function BoardProvider({ boardId, children }: { boardId?: string; children: ReactNode }) {
|
||||
const { data, isFetching, isLoading } = useBoardQuery(boardId);
|
||||
const { post, useMutation } = useApi();
|
||||
const { touch } = useModified();
|
||||
const { toast } = useToast();
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { router, renderUrl } = useNavigation();
|
||||
|
||||
const [board, setBoard] = useState<Partial<Board>>(data ?? defaultBoard);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setBoard(data);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const { mutateAsync, isPending } = useMutation({
|
||||
mutationFn: (boardData: Partial<Board>) => {
|
||||
if (boardData.id) {
|
||||
return post(`/boards/${boardData.id}`, boardData);
|
||||
}
|
||||
return post('/boards', { ...boardData, type: 'dashboard', slug: '' });
|
||||
},
|
||||
});
|
||||
|
||||
const updateBoard = useCallback((data: Partial<Board>) => {
|
||||
setBoard(current => ({ ...current, ...data }));
|
||||
}, []);
|
||||
|
||||
const saveBoard = useCallback(async () => {
|
||||
const defaultName = formatMessage(labels.untitled);
|
||||
const result = await mutateAsync({ ...board, name: board.name || defaultName });
|
||||
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('boards');
|
||||
|
||||
if (board.id) {
|
||||
touch(`board:${board.id}`);
|
||||
} else if (result?.id) {
|
||||
router.push(renderUrl(`/boards/${result.id}`));
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [
|
||||
board,
|
||||
mutateAsync,
|
||||
toast,
|
||||
formatMessage,
|
||||
labels.untitled,
|
||||
messages.saved,
|
||||
touch,
|
||||
router,
|
||||
renderUrl,
|
||||
]);
|
||||
|
||||
if (boardId && isFetching && isLoading) {
|
||||
return <Loading placement="absolute" />;
|
||||
}
|
||||
|
||||
return <BoardContext.Provider value={board}>{children}</BoardContext.Provider>;
|
||||
return (
|
||||
<BoardContext.Provider value={{ board, updateBoard, saveBoard, isPending }}>
|
||||
{children}
|
||||
</BoardContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,69 @@
|
|||
import { Button, Column, Icon, Row } from '@umami/react-zen';
|
||||
import { produce } from 'immer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useBoard } from '@/components/hooks';
|
||||
import { Plus } from '@/components/icons';
|
||||
|
||||
export function BoardBody() {
|
||||
return <h1>i am bored.</h1>;
|
||||
const { board, updateBoard, saveBoard, isPending } = useBoard();
|
||||
|
||||
console.log({ board });
|
||||
|
||||
const handleAddRow = () => {
|
||||
updateBoard({
|
||||
parameters: produce(board.parameters, draft => {
|
||||
if (!draft.rows) {
|
||||
draft.rows = [];
|
||||
}
|
||||
draft.rows.push({ id: uuid(), components: [] });
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Column>
|
||||
{board?.parameters?.rows?.map((row, rowIndex) => {
|
||||
return <BoardRow key={row.id} rowIndex={rowIndex} components={row.components} />;
|
||||
})}
|
||||
<Row>
|
||||
<Button variant="outline" onPress={handleAddRow}>
|
||||
<Icon>
|
||||
<Plus />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Row>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
function BoardComponent() {
|
||||
return <Column>hi</Column>;
|
||||
}
|
||||
|
||||
function BoardRow({ rowIndex, components }: { rowIndex: number; components: any[] }) {
|
||||
const { board, updateBoard } = useBoard();
|
||||
|
||||
const handleAddComponent = () => {
|
||||
updateBoard({
|
||||
parameters: produce(board.parameters, draft => {
|
||||
if (!draft.rows[rowIndex]) {
|
||||
draft.rows[rowIndex] = { id: uuid(), components: [] };
|
||||
}
|
||||
draft.rows[rowIndex].components.push({ id: uuid(), type: 'text', value: '' });
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Row>
|
||||
{components?.map(component => {
|
||||
return <BoardComponent key={component.id} />;
|
||||
})}
|
||||
<Button variant="outline" onPress={handleAddComponent}>
|
||||
<Icon>
|
||||
<Plus />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +1,21 @@
|
|||
import { Column, Grid, Heading, LoadingButton, Row, TextField, useToast } from '@umami/react-zen';
|
||||
import { useState } from 'react';
|
||||
import { useApi, useBoard, useMessages, useModified, useNavigation } from '@/components/hooks';
|
||||
import { Column, Grid, Heading, LoadingButton, Row, TextField } from '@umami/react-zen';
|
||||
import { useBoard, useMessages } from '@/components/hooks';
|
||||
|
||||
export function BoardHeader() {
|
||||
const board = useBoard();
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { touch } = useModified();
|
||||
const { router, renderUrl } = useNavigation();
|
||||
const { toast } = useToast();
|
||||
const { board, updateBoard, saveBoard, isPending } = useBoard();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const defaultName = formatMessage(labels.untitled);
|
||||
|
||||
const [name, setName] = useState(board?.name ?? '');
|
||||
const [description, setDescription] = useState(board?.description ?? '');
|
||||
|
||||
const { mutateAsync, isPending } = useMutation({
|
||||
mutationFn: (data: { name: string; description: string }) => {
|
||||
if (board) {
|
||||
return post(`/boards/${board.id}`, data);
|
||||
}
|
||||
return post('/boards', { ...data, type: 'dashboard', slug: '' });
|
||||
},
|
||||
});
|
||||
|
||||
const handleNameChange = (value: string) => {
|
||||
setName(value);
|
||||
updateBoard({ name: value });
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (value: string) => {
|
||||
setDescription(value);
|
||||
updateBoard({ description: value });
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const result = await mutateAsync({ name: name || defaultName, description });
|
||||
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('boards');
|
||||
|
||||
if (board) {
|
||||
touch(`board:${board.id}`);
|
||||
} else if (result?.id) {
|
||||
router.push(renderUrl(`/boards/${result.id}`));
|
||||
}
|
||||
const handleSave = () => {
|
||||
saveBoard();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -57,26 +31,26 @@ export function BoardHeader() {
|
|||
<TextField
|
||||
variant="quiet"
|
||||
name="name"
|
||||
value={name}
|
||||
value={board?.name ?? ''}
|
||||
placeholder={defaultName}
|
||||
onChange={handleNameChange}
|
||||
autoComplete="off"
|
||||
style={{ fontSize: '2rem', fontWeight: 700, width: '100%' }}
|
||||
>
|
||||
<Heading size="4">{name}</Heading>
|
||||
<Heading size="4">{board?.name}</Heading>
|
||||
</TextField>
|
||||
</Row>
|
||||
<Row>
|
||||
<TextField
|
||||
variant="quiet"
|
||||
name="description"
|
||||
value={description}
|
||||
value={board?.description ?? ''}
|
||||
placeholder={`+ ${formatMessage(labels.addDescription)}`}
|
||||
autoComplete="off"
|
||||
onChange={handleDescriptionChange}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{description}
|
||||
{board?.description}
|
||||
</TextField>
|
||||
</Row>
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useContext } from 'react';
|
||||
import { BoardContext } from '@/app/(main)/boards/BoardProvider';
|
||||
import { BoardContext, type BoardContextValue } from '@/app/(main)/boards/BoardProvider';
|
||||
|
||||
export function useBoard() {
|
||||
export function useBoard(): BoardContextValue {
|
||||
return useContext(BoardContext);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,3 +141,7 @@ export interface ApiError extends Error {
|
|||
code?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface BoardData {
|
||||
rows: { id: string; name: string; value: number }[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue