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]/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(({ 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(({ 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 ( , 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 ( 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>(({ act actions.closeModal(); } }, - [cancelProps?.onClick, onCancel, actions.closeModal], + [cancelProps, onCancel, closeOnCancel, actions], ); const handleConfirm = useCallback( @@ -73,7 +73,7 @@ export const ConfirmModal = createModal>(({ 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 ( - + ); }; 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 = closeSpotlightOnTrigger={interaction.type !== "mode" && interaction.type !== "children"} className={classes.spotlightAction} > - + ); }; 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 ? ( - + ) : 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> { - detailComponent: ({ options }: { options: TParentOptions }) => ReactNode; + DetailComponent: ({ options }: { options: TParentOptions }) => ReactNode; useActions: (options: TParentOptions, query: string) => ChildrenAction[]; } export interface ChildrenAction> { 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, 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; 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["useActions"]; // eslint-disable-next-line @typescript-eslint/no-explicit-any - detailComponent: CreateChildrenOptionsProps["detailComponent"]; + DetailComponent: CreateChildrenOptionsProps["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({ useActions: () => [ { key: "open", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -34,7 +34,7 @@ const appChildrenOptions = createChildrenOptions({ }, { key: "edit", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -47,7 +47,7 @@ const appChildrenOptions = createChildrenOptions({ useInteraction: interaction.link(({ id }) => ({ href: `/manage/apps/edit/${id}` })), }, ], - detailComponent: ({ options }) => { + DetailComponent: ({ options }) => { const t = useI18n(); return ( @@ -75,7 +75,7 @@ const appChildrenOptions = createChildrenOptions({ export const appsSearchGroup = createGroup({ keyPath: "id", title: (t) => t("search.mode.appIntegrationBoard.group.app.title"), - component: (app) => ( + Component: (app) => ( ({ const actions: (ChildrenAction & { hidden?: boolean })[] = [ { key: "open", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -37,7 +37,7 @@ const boardChildrenOptions = createChildrenOptions({ }, { key: "homeBoard", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -61,7 +61,7 @@ const boardChildrenOptions = createChildrenOptions({ }, { key: "settings", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -78,7 +78,7 @@ const boardChildrenOptions = createChildrenOptions({ return actions; }, - detailComponent: ({ options: board }) => { + DetailComponent: ({ options: board }) => { const t = useI18n(); return ( @@ -102,7 +102,7 @@ const boardChildrenOptions = createChildrenOptions({ export const boardsSearchGroup = createGroup({ keyPath: "id", title: "Boards", - component: (board) => ( + Component: (board) => ( {board.logoImageUrl ? ( {board.name} 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) => ( 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 ({ key: localeKey, - component() { + Component() { return ( @@ -53,7 +53,7 @@ export const languageChildrenOptions = createChildrenOptions { + 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 ({ key: kind, - component() { + Component() { return ( @@ -31,7 +31,7 @@ export const newIntegrationChildrenOptions = createChildrenOptions ({ 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 }) => ( {name} 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( 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( })), }, ], - detailComponent({ options }) { + DetailComponent({ options }) { const tChildren = useScopedI18n("search.mode.external.group.searchEngine.children"); return ( @@ -47,7 +47,7 @@ export const searchEnginesChildrenOptions = createChildrenOptions( export const searchEnginesSearchGroups = createGroup({ keyPath: "short", title: (t) => t("search.mode.external.group.searchEngine.title"), - component: ({ iconUrl, name, short, description }) => { + Component: ({ iconUrl, name, short, description }) => { return ( 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) => ( {props.label} 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 }) => ( {name} 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({ useActions: () => [ { key: "detail", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -29,7 +29,7 @@ const groupChildrenOptions = createChildrenOptions({ }, { key: "manageMember", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -42,7 +42,7 @@ const groupChildrenOptions = createChildrenOptions({ }, { key: "managePermission", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -54,7 +54,7 @@ const groupChildrenOptions = createChildrenOptions({ useInteraction: interaction.link(({ id }) => ({ href: `/manage/users/groups/${id}/permissions` })), }, ], - detailComponent: ({ options }) => { + DetailComponent: ({ options }) => { const t = useI18n(); return ( @@ -71,7 +71,7 @@ const groupChildrenOptions = createChildrenOptions({ export const groupsSearchGroup = createGroup({ keyPath: "id", title: "Groups", - component: ({ name }) => ( + Component: ({ name }) => ( {name} 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({ useActions: () => [ { key: "detail", - component: () => { + Component: () => { const t = useI18n(); return ( @@ -30,7 +30,7 @@ const userChildrenOptions = createChildrenOptions({ useInteraction: interaction.link(({ id }) => ({ href: `/manage/users/${id}/general` })), }, ], - detailComponent: ({ options }) => { + DetailComponent: ({ options }) => { const t = useI18n(); return ( @@ -49,7 +49,7 @@ const userChildrenOptions = createChildrenOptions({ export const usersSearchGroup = createGroup({ keyPath: "id", title: (t) => t("search.mode.userGroup.group.user.title"), - component: (user) => ( + Component: (user) => ( {user.name} 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-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 => { - 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 ? {t(`items.${key}.columnTitle`)} : ""), - }; - }; + const columnsDefBase = useCallback( + ({ + key, + showHeader, + align, + }: { + key: keyof ExtendedDownloadClientItem; + showHeader: boolean; + align?: "center" | "left" | "right" | "justify" | "char"; + }): MRT_ColumnDef => { + 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 ? {t(`items.${key}.columnTitle`)} : ""), + }; + }, + [isEditMode, t], + ); //Make columns and cell elements, Memoized to data with deps on data and EditMode const columns = useMemo[]>( @@ -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( - () => items[currentIndex], - [items, currentIndex, opened], - ); + const item = useMemo(() => 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 ( - + {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 ( - - + - {options.cpu && ( - - - {`${healthInfo.cpuUtilization.toFixed(2)}%`} - - - } - sections={[ - { - value: Number(healthInfo.cpuUtilization.toFixed(2)), - color: progressColor(Number(healthInfo.cpuUtilization.toFixed(2))), - }, - ]} - /> - - )} + {options.cpu && } {healthInfo.cpuTemp && options.cpu && ( - - - - {options.fahrenheit - ? `${(healthInfo.cpuTemp * 1.8 + 32).toFixed(1)}°F` - : `${healthInfo.cpuTemp}°C`} - - - - } - sections={[ - { - value: healthInfo.cpuTemp, - color: progressColor(healthInfo.cpuTemp), - }, - ]} - /> - - )} - {options.memory && ( - - - - {memoryUsage.memUsed.GB}GiB - - - - } - sections={[ - { - value: Number(memoryUsage.memUsed.percent), - color: progressColor(Number(memoryUsage.memUsed.percent)), - tooltip: `${memoryUsage.memUsed.percent}%`, - }, - ]} - /> - + )} + {options.memory && } {options.fileSystem && disksData.map((disk) => { return ( - + @@ -266,14 +185,14 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg - + - + {t("widget.healthMonitoring.popover.used")} @@ -291,7 +210,7 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg value={100 - disk.percentage} color="default" > - + {t("widget.healthMonitoring.popover.diskAvailable")} @@ -300,10 +219,10 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg ); })} - + ); })} - + ); } @@ -349,6 +268,95 @@ export const matchFileSystemAndSmart = (fileSystems: FileSystem[], smartData: Sm }); }; +const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => { + const { width, ref } = useElementSize(); + + return ( + + + {`${cpuUtilization.toFixed(2)}%`} + + + } + sections={[ + { + value: Number(cpuUtilization.toFixed(2)), + color: progressColor(Number(cpuUtilization.toFixed(2))), + }, + ]} + /> + + ); +}; + +const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: number }) => { + const { width, ref } = useElementSize(); + return ( + + + + {fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp}°C`} + + + + } + sections={[ + { + value: cpuTemp, + color: progressColor(cpuTemp), + }, + ]} + /> + + ); +}; + +const MemoryRing = ({ available, used }: { available: string; used: string }) => { + const { width, ref } = useElementSize(); + const memoryUsage = formatMemoryUsage(available, used); + + return ( + + + + {memoryUsage.memUsed.GB}GiB + + + + } + sections={[ + { + value: Number(memoryUsage.memUsed.percent), + color: progressColor(Number(memoryUsage.memUsed.percent)), + tooltip: `${memoryUsage.memUsed.percent}%`, + }, + ]} + /> + + ); +}; + 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/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 ( {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, "options">) => { () => undefined, ); } - }, [videoRef]); + }, [options.hasAutoPlay, options.hasControls, options.isMuted, videoRef]); return ( 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: {