diff --git a/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx b/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx index e5e867815..b5b4a5b34 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx @@ -16,7 +16,7 @@ export const CustomMantineProvider = ({ children }: PropsWithChildren) => { return ( <DirectionProvider> <MantineProvider - defaultColorScheme="auto" + defaultColorScheme="dark" colorSchemeManager={manager} theme={createTheme({ primaryColor: "red", @@ -62,6 +62,7 @@ function useColorSchemeManager(): MantineColorSchemeManager { }, set: (value) => { + if (value === "auto") return; try { if (session) { mutateColorScheme({ colorScheme: value }); diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_context.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_context.tsx index 346baff75..808759af1 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_context.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_context.tsx @@ -49,6 +49,7 @@ export const BoardProvider = ({ useEffect(() => { setReadySections((previous) => previous.filter((id) => data.sections.some((section) => section.id === id))); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [data.sections.length, setReadySections]); const markAsReady = useCallback((id: string) => { diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index d11e50b33..22888ea0b 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -77,7 +77,15 @@ export default async function Layout(props: { children: React.ReactNode; params: return ( // Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering - <html lang="en" dir={direction} data-mantine-color-scheme={colorScheme} suppressHydrationWarning> + <html + lang="en" + dir={direction} + data-mantine-color-scheme={colorScheme} + style={{ + backgroundColor: colorScheme === "dark" ? "#242424" : "#fff", + }} + suppressHydrationWarning + > <head> <Analytics /> <SearchEngineOptimization /> @@ -93,5 +101,5 @@ export default async function Layout(props: { children: React.ReactNode; params: } const getColorScheme = () => { - return cookies().get("homarr-color-scheme")?.value ?? "light"; + return cookies().get("homarr-color-scheme")?.value ?? "dark"; }; diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx index 25eacf9b9..791be2938 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx @@ -9,7 +9,7 @@ import { IconInfoCircle } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions"; -import { getAllSecretKindOptions } from "@homarr/definitions"; +import { getAllSecretKindOptions, getIntegrationName } from "@homarr/definitions"; import type { UseFormReturnType } from "@homarr/form"; import { useZodForm } from "@homarr/form"; import { convertIntegrationTestConnectionError } from "@homarr/integrations/client"; @@ -32,7 +32,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => const router = useRouter(); const form = useZodForm(validation.integration.create.omit({ kind: true }), { initialValues: { - name: searchParams.name ?? "", + name: searchParams.name ?? getIntegrationName(searchParams.kind), url: searchParams.url ?? "", secrets: secretKinds[0].map((kind) => ({ kind, @@ -81,7 +81,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => return ( <form onSubmit={form.onSubmit((value) => void handleSubmitAsync(value))}> <Stack> - <TextInput withAsterisk label={t("integration.field.name.label")} {...form.getInputProps("name")} /> + <TextInput withAsterisk label={t("integration.field.name.label")} autoFocus {...form.getInputProps("name")} /> <TextInput withAsterisk label={t("integration.field.url.label")} {...form.getInputProps("url")} /> diff --git a/apps/nextjs/src/app/[locale]/manage/tools/api/components/api-keys.tsx b/apps/nextjs/src/app/[locale]/manage/tools/api/components/api-keys.tsx index 86503427c..71a0efb1b 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/api/components/api-keys.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/api/components/api-keys.tsx @@ -47,7 +47,7 @@ export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => { ), }, ], - [], + [t], ); const table = useMantineReactTable({ diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx index 0099dcbd8..b2d5ab87e 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx @@ -61,7 +61,7 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => { id: user.id, }); }, - [user.id, mutate], + [isProviderCredentials, mutate, user.id], ); return ( diff --git a/apps/nextjs/src/components/board/items/item-move-modal.tsx b/apps/nextjs/src/components/board/items/item-move-modal.tsx index 64df4424b..b04a80195 100644 --- a/apps/nextjs/src/components/board/items/item-move-modal.tsx +++ b/apps/nextjs/src/components/board/items/item-move-modal.tsx @@ -8,7 +8,6 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { z } from "@homarr/validation"; import type { Item } from "~/app/[locale]/boards/_types"; -import { useItemActions } from "./item-actions"; interface InnerProps { gridStack: GridStack; @@ -21,7 +20,6 @@ export const ItemMoveModal = createModal<InnerProps>(({ actions, innerProps }) = const t = useI18n(); // Keep track of the maximum width based on the x offset const maxWidthRef = useRef(innerProps.columnCount - innerProps.item.xOffset); - const { moveAndResizeItem } = useItemActions(); const form = useZodForm( z.object({ xOffset: z @@ -62,7 +60,7 @@ export const ItemMoveModal = createModal<InnerProps>(({ actions, innerProps }) = }); actions.closeModal(); }, - [moveAndResizeItem], + [actions, innerProps.gridStack, innerProps.item.id], ); return ( diff --git a/apps/nextjs/src/components/board/sections/gridstack/gridstack-item.tsx b/apps/nextjs/src/components/board/sections/gridstack/gridstack-item.tsx index 554a376fa..beff12f66 100644 --- a/apps/nextjs/src/components/board/sections/gridstack/gridstack-item.tsx +++ b/apps/nextjs/src/components/board/sections/gridstack/gridstack-item.tsx @@ -39,7 +39,7 @@ export const GridStackItem = ({ if (type !== "section") return; innerRef.current.gridstackNode.minW = minWidth; innerRef.current.gridstackNode.minH = minHeight; - }, [minWidth, minHeight, innerRef]); + }, [minWidth, minHeight, innerRef, type]); return ( <Box diff --git a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts index 636065131..5c3fb207b 100644 --- a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts +++ b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts @@ -215,6 +215,7 @@ export const useGridstack = (section: Omit<Section, "items">, itemIds: string[]) } // Only run this effect when the section items change + // eslint-disable-next-line react-hooks/exhaustive-deps }, [itemIds.length, columnCount]); /** diff --git a/apps/nextjs/src/components/user-avatar-menu.tsx b/apps/nextjs/src/components/user-avatar-menu.tsx index 9a03b4200..1dafdce25 100644 --- a/apps/nextjs/src/components/user-avatar-menu.tsx +++ b/apps/nextjs/src/components/user-avatar-menu.tsx @@ -58,7 +58,7 @@ export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => { router.refresh(); }, }); - }, [openModal, router]); + }, [logoutUrl, openModal, router]); return ( <Menu width={300} withArrow withinPortal> diff --git a/package.json b/package.json index 961682993..35e5bc009 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "vite-tsconfig-paths": "^5.0.1", "vitest": "^2.1.3" }, - "packageManager": "pnpm@9.12.1", + "packageManager": "pnpm@9.12.2", "engines": { "node": ">=20.18.0" }, diff --git a/packages/auth/callbacks.ts b/packages/auth/callbacks.ts index 6a381f57a..d5fac0b7a 100644 --- a/packages/auth/callbacks.ts +++ b/packages/auth/callbacks.ts @@ -36,7 +36,7 @@ export const createSessionAsync = async ( ...user, email: user.email ?? "", permissions: await getCurrentUserPermissionsAsync(db, user.id), - colorScheme: "auto", + colorScheme: "dark", }, } as Session; }; diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts index ec92ee53b..510ddaf89 100644 --- a/packages/db/schema/mysql.ts +++ b/packages/db/schema/mysql.ts @@ -43,7 +43,7 @@ export const users = mysqlTable("user", { homeBoardId: varchar("homeBoardId", { length: 64 }).references((): AnyMySqlColumn => boards.id, { onDelete: "set null", }), - colorScheme: varchar("colorScheme", { length: 5 }).$type<ColorScheme>().default("auto").notNull(), + colorScheme: varchar("colorScheme", { length: 5 }).$type<ColorScheme>().default("dark").notNull(), firstDayOfWeek: tinyint("firstDayOfWeek").$type<DayOfWeek>().default(1).notNull(), // Defaults to Monday pingIconsEnabled: boolean("pingIconsEnabled").default(false).notNull(), }); diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index 2b593b9d5..f0d1a52e4 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -44,7 +44,7 @@ export const users = sqliteTable("user", { homeBoardId: text("homeBoardId").references((): AnySQLiteColumn => boards.id, { onDelete: "set null", }), - colorScheme: text("colorScheme").$type<ColorScheme>().default("auto").notNull(), + colorScheme: text("colorScheme").$type<ColorScheme>().default("dark").notNull(), firstDayOfWeek: int("firstDayOfWeek").$type<DayOfWeek>().default(1).notNull(), // Defaults to Monday pingIconsEnabled: int("pingIconsEnabled", { mode: "boolean" }).default(false).notNull(), }); diff --git a/packages/definitions/src/user.ts b/packages/definitions/src/user.ts index 28f24ae7b..77d8d4138 100644 --- a/packages/definitions/src/user.ts +++ b/packages/definitions/src/user.ts @@ -1,2 +1,2 @@ -export const colorSchemes = ["light", "dark", "auto"] as const; +export const colorSchemes = ["light", "dark"] as const; export type ColorScheme = (typeof colorSchemes)[number]; diff --git a/packages/modals/src/confirm-modal.tsx b/packages/modals/src/confirm-modal.tsx index 752506cb8..9f3f2b4ac 100644 --- a/packages/modals/src/confirm-modal.tsx +++ b/packages/modals/src/confirm-modal.tsx @@ -53,7 +53,7 @@ export const ConfirmModal = createModal<Omit<ConfirmModalProps, "title">>(({ act actions.closeModal(); } }, - [cancelProps?.onClick, onCancel, actions.closeModal], + [cancelProps, onCancel, closeOnCancel, actions], ); const handleConfirm = useCallback( @@ -73,7 +73,7 @@ export const ConfirmModal = createModal<Omit<ConfirmModalProps, "title">>(({ act } setLoading(false); }, - [confirmProps?.onClick, onConfirm, actions.closeModal], + [confirmProps, onConfirm, closeOnConfirm, actions], ); return ( diff --git a/packages/modals/src/index.tsx b/packages/modals/src/index.tsx index f3c9e1386..02bc4cff4 100644 --- a/packages/modals/src/index.tsx +++ b/packages/modals/src/index.tsx @@ -38,7 +38,7 @@ export const ModalProvider = ({ children }: PropsWithChildren) => { (id: string, canceled?: boolean) => { dispatch({ type: "CLOSE", modalId: id, canceled }); }, - [stateRef, dispatch], + [dispatch], ); const openModalInner: ModalContextProps["openModalInner"] = useCallback( @@ -63,10 +63,7 @@ export const ModalProvider = ({ children }: PropsWithChildren) => { [dispatch], ); - const handleCloseModal = useCallback( - () => state.current && closeModal(state.current.id), - [closeModal, state.current?.id], - ); + const handleCloseModal = useCallback(() => state.current && closeModal(state.current.id), [closeModal, state]); const activeModals = state.modals.filter((modal) => modal.id === state.current?.id || modal.props.keepMounted); diff --git a/packages/spotlight/src/components/actions/items/children-action-item.tsx b/packages/spotlight/src/components/actions/items/children-action-item.tsx index c36fb3c65..0b070b147 100644 --- a/packages/spotlight/src/components/actions/items/children-action-item.tsx +++ b/packages/spotlight/src/components/actions/items/children-action-item.tsx @@ -24,7 +24,7 @@ export const ChildrenActionItem = ({ childrenOptions, action, query }: ChildrenA return ( <Spotlight.Action renderRoot={renderRoot} onClick={onClick} className={classes.spotlightAction}> - <action.component {...childrenOptions.option} /> + <action.Component {...childrenOptions.option} /> </Spotlight.Action> ); }; diff --git a/packages/spotlight/src/components/actions/items/group-action-item.tsx b/packages/spotlight/src/components/actions/items/group-action-item.tsx index 1b2ebe1c5..6dd70c5ff 100644 --- a/packages/spotlight/src/components/actions/items/group-action-item.tsx +++ b/packages/spotlight/src/components/actions/items/group-action-item.tsx @@ -48,7 +48,7 @@ export const SpotlightGroupActionItem = <TOption extends Record<string, unknown> closeSpotlightOnTrigger={interaction.type !== "mode" && interaction.type !== "children"} className={classes.spotlightAction} > - <group.component {...option} /> + <group.Component {...option} /> </Spotlight.Action> ); }; diff --git a/packages/spotlight/src/components/spotlight.tsx b/packages/spotlight/src/components/spotlight.tsx index f84af4add..2de76db8c 100644 --- a/packages/spotlight/src/components/spotlight.tsx +++ b/packages/spotlight/src/components/spotlight.tsx @@ -92,7 +92,7 @@ export const Spotlight = () => { {childrenOptions ? ( <Group> - <childrenOptions.detailComponent options={childrenOptions.option as never} /> + <childrenOptions.DetailComponent options={childrenOptions.option as never} /> </Group> ) : null} diff --git a/packages/spotlight/src/lib/children.ts b/packages/spotlight/src/lib/children.ts index 1a792a2b6..69b3c00b1 100644 --- a/packages/spotlight/src/lib/children.ts +++ b/packages/spotlight/src/lib/children.ts @@ -3,13 +3,13 @@ import type { ReactNode } from "react"; import type { inferSearchInteractionDefinition } from "./interaction"; export interface CreateChildrenOptionsProps<TParentOptions extends Record<string, unknown>> { - detailComponent: ({ options }: { options: TParentOptions }) => ReactNode; + DetailComponent: ({ options }: { options: TParentOptions }) => ReactNode; useActions: (options: TParentOptions, query: string) => ChildrenAction<TParentOptions>[]; } export interface ChildrenAction<TParentOptions extends Record<string, unknown>> { key: string; - component: (option: TParentOptions) => JSX.Element; + Component: (option: TParentOptions) => JSX.Element; useInteraction: (option: TParentOptions, query: string) => inferSearchInteractionDefinition<"link" | "javaScript">; hide?: boolean | ((option: TParentOptions) => boolean); } diff --git a/packages/spotlight/src/lib/group.ts b/packages/spotlight/src/lib/group.ts index dda1fa7a8..900990e4a 100644 --- a/packages/spotlight/src/lib/group.ts +++ b/packages/spotlight/src/lib/group.ts @@ -8,7 +8,7 @@ type CommonSearchGroup<TOption extends Record<string, unknown>, TOptionProps ext // key path is used to define the path to a unique key in the option object keyPath: keyof TOption; title: stringOrTranslation; - component: (option: TOption) => JSX.Element; + Component: (option: TOption) => JSX.Element; useInteraction: (option: TOption, query: string) => inferSearchInteractionDefinition<SearchInteraction>; onKeyDown?: ( event: KeyboardEvent, diff --git a/packages/spotlight/src/lib/interaction.ts b/packages/spotlight/src/lib/interaction.ts index 1528a39ca..b917b3bcd 100644 --- a/packages/spotlight/src/lib/interaction.ts +++ b/packages/spotlight/src/lib/interaction.ts @@ -16,7 +16,7 @@ const searchInteractions = [ // eslint-disable-next-line @typescript-eslint/no-explicit-any useActions: CreateChildrenOptionsProps<any>["useActions"]; // eslint-disable-next-line @typescript-eslint/no-explicit-any - detailComponent: CreateChildrenOptionsProps<any>["detailComponent"]; + DetailComponent: CreateChildrenOptionsProps<any>["DetailComponent"]; // eslint-disable-next-line @typescript-eslint/no-explicit-any option: any; }>(), diff --git a/packages/spotlight/src/modes/app-integration-board/apps-search-group.tsx b/packages/spotlight/src/modes/app-integration-board/apps-search-group.tsx index 5d2dc98a6..477842ac2 100644 --- a/packages/spotlight/src/modes/app-integration-board/apps-search-group.tsx +++ b/packages/spotlight/src/modes/app-integration-board/apps-search-group.tsx @@ -16,7 +16,7 @@ const appChildrenOptions = createChildrenOptions<App>({ useActions: () => [ { key: "open", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -34,7 +34,7 @@ const appChildrenOptions = createChildrenOptions<App>({ }, { key: "edit", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -47,7 +47,7 @@ const appChildrenOptions = createChildrenOptions<App>({ useInteraction: interaction.link(({ id }) => ({ href: `/manage/apps/edit/${id}` })), }, ], - detailComponent: ({ options }) => { + DetailComponent: ({ options }) => { const t = useI18n(); return ( @@ -75,7 +75,7 @@ const appChildrenOptions = createChildrenOptions<App>({ export const appsSearchGroup = createGroup<App>({ keyPath: "id", title: (t) => t("search.mode.appIntegrationBoard.group.app.title"), - component: (app) => ( + Component: (app) => ( <Group px="md" py="sm"> <Avatar size="sm" diff --git a/packages/spotlight/src/modes/app-integration-board/boards-search-group.tsx b/packages/spotlight/src/modes/app-integration-board/boards-search-group.tsx index 089fb732c..ca201b5bc 100644 --- a/packages/spotlight/src/modes/app-integration-board/boards-search-group.tsx +++ b/packages/spotlight/src/modes/app-integration-board/boards-search-group.tsx @@ -23,7 +23,7 @@ const boardChildrenOptions = createChildrenOptions<Board>({ const actions: (ChildrenAction<Board> & { hidden?: boolean })[] = [ { key: "open", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -37,7 +37,7 @@ const boardChildrenOptions = createChildrenOptions<Board>({ }, { key: "homeBoard", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -61,7 +61,7 @@ const boardChildrenOptions = createChildrenOptions<Board>({ }, { key: "settings", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -78,7 +78,7 @@ const boardChildrenOptions = createChildrenOptions<Board>({ return actions; }, - detailComponent: ({ options: board }) => { + DetailComponent: ({ options: board }) => { const t = useI18n(); return ( @@ -102,7 +102,7 @@ const boardChildrenOptions = createChildrenOptions<Board>({ export const boardsSearchGroup = createGroup<Board>({ keyPath: "id", title: "Boards", - component: (board) => ( + Component: (board) => ( <Group px="md" py="sm"> {board.logoImageUrl ? ( <img src={board.logoImageUrl} alt={board.name} width={24} height={24} /> diff --git a/packages/spotlight/src/modes/app-integration-board/integrations-search-group.tsx b/packages/spotlight/src/modes/app-integration-board/integrations-search-group.tsx index 55fd14eca..28926a165 100644 --- a/packages/spotlight/src/modes/app-integration-board/integrations-search-group.tsx +++ b/packages/spotlight/src/modes/app-integration-board/integrations-search-group.tsx @@ -10,7 +10,7 @@ import { interaction } from "../../lib/interaction"; export const integrationsSearchGroup = createGroup<{ id: string; kind: IntegrationKind; name: string }>({ keyPath: "id", title: (t) => t("search.mode.appIntegrationBoard.group.integration.title"), - component: (integration) => ( + Component: (integration) => ( <Group px="md" py="sm"> <IntegrationAvatar size="sm" kind={integration.kind} /> diff --git a/packages/spotlight/src/modes/command/children/language.tsx b/packages/spotlight/src/modes/command/children/language.tsx index 7a248275a..848139b68 100644 --- a/packages/spotlight/src/modes/command/children/language.tsx +++ b/packages/spotlight/src/modes/command/children/language.tsx @@ -30,7 +30,7 @@ export const languageChildrenOptions = createChildrenOptions<Record<string, unkn ) .map(({ localeKey, attributes }) => ({ key: localeKey, - component() { + Component() { return ( <Group mx="md" my="sm" wrap="nowrap" justify="space-between" w="100%"> <Group wrap="nowrap"> @@ -53,7 +53,7 @@ export const languageChildrenOptions = createChildrenOptions<Record<string, unkn }, })); }, - detailComponent: () => { + DetailComponent: () => { const t = useI18n(); return ( diff --git a/packages/spotlight/src/modes/command/children/new-integration.tsx b/packages/spotlight/src/modes/command/children/new-integration.tsx index d70a719f3..aebb6ede1 100644 --- a/packages/spotlight/src/modes/command/children/new-integration.tsx +++ b/packages/spotlight/src/modes/command/children/new-integration.tsx @@ -20,7 +20,7 @@ export const newIntegrationChildrenOptions = createChildrenOptions<Record<string ) .map(([kind, integrationDef]) => ({ key: kind, - component() { + Component() { return ( <Group mx="md" my="sm" wrap="nowrap" w="100%"> <IntegrationAvatar kind={kind} size="sm" /> @@ -31,7 +31,7 @@ export const newIntegrationChildrenOptions = createChildrenOptions<Record<string useInteraction: interaction.link(() => ({ href: `/manage/integrations/new?kind=${kind}` })), })); }, - detailComponent() { + DetailComponent() { const t = useI18n(); return ( diff --git a/packages/spotlight/src/modes/command/index.tsx b/packages/spotlight/src/modes/command/index.tsx index 240fd13b5..88f208104 100644 --- a/packages/spotlight/src/modes/command/index.tsx +++ b/packages/spotlight/src/modes/command/index.tsx @@ -44,7 +44,7 @@ export const commandMode = { keyPath: "commandKey", title: "Global commands", useInteraction: (option, query) => option.useInteraction(option, query), - component: ({ icon: Icon, name }) => ( + Component: ({ icon: Icon, name }) => ( <Group px="md" py="sm"> <Icon stroke={1.5} /> <Text>{name}</Text> diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx index 4c76532e9..fd7563207 100644 --- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx +++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx @@ -15,7 +15,7 @@ export const searchEnginesChildrenOptions = createChildrenOptions<SearchEngine>( useActions: () => [ { key: "search", - component: ({ name }) => { + Component: ({ name }) => { const tChildren = useScopedI18n("search.mode.external.group.searchEngine.children"); return ( @@ -30,7 +30,7 @@ export const searchEnginesChildrenOptions = createChildrenOptions<SearchEngine>( })), }, ], - detailComponent({ options }) { + DetailComponent({ options }) { const tChildren = useScopedI18n("search.mode.external.group.searchEngine.children"); return ( <Stack mx="md" my="sm"> @@ -47,7 +47,7 @@ export const searchEnginesChildrenOptions = createChildrenOptions<SearchEngine>( export const searchEnginesSearchGroups = createGroup<SearchEngine>({ keyPath: "short", title: (t) => t("search.mode.external.group.searchEngine.title"), - component: ({ iconUrl, name, short, description }) => { + Component: ({ iconUrl, name, short, description }) => { return ( <Group w="100%" wrap="nowrap" justify="space-between" align="center" px="md" py="xs"> <Group wrap="nowrap"> diff --git a/packages/spotlight/src/modes/index.tsx b/packages/spotlight/src/modes/index.tsx index 077e8fa11..afb613e74 100644 --- a/packages/spotlight/src/modes/index.tsx +++ b/packages/spotlight/src/modes/index.tsx @@ -22,7 +22,7 @@ const helpMode = { keyPath: "character", title: (t) => t("search.mode.help.group.mode.title"), options: searchModesWithoutHelp.map(({ character, modeKey }) => ({ character, modeKey })), - component: ({ modeKey, character }) => { + Component: ({ modeKey, character }) => { const t = useScopedI18n(`search.mode.${modeKey}`); return ( @@ -59,7 +59,7 @@ const helpMode = { }, ]; }, - component: (props) => ( + Component: (props) => ( <Group px="md" py="xs" w="100%" wrap="nowrap" align="center"> <props.icon /> <Text>{props.label}</Text> diff --git a/packages/spotlight/src/modes/page/pages-search-group.tsx b/packages/spotlight/src/modes/page/pages-search-group.tsx index e0b268a12..698f6166c 100644 --- a/packages/spotlight/src/modes/page/pages-search-group.tsx +++ b/packages/spotlight/src/modes/page/pages-search-group.tsx @@ -29,7 +29,7 @@ export const pagesSearchGroup = createGroup<{ }>({ keyPath: "path", title: (t) => t("search.mode.page.group.page.title"), - component: ({ name, icon: Icon }) => ( + Component: ({ name, icon: Icon }) => ( <Group px="md" py="sm"> <Icon stroke={1.5} /> <Text>{name}</Text> diff --git a/packages/spotlight/src/modes/user-group/groups-search-group.tsx b/packages/spotlight/src/modes/user-group/groups-search-group.tsx index 507c7cb80..8ffb2a6e2 100644 --- a/packages/spotlight/src/modes/user-group/groups-search-group.tsx +++ b/packages/spotlight/src/modes/user-group/groups-search-group.tsx @@ -16,7 +16,7 @@ const groupChildrenOptions = createChildrenOptions<Group>({ useActions: () => [ { key: "detail", - component: () => { + Component: () => { const t = useI18n(); return ( <Group mx="md" my="sm"> @@ -29,7 +29,7 @@ const groupChildrenOptions = createChildrenOptions<Group>({ }, { key: "manageMember", - component: () => { + Component: () => { const t = useI18n(); return ( <Group mx="md" my="sm"> @@ -42,7 +42,7 @@ const groupChildrenOptions = createChildrenOptions<Group>({ }, { key: "managePermission", - component: () => { + Component: () => { const t = useI18n(); return ( <Group mx="md" my="sm"> @@ -54,7 +54,7 @@ const groupChildrenOptions = createChildrenOptions<Group>({ useInteraction: interaction.link(({ id }) => ({ href: `/manage/users/groups/${id}/permissions` })), }, ], - detailComponent: ({ options }) => { + DetailComponent: ({ options }) => { const t = useI18n(); return ( <Stack mx="md" my="sm"> @@ -71,7 +71,7 @@ const groupChildrenOptions = createChildrenOptions<Group>({ export const groupsSearchGroup = createGroup<Group>({ keyPath: "id", title: "Groups", - component: ({ name }) => ( + Component: ({ name }) => ( <Group px="md" py="sm"> <Text>{name}</Text> </Group> diff --git a/packages/spotlight/src/modes/user-group/users-search-group.tsx b/packages/spotlight/src/modes/user-group/users-search-group.tsx index ec750f182..b27dfbc37 100644 --- a/packages/spotlight/src/modes/user-group/users-search-group.tsx +++ b/packages/spotlight/src/modes/user-group/users-search-group.tsx @@ -17,7 +17,7 @@ const userChildrenOptions = createChildrenOptions<User>({ useActions: () => [ { key: "detail", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -30,7 +30,7 @@ const userChildrenOptions = createChildrenOptions<User>({ useInteraction: interaction.link(({ id }) => ({ href: `/manage/users/${id}/general` })), }, ], - detailComponent: ({ options }) => { + DetailComponent: ({ options }) => { const t = useI18n(); return ( @@ -49,7 +49,7 @@ const userChildrenOptions = createChildrenOptions<User>({ export const usersSearchGroup = createGroup<User>({ keyPath: "id", title: (t) => t("search.mode.userGroup.group.user.title"), - component: (user) => ( + Component: (user) => ( <Group px="md" py="sm"> <UserAvatar user={user} size="sm" /> <Text>{user.name}</Text> diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index 24a149631..8aecc2fb2 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -576,6 +576,7 @@ export default { tryAgain: "Try again", loading: "Loading", }, + here: "here", iconPicker: { label: "Icon URL", header: "Type name or objects to filter for icons... Homarr will search through {countIcons} icons for you.", @@ -1157,6 +1158,14 @@ export default { }, }, }, + integration: { + noData: "No integration found", + description: "Click {here} to create a new integration", + }, + app: { + noData: "No app found", + description: "Click {here} to create a new app", + }, error: { action: { logs: "Check logs for more details", diff --git a/packages/ui/src/components/table-pagination.tsx b/packages/ui/src/components/table-pagination.tsx index 83420ba54..e2380f821 100644 --- a/packages/ui/src/components/table-pagination.tsx +++ b/packages/ui/src/components/table-pagination.tsx @@ -34,7 +34,7 @@ export const TablePagination = ({ total }: TablePaginationProps) => { (control: ControlType) => { return getItemProps(calculatePageFor(control, current, total)); }, - [current], + [current, getItemProps, total], ); const handleChange = useCallback( @@ -43,7 +43,7 @@ export const TablePagination = ({ total }: TablePaginationProps) => { params.set("page", page.toString()); replace(`${pathName}?${params.toString()}`); }, - [pathName, searchParams], + [pathName, replace, searchParams], ); return ( diff --git a/packages/widgets/src/_inputs/widget-app-input.tsx b/packages/widgets/src/_inputs/widget-app-input.tsx index 04a85d468..ee22b08da 100644 --- a/packages/widgets/src/_inputs/widget-app-input.tsx +++ b/packages/widgets/src/_inputs/widget-app-input.tsx @@ -1,19 +1,22 @@ "use client"; import { memo, useMemo } from "react"; +import Link from "next/link"; import type { SelectProps } from "@mantine/core"; -import { Group, Loader, Select } from "@mantine/core"; +import { Anchor, Group, Loader, Select, Text } from "@mantine/core"; import { IconCheck } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; +import { useI18n } from "@homarr/translation/client"; import type { CommonWidgetInputProps } from "./common"; import { useWidgetInputTranslation } from "./common"; import { useFormContext } from "./form"; -export const WidgetAppInput = ({ property, kind, options }: CommonWidgetInputProps<"app">) => { - const t = useWidgetInputTranslation(kind, property); +export const WidgetAppInput = ({ property, kind }: CommonWidgetInputProps<"app">) => { + const t = useI18n(); + const tInput = useWidgetInputTranslation(kind, property); const form = useFormContext(); const { data: apps, isPending } = clientApi.app.selectable.useQuery(); @@ -24,10 +27,11 @@ export const WidgetAppInput = ({ property, kind, options }: CommonWidgetInputPro return ( <Select - label={t("label")} + label={tInput("label")} searchable limit={10} leftSection={<MemoizedLeftSection isPending={isPending} currentApp={currentApp} />} + nothingFoundMessage={t("widget.common.app.noData")} renderOption={renderSelectOption} data={ apps?.map((app) => ({ @@ -36,7 +40,18 @@ export const WidgetAppInput = ({ property, kind, options }: CommonWidgetInputPro iconUrl: app.iconUrl, })) ?? [] } - description={options.withDescription ? t("description") : undefined} + inputWrapperOrder={["label", "input", "description", "error"]} + description={ + <Text size="xs"> + {t("widget.common.app.description", { + here: ( + <Anchor size="xs" component={Link} target="_blank" href="/manage/apps/new"> + {t("common.here")} + </Anchor> + ), + })} + </Text> + } {...form.getInputProps(`options.${property}`)} /> ); diff --git a/packages/widgets/src/_inputs/widget-location-input.tsx b/packages/widgets/src/_inputs/widget-location-input.tsx index e264aee64..a27be85cb 100644 --- a/packages/widgets/src/_inputs/widget-location-input.tsx +++ b/packages/widgets/src/_inputs/widget-location-input.tsx @@ -46,7 +46,7 @@ export const WidgetLocationInput = ({ property, kind }: CommonWidgetInputProps<" form.clearFieldError(`options.${property}.latitude`); form.clearFieldError(`options.${property}.longitude`); }, - [handleChange], + [form, handleChange, property], ); const onSearch = useCallback(() => { diff --git a/packages/widgets/src/_inputs/widget-multi-text-input.tsx b/packages/widgets/src/_inputs/widget-multi-text-input.tsx index 2da483fac..9d1e9113f 100644 --- a/packages/widgets/src/_inputs/widget-multi-text-input.tsx +++ b/packages/widgets/src/_inputs/widget-multi-text-input.tsx @@ -39,7 +39,7 @@ export const WidgetMultiTextInput = ({ property, kind, options }: CommonWidgetIn success: validationResult.success, result: validationResult, }; - }, [search]); + }, [options.validate, search]); const error = React.useMemo(() => { /* hide the error when nothing is being typed since "" is not valid but is not an explicit error */ diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index 329565e3f..e437787ef 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -2,7 +2,7 @@ import "../widgets-common.css"; -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import type { MantineStyleProp } from "@mantine/core"; import { ActionIcon, @@ -233,7 +233,19 @@ export default function DownloadClientsWidget({ ) //flatMap already sorts by integration by nature, add sorting by integration type (usenet | torrent) .sort(({ type: typeA }, { type: typeB }) => typeA.length - typeB.length), - [currentItems, integrationIds, options], + [ + currentItems, + integrationIds, + integrationsWithInteractions, + mutateDeleteItem, + mutatePauseItem, + mutateResumeItem, + options.activeTorrentThreshold, + options.categoryFilter, + options.filterIsWhitelist, + options.showCompletedTorrent, + options.showCompletedUsenet, + ], ); //Flatten Clients Array for which each elements has the integration and general client infos. @@ -278,7 +290,14 @@ export default function DownloadClientsWidget({ ({ status: statusA }, { status: statusB }) => (statusA?.type.length ?? Infinity) - (statusB?.type.length ?? Infinity), ), - [currentItems, integrationIds, options], + [ + currentItems, + integrationIds, + integrationsWithInteractions, + options.applyFilterToRatio, + options.categoryFilter, + options.filterIsWhitelist, + ], ); //Check existing types between torrents and usenet @@ -333,37 +352,40 @@ export default function DownloadClientsWidget({ }; //Base element in common with all columns - const columnsDefBase = ({ - key, - showHeader, - align, - }: { - key: keyof ExtendedDownloadClientItem; - showHeader: boolean; - align?: "center" | "left" | "right" | "justify" | "char"; - }): MRT_ColumnDef<ExtendedDownloadClientItem> => { - const style: MantineStyleProp = { - minWidth: 0, - width: "var(--column-width)", - height: "var(--ratio-width)", - padding: "var(--space-size)", - transition: "unset", - "--key-width": columnsRatios[key], - "--column-width": "calc((var(--key-width)/var(--total-width) * 100cqw))", - }; - return { - id: key, - accessorKey: key, - header: key, - size: columnsRatios[key], - mantineTableBodyCellProps: { style, align }, - mantineTableHeadCellProps: { - style, - align: isEditMode ? "center" : align, - }, - Header: () => (showHeader && !isEditMode ? <Text fw={700}>{t(`items.${key}.columnTitle`)}</Text> : ""), - }; - }; + const columnsDefBase = useCallback( + ({ + key, + showHeader, + align, + }: { + key: keyof ExtendedDownloadClientItem; + showHeader: boolean; + align?: "center" | "left" | "right" | "justify" | "char"; + }): MRT_ColumnDef<ExtendedDownloadClientItem> => { + const style: MantineStyleProp = { + minWidth: 0, + width: "var(--column-width)", + height: "var(--ratio-width)", + padding: "var(--space-size)", + transition: "unset", + "--key-width": columnsRatios[key], + "--column-width": "calc((var(--key-width)/var(--total-width) * 100cqw))", + }; + return { + id: key, + accessorKey: key, + header: key, + size: columnsRatios[key], + mantineTableBodyCellProps: { style, align }, + mantineTableHeadCellProps: { + style, + align: isEditMode ? "center" : align, + }, + Header: () => (showHeader && !isEditMode ? <Text fw={700}>{t(`items.${key}.columnTitle`)}</Text> : ""), + }; + }, + [isEditMode, t], + ); //Make columns and cell elements, Memoized to data with deps on data and EditMode const columns = useMemo<MRT_ColumnDef<ExtendedDownloadClientItem>[]>( @@ -580,7 +602,7 @@ export default function DownloadClientsWidget({ }, }, ], - [clickedIndex, isEditMode, data, integrationIds, options], + [columnsDefBase, t, tCommon], ); //Table build and config @@ -704,10 +726,7 @@ interface ItemInfoModalProps { } const ItemInfoModal = ({ items, currentIndex, opened, onClose }: ItemInfoModalProps) => { - const item = useMemo<ExtendedDownloadClientItem | undefined>( - () => items[currentIndex], - [items, currentIndex, opened], - ); + const item = useMemo<ExtendedDownloadClientItem | undefined>(() => items[currentIndex], [items, currentIndex]); const t = useScopedI18n("widget.downloads.states"); //The use case for "No item found" should be impossible, hence no translation return ( diff --git a/packages/widgets/src/health-monitoring/component.tsx b/packages/widgets/src/health-monitoring/component.tsx index dec33ff08..9e55872aa 100644 --- a/packages/widgets/src/health-monitoring/component.tsx +++ b/packages/widgets/src/health-monitoring/component.tsx @@ -57,22 +57,19 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg throw new NoIntegrationSelectedError(); } return ( - <Box h="100%" className="health-monitoring"> + <Stack h="100%" gap="2.5cqmin" className="health-monitoring"> {healthData.map(({ integrationId, integrationName, healthInfo }) => { - const memoryUsage = formatMemoryUsage(healthInfo.memAvailable, healthInfo.memUsed); const disksData = matchFileSystemAndSmart(healthInfo.fileSystem, healthInfo.smart); - const { ref, width } = useElementSize(); - const ringSize = width * 0.95; - const ringThickness = width / 10; - const progressSize = width * 0.2; - + const memoryUsage = formatMemoryUsage(healthInfo.memAvailable, healthInfo.memUsed); return ( - <Box + <Stack + gap="2.5cqmin" key={integrationId} h="100%" className={`health-monitoring-information health-monitoring-${integrationName}`} + p="2.5cqmin" > - <Card className="health-monitoring-information-card" m="2.5cqmin" p="2.5cqmin" withBorder> + <Card className="health-monitoring-information-card" p="2.5cqmin" withBorder> <Flex className="health-monitoring-information-card-elements" h="100%" @@ -155,95 +152,17 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg </Stack> </Modal> </Box> - {options.cpu && ( - <Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu"> - <RingProgress - className="health-monitoring-cpu-utilization" - roundCaps - size={ringSize} - thickness={ringThickness} - label={ - <Center style={{ flexDirection: "column" }}> - <Text - className="health-monitoring-cpu-utilization-value" - size="3cqmin" - >{`${healthInfo.cpuUtilization.toFixed(2)}%`}</Text> - <IconCpu className="health-monitoring-cpu-utilization-icon" size="7cqmin" /> - </Center> - } - sections={[ - { - value: Number(healthInfo.cpuUtilization.toFixed(2)), - color: progressColor(Number(healthInfo.cpuUtilization.toFixed(2))), - }, - ]} - /> - </Box> - )} + {options.cpu && <CpuRing cpuUtilization={healthInfo.cpuUtilization} />} {healthInfo.cpuTemp && options.cpu && ( - <Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu-temperature"> - <RingProgress - ref={ref} - className="health-monitoring-cpu-temp" - roundCaps - size={ringSize} - thickness={ringThickness} - label={ - <Center style={{ flexDirection: "column" }}> - <Text className="health-monitoring-cpu-temp-value" size="3cqmin"> - {options.fahrenheit - ? `${(healthInfo.cpuTemp * 1.8 + 32).toFixed(1)}°F` - : `${healthInfo.cpuTemp}°C`} - </Text> - <IconCpu className="health-monitoring-cpu-temp-icon" size="7cqmin" /> - </Center> - } - sections={[ - { - value: healthInfo.cpuTemp, - color: progressColor(healthInfo.cpuTemp), - }, - ]} - /> - </Box> - )} - {options.memory && ( - <Box ref={ref} w="100%" h="100%" className="health-monitoring-memory"> - <RingProgress - className="health-monitoring-memory-use" - roundCaps - size={ringSize} - thickness={ringThickness} - label={ - <Center style={{ flexDirection: "column" }}> - <Text className="health-monitoring-memory-value" size="3cqmin"> - {memoryUsage.memUsed.GB}GiB - </Text> - <IconBrain className="health-monitoring-memory-icon" size="7cqmin" /> - </Center> - } - sections={[ - { - value: Number(memoryUsage.memUsed.percent), - color: progressColor(Number(memoryUsage.memUsed.percent)), - tooltip: `${memoryUsage.memUsed.percent}%`, - }, - ]} - /> - </Box> + <CpuTempRing fahrenheit={options.fahrenheit} cpuTemp={healthInfo.cpuTemp} /> )} + {options.memory && <MemoryRing available={healthInfo.memAvailable} used={healthInfo.memUsed} />} </Flex> </Card> {options.fileSystem && disksData.map((disk) => { return ( - <Card - className="health-monitoring-disk-card" - key={disk.deviceName} - m="2.5cqmin" - p="2.5cqmin" - withBorder - > + <Card className="health-monitoring-disk-card" key={disk.deviceName} p="2.5cqmin" withBorder> <Flex className="health-monitoring-disk-status" justify="space-between" align="center" m="1.5cqmin"> <Group gap="1cqmin"> <IconServer className="health-monitoring-disk-icon" size="5cqmin" /> @@ -266,14 +185,14 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg </Text> </Group> </Flex> - <Progress.Root className="health-monitoring-disk-use" size={progressSize}> + <Progress.Root className="health-monitoring-disk-use" h="6cqmin"> <Tooltip label={disk.used}> <Progress.Section value={disk.percentage} color={progressColor(disk.percentage)} className="health-monitoring-disk-use-percentage" > - <Progress.Label className="health-monitoring-disk-use-value"> + <Progress.Label className="health-monitoring-disk-use-value" fz="2.5cqmin"> {t("widget.healthMonitoring.popover.used")} </Progress.Label> </Progress.Section> @@ -291,7 +210,7 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg value={100 - disk.percentage} color="default" > - <Progress.Label className="health-monitoring-disk-available-value"> + <Progress.Label className="health-monitoring-disk-available-value" fz="2.5cqmin"> {t("widget.healthMonitoring.popover.diskAvailable")} </Progress.Label> </Progress.Section> @@ -300,10 +219,10 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg </Card> ); })} - </Box> + </Stack> ); })} - </Box> + </Stack> ); } @@ -349,6 +268,95 @@ export const matchFileSystemAndSmart = (fileSystems: FileSystem[], smartData: Sm }); }; +const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => { + const { width, ref } = useElementSize(); + + return ( + <Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu"> + <RingProgress + className="health-monitoring-cpu-utilization" + roundCaps + size={width * 0.95} + thickness={width / 10} + label={ + <Center style={{ flexDirection: "column" }}> + <Text + className="health-monitoring-cpu-utilization-value" + size="3cqmin" + >{`${cpuUtilization.toFixed(2)}%`}</Text> + <IconCpu className="health-monitoring-cpu-utilization-icon" size="7cqmin" /> + </Center> + } + sections={[ + { + value: Number(cpuUtilization.toFixed(2)), + color: progressColor(Number(cpuUtilization.toFixed(2))), + }, + ]} + /> + </Box> + ); +}; + +const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: number }) => { + const { width, ref } = useElementSize(); + return ( + <Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu-temperature"> + <RingProgress + className="health-monitoring-cpu-temp" + roundCaps + size={width * 0.95} + thickness={width / 10} + label={ + <Center style={{ flexDirection: "column" }}> + <Text className="health-monitoring-cpu-temp-value" size="3cqmin"> + {fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp}°C`} + </Text> + <IconCpu className="health-monitoring-cpu-temp-icon" size="7cqmin" /> + </Center> + } + sections={[ + { + value: cpuTemp, + color: progressColor(cpuTemp), + }, + ]} + /> + </Box> + ); +}; + +const MemoryRing = ({ available, used }: { available: string; used: string }) => { + const { width, ref } = useElementSize(); + const memoryUsage = formatMemoryUsage(available, used); + + return ( + <Box ref={ref} w="100%" h="100%" className="health-monitoring-memory"> + <RingProgress + className="health-monitoring-memory-use" + roundCaps + size={width * 0.95} + thickness={width / 10} + label={ + <Center style={{ flexDirection: "column" }}> + <Text className="health-monitoring-memory-value" size="3cqmin"> + {memoryUsage.memUsed.GB}GiB + </Text> + <IconBrain className="health-monitoring-memory-icon" size="7cqmin" /> + </Center> + } + sections={[ + { + value: Number(memoryUsage.memUsed.percent), + color: progressColor(Number(memoryUsage.memUsed.percent)), + tooltip: `${memoryUsage.memUsed.percent}%`, + }, + ]} + /> + </Box> + ); +}; + export const formatMemoryUsage = (memFree: string, memUsed: string) => { const memFreeBytes = Number(memFree); const memUsedBytes = Number(memUsed); diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index bc6df6b25..ce67166ba 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -46,7 +46,7 @@ export default function MediaServerWidget({ } return 0; }), - [mediaRequests, integrationIds], + [mediaRequests], ); const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation(); diff --git a/packages/widgets/src/notebook/notebook.tsx b/packages/widgets/src/notebook/notebook.tsx index 9eebb48a0..295227201 100644 --- a/packages/widgets/src/notebook/notebook.tsx +++ b/packages/widgets/src/notebook/notebook.tsx @@ -189,17 +189,31 @@ export function Notebook({ options, isEditMode, boardId, itemId }: WidgetCompone addEventListener("onReadOnlyCheck", handleOnReadOnlyCheck); - const handleEditToggleCallback = (previous: boolean) => { - const current = !previous; - if (!editor) return current; - editor.setEditable(current); + const handleContentUpdate = useCallback( + (contentUpdate: string) => { + setToSaveContent(contentUpdate); + // This is not available in preview mode + if (boardId && itemId) { + void mutateAsync({ boardId, itemId, content: contentUpdate }); + } + }, + [boardId, itemId, mutateAsync], + ); - handleContentUpdate(content); + const handleEditToggleCallback = useCallback( + (previous: boolean) => { + const current = !previous; + if (!editor) return current; + editor.setEditable(current); - return current; - }; + handleContentUpdate(content); - const handleEditCancelCallback = () => { + return current; + }, + [content, editor, handleContentUpdate], + ); + + const handleEditCancelCallback = useCallback(() => { if (!editor) return false; editor.setEditable(false); @@ -207,20 +221,12 @@ export function Notebook({ options, isEditMode, boardId, itemId }: WidgetCompone editor.commands.setContent(toSaveContent); return false; - }; + }, [editor, toSaveContent]); const handleEditCancel = useCallback(() => { setIsEditing(handleEditCancelCallback); }, [setIsEditing, handleEditCancelCallback]); - const handleContentUpdate = (contentUpdate: string) => { - setToSaveContent(contentUpdate); - // This is not available in preview mode - if (boardId && itemId) { - void mutateAsync({ boardId, itemId, content: contentUpdate }); - } - }; - const handleEditToggle = useCallback(() => { setIsEditing(handleEditToggleCallback); }, [setIsEditing, handleEditToggleCallback]); diff --git a/packages/widgets/src/options.ts b/packages/widgets/src/options.ts index abe108dfb..91d547c5c 100644 --- a/packages/widgets/src/options.ts +++ b/packages/widgets/src/options.ts @@ -104,10 +104,10 @@ const optionsFactory = { values: [] as string[], validate: input?.validate, }), - app: (input?: Omit<CommonInput<string>, "defaultValue">) => ({ + app: () => ({ type: "app" as const, defaultValue: "", - withDescription: input?.withDescription ?? false, + withDescription: false, }), }; diff --git a/packages/widgets/src/smart-home/entity-state/component.tsx b/packages/widgets/src/smart-home/entity-state/component.tsx index c7e590b71..b627e5f73 100644 --- a/packages/widgets/src/smart-home/entity-state/component.tsx +++ b/packages/widgets/src/smart-home/entity-state/component.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React, { useCallback, useState } from "react"; import { Center, Stack, Text, UnstyledButton } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; @@ -38,7 +38,7 @@ export default function SmartHomeEntityStateWidget({ const attribute = options.entityUnit.length > 0 ? " " + options.entityUnit : ""; - const handleClick = React.useCallback(() => { + const handleClick = useCallback(() => { if (isEditMode) { return; } @@ -51,7 +51,7 @@ export default function SmartHomeEntityStateWidget({ entityId: options.entityId, integrationId: integrationIds[0] ?? "", }); - }, []); + }, [integrationIds, isEditMode, mutate, options.clickable, options.entityId]); return ( <UnstyledButton diff --git a/packages/widgets/src/smart-home/execute-automation/component.tsx b/packages/widgets/src/smart-home/execute-automation/component.tsx index f05417e60..e174cdb2b 100644 --- a/packages/widgets/src/smart-home/execute-automation/component.tsx +++ b/packages/widgets/src/smart-home/execute-automation/component.tsx @@ -31,7 +31,7 @@ export default function SmartHomeTriggerAutomationWidget({ automationId: options.automationId, integrationId: integrationIds[0] ?? "", }); - }, [isEditMode]); + }, [integrationIds, isEditMode, mutateAsync, options.automationId]); return ( <UnstyledButton onClick={handleClick} style={{ cursor: !isEditMode ? "pointer" : "initial" }} w="100%" h="100%"> {isShowSuccess && ( diff --git a/packages/widgets/src/video/component.tsx b/packages/widgets/src/video/component.tsx index 8832d2390..0893e6d8d 100644 --- a/packages/widgets/src/video/component.tsx +++ b/packages/widgets/src/video/component.tsx @@ -72,7 +72,7 @@ const Feed = ({ options }: Pick<WidgetComponentProps<"video">, "options">) => { () => undefined, ); } - }, [videoRef]); + }, [options.hasAutoPlay, options.hasControls, options.isMuted, videoRef]); return ( <Group justify="center" w="100%" h="100%" pos="relative"> diff --git a/packages/widgets/src/widget-integration-select.tsx b/packages/widgets/src/widget-integration-select.tsx index 39bef4d2d..3aa27fcd6 100644 --- a/packages/widgets/src/widget-integration-select.tsx +++ b/packages/widgets/src/widget-integration-select.tsx @@ -1,7 +1,9 @@ "use client"; import type { FocusEventHandler } from "react"; +import Link from "next/link"; import { + Anchor, Avatar, CheckIcon, CloseButton, @@ -86,7 +88,23 @@ export const WidgetIntegrationSelect = ({ return ( <Combobox store={combobox} onOptionSubmit={handleValueSelect} withinPortal={false}> <Combobox.DropdownTarget> - <PillsInput pointer onClick={() => combobox.toggleDropdown()} {...props}> + <PillsInput + inputWrapperOrder={["label", "input", "description", "error"]} + description={ + <Text size="xs"> + {t("widget.common.integration.description", { + here: ( + <Anchor size="xs" component={Link} target="_blank" href="/manage/integrations"> + {t("common.here")} + </Anchor> + ), + })} + </Text> + } + pointer + onClick={() => combobox.toggleDropdown()} + {...props} + > <Pill.Group> {values.length > 0 ? values : <Input.Placeholder>{t("common.multiSelect.placeholder")}</Input.Placeholder>} @@ -108,7 +126,15 @@ export const WidgetIntegrationSelect = ({ </Combobox.DropdownTarget> <Combobox.Dropdown> - <Combobox.Options>{options}</Combobox.Options> + <Combobox.Options> + {options.length >= 1 ? ( + options + ) : ( + <Text p={4} size="sm" ta="center" c="var(--mantine-color-dimmed)"> + {t("widget.common.integration.noData")} + </Text> + )} + </Combobox.Options> </Combobox.Dropdown> </Combobox> ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afa7095dc..fec5e8b66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: version: 4.3.2(vite@5.4.5(@types/node@20.16.11)(sass@1.79.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) '@vitest/coverage-v8': specifier: ^2.1.3 - version: 2.1.3(vitest@2.1.3) + version: 2.1.3(vitest@2.1.3(@types/node@20.16.11)(@vitest/ui@2.1.3)(jsdom@25.0.1)(sass@1.79.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) '@vitest/ui': specifier: ^2.1.3 version: 2.1.3(vitest@2.1.3) @@ -1655,8 +1655,8 @@ importers: specifier: ^7.37.1 version: 7.37.1(eslint@9.12.0) eslint-plugin-react-hooks: - specifier: ^4.6.2 - version: 4.6.2(eslint@9.12.0) + specifier: ^5.0.0 + version: 5.0.0(eslint@9.12.0) typescript-eslint: specifier: ^8.9.0 version: 8.9.0(eslint@9.12.0)(typescript@5.6.3) @@ -4795,11 +4795,11 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-react-hooks@4.6.2: - resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + eslint-plugin-react-hooks@5.0.0: + resolution: {integrity: sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==} engines: {node: '>=10'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 eslint-plugin-react@7.37.1: resolution: {integrity: sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==} @@ -9967,7 +9967,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@2.1.3(vitest@2.1.3)': + '@vitest/coverage-v8@2.1.3(vitest@2.1.3(@types/node@20.16.11)(@vitest/ui@2.1.3)(jsdom@25.0.1)(sass@1.79.5)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -11361,7 +11361,7 @@ snapshots: safe-regex-test: 1.0.3 string.prototype.includes: 2.0.0 - eslint-plugin-react-hooks@4.6.2(eslint@9.12.0): + eslint-plugin-react-hooks@5.0.0(eslint@9.12.0): dependencies: eslint: 9.12.0 diff --git a/tooling/eslint/package.json b/tooling/eslint/package.json index 599d84db2..a2d50fa83 100644 --- a/tooling/eslint/package.json +++ b/tooling/eslint/package.json @@ -22,7 +22,7 @@ "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.1", - "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-hooks": "^5.0.0", "typescript-eslint": "^8.9.0" }, "devDependencies": { diff --git a/tooling/eslint/react.js b/tooling/eslint/react.js index c36e835df..abbf8f1d1 100644 --- a/tooling/eslint/react.js +++ b/tooling/eslint/react.js @@ -12,9 +12,6 @@ export default [ rules: { ...reactPlugin.configs["jsx-runtime"].rules, ...hooksPlugin.configs.recommended.rules, - // context.getSource is not a function - "react-hooks/rules-of-hooks": "off", - "react-hooks/exhaustive-deps": "off", }, languageOptions: { globals: {