mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 08:20:56 +01:00
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/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",
|
||||
|
||||
@@ -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} />,
|
||||
|
||||
@@ -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>;
|
||||
@@ -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">
|
||||
|
||||
@@ -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())
|
||||
|
||||
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,
|
||||
"tag": "0020_salty_doorman",
|
||||
"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,
|
||||
"tag": "0020_empty_hellfire_club",
|
||||
"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, {
|
||||
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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
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/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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
49
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user