refactor: improve board manage page (#323)
* refactor: improve board manage page * chore: address pull request feedback
This commit is contained in:
17
apps/nextjs/src/app/[locale]/_client-providers/session.tsx
Normal file
17
apps/nextjs/src/app/[locale]/_client-providers/session.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
import type { Session } from "@homarr/auth";
|
||||||
|
import { SessionProvider } from "@homarr/auth/client";
|
||||||
|
|
||||||
|
interface AuthProviderProps {
|
||||||
|
session: Session | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthProvider = ({
|
||||||
|
children,
|
||||||
|
session,
|
||||||
|
}: PropsWithChildren<AuthProviderProps>) => {
|
||||||
|
return <SessionProvider session={session}>{children}</SessionProvider>;
|
||||||
|
};
|
||||||
@@ -5,12 +5,14 @@ import "@homarr/notifications/styles.css";
|
|||||||
import "@homarr/spotlight/styles.css";
|
import "@homarr/spotlight/styles.css";
|
||||||
import "@homarr/ui/styles.css";
|
import "@homarr/ui/styles.css";
|
||||||
|
|
||||||
|
import { auth } from "@homarr/auth";
|
||||||
import { ModalProvider } from "@homarr/modals";
|
import { ModalProvider } from "@homarr/modals";
|
||||||
import { Notifications } from "@homarr/notifications";
|
import { Notifications } from "@homarr/notifications";
|
||||||
import { ColorSchemeScript, createTheme, MantineProvider } from "@homarr/ui";
|
import { ColorSchemeScript, createTheme, MantineProvider } from "@homarr/ui";
|
||||||
|
|
||||||
import { JotaiProvider } from "./_client-providers/jotai";
|
import { JotaiProvider } from "./_client-providers/jotai";
|
||||||
import { NextInternationalProvider } from "./_client-providers/next-international";
|
import { NextInternationalProvider } from "./_client-providers/next-international";
|
||||||
|
import { AuthProvider } from "./_client-providers/session";
|
||||||
import { TRPCReactProvider } from "./_client-providers/trpc";
|
import { TRPCReactProvider } from "./_client-providers/trpc";
|
||||||
import { composeWrappers } from "./compose";
|
import { composeWrappers } from "./compose";
|
||||||
|
|
||||||
@@ -52,6 +54,10 @@ export default function Layout(props: {
|
|||||||
const colorScheme = "dark";
|
const colorScheme = "dark";
|
||||||
|
|
||||||
const StackedProvider = composeWrappers([
|
const StackedProvider = composeWrappers([
|
||||||
|
async (innerProps) => {
|
||||||
|
const session = await auth();
|
||||||
|
return <AuthProvider session={session} {...innerProps} />;
|
||||||
|
},
|
||||||
(innerProps) => <JotaiProvider {...innerProps} />,
|
(innerProps) => <JotaiProvider {...innerProps} />,
|
||||||
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
||||||
(innerProps) => (
|
(innerProps) => (
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { useConfirmModal } from "@homarr/modals";
|
||||||
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
import { IconSettings, IconTrash, Menu } from "@homarr/ui";
|
||||||
|
|
||||||
|
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
|
const iconProps = {
|
||||||
|
size: 16,
|
||||||
|
stroke: 1.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BoardCardMenuDropdownProps {
|
||||||
|
board: Pick<RouterOutputs["board"]["getAll"][number], "id" | "name">;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BoardCardMenuDropdown = ({
|
||||||
|
board,
|
||||||
|
}: BoardCardMenuDropdownProps) => {
|
||||||
|
const t = useScopedI18n("management.page.board.action");
|
||||||
|
const tCommon = useScopedI18n("common");
|
||||||
|
|
||||||
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
|
||||||
|
const { mutateAsync, isPending } = clientApi.board.delete.useMutation({
|
||||||
|
onSettled: async () => {
|
||||||
|
await revalidatePathAction("/manage/boards");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeletion = useCallback(() => {
|
||||||
|
openConfirmModal({
|
||||||
|
title: t("delete.confirm.title"),
|
||||||
|
children: t("delete.confirm.description", {
|
||||||
|
name: board.name,
|
||||||
|
}),
|
||||||
|
onConfirm: async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
id: board.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [board.id, board.name, mutateAsync, openConfirmModal, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item
|
||||||
|
component={Link}
|
||||||
|
href={`/boards/${board.name}/settings`}
|
||||||
|
leftSection={<IconSettings {...iconProps} />}
|
||||||
|
>
|
||||||
|
{t("settings.label")}
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Label c="red.7">
|
||||||
|
{tCommon("menu.section.dangerZone.title")}
|
||||||
|
</Menu.Label>
|
||||||
|
<Menu.Item
|
||||||
|
c="red.7"
|
||||||
|
leftSection={<IconTrash {...iconProps} />}
|
||||||
|
onClick={handleDeletion}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{t("delete.label")}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -41,7 +41,7 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
>
|
>
|
||||||
{t("management.page.board.button.create")}
|
{t("management.page.board.action.new.label")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
"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 (
|
|
||||||
<Button onClick={onClick} loading={isPending} color="red">
|
|
||||||
{t("management.page.board.button.delete")}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,28 @@
|
|||||||
import React from "react";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
import { Card, Grid, GridCol, Group, Text, Title } from "@homarr/ui";
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardSection,
|
||||||
|
Grid,
|
||||||
|
GridCol,
|
||||||
|
Group,
|
||||||
|
IconDotsVertical,
|
||||||
|
IconLock,
|
||||||
|
IconWorld,
|
||||||
|
Menu,
|
||||||
|
MenuTarget,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from "@homarr/ui";
|
||||||
|
|
||||||
|
import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown";
|
||||||
import { CreateBoardButton } from "./_components/create-board-button";
|
import { CreateBoardButton } from "./_components/create-board-button";
|
||||||
import { DeleteBoardButton } from "./_components/delete-board-button";
|
|
||||||
|
|
||||||
export default async function ManageBoardsPage() {
|
export default async function ManageBoardsPage() {
|
||||||
const t = await getScopedI18n("management.page.board");
|
const t = await getScopedI18n("management.page.board");
|
||||||
@@ -22,25 +39,58 @@ export default async function ManageBoardsPage() {
|
|||||||
<Grid>
|
<Grid>
|
||||||
{boards.map((board) => (
|
{boards.map((board) => (
|
||||||
<GridCol span={{ xs: 12, md: 4 }} key={board.id}>
|
<GridCol span={{ xs: 12, md: 4 }} key={board.id}>
|
||||||
<Card>
|
<BoardCard board={board} />
|
||||||
<Text fw="bolder" tt="uppercase">
|
|
||||||
{board.name}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
size="sm"
|
|
||||||
my="md"
|
|
||||||
c="dimmed"
|
|
||||||
style={{ lineBreak: "anywhere" }}
|
|
||||||
>
|
|
||||||
{JSON.stringify(board)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<DeleteBoardButton id={board.id} />
|
|
||||||
</Card>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BoardCardProps {
|
||||||
|
board: RouterOutputs["board"]["getAll"][number];
|
||||||
|
}
|
||||||
|
|
||||||
|
const BoardCard = async ({ board }: BoardCardProps) => {
|
||||||
|
const t = await getScopedI18n("management.page.board");
|
||||||
|
const visibility = board.isPublic ? "public" : "private";
|
||||||
|
const VisibilityIcon = board.isPublic ? IconWorld : IconLock;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardSection p="sm" withBorder>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label={t(`visibility.${visibility}`)}>
|
||||||
|
<VisibilityIcon size={20} stroke={1.5} />
|
||||||
|
</Tooltip>
|
||||||
|
<Text fw="bolder" tt="uppercase">
|
||||||
|
{board.name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</CardSection>
|
||||||
|
|
||||||
|
<CardSection p="sm">
|
||||||
|
<Group wrap="nowrap">
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
href={`/boards/${board.name}`}
|
||||||
|
variant="default"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{t("action.open.label")}
|
||||||
|
</Button>
|
||||||
|
<Menu position="bottom-end">
|
||||||
|
<MenuTarget>
|
||||||
|
<ActionIcon variant="default" size="lg">
|
||||||
|
<IconDotsVertical size={16} stroke={1.5} />
|
||||||
|
</ActionIcon>
|
||||||
|
</MenuTarget>
|
||||||
|
<BoardCardMenuDropdown board={board} />
|
||||||
|
</Menu>
|
||||||
|
</Group>
|
||||||
|
</CardSection>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -57,5 +57,5 @@ export const AddBoardModal = createModal<InnerProps>(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
).withOptions({
|
).withOptions({
|
||||||
defaultTitle: (t) => t("management.page.board.button.create"),
|
defaultTitle: (t) => t("management.page.board.action.new.label"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
isPublic: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
sections: {
|
sections: {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { signIn, signOut } from "next-auth/react";
|
export { signIn, signOut, useSession, SessionProvider } from "next-auth/react";
|
||||||
|
|||||||
@@ -234,6 +234,13 @@ export default {
|
|||||||
navigateDefaultBoard: "Navigate to default board",
|
navigateDefaultBoard: "Navigate to default board",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
menu: {
|
||||||
|
section: {
|
||||||
|
dangerZone: {
|
||||||
|
title: "Danger Zone",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
noResults: "No results found",
|
noResults: "No results found",
|
||||||
preview: {
|
preview: {
|
||||||
show: "Show preview",
|
show: "Show preview",
|
||||||
@@ -816,10 +823,28 @@ export default {
|
|||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
board: {
|
board: {
|
||||||
title: "Manage boards",
|
title: "Your boards",
|
||||||
button: {
|
action: {
|
||||||
create: "Create board",
|
new: {
|
||||||
delete: "Delete board",
|
label: "New board",
|
||||||
|
},
|
||||||
|
open: {
|
||||||
|
label: "Open board",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
label: "Settings",
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
label: "Delete permanently",
|
||||||
|
confirm: {
|
||||||
|
title: "Delete board",
|
||||||
|
description: "Are you sure you want to delete the {name} board?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visibility: {
|
||||||
|
public: "This board is public",
|
||||||
|
private: "This board is private",
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
createBoard: {
|
createBoard: {
|
||||||
|
|||||||
Reference in New Issue
Block a user