diff --git a/ui/v2.5/src/components/List/Filters/BooleanFilter.tsx b/ui/v2.5/src/components/List/Filters/BooleanFilter.tsx index 18df1b9f1..657e9ddbd 100644 --- a/ui/v2.5/src/components/List/Filters/BooleanFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/BooleanFilter.tsx @@ -54,6 +54,7 @@ interface ISidebarFilter { option: CriterionOption; filter: ListFilterModel; setFilter: (f: ListFilterModel) => void; + sectionID?: string; } export const SidebarBooleanFilter: React.FC = ({ @@ -61,6 +62,7 @@ export const SidebarBooleanFilter: React.FC = ({ option, filter, setFilter, + sectionID, }) => { const intl = useIntl(); @@ -127,6 +129,7 @@ export const SidebarBooleanFilter: React.FC = ({ onUnselect={onUnselect} selected={selected} singleValue + sectionID={sectionID} /> ); diff --git a/ui/v2.5/src/components/List/Filters/FilterSidebar.tsx b/ui/v2.5/src/components/List/Filters/FilterSidebar.tsx index 1623c83d7..6b6503993 100644 --- a/ui/v2.5/src/components/List/Filters/FilterSidebar.tsx +++ b/ui/v2.5/src/components/List/Filters/FilterSidebar.tsx @@ -10,6 +10,8 @@ import ScreenUtils from "src/utils/screen"; import Mousetrap from "mousetrap"; import { Button } from "react-bootstrap"; +const savedFiltersSectionID = "saved-filters"; + export const FilteredSidebarHeader: React.FC<{ sidebarOpen: boolean; showEditFilter: () => void; @@ -60,6 +62,7 @@ export const FilteredSidebarHeader: React.FC<{ } + sectionID={savedFiltersSectionID} > void; filterHook?: (f: ListFilterModel) => ListFilterModel; -}> = ({ title, option, filter, setFilter, filterHook }) => { + sectionID?: string; +}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => { const state = useLabeledIdFilterState({ filter, setFilter, @@ -119,7 +120,7 @@ export const SidebarPerformersFilter: React.FC<{ useQuery: usePerformerQueryFilter, }); - return ; + return ; }; export default PerformersFilter; diff --git a/ui/v2.5/src/components/List/Filters/RatingFilter.tsx b/ui/v2.5/src/components/List/Filters/RatingFilter.tsx index 86d6a905b..1a0f14452 100644 --- a/ui/v2.5/src/components/List/Filters/RatingFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/RatingFilter.tsx @@ -77,6 +77,7 @@ interface ISidebarFilter { option: CriterionOption; filter: ListFilterModel; setFilter: (f: ListFilterModel) => void; + sectionID?: string; } const any = "any"; @@ -87,6 +88,7 @@ export const SidebarRatingFilter: React.FC = ({ option, filter, setFilter, + sectionID, }) => { const intl = useIntl(); @@ -199,6 +201,7 @@ export const SidebarRatingFilter: React.FC = ({ singleValue preCandidates={ratingValue === null ? ratingStars : undefined} preSelected={ratingValue !== null ? ratingStars : undefined} + sectionID={sectionID} />
diff --git a/ui/v2.5/src/components/List/Filters/SidebarListFilter.tsx b/ui/v2.5/src/components/List/Filters/SidebarListFilter.tsx index 71a56f23d..6754787db 100644 --- a/ui/v2.5/src/components/List/Filters/SidebarListFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/SidebarListFilter.tsx @@ -275,7 +275,8 @@ export const SidebarListFilter: React.FC<{ postSelected?: React.ReactNode; preCandidates?: React.ReactNode; postCandidates?: React.ReactNode; - onOpen?: () => void; + // used to store open/closed state in SidebarStateContext + sectionID?: string; }> = ({ title, selected, @@ -291,7 +292,7 @@ export const SidebarListFilter: React.FC<{ postCandidates, preSelected, postSelected, - onOpen, + sectionID, }) => { // TODO - sort items? @@ -325,6 +326,7 @@ export const SidebarListFilter: React.FC<{ {preSelected ?
{preSelected}
: null} @@ -342,7 +344,6 @@ export const SidebarListFilter: React.FC<{ {postSelected ?
{postSelected}
: null} } - onOpen={onOpen} > {preCandidates ?
{preCandidates}
: null} void; filterHook?: (f: ListFilterModel) => ListFilterModel; -}> = ({ title, option, filter, setFilter, filterHook }) => { + sectionID?: string; +}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => { const state = useLabeledIdFilterState({ filter, setFilter, @@ -110,7 +111,7 @@ export const SidebarStudiosFilter: React.FC<{ includeSubMessageID: "subsidiary_studios", }); - return ; + return ; }; export default StudiosFilter; diff --git a/ui/v2.5/src/components/List/Filters/TagsFilter.tsx b/ui/v2.5/src/components/List/Filters/TagsFilter.tsx index e8e25db56..f4c618ffa 100644 --- a/ui/v2.5/src/components/List/Filters/TagsFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/TagsFilter.tsx @@ -103,7 +103,8 @@ export const SidebarTagsFilter: React.FC<{ filter: ListFilterModel; setFilter: (f: ListFilterModel) => void; filterHook?: (f: ListFilterModel) => ListFilterModel; -}> = ({ title, option, filter, setFilter, filterHook }) => { + sectionID?: string; +}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => { const state = useLabeledIdFilterState({ filter, setFilter, @@ -114,7 +115,7 @@ export const SidebarTagsFilter: React.FC<{ includeSubMessageID: "sub_tags", }); - return ; + return ; }; export default TagsFilter; diff --git a/ui/v2.5/src/components/Scenes/SceneList.tsx b/ui/v2.5/src/components/Scenes/SceneList.tsx index f9257c9ad..3936be6f2 100644 --- a/ui/v2.5/src/components/Scenes/SceneList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneList.tsx @@ -41,6 +41,7 @@ import { Sidebar, SidebarPane, SidebarPaneContent, + SidebarStateContext, useSidebarState, } from "../Shared/Sidebar"; import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter"; @@ -290,6 +291,7 @@ const SidebarContent: React.FC<{ filter={filter} setFilter={setFilter} filterHook={filterHook} + sectionID="studios" /> )} } @@ -307,6 +310,7 @@ const SidebarContent: React.FC<{ filter={filter} setFilter={setFilter} filterHook={filterHook} + sectionID="tags" /> } @@ -314,6 +318,7 @@ const SidebarContent: React.FC<{ option={RatingCriterionOption} filter={filter} setFilter={setFilter} + sectionID="rating" /> } @@ -321,6 +326,7 @@ const SidebarContent: React.FC<{ option={OrganizedCriterionOption} filter={filter} setFilter={setFilter} + sectionID="organized" /> @@ -447,6 +453,8 @@ export const FilteredSceneList = (props: IFilteredScenes) => { showSidebar, setShowSidebar, loading: sidebarStateLoading, + sectionOpen, + setSectionOpen, } = useSidebarState(view); const { filterState, queryResult, modalState, listSelect, showEditFilter } = @@ -695,86 +703,90 @@ export const FilteredSceneList = (props: IFilteredScenes) => { > {modal} - - setShowSidebar(false)}> - setShowSidebar(false)} - count={cachedResult.loading ? undefined : totalCount} - focus={searchFocus} - /> - - - setShowSidebar(!showSidebar)} - onEditCriterion={(c) => - showEditFilter(c?.criterionOption.type) - } - onRemoveCriterion={removeCriterion} - onRemoveAllCriterion={() => clearAllCriteria(true)} - onEditSearchTerm={() => { - setShowSidebar(true); - setSearchFocus(true); - }} - onRemoveSearchTerm={() => setFilter(filter.clearSearchTerm())} - view={view} - /> - } - selectionSection={ - setShowSidebar(!showSidebar)} - onSelectAll={() => onSelectAll()} - onSelectNone={() => onSelectNone()} - operations={operations} - /> - } - operationSection={operations} - /> - - setFilter(newFilter)} - /> - - - + + setShowSidebar(false)}> + setShowSidebar(false)} + count={cachedResult.loading ? undefined : totalCount} + focus={searchFocus} + /> + + + setShowSidebar(!showSidebar)} + onEditCriterion={(c) => + showEditFilter(c?.criterionOption.type) + } + onRemoveCriterion={removeCriterion} + onRemoveAllCriterion={() => clearAllCriteria(true)} + onEditSearchTerm={() => { + setShowSidebar(true); + setSearchFocus(true); + }} + onRemoveSearchTerm={() => + setFilter(filter.clearSearchTerm()) + } + view={view} + /> + } + selectionSection={ + setShowSidebar(!showSidebar)} + onSelectAll={() => onSelectAll()} + onSelectNone={() => onSelectNone()} + operations={operations} + /> + } + operationSection={operations} /> - - {totalCount > filter.itemsPerPage && ( -
- setFilter(newFilter)} + /> + + + -
- )} -
-
+ + + {totalCount > filter.itemsPerPage && ( +
+ +
+ )} + + + ); diff --git a/ui/v2.5/src/components/Shared/CollapseButton.tsx b/ui/v2.5/src/components/Shared/CollapseButton.tsx index d0b192162..0d05f6e64 100644 --- a/ui/v2.5/src/components/Shared/CollapseButton.tsx +++ b/ui/v2.5/src/components/Shared/CollapseButton.tsx @@ -3,7 +3,7 @@ import { faChevronRight, faChevronUp, } from "@fortawesome/free-solid-svg-icons"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Button, Collapse, CollapseProps } from "react-bootstrap"; import { Icon } from "./Icon"; @@ -12,22 +12,27 @@ interface IProps { text: React.ReactNode; collapseProps?: Partial; outsideCollapse?: React.ReactNode; - onOpen?: () => void; + onOpenChanged?: (o: boolean) => void; + open?: boolean; } export const CollapseButton: React.FC> = ( props: React.PropsWithChildren ) => { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(props.open ?? false); function toggleOpen() { const nv = !open; setOpen(nv); - if (props.onOpen && nv) { - props.onOpen(); - } + props.onOpenChanged?.(nv); } + useEffect(() => { + if (props.open !== undefined) { + setOpen(props.open); + } + }, [props.open]); + return (
diff --git a/ui/v2.5/src/components/Shared/Sidebar.tsx b/ui/v2.5/src/components/Shared/Sidebar.tsx index 2fe0c48af..0130fe85f 100644 --- a/ui/v2.5/src/components/Shared/Sidebar.tsx +++ b/ui/v2.5/src/components/Shared/Sidebar.tsx @@ -15,6 +15,9 @@ import { Button, CollapseProps } from "react-bootstrap"; import { useIntl } from "react-intl"; import { Icon } from "./Icon"; import { faSliders } from "@fortawesome/free-solid-svg-icons"; +import { useHistory } from "react-router-dom"; + +export type SidebarSectionStates = Record; // this needs to correspond to the CSS media query that overlaps the sidebar over content const fixedSidebarMediaQuery = "only screen and (max-width: 767px)"; @@ -61,14 +64,35 @@ export const SidebarPaneContent: React.FC = ({ children }) => { return
{children}
; }; +interface IContext { + sectionOpen: SidebarSectionStates; + setSectionOpen: (section: string, open: boolean) => void; +} + +export const SidebarStateContext = React.createContext(null); + export const SidebarSection: React.FC< PropsWithChildren<{ text: React.ReactNode; className?: string; outsideCollapse?: React.ReactNode; - onOpen?: () => void; + // used to store open/closed state in SidebarStateContext + sectionID?: string; }> -> = ({ className = "", text, outsideCollapse, onOpen, children }) => { +> = ({ className = "", text, outsideCollapse, sectionID = "", children }) => { + // this is optional + const contextState = React.useContext(SidebarStateContext); + const openState = + !contextState || !sectionID + ? undefined + : contextState.sectionOpen[sectionID] ?? undefined; + + function onOpenInternal(open: boolean) { + if (contextState && sectionID) { + contextState.setSectionOpen(sectionID, open); + } + } + const collapseProps: Partial = { mountOnEnter: true, unmountOnExit: true, @@ -79,7 +103,8 @@ export const SidebarSection: React.FC< collapseProps={collapseProps} text={text} outsideCollapse={outsideCollapse} - onOpen={onOpen} + onOpenChanged={onOpenInternal} + open={openState} > {children} @@ -110,6 +135,7 @@ export function defaultShowSidebar() { export function useSidebarState(view?: View) { const [interfaceLocalForage, setInterfaceLocalForage] = useInterfaceLocalForage(); + const history = useHistory(); const { data: interfaceLocalForageData, loading } = interfaceLocalForage; @@ -118,6 +144,7 @@ export function useSidebarState(view?: View) { }, [view, interfaceLocalForageData]); const [showSidebar, setShowSidebar] = useState(); + const [sectionOpen, setSectionOpen] = useState(); // set initial state once loading is done useEffect(() => { @@ -132,7 +159,17 @@ export function useSidebarState(view?: View) { // only show sidebar by default on large screens setShowSidebar(!!viewConfig.showSidebar && defaultShowSidebar()); - }, [view, loading, showSidebar, viewConfig.showSidebar]); + setSectionOpen( + (history.location.state as { sectionOpen?: SidebarSectionStates }) + ?.sectionOpen || {} + ); + }, [ + view, + loading, + showSidebar, + viewConfig.showSidebar, + history.location.state, + ]); const onSetShowSidebar = useCallback( (show: boolean | ((prevState: boolean | undefined) => boolean)) => { @@ -154,9 +191,28 @@ export function useSidebarState(view?: View) { [showSidebar, setInterfaceLocalForage, view, viewConfig] ); + const onSetSectionOpen = useCallback( + (section: string, open: boolean) => { + const newSectionOpen = { ...sectionOpen, [section]: open }; + setSectionOpen(newSectionOpen); + if (view === undefined) return; + + history.replace({ + ...history.location, + state: { + ...(history.location.state as {}), + sectionOpen: newSectionOpen, + }, + }); + }, + [sectionOpen, view, history] + ); + return { showSidebar: showSidebar ?? defaultShowSidebar(), + sectionOpen: sectionOpen || {}, setShowSidebar: onSetShowSidebar, + setSectionOpen: onSetSectionOpen, loading: showSidebar === undefined, }; }