diff --git a/.gitignore b/.gitignore index a9669defa..43091d08b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules .pnp .pnp.js +.idea/ # testing coverage diff --git a/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx b/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx new file mode 100644 index 000000000..339ae07d8 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx @@ -0,0 +1,28 @@ +"use client"; + +import React from "react"; + +import { clientApi } from "@homarr/api/client"; +import { useI18n } from "@homarr/translation/client"; +import { Button } from "@homarr/ui"; + +import { revalidatePathAction } from "~/app/revalidatePathAction"; + +export const CreateBoardButton = () => { + const t = useI18n(); + const { mutateAsync, isPending } = clientApi.board.create.useMutation({ + onSettled: async () => { + await revalidatePathAction("/manage/boards"); + }, + }); + + const onClick = React.useCallback(async () => { + await mutateAsync({ name: "default" }); + }, [mutateAsync]); + + return ( + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/boards/_components/delete-board-button.tsx b/apps/nextjs/src/app/[locale]/manage/boards/_components/delete-board-button.tsx new file mode 100644 index 000000000..4d49ed17a --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/boards/_components/delete-board-button.tsx @@ -0,0 +1,34 @@ +"use client"; + +import React from "react"; + +import { clientApi } from "@homarr/api/client"; +import { useI18n } from "@homarr/translation/client"; +import { Button } from "@homarr/ui"; + +import { revalidatePathAction } from "~/app/revalidatePathAction"; + +interface Props { + id: string; +} + +export const DeleteBoardButton = ({ id }: Props) => { + const t = useI18n(); + const { mutateAsync, isPending } = clientApi.board.delete.useMutation({ + onSettled: async () => { + await revalidatePathAction("/manage/boards"); + }, + }); + + const onClick = React.useCallback(async () => { + await mutateAsync({ + id, + }); + }, [id, mutateAsync]); + + return ( + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx new file mode 100644 index 000000000..a5baaa881 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +import { getScopedI18n } from "@homarr/translation/server"; +import { Card, Grid, GridCol, Text, Title } from "@homarr/ui"; + +import { api } from "~/trpc/server"; +import { CreateBoardButton } from "./_components/create-board-button"; +import { DeleteBoardButton } from "./_components/delete-board-button"; + +export default async function ManageBoardsPage() { + const t = await getScopedI18n("management.page.board"); + + const boards = await api.board.getAll.query(); + + return ( + <> + {t("title")} + + + + + {boards.map((board) => ( + + + {board.name} + + + {JSON.stringify(board)} + + + + + + ))} + + + ); +} diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index 1560e4f0b..246b78201 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server"; import superjson from "superjson"; import type { Database } from "@homarr/db"; -import { and, db, eq, inArray } from "@homarr/db"; +import { and, createId, db, eq, inArray } from "@homarr/db"; import { boards, integrationItems, @@ -47,6 +47,43 @@ const filterUpdatedItems = ( ); export const boardRouter = createTRPCRouter({ + getAll: publicProcedure.query(async () => { + return await db.query.boards.findMany({ + columns: { + id: true, + name: true, + }, + with: { + sections: { + with: { + items: true, + }, + }, + }, + }); + }), + create: publicProcedure + .input(validation.board.create) + .mutation(async ({ ctx, input }) => { + const boardId = createId(); + await ctx.db.transaction(async (transaction) => { + await transaction.insert(boards).values({ + id: boardId, + name: input.name, + }); + await transaction.insert(sections).values({ + id: createId(), + kind: "empty", + position: 0, + boardId, + }); + }); + }), + delete: publicProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ ctx, input }) => { + await ctx.db.delete(boards).where(eq(boards.id, input.id)); + }), default: publicProcedure.query(async ({ ctx }) => { return await getFullBoardByName(ctx.db, "default"); }), diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index f9fd3da8e..a154cf4f7 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -116,7 +116,7 @@ export const integrationSecrets = sqliteTable( export const boards = sqliteTable("board", { id: text("id").notNull().primaryKey(), - name: text("name").notNull(), + name: text("name").unique().notNull(), isPublic: int("is_public", { mode: "boolean" }).default(false).notNull(), pageTitle: text("page_title"), metaTitle: text("meta_title"), diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index 71cb4c950..62acddd59 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -363,5 +363,14 @@ export default { about: "About", }, }, + page: { + board: { + title: "Manage boards", + button: { + create: "Create board", + delete: "Delete board", + }, + }, + }, }, } as const; diff --git a/packages/validation/src/board.ts b/packages/validation/src/board.ts index dc2a93989..6b56666fb 100644 --- a/packages/validation/src/board.ts +++ b/packages/validation/src/board.ts @@ -36,8 +36,11 @@ const saveSchema = z.object({ sections: z.array(createSectionSchema(commonItemSchema)), }); +const createSchema = z.object({ name: z.string() }); + export const boardSchemas = { byName: byNameSchema, saveGeneralSettings: saveGeneralSettingsSchema, save: saveSchema, + create: createSchema, };