From a6df910423a7151289d9ccecbb70d7cc854e7be9 Mon Sep 17 00:00:00 2001 From: Harshith Mohan <26010946+harshithmohan@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:54:54 +0530 Subject: [PATCH] Fix topnav modals, optimize renders (#979) --- .../Tags/TagsSearchAndFilterPanel.tsx | 2 +- .../Dashboard/DashboardSettingsModal.tsx | 111 ++++++++------- src/components/Input/DropdownButton.tsx | 3 +- src/components/Input/InputSmall.tsx | 4 +- src/components/Layout/TopNav.tsx | 134 +++++++++--------- src/components/ShokoIcon.tsx | 8 +- 6 files changed, 135 insertions(+), 127 deletions(-) diff --git a/src/components/Collection/Tags/TagsSearchAndFilterPanel.tsx b/src/components/Collection/Tags/TagsSearchAndFilterPanel.tsx index 88c438a29..f1cf0bed0 100644 --- a/src/components/Collection/Tags/TagsSearchAndFilterPanel.tsx +++ b/src/components/Collection/Tags/TagsSearchAndFilterPanel.tsx @@ -1,13 +1,13 @@ import React, { useMemo } from 'react'; import { mdiMagnify, mdiPlayCircleOutline } from '@mdi/js'; import Icon from '@mdi/react'; -import { useEventCallback } from 'usehooks-ts'; import Checkbox from '@/components/Input/Checkbox'; import Input from '@/components/Input/Input'; import ShokoPanel from '@/components/Panels/ShokoPanel'; import toast from '@/components/Toast'; import { useRefreshSeriesAniDBInfoMutation } from '@/core/react-query/series/mutations'; +import useEventCallback from '@/hooks/useEventCallback'; import type { TagType } from '@/core/types/api/tags'; diff --git a/src/components/Dashboard/DashboardSettingsModal.tsx b/src/components/Dashboard/DashboardSettingsModal.tsx index ad43ea1b6..76dff5c7a 100644 --- a/src/components/Dashboard/DashboardSettingsModal.tsx +++ b/src/components/Dashboard/DashboardSettingsModal.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import cx from 'classnames'; -import { cloneDeep, toNumber } from 'lodash'; +import { produce } from 'immer'; +import { toNumber } from 'lodash'; import Button from '@/components/Input/Button'; import Checkbox from '@/components/Input/Checkbox'; @@ -17,7 +18,7 @@ type Props = { show: boolean; }; -const Title = ({ onClose }: { onClose: () => void }) => { +const Title = React.memo(({ onClose }: { onClose: () => void }) => { const dispatch = useDispatch(); const handleEdit = useEventCallback(() => { @@ -37,7 +38,7 @@ const Title = ({ onClose }: { onClose: () => void }) => { ); -}; +}); const DashboardSettingsModal = ({ onClose, show }: Props) => { const dispatch = useDispatch(); @@ -71,15 +72,10 @@ const DashboardSettingsModal = ({ onClose, show }: Props) => { shokoNewsPostsCount, } = newSettings.WebUI_Settings.dashboard; - const updateSetting = (key: keyof typeof settings.WebUI_Settings.dashboard, value: boolean | number) => { - const tempSettings = cloneDeep(newSettings); - if ( - key === 'recentlyImportedEpisodesCount' || key === 'recentlyImportedSeriesCount' || key === 'shokoNewsPostsCount' - ) { - tempSettings.WebUI_Settings.dashboard[key] = value as number; - } else { - tempSettings.WebUI_Settings.dashboard[key] = value as boolean; - } + const updateSetting = (key: string, value: boolean | number) => { + const tempSettings = produce(newSettings, (draftState) => { + draftState.WebUI_Settings.dashboard[key] = value; + }); setNewSettings(tempSettings); }; @@ -94,6 +90,13 @@ const DashboardSettingsModal = ({ onClose, show }: Props) => { onClose(); }); + const handleUpdate = useEventCallback((event: React.ChangeEvent) => { + updateSetting( + event.target.id, + event.target.type === 'checkbox' ? event.target.checked : Math.min(toNumber(event.target.value), 100), + ); + }); + return ( { updateSetting('hideQueueProcessor', event.target.checked)} + onChange={handleUpdate} /> updateSetting('hideUnrecognizedFiles', event.target.checked)} + onChange={handleUpdate} /> updateSetting('hideRecentlyImported', event.target.checked)} + onChange={handleUpdate} /> updateSetting('hideCollectionStats', event.target.checked)} + onChange={handleUpdate} /> updateSetting('hideMediaType', event.target.checked)} + onChange={handleUpdate} /> updateSetting('hideImportFolders', event.target.checked)} + onChange={handleUpdate} /> updateSetting('hideShokoNews', event.target.checked)} - /> - updateSetting('hideContinueWatching', event.target.checked)} + onChange={handleUpdate} /> updateSetting('hideNextUp', event.target.checked)} + onChange={handleUpdate} /> + {!combineContinueWatching && ( + + )} updateSetting('hideUpcomingAnime', event.target.checked)} + onChange={handleUpdate} /> updateSetting('hideRecommendedAnime', event.target.checked)} + onChange={handleUpdate} /> @@ -223,46 +226,44 @@ const DashboardSettingsModal = ({ onClose, show }: Props) => { updateSetting('combineContinueWatching', event.target.checked)} + onChange={handleUpdate} /> updateSetting('hideR18Content', event.target.checked)} + onChange={handleUpdate} />
Shoko News Posts updateSetting('shokoNewsPostsCount', toNumber(event.target.value))} + onChange={handleUpdate} className="w-14 px-2 py-0.5 text-center" />
Recently Imported Episodes - updateSetting('recentlyImportedEpisodesCount', Math.min(Number(event.target.value), 100))} + value={recentlyImportedEpisodesCount} + onChange={handleUpdate} className="w-14 px-2 py-0.5 text-center" />
Recently Imported Series - updateSetting('recentlyImportedSeriesCount', Math.min(Number(event.target.value), 100))} + value={recentlyImportedSeriesCount} + onChange={handleUpdate} className="w-14 px-2 py-0.5 text-center" />
diff --git a/src/components/Input/DropdownButton.tsx b/src/components/Input/DropdownButton.tsx index b8db57140..b2a60d1be 100644 --- a/src/components/Input/DropdownButton.tsx +++ b/src/components/Input/DropdownButton.tsx @@ -3,7 +3,8 @@ import useMeasure from 'react-use-measure'; import { mdiChevronDown, mdiLoading } from '@mdi/js'; import Icon from '@mdi/react'; import cx from 'classnames'; -import { useEventCallback } from 'usehooks-ts'; + +import useEventCallback from '@/hooks/useEventCallback'; type Props = { buttonTypes?: 'primary' | 'secondary' | 'danger'; diff --git a/src/components/Input/InputSmall.tsx b/src/components/Input/InputSmall.tsx index a051335ef..6ee9fbcf7 100644 --- a/src/components/Input/InputSmall.tsx +++ b/src/components/Input/InputSmall.tsx @@ -14,7 +14,7 @@ type Props = { suffixes?: React.ReactNode; }; -function InputSmall(props: Props) { +const InputSmall = React.memo((props: Props) => { const { autoComplete, autoFocus, @@ -47,6 +47,6 @@ function InputSmall(props: Props) { {suffixes} ); -} +}); export default InputSmall; diff --git a/src/components/Layout/TopNav.tsx b/src/components/Layout/TopNav.tsx index 978d39fb4..a33d5cd1d 100644 --- a/src/components/Layout/TopNav.tsx +++ b/src/components/Layout/TopNav.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState } from 'react'; import AnimateHeight from 'react-animate-height'; import { useDispatch, useSelector } from 'react-redux'; -import { NavLink, useLocation, useNavigate } from 'react-router-dom'; +import { Link, NavLink, useLocation, useNavigate } from 'react-router-dom'; import { mdiCogOutline, mdiDownloadCircleOutline, @@ -27,7 +27,6 @@ import { Icon } from '@mdi/react'; import cx from 'classnames'; import semver from 'semver'; import { siDiscord } from 'simple-icons'; -import { useEventCallback } from 'usehooks-ts'; import DashboardSettingsModal from '@/components/Dashboard/DashboardSettingsModal'; import ActionsModal from '@/components/Dialogs/ActionsModal'; @@ -41,6 +40,7 @@ import { useCurrentUserQuery } from '@/core/react-query/user/queries'; import { useUpdateWebuiMutation } from '@/core/react-query/webui/mutations'; import { useWebuiUpdateCheckQuery } from '@/core/react-query/webui/queries'; import { NetworkAvailabilityEnum } from '@/core/signalr/types'; +import useEventCallback from '@/hooks/useEventCallback'; import AniDBBanDetectionItem from './AniDBBanDetectionItem'; @@ -48,7 +48,7 @@ import type { RootState } from '@/core/store'; const { DEV, VITE_APPVERSION } = import.meta.env; -const MenuItem = ( +const MenuItem = React.memo(( { icon, id, isHighlighted, onClick, text }: { id: string; text: string; @@ -56,23 +56,27 @@ const MenuItem = ( onClick: () => void; isHighlighted: boolean; }, -) => ( - - cx('flex items-center gap-x-3', (isActive || isHighlighted) && 'text-topnav-text-primary')} - onClick={(e) => { - e.preventDefault(); - onClick(); - }} - > - - {text} - -); +) => { + const handleClick = useEventCallback((event: React.MouseEvent) => { + event.preventDefault(); + onClick(); + }); -const LinkMenuItem = ( + return ( + + cx('flex items-center gap-x-3', (isActive || isHighlighted) && 'text-topnav-text-primary')} + onClick={handleClick} + > + + {text} + + ); +}); + +const LinkMenuItem = React.memo(( props: { icon: string, onClick: () => void, path: string, text: string }, ) => { const { icon, onClick, path, text } = props; @@ -87,9 +91,9 @@ const LinkMenuItem = ( {text} ); -}; +}); -const ExternalLinkMenuItem = ({ icon, name, url }: { url: string, name: string, icon: string }) => ( +const ExternalLinkMenuItem = React.memo(({ icon, name, url }: { url: string, name: string, icon: string }) => ( -); +)); const QueueCount = () => { const queue = useSelector((state: RootState) => state.mainpage.queueStatus); @@ -157,15 +161,15 @@ function TopNav() { [networkStatus], ); - const closeModalsAndSubmenus = (event?: React.MouseEvent) => { + const closeModalsAndSubmenus = useEventCallback((event?: React.MouseEvent, id?: string) => { if (layoutEditMode && event) { event.preventDefault(); return; } setShowActionsModal(false); setShowDashboardSettingsModal(false); - setShowUtilitiesMenu(false); - }; + if (id !== 'utilities') setShowUtilitiesMenu(false); + }); const handleLogout = useEventCallback(() => { dispatch({ type: Events.AUTH_LOGOUT }); @@ -211,12 +215,17 @@ function TopNav() { return ( <> -
+
-
+ Shoko -
+
@@ -252,42 +261,39 @@ function TopNav() { path="dashboard" text="Dashboard" /> -
- - { - closeModalsAndSubmenus(); - setShowUtilitiesMenu(prev => !prev); - }} - isHighlighted={showUtilitiesMenu} - /> - - { - closeModalsAndSubmenus(); - setShowActionsModal(true); - }} - isHighlighted={showActionsModal} - /> -
+ + + { + closeModalsAndSubmenus(undefined, 'utilities'); + setShowUtilitiesMenu(prev => !prev); + }} + isHighlighted={showUtilitiesMenu} + /> + + { + closeModalsAndSubmenus(); + setShowActionsModal(true); + }} + isHighlighted={showActionsModal} + />
{pathname === '/webui/dashboard' && ( diff --git a/src/components/ShokoIcon.tsx b/src/components/ShokoIcon.tsx index 3765c8ce2..0ab480e00 100644 --- a/src/components/ShokoIcon.tsx +++ b/src/components/ShokoIcon.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; +import cx from 'classnames'; -const SVGComponent = props => ( +const SVGComponent = React.memo(({ className }: { className?: string }) => ( ( /> -); +)); export default SVGComponent;