feat(user): add search in new tab preference (#2125)

This commit is contained in:
Meier Lukas
2025-01-26 22:37:48 +01:00
committed by GitHub
parent c43a2f0488
commit 92f70f5a03
28 changed files with 3673 additions and 86 deletions

View File

@@ -36,6 +36,7 @@
"@homarr/old-schema": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/settings": "workspace:^0.1.0",
"@homarr/spotlight": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",

View File

@@ -9,10 +9,14 @@ import "~/styles/scroll-area.scss";
import { notFound } from "next/navigation";
import { NextIntlClientProvider } from "next-intl";
import { api } from "@homarr/api/server";
import { env } from "@homarr/auth/env";
import { auth } from "@homarr/auth/next";
import { db } from "@homarr/db";
import { getServerSettingsAsync } from "@homarr/db/queries";
import { ModalProvider } from "@homarr/modals";
import { Notifications } from "@homarr/notifications";
import { SettingsProvider } from "@homarr/settings";
import { SpotlightProvider } from "@homarr/spotlight";
import type { SupportedLanguage } from "@homarr/translation";
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
@@ -73,6 +77,8 @@ export default async function Layout(props: {
}
const session = await auth();
const user = session ? await api.user.getById({ userId: session.user.id }).catch(() => null) : null;
const serverSettings = await getServerSettingsAsync(db);
const colorScheme = await getCurrentColorSchemeAsync();
const direction = isLocaleRTL((await props.params).locale) ? "rtl" : "ltr";
const i18nMessages = await getI18nMessages();
@@ -81,6 +87,19 @@ export default async function Layout(props: {
(innerProps) => {
return <AuthProvider session={session} logoutUrl={env.AUTH_LOGOUT_REDIRECT_URL} {...innerProps} />;
},
(innerProps) => (
<SettingsProvider
user={user}
serverSettings={{
board: {
homeBoardId: serverSettings.board.homeBoardId,
mobileHomeBoardId: serverSettings.board.mobileHomeBoardId,
},
search: { defaultSearchEngineId: serverSettings.search.defaultSearchEngineId },
}}
{...innerProps}
/>
),
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => <DayJsLoader {...innerProps} />,

View File

@@ -1,6 +1,6 @@
"use client";
import { Button, Group, Select, Stack } from "@mantine/core";
import { Button, Group, Select, Stack, Switch } from "@mantine/core";
import type { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
@@ -11,34 +11,36 @@ import { showErrorNotification, showSuccessNotification } from "@homarr/notifica
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
interface ChangeDefaultSearchEngineFormProps {
interface ChangeSearchPreferencesFormProps {
user: RouterOutputs["user"]["getById"];
searchEnginesData: { value: string; label: string }[];
}
export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: ChangeDefaultSearchEngineFormProps) => {
export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeSearchPreferencesFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeDefaultSearchEngine.useMutation({
const { mutate, isPending } = clientApi.user.changeSearchPreferences.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
defaultSearchEngineId: variables.defaultSearchEngineId,
openInNewTab: variables.openInNewTab,
});
showSuccessNotification({
message: t("user.action.changeDefaultSearchEngine.notification.success.message"),
message: t("user.action.changeSearchPreferences.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeDefaultSearchEngine.notification.error.message"),
message: t("user.action.changeSearchPreferences.notification.error.message"),
});
},
});
const form = useZodForm(validation.user.changeDefaultSearchEngine, {
const form = useZodForm(validation.user.changeSearchPreferences, {
initialValues: {
defaultSearchEngineId: user.defaultSearchEngineId ?? "",
defaultSearchEngineId: user.defaultSearchEngineId,
openInNewTab: user.openSearchInNewTab,
},
});
@@ -52,7 +54,16 @@ export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: Chang
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Select w="100%" data={searchEnginesData} {...form.getInputProps("defaultSearchEngineId")} />
<Select
label={t("user.field.defaultSearchEngine.label")}
w="100%"
data={searchEnginesData}
{...form.getInputProps("defaultSearchEngineId")}
/>
<Switch
label={t("user.field.openSearchInNewTab.label")}
{...form.getInputProps("openInNewTab", { type: "checkbox" })}
/>
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
@@ -64,4 +75,4 @@ export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: Chang
);
};
type FormType = z.infer<typeof validation.user.changeDefaultSearchEngine>;
type FormType = z.infer<typeof validation.user.changeSearchPreferences>;

View File

@@ -11,8 +11,8 @@ import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone"
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access";
import { ChangeDefaultSearchEngineForm } from "./_components/_change-default-search-engine";
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
import { ChangeSearchPreferencesForm } from "./_components/_change-search-preferences";
import { DeleteUserButton } from "./_components/_delete-user-button";
import { FirstDayOfWeek } from "./_components/_first-day-of-week";
import { PingIconsEnabled } from "./_components/_ping-icons-enabled";
@@ -102,8 +102,8 @@ export default async function EditUserPage(props: Props) {
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.defaultSearchEngine")}</Title>
<ChangeDefaultSearchEngineForm user={user} searchEnginesData={searchEngines} />
<Title order={2}>{tGeneral("item.search")}</Title>
<ChangeSearchPreferencesForm user={user} searchEnginesData={searchEngines} />
</Stack>
<Stack mb="lg">

View File

@@ -22,6 +22,7 @@ import {
import { throwIfActionForbiddenAsync } from "./board/board-access";
import { throwIfCredentialsDisabled } from "./invite/checks";
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from "./user/change-search-preferences";
export const userRouter = createTRPCRouter({
initUser: onboardingProcedure
@@ -215,6 +216,7 @@ export const userRouter = createTRPCRouter({
firstDayOfWeek: true,
pingIconsEnabled: true,
defaultSearchEngineId: true,
openSearchInNewTab: true,
}),
)
.meta({ openapi: { method: "GET", path: "/api/users/{userId}", tags: ["users"], protect: true } })
@@ -239,6 +241,7 @@ export const userRouter = createTRPCRouter({
firstDayOfWeek: true,
pingIconsEnabled: true,
defaultSearchEngineId: true,
openSearchInNewTab: true,
},
where: eq(users.id, input.userId),
});
@@ -423,40 +426,32 @@ export const userRouter = createTRPCRouter({
}),
changeDefaultSearchEngine: protectedProcedure
.input(
convertIntersectionToZodObject(validation.user.changeDefaultSearchEngine.and(z.object({ userId: z.string() }))),
convertIntersectionToZodObject(
validation.user.changeSearchPreferences.omit({ openInNewTab: true }).and(z.object({ userId: z.string() })),
),
)
.output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/changeSearchEngine", tags: ["users"], protect: true } })
.meta({
openapi: {
method: "PATCH",
path: "/api/users/changeSearchEngine",
tags: ["users"],
protect: true,
deprecated: true,
},
})
.mutation(async ({ input, ctx }) => {
const user = ctx.session.user;
// Only admins can change other users passwords
if (!user.permissions.includes("admin") && user.id !== input.userId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const dbUser = await ctx.db.query.users.findFirst({
columns: {
id: true,
},
where: eq(users.id, input.userId),
await changeSearchPreferencesAsync(ctx.db, ctx.session, {
...input,
openInNewTab: undefined,
});
if (!dbUser) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
await ctx.db
.update(users)
.set({
defaultSearchEngineId: input.defaultSearchEngineId,
})
.where(eq(users.id, input.userId));
}),
changeSearchPreferences: protectedProcedure
.input(convertIntersectionToZodObject(changeSearchPreferencesInputSchema))
.output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/search-preferences", tags: ["users"], protect: true } })
.mutation(async ({ input, ctx }) => {
await changeSearchPreferencesAsync(ctx.db, ctx.session, input);
}),
changeColorScheme: protectedProcedure
.input(validation.user.changeColorScheme)
@@ -470,21 +465,6 @@ export const userRouter = createTRPCRouter({
})
.where(eq(users.id, ctx.session.user.id));
}),
getPingIconsEnabledOrDefault: publicProcedure.query(async ({ ctx }) => {
if (!ctx.session?.user) {
return false;
}
const user = await ctx.db.query.users.findFirst({
columns: {
id: true,
pingIconsEnabled: true,
},
where: eq(users.id, ctx.session.user.id),
});
return user?.pingIconsEnabled ?? false;
}),
changePingIconsEnabled: protectedProcedure
.input(validation.user.pingIconsEnabled.and(validation.common.byId))
.mutation(async ({ input, ctx }) => {
@@ -503,21 +483,6 @@ export const userRouter = createTRPCRouter({
})
.where(eq(users.id, ctx.session.user.id));
}),
getFirstDayOfWeekForUserOrDefault: publicProcedure.input(z.undefined()).query(async ({ ctx }) => {
if (!ctx.session?.user) {
return 1 as const;
}
const user = await ctx.db.query.users.findFirst({
columns: {
id: true,
firstDayOfWeek: true,
},
where: eq(users.id, ctx.session.user.id),
});
return user?.firstDayOfWeek ?? (1 as const);
}),
changeFirstDayOfWeek: protectedProcedure
.input(convertIntersectionToZodObject(validation.user.firstDayOfWeek.and(validation.common.byId)))
.output(z.void())

View File

@@ -0,0 +1,50 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import type { Session } from "@homarr/auth";
import type { Modify } from "@homarr/common/types";
import { eq } from "@homarr/db";
import type { Database } from "@homarr/db";
import { users } from "@homarr/db/schema";
import { validation } from "@homarr/validation";
export const changeSearchPreferencesInputSchema = validation.user.changeSearchPreferences.and(
z.object({ userId: z.string() }),
);
export const changeSearchPreferencesAsync = async (
db: Database,
session: Session,
input: Modify<z.infer<typeof changeSearchPreferencesInputSchema>, { openInNewTab: boolean | undefined }>,
) => {
const user = session.user;
// Only admins can change other users passwords
if (!user.permissions.includes("admin") && user.id !== input.userId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const dbUser = await db.query.users.findFirst({
columns: {
id: true,
},
where: eq(users.id, input.userId),
});
if (!dbUser) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
await db
.update(users)
.set({
defaultSearchEngineId: input.defaultSearchEngineId,
openSearchInNewTab: input.openInNewTab,
})
.where(eq(users.id, input.userId));
};

View File

@@ -0,0 +1 @@
ALTER TABLE `user` ADD `open_search_in_new_tab` boolean DEFAULT false NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@@ -148,6 +148,13 @@
"when": 1736514409126,
"tag": "0020_salty_doorman",
"breakpoints": true
},
{
"idx": 21,
"version": "5",
"when": 1737883744729,
"tag": "0021_fluffy_jocasta",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1 @@
ALTER TABLE `user` ADD `open_search_in_new_tab` integer DEFAULT true NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@@ -148,6 +148,13 @@
"when": 1736510755691,
"tag": "0020_empty_hellfire_club",
"breakpoints": true
},
{
"idx": 21,
"version": "6",
"when": 1737883733050,
"tag": "0021_famous_bruce_banner",
"breakpoints": true
}
]
}

View File

@@ -68,6 +68,7 @@ export const users = mysqlTable("user", {
defaultSearchEngineId: varchar({ length: 64 }).references(() => searchEngines.id, {
onDelete: "set null",
}),
openSearchInNewTab: boolean().default(false).notNull(),
colorScheme: varchar({ length: 5 }).$type<ColorScheme>().default("dark").notNull(),
firstDayOfWeek: tinyint().$type<DayOfWeek>().default(1).notNull(), // Defaults to Monday
pingIconsEnabled: boolean().default(false).notNull(),

View File

@@ -51,6 +51,7 @@ export const users = sqliteTable("user", {
defaultSearchEngineId: text().references(() => searchEngines.id, {
onDelete: "set null",
}),
openSearchInNewTab: int({ mode: "boolean" }).default(true).notNull(),
colorScheme: text().$type<ColorScheme>().default("dark").notNull(),
firstDayOfWeek: int().$type<DayOfWeek>().default(1).notNull(), // Defaults to Monday
pingIconsEnabled: int({ mode: "boolean" }).default(false).notNull(),

View File

@@ -0,0 +1,9 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];

View File

@@ -0,0 +1 @@
export * from "./src/context";

View File

@@ -0,0 +1,40 @@
{
"name": "@homarr/settings",
"version": "0.1.0",
"private": true,
"license": "MIT",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/api": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^7.16.2",
"next": "15.1.6",
"react": "19.0.0",
"react-dom": "19.0.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.19.0",
"typescript": "^5.7.3"
}
}

View File

@@ -0,0 +1,55 @@
"use client";
import type { PropsWithChildren } from "react";
import { createContext, useContext } from "react";
import type { DayOfWeek } from "@mantine/dates";
import type { RouterOutputs } from "@homarr/api";
import type { User } from "@homarr/db/schema";
import type { ServerSettings } from "@homarr/server-settings";
type SettingsContextProps = Pick<
User,
| "firstDayOfWeek"
| "defaultSearchEngineId"
| "homeBoardId"
| "mobileHomeBoardId"
| "openSearchInNewTab"
| "pingIconsEnabled"
>;
interface PublicServerSettings {
search: Pick<ServerSettings["search"], "defaultSearchEngineId">;
board: Pick<ServerSettings["board"], "homeBoardId" | "mobileHomeBoardId">;
}
const SettingsContext = createContext<SettingsContextProps | null>(null);
export const SettingsProvider = ({
user,
serverSettings,
children,
}: PropsWithChildren<{ user: RouterOutputs["user"]["getById"] | null; serverSettings: PublicServerSettings }>) => {
return (
<SettingsContext.Provider
value={{
defaultSearchEngineId: user?.defaultSearchEngineId ?? serverSettings.search.defaultSearchEngineId,
openSearchInNewTab: user?.openSearchInNewTab ?? true,
firstDayOfWeek: (user?.firstDayOfWeek as DayOfWeek | undefined) ?? (1 as const),
homeBoardId: user?.homeBoardId ?? serverSettings.board.homeBoardId,
mobileHomeBoardId: user?.mobileHomeBoardId ?? serverSettings.board.mobileHomeBoardId,
pingIconsEnabled: user?.pingIconsEnabled ?? false,
}}
>
{children}
</SettingsContext.Provider>
);
};
export const useSettings = () => {
const context = useContext(SettingsContext);
if (!context) throw new Error("useSettingsContext must be used within a SettingsProvider");
return context;
};

View File

@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}

View File

@@ -30,6 +30,7 @@
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/modals": "workspace:^0.1.0",
"@homarr/modals-collection": "workspace:^0.1.0",
"@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^7.16.2",

View File

@@ -7,6 +7,7 @@ import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationKindsByCategory, getIntegrationName } from "@homarr/definitions";
import { useModalAction } from "@homarr/modals";
import { RequestMediaModal } from "@homarr/modals-collection";
import { useSettings } from "@homarr/settings";
import { useScopedI18n } from "@homarr/translation/client";
import { createChildrenOptions } from "../../lib/children";
@@ -39,6 +40,8 @@ export const useFromIntegrationSearchInteraction = (
searchEngine: SearchEngine,
searchResult: FromIntegrationSearchResult,
): inferSearchInteractionDefinition<"link" | "javaScript" | "children"> => {
const { openSearchInNewTab } = useSettings();
if (searchEngine.type !== "fromIntegration") {
throw new Error("Invalid search engine type");
}
@@ -58,7 +61,7 @@ export const useFromIntegrationSearchInteraction = (
return {
type: "link",
href: searchResult.link,
newTab: true,
newTab: openSearchInNewTab,
};
}
@@ -127,10 +130,11 @@ const mediaRequestsChildrenOptions = createChildrenOptions<MediaRequestChildrenP
);
},
useInteraction({ result }) {
const { openSearchInNewTab } = useSettings();
return {
type: "link",
href: result.link,
newTab: true,
newTab: openSearchInNewTab,
};
},
},
@@ -166,6 +170,7 @@ export const searchEnginesChildrenOptions = createChildrenOptions<SearchEngine>(
enabled: searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && query.length > 0,
},
);
const { openSearchInNewTab } = useSettings();
if (searchEngine.type === "generic") {
return [
@@ -184,6 +189,7 @@ export const searchEnginesChildrenOptions = createChildrenOptions<SearchEngine>(
useInteraction: interaction.link(({ urlTemplate }, query) => ({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
href: urlTemplate!.replace("%s", query),
newTab: openSearchInNewTab,
})),
},
];
@@ -258,11 +264,12 @@ export const searchEnginesSearchGroups = createGroup<SearchEngine>({
setChildrenOptions(searchEnginesChildrenOptions(engine));
},
useInteraction: (searchEngine, query) => {
const { openSearchInNewTab } = useSettings();
if (searchEngine.type === "generic" && searchEngine.urlTemplate) {
return {
type: "link" as const,
href: searchEngine.urlTemplate.replace("%s", query),
newTab: true,
newTab: openSearchInNewTab,
};
}

View File

@@ -6,6 +6,7 @@ import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import type { Session } from "@homarr/auth";
import { useSession } from "@homarr/auth/client";
import { useSettings } from "@homarr/settings";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
@@ -135,10 +136,12 @@ const createDefaultSearchEntries = (
}),
icon: defaultSearchEngine.iconUrl,
useInteraction(query) {
const { openSearchInNewTab } = useSettings();
return {
type: "link",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
href: defaultSearchEngine.urlTemplate!.replace("%s", query),
newTab: openSearchInNewTab,
};
},
},

View File

@@ -151,6 +151,12 @@
},
"pingIconsEnabled": {
"label": "Use icons for pings"
},
"defaultSearchEngine": {
"label": "Default search engine"
},
"openSearchInNewTab": {
"label": "Open search results in new tab"
}
},
"error": {
@@ -210,13 +216,13 @@
}
}
},
"changeDefaultSearchEngine": {
"changeSearchPreferences": {
"notification": {
"success": {
"message": "Default search engine changed successfully"
"message": "Search preferences changed successfully"
},
"error": {
"message": "Unable to change default search engine"
"message": "Unable to change search preferences"
}
}
},
@@ -2281,7 +2287,7 @@
"mobile": "Mobile"
}
},
"defaultSearchEngine": "Default search engine",
"search": "Search",
"firstDayOfWeek": "First day of the week",
"accessibility": "Accessibility"
}

View File

@@ -110,8 +110,9 @@ const changeHomeBoardSchema = z.object({
mobileHomeBoardId: z.string().nullable(),
});
const changeDefaultSearchEngineSchema = z.object({
defaultSearchEngineId: z.string().min(1),
const changeSearchPreferencesSchema = z.object({
defaultSearchEngineId: z.string().min(1).nullable(),
openInNewTab: z.boolean(),
});
const changeColorSchemeSchema = z.object({
@@ -137,7 +138,7 @@ export const userSchemas = {
editProfile: editProfileSchema,
changePassword: changePasswordSchema,
changeHomeBoards: changeHomeBoardSchema,
changeDefaultSearchEngine: changeDefaultSearchEngineSchema,
changeSearchPreferences: changeSearchPreferencesSchema,
changePasswordApi: changePasswordApiSchema,
changeColorScheme: changeColorSchemeSchema,
firstDayOfWeek: firstDayOfWeekSchema,

View File

@@ -36,6 +36,7 @@
"@homarr/modals": "workspace:^0.1.0",
"@homarr/notifications": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/settings": "workspace:^0.1.0",
"@homarr/spotlight": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",

View File

@@ -1,7 +1,7 @@
import type { MantineColor } from "@mantine/core";
import { Box, Tooltip } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useSettings } from "@homarr/settings";
import type { TablerIcon } from "@homarr/ui";
interface PingDotProps {
@@ -11,7 +11,7 @@ interface PingDotProps {
}
export const PingDot = ({ color, tooltip, ...props }: PingDotProps) => {
const [pingIconsEnabled] = clientApi.user.getPingIconsEnabledOrDefault.useSuspenseQuery();
const { pingIconsEnabled } = useSettings();
return (
<Box bottom="2.5cqmin" right="2.5cqmin" pos="absolute">

View File

@@ -8,6 +8,7 @@ import dayjs from "dayjs";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import type { CalendarEvent } from "@homarr/integrations/types";
import { useSettings } from "@homarr/settings";
import type { WidgetComponentProps } from "../definition";
import { CalendarDay } from "./calender-day";
@@ -58,7 +59,7 @@ interface CalendarBaseProps {
const CalendarBase = ({ isEditMode, events, month, setMonth, options }: CalendarBaseProps) => {
const params = useParams();
const locale = params.locale as string;
const [firstDayOfWeek] = clientApi.user.getFirstDayOfWeekForUserOrDefault.useSuspenseQuery();
const { firstDayOfWeek } = useSettings();
return (
<Calendar

49
pnpm-lock.yaml generated
View File

@@ -145,6 +145,9 @@ importers:
'@homarr/server-settings':
specifier: workspace:^0.1.0
version: link:../../packages/server-settings
'@homarr/settings':
specifier: workspace:^0.1.0
version: link:../../packages/settings
'@homarr/spotlight':
specifier: workspace:^0.1.0
version: link:../../packages/spotlight
@@ -1600,6 +1603,46 @@ importers:
specifier: ^5.7.3
version: 5.7.3
packages/settings:
dependencies:
'@homarr/api':
specifier: workspace:^0.1.0
version: link:../api
'@homarr/db':
specifier: workspace:^0.1.0
version: link:../db
'@homarr/server-settings':
specifier: workspace:^0.1.0
version: link:../server-settings
'@mantine/dates':
specifier: ^7.16.2
version: 7.16.2(@mantine/core@7.16.2(@mantine/hooks@7.16.2(react@19.0.0))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.16.2(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next:
specifier: 15.1.6
version: 15.1.6(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.4)
react:
specifier: 19.0.0
version: 19.0.0
react-dom:
specifier: 19.0.0
version: 19.0.0(react@19.0.0)
devDependencies:
'@homarr/eslint-config':
specifier: workspace:^0.2.0
version: link:../../tooling/eslint
'@homarr/prettier-config':
specifier: workspace:^0.1.0
version: link:../../tooling/prettier
'@homarr/tsconfig':
specifier: workspace:^0.1.0
version: link:../../tooling/typescript
eslint:
specifier: ^9.19.0
version: 9.19.0
typescript:
specifier: ^5.7.3
version: 5.7.3
packages/spotlight:
dependencies:
'@homarr/api':
@@ -1623,6 +1666,9 @@ importers:
'@homarr/modals-collection':
specifier: workspace:^0.1.0
version: link:../modals-collection
'@homarr/settings':
specifier: workspace:^0.1.0
version: link:../settings
'@homarr/translation':
specifier: workspace:^0.1.0
version: link:../translation
@@ -1849,6 +1895,9 @@ importers:
'@homarr/redis':
specifier: workspace:^0.1.0
version: link:../redis
'@homarr/settings':
specifier: workspace:^0.1.0
version: link:../settings
'@homarr/spotlight':
specifier: workspace:^0.1.0
version: link:../spotlight