feat(user): add search in new tab preference (#2125)
This commit is contained in:
@@ -36,6 +36,7 @@
|
|||||||
"@homarr/old-schema": "workspace:^0.1.0",
|
"@homarr/old-schema": "workspace:^0.1.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
|
"@homarr/settings": "workspace:^0.1.0",
|
||||||
"@homarr/spotlight": "workspace:^0.1.0",
|
"@homarr/spotlight": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ import "~/styles/scroll-area.scss";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
|
|
||||||
|
import { api } from "@homarr/api/server";
|
||||||
import { env } from "@homarr/auth/env";
|
import { env } from "@homarr/auth/env";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
|
import { db } from "@homarr/db";
|
||||||
|
import { getServerSettingsAsync } from "@homarr/db/queries";
|
||||||
import { ModalProvider } from "@homarr/modals";
|
import { ModalProvider } from "@homarr/modals";
|
||||||
import { Notifications } from "@homarr/notifications";
|
import { Notifications } from "@homarr/notifications";
|
||||||
|
import { SettingsProvider } from "@homarr/settings";
|
||||||
import { SpotlightProvider } from "@homarr/spotlight";
|
import { SpotlightProvider } from "@homarr/spotlight";
|
||||||
import type { SupportedLanguage } from "@homarr/translation";
|
import type { SupportedLanguage } from "@homarr/translation";
|
||||||
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
||||||
@@ -73,6 +77,8 @@ export default async function Layout(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = await auth();
|
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 colorScheme = await getCurrentColorSchemeAsync();
|
||||||
const direction = isLocaleRTL((await props.params).locale) ? "rtl" : "ltr";
|
const direction = isLocaleRTL((await props.params).locale) ? "rtl" : "ltr";
|
||||||
const i18nMessages = await getI18nMessages();
|
const i18nMessages = await getI18nMessages();
|
||||||
@@ -81,6 +87,19 @@ export default async function Layout(props: {
|
|||||||
(innerProps) => {
|
(innerProps) => {
|
||||||
return <AuthProvider session={session} logoutUrl={env.AUTH_LOGOUT_REDIRECT_URL} {...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) => <JotaiProvider {...innerProps} />,
|
||||||
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
||||||
(innerProps) => <DayJsLoader {...innerProps} />,
|
(innerProps) => <DayJsLoader {...innerProps} />,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"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 { z } from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
@@ -11,34 +11,36 @@ import { showErrorNotification, showSuccessNotification } from "@homarr/notifica
|
|||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
interface ChangeDefaultSearchEngineFormProps {
|
interface ChangeSearchPreferencesFormProps {
|
||||||
user: RouterOutputs["user"]["getById"];
|
user: RouterOutputs["user"]["getById"];
|
||||||
searchEnginesData: { value: string; label: string }[];
|
searchEnginesData: { value: string; label: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: ChangeDefaultSearchEngineFormProps) => {
|
export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeSearchPreferencesFormProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutate, isPending } = clientApi.user.changeDefaultSearchEngine.useMutation({
|
const { mutate, isPending } = clientApi.user.changeSearchPreferences.useMutation({
|
||||||
async onSettled() {
|
async onSettled() {
|
||||||
await revalidatePathActionAsync(`/manage/users/${user.id}`);
|
await revalidatePathActionAsync(`/manage/users/${user.id}`);
|
||||||
},
|
},
|
||||||
onSuccess(_, variables) {
|
onSuccess(_, variables) {
|
||||||
form.setInitialValues({
|
form.setInitialValues({
|
||||||
defaultSearchEngineId: variables.defaultSearchEngineId,
|
defaultSearchEngineId: variables.defaultSearchEngineId,
|
||||||
|
openInNewTab: variables.openInNewTab,
|
||||||
});
|
});
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
message: t("user.action.changeDefaultSearchEngine.notification.success.message"),
|
message: t("user.action.changeSearchPreferences.notification.success.message"),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
showErrorNotification({
|
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: {
|
initialValues: {
|
||||||
defaultSearchEngineId: user.defaultSearchEngineId ?? "",
|
defaultSearchEngineId: user.defaultSearchEngineId,
|
||||||
|
openInNewTab: user.openSearchInNewTab,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,7 +54,16 @@ export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: Chang
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
<Stack gap="md">
|
<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">
|
<Group justify="end">
|
||||||
<Button type="submit" color="teal" loading={isPending}>
|
<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>;
|
||||||
@@ -11,8 +11,8 @@ import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone"
|
|||||||
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
|
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
|
||||||
import { createMetaTitle } from "~/metadata";
|
import { createMetaTitle } from "~/metadata";
|
||||||
import { canAccessUserEditPage } from "../access";
|
import { canAccessUserEditPage } from "../access";
|
||||||
import { ChangeDefaultSearchEngineForm } from "./_components/_change-default-search-engine";
|
|
||||||
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
|
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
|
||||||
|
import { ChangeSearchPreferencesForm } from "./_components/_change-search-preferences";
|
||||||
import { DeleteUserButton } from "./_components/_delete-user-button";
|
import { DeleteUserButton } from "./_components/_delete-user-button";
|
||||||
import { FirstDayOfWeek } from "./_components/_first-day-of-week";
|
import { FirstDayOfWeek } from "./_components/_first-day-of-week";
|
||||||
import { PingIconsEnabled } from "./_components/_ping-icons-enabled";
|
import { PingIconsEnabled } from "./_components/_ping-icons-enabled";
|
||||||
@@ -102,8 +102,8 @@ export default async function EditUserPage(props: Props) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack mb="lg">
|
<Stack mb="lg">
|
||||||
<Title order={2}>{tGeneral("item.defaultSearchEngine")}</Title>
|
<Title order={2}>{tGeneral("item.search")}</Title>
|
||||||
<ChangeDefaultSearchEngineForm user={user} searchEnginesData={searchEngines} />
|
<ChangeSearchPreferencesForm user={user} searchEnginesData={searchEngines} />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack mb="lg">
|
<Stack mb="lg">
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||||
import { throwIfCredentialsDisabled } from "./invite/checks";
|
import { throwIfCredentialsDisabled } from "./invite/checks";
|
||||||
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
||||||
|
import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from "./user/change-search-preferences";
|
||||||
|
|
||||||
export const userRouter = createTRPCRouter({
|
export const userRouter = createTRPCRouter({
|
||||||
initUser: onboardingProcedure
|
initUser: onboardingProcedure
|
||||||
@@ -215,6 +216,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
firstDayOfWeek: true,
|
firstDayOfWeek: true,
|
||||||
pingIconsEnabled: true,
|
pingIconsEnabled: true,
|
||||||
defaultSearchEngineId: true,
|
defaultSearchEngineId: true,
|
||||||
|
openSearchInNewTab: true,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.meta({ openapi: { method: "GET", path: "/api/users/{userId}", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "GET", path: "/api/users/{userId}", tags: ["users"], protect: true } })
|
||||||
@@ -239,6 +241,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
firstDayOfWeek: true,
|
firstDayOfWeek: true,
|
||||||
pingIconsEnabled: true,
|
pingIconsEnabled: true,
|
||||||
defaultSearchEngineId: true,
|
defaultSearchEngineId: true,
|
||||||
|
openSearchInNewTab: true,
|
||||||
},
|
},
|
||||||
where: eq(users.id, input.userId),
|
where: eq(users.id, input.userId),
|
||||||
});
|
});
|
||||||
@@ -423,40 +426,32 @@ export const userRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
changeDefaultSearchEngine: protectedProcedure
|
changeDefaultSearchEngine: protectedProcedure
|
||||||
.input(
|
.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())
|
.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 }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const user = ctx.session.user;
|
await changeSearchPreferencesAsync(ctx.db, ctx.session, {
|
||||||
// Only admins can change other users passwords
|
...input,
|
||||||
if (!user.permissions.includes("admin") && user.id !== input.userId) {
|
openInNewTab: undefined,
|
||||||
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),
|
|
||||||
});
|
});
|
||||||
|
}),
|
||||||
if (!dbUser) {
|
changeSearchPreferences: protectedProcedure
|
||||||
throw new TRPCError({
|
.input(convertIntersectionToZodObject(changeSearchPreferencesInputSchema))
|
||||||
code: "NOT_FOUND",
|
.output(z.void())
|
||||||
message: "User not found",
|
.meta({ openapi: { method: "PATCH", path: "/api/users/search-preferences", tags: ["users"], protect: true } })
|
||||||
});
|
.mutation(async ({ input, ctx }) => {
|
||||||
}
|
await changeSearchPreferencesAsync(ctx.db, ctx.session, input);
|
||||||
|
|
||||||
await ctx.db
|
|
||||||
.update(users)
|
|
||||||
.set({
|
|
||||||
defaultSearchEngineId: input.defaultSearchEngineId,
|
|
||||||
})
|
|
||||||
.where(eq(users.id, input.userId));
|
|
||||||
}),
|
}),
|
||||||
changeColorScheme: protectedProcedure
|
changeColorScheme: protectedProcedure
|
||||||
.input(validation.user.changeColorScheme)
|
.input(validation.user.changeColorScheme)
|
||||||
@@ -470,21 +465,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.where(eq(users.id, ctx.session.user.id));
|
.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
|
changePingIconsEnabled: protectedProcedure
|
||||||
.input(validation.user.pingIconsEnabled.and(validation.common.byId))
|
.input(validation.user.pingIconsEnabled.and(validation.common.byId))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -503,21 +483,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.where(eq(users.id, ctx.session.user.id));
|
.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
|
changeFirstDayOfWeek: protectedProcedure
|
||||||
.input(convertIntersectionToZodObject(validation.user.firstDayOfWeek.and(validation.common.byId)))
|
.input(convertIntersectionToZodObject(validation.user.firstDayOfWeek.and(validation.common.byId)))
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
|
|||||||
50
packages/api/src/router/user/change-search-preferences.ts
Normal file
50
packages/api/src/router/user/change-search-preferences.ts
Normal 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));
|
||||||
|
};
|
||||||
1
packages/db/migrations/mysql/0021_fluffy_jocasta.sql
Normal file
1
packages/db/migrations/mysql/0021_fluffy_jocasta.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `user` ADD `open_search_in_new_tab` boolean DEFAULT false NOT NULL;
|
||||||
1708
packages/db/migrations/mysql/meta/0021_snapshot.json
Normal file
1708
packages/db/migrations/mysql/meta/0021_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -148,6 +148,13 @@
|
|||||||
"when": 1736514409126,
|
"when": 1736514409126,
|
||||||
"tag": "0020_salty_doorman",
|
"tag": "0020_salty_doorman",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 21,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1737883744729,
|
||||||
|
"tag": "0021_fluffy_jocasta",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `user` ADD `open_search_in_new_tab` integer DEFAULT true NOT NULL;
|
||||||
1633
packages/db/migrations/sqlite/meta/0021_snapshot.json
Normal file
1633
packages/db/migrations/sqlite/meta/0021_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -148,6 +148,13 @@
|
|||||||
"when": 1736510755691,
|
"when": 1736510755691,
|
||||||
"tag": "0020_empty_hellfire_club",
|
"tag": "0020_empty_hellfire_club",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 21,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1737883733050,
|
||||||
|
"tag": "0021_famous_bruce_banner",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export const users = mysqlTable("user", {
|
|||||||
defaultSearchEngineId: varchar({ length: 64 }).references(() => searchEngines.id, {
|
defaultSearchEngineId: varchar({ length: 64 }).references(() => searchEngines.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
openSearchInNewTab: boolean().default(false).notNull(),
|
||||||
colorScheme: varchar({ length: 5 }).$type<ColorScheme>().default("dark").notNull(),
|
colorScheme: varchar({ length: 5 }).$type<ColorScheme>().default("dark").notNull(),
|
||||||
firstDayOfWeek: tinyint().$type<DayOfWeek>().default(1).notNull(), // Defaults to Monday
|
firstDayOfWeek: tinyint().$type<DayOfWeek>().default(1).notNull(), // Defaults to Monday
|
||||||
pingIconsEnabled: boolean().default(false).notNull(),
|
pingIconsEnabled: boolean().default(false).notNull(),
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const users = sqliteTable("user", {
|
|||||||
defaultSearchEngineId: text().references(() => searchEngines.id, {
|
defaultSearchEngineId: text().references(() => searchEngines.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
openSearchInNewTab: int({ mode: "boolean" }).default(true).notNull(),
|
||||||
colorScheme: text().$type<ColorScheme>().default("dark").notNull(),
|
colorScheme: text().$type<ColorScheme>().default("dark").notNull(),
|
||||||
firstDayOfWeek: int().$type<DayOfWeek>().default(1).notNull(), // Defaults to Monday
|
firstDayOfWeek: int().$type<DayOfWeek>().default(1).notNull(), // Defaults to Monday
|
||||||
pingIconsEnabled: int({ mode: "boolean" }).default(false).notNull(),
|
pingIconsEnabled: int({ mode: "boolean" }).default(false).notNull(),
|
||||||
|
|||||||
9
packages/settings/eslint.config.js
Normal file
9
packages/settings/eslint.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import baseConfig from "@homarr/eslint-config/base";
|
||||||
|
|
||||||
|
/** @type {import('typescript-eslint').Config} */
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: [],
|
||||||
|
},
|
||||||
|
...baseConfig,
|
||||||
|
];
|
||||||
1
packages/settings/index.ts
Normal file
1
packages/settings/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./src/context";
|
||||||
40
packages/settings/package.json
Normal file
40
packages/settings/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
packages/settings/src/context.tsx
Normal file
55
packages/settings/src/context.tsx
Normal 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;
|
||||||
|
};
|
||||||
8
packages/settings/tsconfig.json
Normal file
8
packages/settings/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "@homarr/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
|
},
|
||||||
|
"include": ["*.ts", "src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
"@homarr/modals": "workspace:^0.1.0",
|
"@homarr/modals": "workspace:^0.1.0",
|
||||||
"@homarr/modals-collection": "workspace:^0.1.0",
|
"@homarr/modals-collection": "workspace:^0.1.0",
|
||||||
|
"@homarr/settings": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.16.2",
|
"@mantine/core": "^7.16.2",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { IntegrationKind } from "@homarr/definitions";
|
|||||||
import { getIntegrationKindsByCategory, getIntegrationName } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory, getIntegrationName } from "@homarr/definitions";
|
||||||
import { useModalAction } from "@homarr/modals";
|
import { useModalAction } from "@homarr/modals";
|
||||||
import { RequestMediaModal } from "@homarr/modals-collection";
|
import { RequestMediaModal } from "@homarr/modals-collection";
|
||||||
|
import { useSettings } from "@homarr/settings";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { createChildrenOptions } from "../../lib/children";
|
import { createChildrenOptions } from "../../lib/children";
|
||||||
@@ -39,6 +40,8 @@ export const useFromIntegrationSearchInteraction = (
|
|||||||
searchEngine: SearchEngine,
|
searchEngine: SearchEngine,
|
||||||
searchResult: FromIntegrationSearchResult,
|
searchResult: FromIntegrationSearchResult,
|
||||||
): inferSearchInteractionDefinition<"link" | "javaScript" | "children"> => {
|
): inferSearchInteractionDefinition<"link" | "javaScript" | "children"> => {
|
||||||
|
const { openSearchInNewTab } = useSettings();
|
||||||
|
|
||||||
if (searchEngine.type !== "fromIntegration") {
|
if (searchEngine.type !== "fromIntegration") {
|
||||||
throw new Error("Invalid search engine type");
|
throw new Error("Invalid search engine type");
|
||||||
}
|
}
|
||||||
@@ -58,7 +61,7 @@ export const useFromIntegrationSearchInteraction = (
|
|||||||
return {
|
return {
|
||||||
type: "link",
|
type: "link",
|
||||||
href: searchResult.link,
|
href: searchResult.link,
|
||||||
newTab: true,
|
newTab: openSearchInNewTab,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,10 +130,11 @@ const mediaRequestsChildrenOptions = createChildrenOptions<MediaRequestChildrenP
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
useInteraction({ result }) {
|
useInteraction({ result }) {
|
||||||
|
const { openSearchInNewTab } = useSettings();
|
||||||
return {
|
return {
|
||||||
type: "link",
|
type: "link",
|
||||||
href: result.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,
|
enabled: searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && query.length > 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const { openSearchInNewTab } = useSettings();
|
||||||
|
|
||||||
if (searchEngine.type === "generic") {
|
if (searchEngine.type === "generic") {
|
||||||
return [
|
return [
|
||||||
@@ -184,6 +189,7 @@ export const searchEnginesChildrenOptions = createChildrenOptions<SearchEngine>(
|
|||||||
useInteraction: interaction.link(({ urlTemplate }, query) => ({
|
useInteraction: interaction.link(({ urlTemplate }, query) => ({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
href: urlTemplate!.replace("%s", query),
|
href: urlTemplate!.replace("%s", query),
|
||||||
|
newTab: openSearchInNewTab,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -258,11 +264,12 @@ export const searchEnginesSearchGroups = createGroup<SearchEngine>({
|
|||||||
setChildrenOptions(searchEnginesChildrenOptions(engine));
|
setChildrenOptions(searchEnginesChildrenOptions(engine));
|
||||||
},
|
},
|
||||||
useInteraction: (searchEngine, query) => {
|
useInteraction: (searchEngine, query) => {
|
||||||
|
const { openSearchInNewTab } = useSettings();
|
||||||
if (searchEngine.type === "generic" && searchEngine.urlTemplate) {
|
if (searchEngine.type === "generic" && searchEngine.urlTemplate) {
|
||||||
return {
|
return {
|
||||||
type: "link" as const,
|
type: "link" as const,
|
||||||
href: searchEngine.urlTemplate.replace("%s", query),
|
href: searchEngine.urlTemplate.replace("%s", query),
|
||||||
newTab: true,
|
newTab: openSearchInNewTab,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { RouterOutputs } from "@homarr/api";
|
|||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import { useSession } from "@homarr/auth/client";
|
import { useSession } from "@homarr/auth/client";
|
||||||
|
import { useSettings } from "@homarr/settings";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
@@ -135,10 +136,12 @@ const createDefaultSearchEntries = (
|
|||||||
}),
|
}),
|
||||||
icon: defaultSearchEngine.iconUrl,
|
icon: defaultSearchEngine.iconUrl,
|
||||||
useInteraction(query) {
|
useInteraction(query) {
|
||||||
|
const { openSearchInNewTab } = useSettings();
|
||||||
return {
|
return {
|
||||||
type: "link",
|
type: "link",
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
href: defaultSearchEngine.urlTemplate!.replace("%s", query),
|
href: defaultSearchEngine.urlTemplate!.replace("%s", query),
|
||||||
|
newTab: openSearchInNewTab,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -151,6 +151,12 @@
|
|||||||
},
|
},
|
||||||
"pingIconsEnabled": {
|
"pingIconsEnabled": {
|
||||||
"label": "Use icons for pings"
|
"label": "Use icons for pings"
|
||||||
|
},
|
||||||
|
"defaultSearchEngine": {
|
||||||
|
"label": "Default search engine"
|
||||||
|
},
|
||||||
|
"openSearchInNewTab": {
|
||||||
|
"label": "Open search results in new tab"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -210,13 +216,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"changeDefaultSearchEngine": {
|
"changeSearchPreferences": {
|
||||||
"notification": {
|
"notification": {
|
||||||
"success": {
|
"success": {
|
||||||
"message": "Default search engine changed successfully"
|
"message": "Search preferences changed successfully"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Unable to change default search engine"
|
"message": "Unable to change search preferences"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2281,7 +2287,7 @@
|
|||||||
"mobile": "Mobile"
|
"mobile": "Mobile"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultSearchEngine": "Default search engine",
|
"search": "Search",
|
||||||
"firstDayOfWeek": "First day of the week",
|
"firstDayOfWeek": "First day of the week",
|
||||||
"accessibility": "Accessibility"
|
"accessibility": "Accessibility"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,8 +110,9 @@ const changeHomeBoardSchema = z.object({
|
|||||||
mobileHomeBoardId: z.string().nullable(),
|
mobileHomeBoardId: z.string().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeDefaultSearchEngineSchema = z.object({
|
const changeSearchPreferencesSchema = z.object({
|
||||||
defaultSearchEngineId: z.string().min(1),
|
defaultSearchEngineId: z.string().min(1).nullable(),
|
||||||
|
openInNewTab: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeColorSchemeSchema = z.object({
|
const changeColorSchemeSchema = z.object({
|
||||||
@@ -137,7 +138,7 @@ export const userSchemas = {
|
|||||||
editProfile: editProfileSchema,
|
editProfile: editProfileSchema,
|
||||||
changePassword: changePasswordSchema,
|
changePassword: changePasswordSchema,
|
||||||
changeHomeBoards: changeHomeBoardSchema,
|
changeHomeBoards: changeHomeBoardSchema,
|
||||||
changeDefaultSearchEngine: changeDefaultSearchEngineSchema,
|
changeSearchPreferences: changeSearchPreferencesSchema,
|
||||||
changePasswordApi: changePasswordApiSchema,
|
changePasswordApi: changePasswordApiSchema,
|
||||||
changeColorScheme: changeColorSchemeSchema,
|
changeColorScheme: changeColorSchemeSchema,
|
||||||
firstDayOfWeek: firstDayOfWeekSchema,
|
firstDayOfWeek: firstDayOfWeekSchema,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"@homarr/modals": "workspace:^0.1.0",
|
"@homarr/modals": "workspace:^0.1.0",
|
||||||
"@homarr/notifications": "workspace:^0.1.0",
|
"@homarr/notifications": "workspace:^0.1.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
|
"@homarr/settings": "workspace:^0.1.0",
|
||||||
"@homarr/spotlight": "workspace:^0.1.0",
|
"@homarr/spotlight": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { MantineColor } from "@mantine/core";
|
import type { MantineColor } from "@mantine/core";
|
||||||
import { Box, Tooltip } 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";
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
interface PingDotProps {
|
interface PingDotProps {
|
||||||
@@ -11,7 +11,7 @@ interface PingDotProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PingDot = ({ color, tooltip, ...props }: PingDotProps) => {
|
export const PingDot = ({ color, tooltip, ...props }: PingDotProps) => {
|
||||||
const [pingIconsEnabled] = clientApi.user.getPingIconsEnabledOrDefault.useSuspenseQuery();
|
const { pingIconsEnabled } = useSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box bottom="2.5cqmin" right="2.5cqmin" pos="absolute">
|
<Box bottom="2.5cqmin" right="2.5cqmin" pos="absolute">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import dayjs from "dayjs";
|
|||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import type { CalendarEvent } from "@homarr/integrations/types";
|
import type { CalendarEvent } from "@homarr/integrations/types";
|
||||||
|
import { useSettings } from "@homarr/settings";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
import { CalendarDay } from "./calender-day";
|
import { CalendarDay } from "./calender-day";
|
||||||
@@ -58,7 +59,7 @@ interface CalendarBaseProps {
|
|||||||
const CalendarBase = ({ isEditMode, events, month, setMonth, options }: CalendarBaseProps) => {
|
const CalendarBase = ({ isEditMode, events, month, setMonth, options }: CalendarBaseProps) => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const locale = params.locale as string;
|
const locale = params.locale as string;
|
||||||
const [firstDayOfWeek] = clientApi.user.getFirstDayOfWeekForUserOrDefault.useSuspenseQuery();
|
const { firstDayOfWeek } = useSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Calendar
|
<Calendar
|
||||||
|
|||||||
49
pnpm-lock.yaml
generated
49
pnpm-lock.yaml
generated
@@ -145,6 +145,9 @@ importers:
|
|||||||
'@homarr/server-settings':
|
'@homarr/server-settings':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../packages/server-settings
|
version: link:../../packages/server-settings
|
||||||
|
'@homarr/settings':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../../packages/settings
|
||||||
'@homarr/spotlight':
|
'@homarr/spotlight':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../packages/spotlight
|
version: link:../../packages/spotlight
|
||||||
@@ -1600,6 +1603,46 @@ importers:
|
|||||||
specifier: ^5.7.3
|
specifier: ^5.7.3
|
||||||
version: 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:
|
packages/spotlight:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@homarr/api':
|
'@homarr/api':
|
||||||
@@ -1623,6 +1666,9 @@ importers:
|
|||||||
'@homarr/modals-collection':
|
'@homarr/modals-collection':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../modals-collection
|
version: link:../modals-collection
|
||||||
|
'@homarr/settings':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../settings
|
||||||
'@homarr/translation':
|
'@homarr/translation':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../translation
|
version: link:../translation
|
||||||
@@ -1849,6 +1895,9 @@ importers:
|
|||||||
'@homarr/redis':
|
'@homarr/redis':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../redis
|
version: link:../redis
|
||||||
|
'@homarr/settings':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../settings
|
||||||
'@homarr/spotlight':
|
'@homarr/spotlight':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../spotlight
|
version: link:../spotlight
|
||||||
|
|||||||
Reference in New Issue
Block a user