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:
Meier Lukas
2024-05-18 16:57:00 +02:00
committed by GitHub
parent dfed804f65
commit 7e339c09c8
36 changed files with 2509 additions and 62 deletions

View File

@@ -4,6 +4,6 @@ import { createBoardContentPage } from "../_creator";
export default createBoardContentPage<{ locale: string }>({ export default createBoardContentPage<{ locale: string }>({
async getInitialBoardAsync() { async getInitialBoardAsync() {
return await api.board.getDefaultBoard(); return await api.board.getHomeBoard();
}, },
}); });

View File

@@ -19,8 +19,8 @@ export const updateBoardName = (name: string | null) => {
}; };
type UpdateCallback = ( type UpdateCallback = (
prev: RouterOutputs["board"]["getDefaultBoard"], prev: RouterOutputs["board"]["getHomeBoard"],
) => RouterOutputs["board"]["getDefaultBoard"]; ) => RouterOutputs["board"]["getHomeBoard"];
export const useUpdateBoard = () => { export const useUpdateBoard = () => {
const utils = clientApi.useUtils(); const utils = clientApi.useUtils();

View File

@@ -16,7 +16,7 @@ import { clientApi } from "@homarr/api/client";
import { updateBoardName } from "./_client"; import { updateBoardName } from "./_client";
const BoardContext = createContext<{ const BoardContext = createContext<{
board: RouterOutputs["board"]["getDefaultBoard"]; board: RouterOutputs["board"]["getHomeBoard"];
isReady: boolean; isReady: boolean;
markAsReady: (id: string) => void; markAsReady: (id: string) => void;
} | null>(null); } | null>(null);

View File

@@ -1,11 +1,12 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { capitalize } from "@homarr/common";
// Placed here because gridstack styles are used for board content // Placed here because gridstack styles are used for board content
import "~/styles/gridstack.scss"; import "~/styles/gridstack.scss";
import { getI18n } from "@homarr/translation/server";
import { createMetaTitle } from "~/metadata";
import { createBoardLayout } from "../_layout-creator"; import { createBoardLayout } from "../_layout-creator";
import type { Board } from "../_types"; import type { Board } from "../_types";
import { ClientBoard } from "./_client"; import { ClientBoard } from "./_client";
@@ -38,9 +39,14 @@ export const createBoardContentPage = <
}): Promise<Metadata> => { }): Promise<Metadata> => {
try { try {
const board = await getInitialBoard(params); const board = await getInitialBoard(params);
const t = await getI18n();
return { return {
title: board.metaTitle ?? `${capitalize(board.name)} board | Homarr`, title:
board.metaTitle ??
createMetaTitle(
t("board.content.metaTitle", { boardName: board.name }),
),
icons: { icons: {
icon: board.faviconImageUrl ? board.faviconImageUrl : undefined, icon: board.faviconImageUrl ? board.faviconImageUrl : undefined,
}, },

View File

@@ -52,7 +52,7 @@ export const DangerZoneSettingsContent = () => {
{ {
onSettled() { onSettled() {
void utils.board.getBoardByName.invalidate({ name: board.name }); 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, changeVisibility,
t, t,
utils.board.getBoardByName, utils.board.getBoardByName,
utils.board.getDefaultBoard, utils.board.getHomeBoard,
visibility, visibility,
openConfirmModal, openConfirmModal,
]); ]);

View File

@@ -21,6 +21,7 @@ import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { validation } from "@homarr/validation";
import { createMetaTitle } from "~/metadata";
import type { Board } from "../../_types"; import type { Board } from "../../_types";
import { useUpdateBoard } from "../../(content)/_client"; import { useUpdateBoard } from "../../(content)/_client";
import { useSavePartialSettingsMutation } from "./_shared"; import { useSavePartialSettingsMutation } from "./_shared";
@@ -105,7 +106,9 @@ export const GeneralSettingsContent = ({ board }: Props) => {
<Grid.Col span={{ xs: 12, md: 6 }}> <Grid.Col span={{ xs: 12, md: 6 }}>
<TextInput <TextInput
label={t("board.field.metaTitle.label")} label={t("board.field.metaTitle.label")}
placeholder="Default Board | Homarr" placeholder={createMetaTitle(
t("board.content.metaTitle", { boardName: board.name }),
)}
rightSection={<PendingOrInvalidIndicator {...metaTitleStatus} />} rightSection={<PendingOrInvalidIndicator {...metaTitleStatus} />}
{...form.getInputProps("metaTitle")} {...form.getInputProps("metaTitle")}
/> />

View File

@@ -7,7 +7,7 @@ export const useSavePartialSettingsMutation = (board: Board) => {
return clientApi.board.savePartialBoardSettings.useMutation({ return clientApi.board.savePartialBoardSettings.useMutation({
onSettled() { onSettled() {
void utils.board.getBoardByName.invalidate({ name: board.name }); void utils.board.getBoardByName.invalidate({ name: board.name });
void utils.board.getDefaultBoard.invalidate(); void utils.board.getHomeBoard.invalidate();
}, },
}); });
}; };

View File

@@ -1,7 +1,7 @@
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import type { WidgetKind } from "@homarr/definitions"; 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 Section = Board["sections"][number];
export type Item = Section["items"][number]; export type Item = Section["items"][number];

View File

@@ -21,6 +21,7 @@ import { setStaticParamsLocale } from "next-international/server";
import { getScopedI18n, getStaticParams } from "@homarr/translation/server"; import { getScopedI18n, getStaticParams } from "@homarr/translation/server";
import { createMetaTitle } from "~/metadata";
import { getPackageAttributesAsync } from "~/versions/package-reader"; import { getPackageAttributesAsync } from "~/versions/package-reader";
import contributorsData from "../../../../../../../static-data/contributors.json"; import contributorsData from "../../../../../../../static-data/contributors.json";
import translatorsData from "../../../../../../../static-data/translators.json"; import translatorsData from "../../../../../../../static-data/translators.json";
@@ -29,10 +30,9 @@ import classes from "./about.module.css";
export async function generateMetadata() { export async function generateMetadata() {
const t = await getScopedI18n("management"); const t = await getScopedI18n("management");
const metaTitle = `${t("metaTitle")} • Homarr`;
return { return {
title: metaTitle, title: createMetaTitle(t("metaTitle")),
}; };
} }

View File

@@ -3,7 +3,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import Link from "next/link"; import Link from "next/link";
import { Menu } from "@mantine/core"; 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 type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
@@ -40,7 +40,13 @@ export const BoardCardMenuDropdown = ({
const { openConfirmModal } = useConfirmModal(); 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 () => { onSettled: async () => {
await revalidatePathActionAsync("/manage/boards"); await revalidatePathActionAsync("/manage/boards");
}, },
@@ -54,23 +60,36 @@ export const BoardCardMenuDropdown = ({
}), }),
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
onConfirm: async () => { onConfirm: async () => {
await mutateAsync({ await deleteBoardMutation.mutateAsync({
id: board.id, 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 ( return (
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item
onClick={handleSetHomeBoard}
leftSection={<IconHome {...iconProps} />}
>
{t("setHomeBoard.label")}
</Menu.Item>
{hasChangeAccess && ( {hasChangeAccess && (
<Menu.Item <>
component={Link} <Menu.Divider />
href={`/boards/${board.name}/settings`} <Menu.Item
leftSection={<IconSettings {...iconProps} />} component={Link}
> href={`/boards/${board.name}/settings`}
{t("settings.label")} leftSection={<IconSettings {...iconProps} />}
</Menu.Item> >
{t("settings.label")}
</Menu.Item>
</>
)} )}
{hasFullAccess && ( {hasFullAccess && (
<> <>
@@ -80,7 +99,7 @@ export const BoardCardMenuDropdown = ({
c="red.7" c="red.7"
leftSection={<IconTrash {...iconProps} />} leftSection={<IconTrash {...iconProps} />}
onClick={handleDeletion} onClick={handleDeletion}
disabled={isPending} disabled={deleteBoardMutation.isPending}
> >
{t("delete.label")} {t("delete.label")}
</Menu.Item> </Menu.Item>

View File

@@ -1,6 +1,7 @@
import Link from "next/link"; import Link from "next/link";
import { import {
ActionIcon, ActionIcon,
Badge,
Button, Button,
Card, Card,
CardSection, CardSection,
@@ -13,7 +14,12 @@ import {
Title, Title,
Tooltip, Tooltip,
} from "@mantine/core"; } 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 type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";
@@ -71,12 +77,27 @@ const BoardCard = async ({ board }: BoardCardProps) => {
</Text> </Text>
</Group> </Group>
{board.creator && ( <Group>
<Group gap="xs"> {board.isHome && (
<UserAvatar user={board.creator} size="sm" /> <Tooltip label={t("action.setHomeBoard.badge.tooltip")}>
<Text>{board.creator?.name}</Text> <Badge
</Group> 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> </Group>
</CardSection> </CardSection>

View File

@@ -5,6 +5,7 @@ import { IconArrowRight } from "@tabler/icons-react";
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 { createMetaTitle } from "~/metadata";
import { HeroBanner } from "./_components/hero-banner"; import { HeroBanner } from "./_components/hero-banner";
interface LinkProps { interface LinkProps {
@@ -16,10 +17,9 @@ interface LinkProps {
export async function generateMetadata() { export async function generateMetadata() {
const t = await getScopedI18n("management"); const t = await getScopedI18n("management");
const metaTitle = `${t("metaTitle")} • Homarr`;
return { return {
title: metaTitle, title: createMetaTitle(t("metaTitle")),
}; };
} }

View File

@@ -7,13 +7,13 @@ import "@xterm/xterm/css/xterm.css";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { fullHeightWithoutHeaderAndFooter } from "~/constants"; import { fullHeightWithoutHeaderAndFooter } from "~/constants";
import { createMetaTitle } from "~/metadata";
export async function generateMetadata() { export async function generateMetadata() {
const t = await getScopedI18n("management"); const t = await getScopedI18n("management");
const metaTitle = `${t("metaTitle")} • Homarr`;
return { return {
title: metaTitle, title: createMetaTitle(t("metaTitle")),
}; };
} }

View File

@@ -10,6 +10,7 @@ import {
DangerZoneRoot, DangerZoneRoot,
} from "~/components/manage/danger-zone"; } from "~/components/manage/danger-zone";
import { catchTrpcNotFound } from "~/errors/trpc-not-found"; import { catchTrpcNotFound } from "~/errors/trpc-not-found";
import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access"; import { canAccessUserEditPage } from "../access";
import { DeleteUserButton } from "./_components/_delete-user-button"; import { DeleteUserButton } from "./_components/_delete-user-button";
import { UserProfileAvatarForm } from "./_components/_profile-avatar-form"; 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 t = await getScopedI18n("management.page.user.edit");
const metaTitle = `${t("metaTitle", { username: user?.name })} • Homarr`;
return { return {
title: metaTitle, title: createMetaTitle(t("metaTitle", { username: user?.name })),
}; };
} }

View File

@@ -1,13 +1,13 @@
import { getScopedI18n } from "@homarr/translation/server"; import { getScopedI18n } from "@homarr/translation/server";
import { createMetaTitle } from "~/metadata";
import { UserCreateStepperComponent } from "./_components/create-user-stepper"; import { UserCreateStepperComponent } from "./_components/create-user-stepper";
export async function generateMetadata() { export async function generateMetadata() {
const t = await getScopedI18n("management.page.user.create"); const t = await getScopedI18n("management.page.user.create");
const metaTitle = `${t("metaTitle")} • Homarr`;
return { return {
title: metaTitle, title: createMetaTitle(t("metaTitle")),
}; };
} }

View File

@@ -1,14 +1,14 @@
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 { createMetaTitle } from "~/metadata";
import { UserListComponent } from "./_components/user-list.component"; import { UserListComponent } from "./_components/user-list.component";
export async function generateMetadata() { export async function generateMetadata() {
const t = await getScopedI18n("management.page.user.list"); const t = await getScopedI18n("management.page.user.list");
const metaTitle = `${t("metaTitle")} • Homarr`;
return { return {
title: metaTitle, title: createMetaTitle(t("metaTitle")),
}; };
} }

View File

@@ -24,7 +24,7 @@ export const BoardRenameModal = createModal<InnerProps>(
void utils.board.getBoardByName.invalidate({ void utils.board.getBoardByName.invalidate({
name: innerProps.previousName, name: innerProps.previousName,
}); });
void utils.board.getDefaultBoard.invalidate(); void utils.board.getHomeBoard.invalidate();
}, },
}); });
const form = useZodForm(validation.board.rename.omit({ id: true }), { const form = useZodForm(validation.board.rename.omit({ id: true }), {

View File

@@ -14,7 +14,7 @@ import {
import { useTimeout } from "@mantine/hooks"; import { useTimeout } from "@mantine/hooks";
import { import {
IconCheck, IconCheck,
IconDashboard, IconHome,
IconLogin, IconLogin,
IconLogout, IconLogout,
IconMoon, IconMoon,
@@ -72,9 +72,9 @@ export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => {
<Menu.Item <Menu.Item
component={Link} component={Link}
href="/boards" href="/boards"
leftSection={<IconDashboard size="1rem" />} leftSection={<IconHome size="1rem" />}
> >
{t("navigateDefaultBoard")} {t("homeBoard")}
</Menu.Item> </Menu.Item>
<Menu.Divider /> <Menu.Divider />

View File

@@ -0,0 +1 @@
export const createMetaTitle = (name: string) => `${name} • Homarr`;

View File

@@ -12,6 +12,7 @@ import {
integrationItems, integrationItems,
items, items,
sections, sections,
users,
} from "@homarr/db/schema/sqlite"; } from "@homarr/db/schema/sqlite";
import type { WidgetKind } from "@homarr/definitions"; import type { WidgetKind } from "@homarr/definitions";
import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions"; import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions";
@@ -33,14 +34,15 @@ import { throwIfActionForbiddenAsync } from "./board/board-access";
export const boardRouter = createTRPCRouter({ export const boardRouter = createTRPCRouter({
getAllBoards: publicProcedure.query(async ({ ctx }) => { getAllBoards: publicProcedure.query(async ({ ctx }) => {
const userId = ctx.session?.user.id;
const permissionsOfCurrentUserWhenPresent = const permissionsOfCurrentUserWhenPresent =
await ctx.db.query.boardUserPermissions.findMany({ await ctx.db.query.boardUserPermissions.findMany({
where: eq(boardUserPermissions.userId, ctx.session?.user.id ?? ""), where: eq(boardUserPermissions.userId, userId ?? ""),
}); });
const permissionsOfCurrentUserGroupsWhenPresent = const permissionsOfCurrentUserGroupsWhenPresent =
await ctx.db.query.groupMembers.findMany({ await ctx.db.query.groupMembers.findMany({
where: eq(groupMembers.userId, ctx.session?.user.id ?? ""), where: eq(groupMembers.userId, userId ?? ""),
with: { with: {
group: { group: {
with: { with: {
@@ -60,6 +62,11 @@ export const boardRouter = createTRPCRouter({
) )
.flat(), .flat(),
); );
const currentUserWhenPresent = await ctx.db.query.users.findFirst({
where: eq(users.id, userId ?? ""),
});
const dbBoards = await ctx.db.query.boards.findMany({ const dbBoards = await ctx.db.query.boards.findMany({
columns: { columns: {
id: true, id: true,
@@ -98,7 +105,10 @@ export const boardRouter = createTRPCRouter({
boardIds.length > 0 ? inArray(boards.id, boardIds) : undefined, boardIds.length > 0 ? inArray(boards.id, boardIds) : undefined,
), ),
}); });
return dbBoards; return dbBoards.map((board) => ({
...board,
isHome: currentUserWhenPresent?.homeBoardId === board.id,
}));
}), }),
createBoard: permissionRequiredProcedure createBoard: permissionRequiredProcedure
.requiresPermission("board-create") .requiresPermission("board-create")
@@ -160,8 +170,31 @@ export const boardRouter = createTRPCRouter({
await ctx.db.delete(boards).where(eq(boards.id, input.id)); await ctx.db.delete(boards).where(eq(boards.id, input.id));
}), }),
getDefaultBoard: publicProcedure.query(async ({ ctx }) => { setHomeBoard: protectedProcedure
const boardWhere = eq(boards.name, "default"); .input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(
ctx,
eq(boards.id, input.id),
"board-view",
);
await ctx.db
.update(users)
.set({ homeBoardId: input.id })
.where(eq(users.id, ctx.session.user.id));
}),
getHomeBoard: publicProcedure.query(async ({ ctx }) => {
const userId = ctx.session?.user.id;
const user = userId
? await ctx.db.query.users.findFirst({
where: eq(users.id, userId),
})
: null;
const boardWhere = user?.homeBoardId
? eq(boards.id, user.homeBoardId)
: eq(boards.name, "home");
await throwIfActionForbiddenAsync(ctx, boardWhere, "board-view"); await throwIfActionForbiddenAsync(ctx, boardWhere, "board-view");
return await getFullBoardWithWhereAsync( return await getFullBoardWithWhereAsync(

View File

@@ -41,6 +41,7 @@ const createRandomUserAsync = async (db: Database) => {
const userId = createId(); const userId = createId();
await db.insert(users).values({ await db.insert(users).values({
id: userId, id: userId,
homeBoardId: null,
}); });
return userId; return userId;
}; };
@@ -493,21 +494,21 @@ describe("deleteBoard should delete board", () => {
}); });
}); });
describe("getDefaultBoard should return default board", () => { describe("getHomeBoard should return home board", () => {
it("should return default board", async () => { it("should return home board", async () => {
// Arrange // Arrange
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync"); const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
const db = createDb(); const db = createDb();
const caller = boardRouter.createCaller({ db, session: defaultSession }); const caller = boardRouter.createCaller({ db, session: defaultSession });
const fullBoardProps = await createFullBoardAsync(db, "default"); const fullBoardProps = await createFullBoardAsync(db, "home");
// Act // Act
const result = await caller.getDefaultBoard(); const result = await caller.getHomeBoard();
// Assert // Assert
expectInputToBeFullBoardWithName(result, { expectInputToBeFullBoardWithName(result, {
name: "default", name: "home",
...fullBoardProps, ...fullBoardProps,
}); });
expect(spy).toHaveBeenCalledWith( expect(spy).toHaveBeenCalledWith(
@@ -1339,7 +1340,7 @@ describe("saveGroupBoardPermissions should save group board permissions", () =>
}); });
const expectInputToBeFullBoardWithName = ( const expectInputToBeFullBoardWithName = (
input: RouterOutputs["board"]["getDefaultBoard"], input: RouterOutputs["board"]["getHomeBoard"],
props: { name: string } & Awaited<ReturnType<typeof createFullBoardAsync>>, props: { name: string } & Awaited<ReturnType<typeof createFullBoardAsync>>,
) => { ) => {
expect(input.id).toBe(props.boardId); expect(input.id).toBe(props.boardId);

View File

@@ -232,6 +232,7 @@ describe("editProfile shoud update user", () => {
salt: null, salt: null,
password: null, password: null,
image: null, image: null,
homeBoardId: null,
}); });
}); });
@@ -274,6 +275,7 @@ describe("editProfile shoud update user", () => {
salt: null, salt: null,
password: null, password: null,
image: null, image: null,
homeBoardId: null,
}); });
}); });
}); });
@@ -297,6 +299,7 @@ describe("delete should delete user", () => {
image: null, image: null,
password: null, password: null,
salt: null, salt: null,
homeBoardId: null,
}, },
{ {
id: userToDelete, id: userToDelete,
@@ -306,6 +309,7 @@ describe("delete should delete user", () => {
image: null, image: null,
password: null, password: null,
salt: null, salt: null,
homeBoardId: null,
}, },
{ {
id: createId(), id: createId(),
@@ -315,6 +319,7 @@ describe("delete should delete user", () => {
image: null, image: null,
password: null, password: null,
salt: null, salt: null,
homeBoardId: null,
}, },
]; ];

View File

@@ -0,0 +1,2 @@
ALTER TABLE `user` ADD `homeBoardId` varchar(64);--> statement-breakpoint
ALTER TABLE `user` ADD CONSTRAINT `user_homeBoardId_board_id_fk` FOREIGN KEY (`homeBoardId`) REFERENCES `board`(`id`) ON DELETE set null ON UPDATE no action;

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,13 @@
"when": 1715334452118, "when": 1715334452118,
"tag": "0000_harsh_photon", "tag": "0000_harsh_photon",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1715885855801,
"tag": "0001_wild_alex_wilder",
"breakpoints": true
} }
] ]
} }

View File

@@ -0,0 +1,33 @@
COMMIT TRANSACTION;
--> statement-breakpoint
PRAGMA foreign_keys = OFF;
--> statement-breakpoint
BEGIN TRANSACTION;
--> statement-breakpoint
ALTER TABLE `user` RENAME TO `__user_old`;
--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`name` text,
`email` text,
`emailVerified` integer,
`image` text,
`password` text,
`salt` text,
`homeBoardId` text,
FOREIGN KEY (`homeBoardId`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
INSERT INTO `user` SELECT `id`, `name`, `email`, `emailVerified`, `image`, `password`, `salt`, null FROM `__user_old`;
--> statement-breakpoint
DROP TABLE `__user_old`;
--> statement-breakpoint
ALTER TABLE `user` RENAME TO `__user_old`;
--> statement-breakpoint
ALTER TABLE `__user_old` RENAME TO `user`;
--> statement-breakpoint
COMMIT TRANSACTION;
--> statement-breakpoint
PRAGMA foreign_keys = ON;
--> statement-breakpoint
BEGIN TRANSACTION;

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,13 @@
"when": 1715334238443, "when": 1715334238443,
"tag": "0000_talented_ben_parker", "tag": "0000_talented_ben_parker",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1715871797713,
"tag": "0001_mixed_titanium_man",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,5 +1,6 @@
import type { AdapterAccount } from "@auth/core/adapters"; import type { AdapterAccount } from "@auth/core/adapters";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import type { AnyMySqlColumn } from "drizzle-orm/mysql-core";
import { import {
boolean, boolean,
index, index,
@@ -36,6 +37,12 @@ export const users = mysqlTable("user", {
image: text("image"), image: text("image"),
password: text("password"), password: text("password"),
salt: text("salt"), salt: text("salt"),
homeBoardId: varchar("homeBoardId", { length: 64 }).references(
(): AnyMySqlColumn => boards.id,
{
onDelete: "set null",
},
),
}); });
export const accounts = mysqlTable( export const accounts = mysqlTable(

View File

@@ -1,6 +1,7 @@
import type { AdapterAccount } from "@auth/core/adapters"; import type { AdapterAccount } from "@auth/core/adapters";
import type { InferSelectModel } from "drizzle-orm"; import type { InferSelectModel } from "drizzle-orm";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import type { AnySQLiteColumn } from "drizzle-orm/sqlite-core";
import { import {
index, index,
int, int,
@@ -35,6 +36,12 @@ export const users = sqliteTable("user", {
image: text("image"), image: text("image"),
password: text("password"), password: text("password"),
salt: text("salt"), salt: text("salt"),
homeBoardId: text("homeBoardId").references(
(): AnySQLiteColumn => boards.id,
{
onDelete: "set null",
},
),
}); });
export const accounts = sqliteTable( export const accounts = sqliteTable(

View File

@@ -4,11 +4,16 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import { schema } from ".."; import { schema } from "..";
export const createDb = () => { export const createDb = (debug?: boolean) => {
const sqlite = new Database(":memory:"); const sqlite = new Database(":memory:");
const db = drizzle(sqlite, { schema }); const db = drizzle(sqlite, { schema, logger: debug });
migrate(db, { migrate(db, {
migrationsFolder: "./packages/db/migrations/sqlite", migrationsFolder: "./packages/db/migrations/sqlite",
}); });
if (debug) {
console.log("Database created");
}
return db; return db;
}; };

View File

@@ -502,7 +502,7 @@ export default {
preferences: "Your preferences", preferences: "Your preferences",
logout: "Logout", logout: "Logout",
login: "Login", login: "Login",
navigateDefaultBoard: "Navigate to default board", homeBoard: "Your home board",
loggedOut: "Logged out", loggedOut: "Logged out",
}, },
}, },
@@ -970,6 +970,9 @@ export default {
label: "Name", label: "Name",
}, },
}, },
content: {
metaTitle: "{boardName} board",
},
setting: { setting: {
title: "Settings for {boardName} board", title: "Settings for {boardName} board",
section: { section: {
@@ -1152,6 +1155,13 @@ export default {
settings: { settings: {
label: "Settings", label: "Settings",
}, },
setHomeBoard: {
label: "Set as your home board",
badge: {
label: "Home",
tooltip: "This board will show as your home board",
},
},
delete: { delete: {
label: "Delete permanently", label: "Delete permanently",
confirm: { confirm: {

View File

@@ -7,7 +7,7 @@ import { reduceWidgetOptionsWithDefaultValues, widgetImports } from "..";
import { ClientServerDataInitalizer } from "./client"; import { ClientServerDataInitalizer } from "./client";
import { GlobalItemServerDataProvider } from "./provider"; import { GlobalItemServerDataProvider } from "./provider";
type Board = RouterOutputs["board"]["getDefaultBoard"]; type Board = RouterOutputs["board"]["getHomeBoard"];
type Props = PropsWithChildren<{ type Props = PropsWithChildren<{
shouldRun: boolean; shouldRun: boolean;