feat: add home board for users (#505)
* feat: add home board for users * fix: format issues * fix: deepsource issue * chore: address pull request feedback * fix: typecheck issue
This commit is contained in:
@@ -4,6 +4,6 @@ import { createBoardContentPage } from "../_creator";
|
||||
|
||||
export default createBoardContentPage<{ locale: string }>({
|
||||
async getInitialBoardAsync() {
|
||||
return await api.board.getDefaultBoard();
|
||||
return await api.board.getHomeBoard();
|
||||
},
|
||||
});
|
||||
@@ -19,8 +19,8 @@ export const updateBoardName = (name: string | null) => {
|
||||
};
|
||||
|
||||
type UpdateCallback = (
|
||||
prev: RouterOutputs["board"]["getDefaultBoard"],
|
||||
) => RouterOutputs["board"]["getDefaultBoard"];
|
||||
prev: RouterOutputs["board"]["getHomeBoard"],
|
||||
) => RouterOutputs["board"]["getHomeBoard"];
|
||||
|
||||
export const useUpdateBoard = () => {
|
||||
const utils = clientApi.useUtils();
|
||||
|
||||
@@ -16,7 +16,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { updateBoardName } from "./_client";
|
||||
|
||||
const BoardContext = createContext<{
|
||||
board: RouterOutputs["board"]["getDefaultBoard"];
|
||||
board: RouterOutputs["board"]["getHomeBoard"];
|
||||
isReady: boolean;
|
||||
markAsReady: (id: string) => void;
|
||||
} | null>(null);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { Metadata } from "next";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { capitalize } from "@homarr/common";
|
||||
|
||||
// Placed here because gridstack styles are used for board content
|
||||
import "~/styles/gridstack.scss";
|
||||
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { createBoardLayout } from "../_layout-creator";
|
||||
import type { Board } from "../_types";
|
||||
import { ClientBoard } from "./_client";
|
||||
@@ -38,9 +39,14 @@ export const createBoardContentPage = <
|
||||
}): Promise<Metadata> => {
|
||||
try {
|
||||
const board = await getInitialBoard(params);
|
||||
const t = await getI18n();
|
||||
|
||||
return {
|
||||
title: board.metaTitle ?? `${capitalize(board.name)} board | Homarr`,
|
||||
title:
|
||||
board.metaTitle ??
|
||||
createMetaTitle(
|
||||
t("board.content.metaTitle", { boardName: board.name }),
|
||||
),
|
||||
icons: {
|
||||
icon: board.faviconImageUrl ? board.faviconImageUrl : undefined,
|
||||
},
|
||||
|
||||
@@ -52,7 +52,7 @@ export const DangerZoneSettingsContent = () => {
|
||||
{
|
||||
onSettled() {
|
||||
void utils.board.getBoardByName.invalidate({ name: board.name });
|
||||
void utils.board.getDefaultBoard.invalidate();
|
||||
void utils.board.getHomeBoard.invalidate();
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -64,7 +64,7 @@ export const DangerZoneSettingsContent = () => {
|
||||
changeVisibility,
|
||||
t,
|
||||
utils.board.getBoardByName,
|
||||
utils.board.getDefaultBoard,
|
||||
utils.board.getHomeBoard,
|
||||
visibility,
|
||||
openConfirmModal,
|
||||
]);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useZodForm } from "@homarr/form";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import type { Board } from "../../_types";
|
||||
import { useUpdateBoard } from "../../(content)/_client";
|
||||
import { useSavePartialSettingsMutation } from "./_shared";
|
||||
@@ -105,7 +106,9 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
||||
<Grid.Col span={{ xs: 12, md: 6 }}>
|
||||
<TextInput
|
||||
label={t("board.field.metaTitle.label")}
|
||||
placeholder="Default Board | Homarr"
|
||||
placeholder={createMetaTitle(
|
||||
t("board.content.metaTitle", { boardName: board.name }),
|
||||
)}
|
||||
rightSection={<PendingOrInvalidIndicator {...metaTitleStatus} />}
|
||||
{...form.getInputProps("metaTitle")}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,7 @@ export const useSavePartialSettingsMutation = (board: Board) => {
|
||||
return clientApi.board.savePartialBoardSettings.useMutation({
|
||||
onSettled() {
|
||||
void utils.board.getBoardByName.invalidate({ name: board.name });
|
||||
void utils.board.getDefaultBoard.invalidate();
|
||||
void utils.board.getHomeBoard.invalidate();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
|
||||
export type Board = RouterOutputs["board"]["getDefaultBoard"];
|
||||
export type Board = RouterOutputs["board"]["getHomeBoard"];
|
||||
export type Section = Board["sections"][number];
|
||||
export type Item = Section["items"][number];
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { setStaticParamsLocale } from "next-international/server";
|
||||
|
||||
import { getScopedI18n, getStaticParams } from "@homarr/translation/server";
|
||||
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { getPackageAttributesAsync } from "~/versions/package-reader";
|
||||
import contributorsData from "../../../../../../../static-data/contributors.json";
|
||||
import translatorsData from "../../../../../../../static-data/translators.json";
|
||||
@@ -29,10 +30,9 @@ import classes from "./about.module.css";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getScopedI18n("management");
|
||||
const metaTitle = `${t("metaTitle")} • Homarr`;
|
||||
|
||||
return {
|
||||
title: metaTitle,
|
||||
title: createMetaTitle(t("metaTitle")),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { Menu } from "@mantine/core";
|
||||
import { IconSettings, IconTrash } from "@tabler/icons-react";
|
||||
import { IconHome, IconSettings, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
@@ -40,7 +40,13 @@ export const BoardCardMenuDropdown = ({
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const { mutateAsync, isPending } = clientApi.board.deleteBoard.useMutation({
|
||||
const setHomeBoardMutation = clientApi.board.setHomeBoard.useMutation({
|
||||
onSettled: async () => {
|
||||
// Revalidate all as it's part of the user settings, /boards page and board manage page
|
||||
await revalidatePathActionAsync("/");
|
||||
},
|
||||
});
|
||||
const deleteBoardMutation = clientApi.board.deleteBoard.useMutation({
|
||||
onSettled: async () => {
|
||||
await revalidatePathActionAsync("/manage/boards");
|
||||
},
|
||||
@@ -54,23 +60,36 @@ export const BoardCardMenuDropdown = ({
|
||||
}),
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
onConfirm: async () => {
|
||||
await mutateAsync({
|
||||
await deleteBoardMutation.mutateAsync({
|
||||
id: board.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [board.id, board.name, mutateAsync, openConfirmModal, t]);
|
||||
}, [board.id, board.name, deleteBoardMutation, openConfirmModal, t]);
|
||||
|
||||
const handleSetHomeBoard = useCallback(async () => {
|
||||
await setHomeBoardMutation.mutateAsync({ id: board.id });
|
||||
}, [board.id, setHomeBoardMutation]);
|
||||
|
||||
return (
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={handleSetHomeBoard}
|
||||
leftSection={<IconHome {...iconProps} />}
|
||||
>
|
||||
{t("setHomeBoard.label")}
|
||||
</Menu.Item>
|
||||
{hasChangeAccess && (
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
href={`/boards/${board.name}/settings`}
|
||||
leftSection={<IconSettings {...iconProps} />}
|
||||
>
|
||||
{t("settings.label")}
|
||||
</Menu.Item>
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
href={`/boards/${board.name}/settings`}
|
||||
leftSection={<IconSettings {...iconProps} />}
|
||||
>
|
||||
{t("settings.label")}
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
{hasFullAccess && (
|
||||
<>
|
||||
@@ -80,7 +99,7 @@ export const BoardCardMenuDropdown = ({
|
||||
c="red.7"
|
||||
leftSection={<IconTrash {...iconProps} />}
|
||||
onClick={handleDeletion}
|
||||
disabled={isPending}
|
||||
disabled={deleteBoardMutation.isPending}
|
||||
>
|
||||
{t("delete.label")}
|
||||
</Menu.Item>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardSection,
|
||||
@@ -13,7 +14,12 @@ import {
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { IconDotsVertical, IconLock, IconWorld } from "@tabler/icons-react";
|
||||
import {
|
||||
IconDotsVertical,
|
||||
IconHomeFilled,
|
||||
IconLock,
|
||||
IconWorld,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
@@ -71,12 +77,27 @@ const BoardCard = async ({ board }: BoardCardProps) => {
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{board.creator && (
|
||||
<Group gap="xs">
|
||||
<UserAvatar user={board.creator} size="sm" />
|
||||
<Text>{board.creator?.name}</Text>
|
||||
</Group>
|
||||
)}
|
||||
<Group>
|
||||
{board.isHome && (
|
||||
<Tooltip label={t("action.setHomeBoard.badge.tooltip")}>
|
||||
<Badge
|
||||
tt="none"
|
||||
color="yellow"
|
||||
variant="light"
|
||||
leftSection={<IconHomeFilled size=".7rem" />}
|
||||
>
|
||||
{t("action.setHomeBoard.badge.label")}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{board.creator && (
|
||||
<Group gap="xs">
|
||||
<UserAvatar user={board.creator} size="sm" />
|
||||
<Text>{board.creator?.name}</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
</CardSection>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IconArrowRight } from "@tabler/icons-react";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { HeroBanner } from "./_components/hero-banner";
|
||||
|
||||
interface LinkProps {
|
||||
@@ -16,10 +17,9 @@ interface LinkProps {
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getScopedI18n("management");
|
||||
const metaTitle = `${t("metaTitle")} • Homarr`;
|
||||
|
||||
return {
|
||||
title: metaTitle,
|
||||
title: createMetaTitle(t("metaTitle")),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ import "@xterm/xterm/css/xterm.css";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getScopedI18n("management");
|
||||
const metaTitle = `${t("metaTitle")} • Homarr`;
|
||||
|
||||
return {
|
||||
title: metaTitle,
|
||||
title: createMetaTitle(t("metaTitle")),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DangerZoneRoot,
|
||||
} from "~/components/manage/danger-zone";
|
||||
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { canAccessUserEditPage } from "../access";
|
||||
import { DeleteUserButton } from "./_components/_delete-user-button";
|
||||
import { UserProfileAvatarForm } from "./_components/_profile-avatar-form";
|
||||
@@ -35,10 +36,9 @@ export async function generateMetadata({ params }: Props) {
|
||||
}
|
||||
|
||||
const t = await getScopedI18n("management.page.user.edit");
|
||||
const metaTitle = `${t("metaTitle", { username: user?.name })} • Homarr`;
|
||||
|
||||
return {
|
||||
title: metaTitle,
|
||||
title: createMetaTitle(t("metaTitle", { username: user?.name })),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { UserCreateStepperComponent } from "./_components/create-user-stepper";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getScopedI18n("management.page.user.create");
|
||||
const metaTitle = `${t("metaTitle")} • Homarr`;
|
||||
|
||||
return {
|
||||
title: metaTitle,
|
||||
title: createMetaTitle(t("metaTitle")),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { api } from "@homarr/api/server";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { UserListComponent } from "./_components/user-list.component";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getScopedI18n("management.page.user.list");
|
||||
const metaTitle = `${t("metaTitle")} • Homarr`;
|
||||
|
||||
return {
|
||||
title: metaTitle,
|
||||
title: createMetaTitle(t("metaTitle")),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export const BoardRenameModal = createModal<InnerProps>(
|
||||
void utils.board.getBoardByName.invalidate({
|
||||
name: innerProps.previousName,
|
||||
});
|
||||
void utils.board.getDefaultBoard.invalidate();
|
||||
void utils.board.getHomeBoard.invalidate();
|
||||
},
|
||||
});
|
||||
const form = useZodForm(validation.board.rename.omit({ id: true }), {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { useTimeout } from "@mantine/hooks";
|
||||
import {
|
||||
IconCheck,
|
||||
IconDashboard,
|
||||
IconHome,
|
||||
IconLogin,
|
||||
IconLogout,
|
||||
IconMoon,
|
||||
@@ -72,9 +72,9 @@ export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => {
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
href="/boards"
|
||||
leftSection={<IconDashboard size="1rem" />}
|
||||
leftSection={<IconHome size="1rem" />}
|
||||
>
|
||||
{t("navigateDefaultBoard")}
|
||||
{t("homeBoard")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
|
||||
1
apps/nextjs/src/metadata.ts
Normal file
1
apps/nextjs/src/metadata.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const createMetaTitle = (name: string) => `${name} • Homarr`;
|
||||
Reference in New Issue
Block a user