mirror of
https://github.com/umami-software/umami.git
synced 2026-02-08 14:47:14 +01:00
Boards components.
Some checks failed
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
Some checks failed
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
This commit is contained in:
parent
7edddf15a7
commit
a39ebffd8b
20 changed files with 450 additions and 33 deletions
|
|
@ -16,7 +16,7 @@ export function BoardAddButton() {
|
|||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button data-test="button-website-add" variant="primary">
|
||||
<Button data-test="button-board-add" variant="primary">
|
||||
<Icon>
|
||||
<Plus />
|
||||
</Icon>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Button, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen';
|
||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
import { DOMAIN_REGEX } from '@/lib/constants';
|
||||
|
||||
export function BoardAddForm({
|
||||
teamId,
|
||||
|
|
@ -11,39 +10,38 @@ export function BoardAddForm({
|
|||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { mutateAsync, error, isPending } = useUpdateQuery('/websites', { teamId });
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { mutateAsync, error, isPending } = useUpdateQuery('/boards', { teamId });
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
await mutateAsync(data, {
|
||||
onSuccess: async () => {
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
await mutateAsync(
|
||||
{ type: 'board', ...data },
|
||||
{
|
||||
onSuccess: async () => {
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={error?.message}>
|
||||
<FormField
|
||||
label={formatMessage(labels.name)}
|
||||
data-test="input-name"
|
||||
name="name"
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
<TextField autoComplete="off" />
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label={formatMessage(labels.domain)}
|
||||
data-test="input-domain"
|
||||
name="domain"
|
||||
label={formatMessage(labels.description)}
|
||||
name="description"
|
||||
rules={{
|
||||
required: formatMessage(labels.required),
|
||||
pattern: { value: DOMAIN_REGEX, message: formatMessage(messages.invalidDomain) },
|
||||
}}
|
||||
>
|
||||
<TextField autoComplete="off" />
|
||||
<TextField asTextArea autoComplete="off" />
|
||||
</FormField>
|
||||
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||
{onClose && (
|
||||
|
|
|
|||
14
src/app/(main)/boards/BoardsDataTable.tsx
Normal file
14
src/app/(main)/boards/BoardsDataTable.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { DataGrid } from '@/components/common/DataGrid';
|
||||
import { useBoardsQuery, useNavigation } from '@/components/hooks';
|
||||
import { BoardsTable } from './BoardsTable';
|
||||
|
||||
export function BoardsDataTable() {
|
||||
const { teamId } = useNavigation();
|
||||
const query = useBoardsQuery({ teamId });
|
||||
|
||||
return (
|
||||
<DataGrid query={query} allowSearch={true} autoFocus={false} allowPaging={true}>
|
||||
{({ data }) => <BoardsTable data={data} />}
|
||||
</DataGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,16 +1,27 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { Column, IconLabel } from '@umami/react-zen';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { BoardAddButton } from './BoardAddButton';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { Plus } from '@/components/icons';
|
||||
import { BoardsDataTable } from './BoardsDataTable';
|
||||
|
||||
export function BoardsPage() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<PageBody>
|
||||
<Column margin="2">
|
||||
<PageHeader title="My Boards">
|
||||
<BoardAddButton />
|
||||
<PageHeader title={formatMessage(labels.boards)}>
|
||||
<LinkButton href="/boards/create" variant="primary">
|
||||
<IconLabel icon={<Plus />} label={formatMessage(labels.addBoard)} />
|
||||
</LinkButton>
|
||||
</PageHeader>
|
||||
<Panel>
|
||||
<BoardsDataTable />
|
||||
</Panel>
|
||||
</Column>
|
||||
</PageBody>
|
||||
);
|
||||
|
|
|
|||
29
src/app/(main)/boards/BoardsTable.tsx
Normal file
29
src/app/(main)/boards/BoardsTable.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
|
||||
import Board from 'next/link';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
||||
|
||||
export function BoardsTable(props: DataTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { websiteId, renderUrl } = useNavigation();
|
||||
const { getSlugUrl } = useSlug('link');
|
||||
|
||||
return (
|
||||
<DataTable {...props}>
|
||||
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||
{({ id, name }: any) => {
|
||||
return <Board href={renderUrl(`/boards/${id}`)}>{name}</Board>;
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="description" label={formatMessage(labels.description)} />
|
||||
<DataColumn id="created" label={formatMessage(labels.created)} width="200px">
|
||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||
</DataColumn>
|
||||
<DataColumn id="action" align="end" width="100px">
|
||||
{({ id, name }: any) => {
|
||||
return <Row></Row>;
|
||||
}}
|
||||
</DataColumn>
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { Column, Heading } from '@umami/react-zen';
|
||||
|
||||
export function Board({ boardId }: { boardId: string }) {
|
||||
return (
|
||||
<Column>
|
||||
<Heading>Board title</Heading>
|
||||
<div>{boardId}</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
43
src/app/(main)/boards/[boardId]/BoardHeader.tsx
Normal file
43
src/app/(main)/boards/[boardId]/BoardHeader.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { Button, Column, Grid, InlineEditField, Row } from '@umami/react-zen';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export function BoardHeader() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const defaultName = formatMessage(labels.untitled);
|
||||
const name = 'My Board';
|
||||
const description = 'This is my board';
|
||||
|
||||
const handleNameChange = (name: string) => {
|
||||
//updateReport({ name: name || defaultName });
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (description: string) => {
|
||||
//updateReport({ description });
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid columns="1fr 1fr">
|
||||
<Column>
|
||||
<Row>
|
||||
<InlineEditField
|
||||
name="name"
|
||||
value={name}
|
||||
placeholder={defaultName}
|
||||
onCommit={handleNameChange}
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<InlineEditField
|
||||
name="description"
|
||||
value={description}
|
||||
placeholder={`+ ${formatMessage(labels.addDescription)}`}
|
||||
onCommit={handleDescriptionChange}
|
||||
/>
|
||||
</Row>
|
||||
</Column>
|
||||
<Row justifyContent="flex-end">
|
||||
<Button variant="primary">{formatMessage(labels.save)}</Button>
|
||||
</Row>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
17
src/app/(main)/boards/[boardId]/BoardPage.tsx
Normal file
17
src/app/(main)/boards/[boardId]/BoardPage.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { BoardHeader } from '@/app/(main)/boards/[boardId]/BoardHeader';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export function BoardPage() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<PageBody>
|
||||
<Column margin="2">
|
||||
<BoardHeader />
|
||||
</Column>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Board } from './Board';
|
||||
import { BoardPage } from './BoardPage';
|
||||
|
||||
export default async function ({ params }: { params: Promise<{ boardId: string }> }) {
|
||||
const { boardId } = await params;
|
||||
|
||||
return <Board boardId={boardId} />;
|
||||
return <BoardPage boardId={boardId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
|
|||
78
src/app/api/boards/[boardId]/route.ts
Normal file
78
src/app/api/boards/[boardId]/route.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { z } from 'zod';
|
||||
import { SHARE_ID_REGEX } from '@/lib/constants';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
|
||||
import { canDeleteBoard, canUpdateBoard, canViewBoard } from '@/permissions';
|
||||
import { deleteBoard, getBoard, updateBoard } from '@/queries/prisma';
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ boardId: string }> }) {
|
||||
const { auth, error } = await parseRequest(request);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { boardId } = await params;
|
||||
|
||||
if (!(await canViewBoard(auth, boardId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const board = await getBoard(boardId);
|
||||
|
||||
return json(board);
|
||||
}
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ boardId: string }> }) {
|
||||
const schema = z.object({
|
||||
name: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { boardId } = await params;
|
||||
const { name, domain, shareId } = body;
|
||||
|
||||
if (!(await canUpdateBoard(auth, boardId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
try {
|
||||
const board = await updateBoard(boardId, { name, domain, shareId });
|
||||
|
||||
return Response.json(board);
|
||||
} catch (e: any) {
|
||||
if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('slug')) {
|
||||
return badRequest({ message: 'That slug is already taken.' });
|
||||
}
|
||||
|
||||
return serverError(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ boardId: string }> },
|
||||
) {
|
||||
const { auth, error } = await parseRequest(request);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { boardId } = await params;
|
||||
|
||||
if (!(await canDeleteBoard(auth, boardId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
await deleteBoard(boardId);
|
||||
|
||||
return ok();
|
||||
}
|
||||
61
src/app/api/boards/route.ts
Normal file
61
src/app/api/boards/route.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { z } from 'zod';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions';
|
||||
import { createBoard, getUserBoards } from '@/queries/prisma';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const boards = await getUserBoards(auth.user.id, filters);
|
||||
|
||||
return json(boards);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
type: z.string(),
|
||||
name: z.string().max(100),
|
||||
description: z.string().max(500).optional(),
|
||||
slug: z.string().max(100),
|
||||
userId: z.uuid().nullable().optional(),
|
||||
teamId: z.uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { teamId } = body;
|
||||
|
||||
if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = {
|
||||
...body,
|
||||
id: uuid(),
|
||||
parameters: {},
|
||||
slug: uuid(),
|
||||
userId: !teamId ? auth.user.id : undefined,
|
||||
};
|
||||
|
||||
const result = await createBoard(data);
|
||||
|
||||
return json(result);
|
||||
}
|
||||
29
src/app/api/teams/[teamId]/boards/route.ts
Normal file
29
src/app/api/teams/[teamId]/boards/route.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { z } from 'zod';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { canViewTeam } from '@/permissions';
|
||||
import { getTeamPixels } from '@/queries/prisma';
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
});
|
||||
const { teamId } = await params;
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
if (!(await canViewTeam(auth, teamId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const websites = await getTeamPixels(teamId, filters);
|
||||
|
||||
return json(websites);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue