mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Save sidebar state (#6217)
* Save sidebar section open state in browser history state This means that state is saved when going back, but not when navigating to the scenes page from elsewhere.
This commit is contained in:
parent
299e1ac1f9
commit
336fa3b70e
10 changed files with 182 additions and 96 deletions
|
|
@ -54,6 +54,7 @@ interface ISidebarFilter {
|
||||||
option: CriterionOption;
|
option: CriterionOption;
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
setFilter: (f: ListFilterModel) => void;
|
setFilter: (f: ListFilterModel) => void;
|
||||||
|
sectionID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarBooleanFilter: React.FC<ISidebarFilter> = ({
|
export const SidebarBooleanFilter: React.FC<ISidebarFilter> = ({
|
||||||
|
|
@ -61,6 +62,7 @@ export const SidebarBooleanFilter: React.FC<ISidebarFilter> = ({
|
||||||
option,
|
option,
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
sectionID,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
|
@ -127,6 +129,7 @@ export const SidebarBooleanFilter: React.FC<ISidebarFilter> = ({
|
||||||
onUnselect={onUnselect}
|
onUnselect={onUnselect}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
singleValue
|
singleValue
|
||||||
|
sectionID={sectionID}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import ScreenUtils from "src/utils/screen";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
|
|
||||||
|
const savedFiltersSectionID = "saved-filters";
|
||||||
|
|
||||||
export const FilteredSidebarHeader: React.FC<{
|
export const FilteredSidebarHeader: React.FC<{
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
showEditFilter: () => void;
|
showEditFilter: () => void;
|
||||||
|
|
@ -60,6 +62,7 @@ export const FilteredSidebarHeader: React.FC<{
|
||||||
<SidebarSection
|
<SidebarSection
|
||||||
className="sidebar-saved-filters"
|
className="sidebar-saved-filters"
|
||||||
text={<FormattedMessage id="search_filter.saved_filters" />}
|
text={<FormattedMessage id="search_filter.saved_filters" />}
|
||||||
|
sectionID={savedFiltersSectionID}
|
||||||
>
|
>
|
||||||
<SidebarSavedFilterList
|
<SidebarSavedFilterList
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,8 @@ export const SidebarPerformersFilter: React.FC<{
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
setFilter: (f: ListFilterModel) => void;
|
setFilter: (f: ListFilterModel) => void;
|
||||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||||
}> = ({ title, option, filter, setFilter, filterHook }) => {
|
sectionID?: string;
|
||||||
|
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||||
const state = useLabeledIdFilterState({
|
const state = useLabeledIdFilterState({
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
|
@ -119,7 +120,7 @@ export const SidebarPerformersFilter: React.FC<{
|
||||||
useQuery: usePerformerQueryFilter,
|
useQuery: usePerformerQueryFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <SidebarListFilter {...state} title={title} />;
|
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PerformersFilter;
|
export default PerformersFilter;
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ interface ISidebarFilter {
|
||||||
option: CriterionOption;
|
option: CriterionOption;
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
setFilter: (f: ListFilterModel) => void;
|
setFilter: (f: ListFilterModel) => void;
|
||||||
|
sectionID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const any = "any";
|
const any = "any";
|
||||||
|
|
@ -87,6 +88,7 @@ export const SidebarRatingFilter: React.FC<ISidebarFilter> = ({
|
||||||
option,
|
option,
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
sectionID,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
|
@ -199,6 +201,7 @@ export const SidebarRatingFilter: React.FC<ISidebarFilter> = ({
|
||||||
singleValue
|
singleValue
|
||||||
preCandidates={ratingValue === null ? ratingStars : undefined}
|
preCandidates={ratingValue === null ? ratingStars : undefined}
|
||||||
preSelected={ratingValue !== null ? ratingStars : undefined}
|
preSelected={ratingValue !== null ? ratingStars : undefined}
|
||||||
|
sectionID={sectionID}
|
||||||
/>
|
/>
|
||||||
<div></div>
|
<div></div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,8 @@ export const SidebarListFilter: React.FC<{
|
||||||
postSelected?: React.ReactNode;
|
postSelected?: React.ReactNode;
|
||||||
preCandidates?: React.ReactNode;
|
preCandidates?: React.ReactNode;
|
||||||
postCandidates?: React.ReactNode;
|
postCandidates?: React.ReactNode;
|
||||||
onOpen?: () => void;
|
// used to store open/closed state in SidebarStateContext
|
||||||
|
sectionID?: string;
|
||||||
}> = ({
|
}> = ({
|
||||||
title,
|
title,
|
||||||
selected,
|
selected,
|
||||||
|
|
@ -291,7 +292,7 @@ export const SidebarListFilter: React.FC<{
|
||||||
postCandidates,
|
postCandidates,
|
||||||
preSelected,
|
preSelected,
|
||||||
postSelected,
|
postSelected,
|
||||||
onOpen,
|
sectionID,
|
||||||
}) => {
|
}) => {
|
||||||
// TODO - sort items?
|
// TODO - sort items?
|
||||||
|
|
||||||
|
|
@ -325,6 +326,7 @@ export const SidebarListFilter: React.FC<{
|
||||||
<SidebarSection
|
<SidebarSection
|
||||||
className="sidebar-list-filter"
|
className="sidebar-list-filter"
|
||||||
text={title}
|
text={title}
|
||||||
|
sectionID={sectionID}
|
||||||
outsideCollapse={
|
outsideCollapse={
|
||||||
<>
|
<>
|
||||||
{preSelected ? <div className="extra">{preSelected}</div> : null}
|
{preSelected ? <div className="extra">{preSelected}</div> : null}
|
||||||
|
|
@ -342,7 +344,6 @@ export const SidebarListFilter: React.FC<{
|
||||||
{postSelected ? <div className="extra">{postSelected}</div> : null}
|
{postSelected ? <div className="extra">{postSelected}</div> : null}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
onOpen={onOpen}
|
|
||||||
>
|
>
|
||||||
{preCandidates ? <div className="extra">{preCandidates}</div> : null}
|
{preCandidates ? <div className="extra">{preCandidates}</div> : null}
|
||||||
<CandidateList
|
<CandidateList
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,8 @@ export const SidebarStudiosFilter: React.FC<{
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
setFilter: (f: ListFilterModel) => void;
|
setFilter: (f: ListFilterModel) => void;
|
||||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||||
}> = ({ title, option, filter, setFilter, filterHook }) => {
|
sectionID?: string;
|
||||||
|
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||||
const state = useLabeledIdFilterState({
|
const state = useLabeledIdFilterState({
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
|
@ -110,7 +111,7 @@ export const SidebarStudiosFilter: React.FC<{
|
||||||
includeSubMessageID: "subsidiary_studios",
|
includeSubMessageID: "subsidiary_studios",
|
||||||
});
|
});
|
||||||
|
|
||||||
return <SidebarListFilter {...state} title={title} />;
|
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StudiosFilter;
|
export default StudiosFilter;
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,8 @@ export const SidebarTagsFilter: React.FC<{
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
setFilter: (f: ListFilterModel) => void;
|
setFilter: (f: ListFilterModel) => void;
|
||||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||||
}> = ({ title, option, filter, setFilter, filterHook }) => {
|
sectionID?: string;
|
||||||
|
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||||
const state = useLabeledIdFilterState({
|
const state = useLabeledIdFilterState({
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
|
@ -114,7 +115,7 @@ export const SidebarTagsFilter: React.FC<{
|
||||||
includeSubMessageID: "sub_tags",
|
includeSubMessageID: "sub_tags",
|
||||||
});
|
});
|
||||||
|
|
||||||
return <SidebarListFilter {...state} title={title} />;
|
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagsFilter;
|
export default TagsFilter;
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarPane,
|
SidebarPane,
|
||||||
SidebarPaneContent,
|
SidebarPaneContent,
|
||||||
|
SidebarStateContext,
|
||||||
useSidebarState,
|
useSidebarState,
|
||||||
} from "../Shared/Sidebar";
|
} from "../Shared/Sidebar";
|
||||||
import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter";
|
import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter";
|
||||||
|
|
@ -290,6 +291,7 @@ const SidebarContent: React.FC<{
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
|
sectionID="studios"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SidebarPerformersFilter
|
<SidebarPerformersFilter
|
||||||
|
|
@ -299,6 +301,7 @@ const SidebarContent: React.FC<{
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
|
sectionID="performers"
|
||||||
/>
|
/>
|
||||||
<SidebarTagsFilter
|
<SidebarTagsFilter
|
||||||
title={<FormattedMessage id="tags" />}
|
title={<FormattedMessage id="tags" />}
|
||||||
|
|
@ -307,6 +310,7 @@ const SidebarContent: React.FC<{
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
|
sectionID="tags"
|
||||||
/>
|
/>
|
||||||
<SidebarRatingFilter
|
<SidebarRatingFilter
|
||||||
title={<FormattedMessage id="rating" />}
|
title={<FormattedMessage id="rating" />}
|
||||||
|
|
@ -314,6 +318,7 @@ const SidebarContent: React.FC<{
|
||||||
option={RatingCriterionOption}
|
option={RatingCriterionOption}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
|
sectionID="rating"
|
||||||
/>
|
/>
|
||||||
<SidebarBooleanFilter
|
<SidebarBooleanFilter
|
||||||
title={<FormattedMessage id="organized" />}
|
title={<FormattedMessage id="organized" />}
|
||||||
|
|
@ -321,6 +326,7 @@ const SidebarContent: React.FC<{
|
||||||
option={OrganizedCriterionOption}
|
option={OrganizedCriterionOption}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
|
sectionID="organized"
|
||||||
/>
|
/>
|
||||||
</ScenesFilterSidebarSections>
|
</ScenesFilterSidebarSections>
|
||||||
|
|
||||||
|
|
@ -447,6 +453,8 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
||||||
showSidebar,
|
showSidebar,
|
||||||
setShowSidebar,
|
setShowSidebar,
|
||||||
loading: sidebarStateLoading,
|
loading: sidebarStateLoading,
|
||||||
|
sectionOpen,
|
||||||
|
setSectionOpen,
|
||||||
} = useSidebarState(view);
|
} = useSidebarState(view);
|
||||||
|
|
||||||
const { filterState, queryResult, modalState, listSelect, showEditFilter } =
|
const { filterState, queryResult, modalState, listSelect, showEditFilter } =
|
||||||
|
|
@ -695,86 +703,90 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
||||||
>
|
>
|
||||||
{modal}
|
{modal}
|
||||||
|
|
||||||
<SidebarPane hideSidebar={!showSidebar}>
|
<SidebarStateContext.Provider value={{ sectionOpen, setSectionOpen }}>
|
||||||
<Sidebar hide={!showSidebar} onHide={() => setShowSidebar(false)}>
|
<SidebarPane hideSidebar={!showSidebar}>
|
||||||
<SidebarContent
|
<Sidebar hide={!showSidebar} onHide={() => setShowSidebar(false)}>
|
||||||
filter={filter}
|
<SidebarContent
|
||||||
setFilter={setFilter}
|
filter={filter}
|
||||||
filterHook={filterHook}
|
setFilter={setFilter}
|
||||||
showEditFilter={showEditFilter}
|
filterHook={filterHook}
|
||||||
view={view}
|
showEditFilter={showEditFilter}
|
||||||
sidebarOpen={showSidebar}
|
view={view}
|
||||||
onClose={() => setShowSidebar(false)}
|
sidebarOpen={showSidebar}
|
||||||
count={cachedResult.loading ? undefined : totalCount}
|
onClose={() => setShowSidebar(false)}
|
||||||
focus={searchFocus}
|
count={cachedResult.loading ? undefined : totalCount}
|
||||||
/>
|
focus={searchFocus}
|
||||||
</Sidebar>
|
/>
|
||||||
<SidebarPaneContent>
|
</Sidebar>
|
||||||
<FilteredListToolbar2
|
<SidebarPaneContent>
|
||||||
className="scene-list-toolbar"
|
<FilteredListToolbar2
|
||||||
hasSelection={hasSelection}
|
className="scene-list-toolbar"
|
||||||
filterSection={
|
hasSelection={hasSelection}
|
||||||
<ToolbarFilterSection
|
filterSection={
|
||||||
filter={filter}
|
<ToolbarFilterSection
|
||||||
onSetFilter={setFilter}
|
filter={filter}
|
||||||
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
onSetFilter={setFilter}
|
||||||
onEditCriterion={(c) =>
|
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
||||||
showEditFilter(c?.criterionOption.type)
|
onEditCriterion={(c) =>
|
||||||
}
|
showEditFilter(c?.criterionOption.type)
|
||||||
onRemoveCriterion={removeCriterion}
|
}
|
||||||
onRemoveAllCriterion={() => clearAllCriteria(true)}
|
onRemoveCriterion={removeCriterion}
|
||||||
onEditSearchTerm={() => {
|
onRemoveAllCriterion={() => clearAllCriteria(true)}
|
||||||
setShowSidebar(true);
|
onEditSearchTerm={() => {
|
||||||
setSearchFocus(true);
|
setShowSidebar(true);
|
||||||
}}
|
setSearchFocus(true);
|
||||||
onRemoveSearchTerm={() => setFilter(filter.clearSearchTerm())}
|
}}
|
||||||
view={view}
|
onRemoveSearchTerm={() =>
|
||||||
/>
|
setFilter(filter.clearSearchTerm())
|
||||||
}
|
}
|
||||||
selectionSection={
|
view={view}
|
||||||
<ToolbarSelectionSection
|
/>
|
||||||
selected={selectedIds.size}
|
}
|
||||||
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
selectionSection={
|
||||||
onSelectAll={() => onSelectAll()}
|
<ToolbarSelectionSection
|
||||||
onSelectNone={() => onSelectNone()}
|
selected={selectedIds.size}
|
||||||
operations={operations}
|
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
||||||
/>
|
onSelectAll={() => onSelectAll()}
|
||||||
}
|
onSelectNone={() => onSelectNone()}
|
||||||
operationSection={operations}
|
operations={operations}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
<ListResultsHeader
|
operationSection={operations}
|
||||||
loading={cachedResult.loading}
|
|
||||||
filter={filter}
|
|
||||||
totalCount={totalCount}
|
|
||||||
metadataByline={metadataByline}
|
|
||||||
onChangeFilter={(newFilter) => setFilter(newFilter)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LoadedContent loading={result.loading} error={result.error}>
|
|
||||||
<SceneList
|
|
||||||
filter={effectiveFilter}
|
|
||||||
scenes={items}
|
|
||||||
selectedIds={selectedIds}
|
|
||||||
onSelectChange={onSelectChange}
|
|
||||||
fromGroupId={fromGroupId}
|
|
||||||
/>
|
/>
|
||||||
</LoadedContent>
|
|
||||||
|
|
||||||
{totalCount > filter.itemsPerPage && (
|
<ListResultsHeader
|
||||||
<div className="pagination-footer">
|
loading={cachedResult.loading}
|
||||||
<Pagination
|
filter={filter}
|
||||||
itemsPerPage={filter.itemsPerPage}
|
totalCount={totalCount}
|
||||||
currentPage={filter.currentPage}
|
metadataByline={metadataByline}
|
||||||
totalItems={totalCount}
|
onChangeFilter={(newFilter) => setFilter(newFilter)}
|
||||||
metadataByline={metadataByline}
|
/>
|
||||||
onChangePage={setPage}
|
|
||||||
pagePopupPlacement="top"
|
<LoadedContent loading={result.loading} error={result.error}>
|
||||||
|
<SceneList
|
||||||
|
filter={effectiveFilter}
|
||||||
|
scenes={items}
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
onSelectChange={onSelectChange}
|
||||||
|
fromGroupId={fromGroupId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</LoadedContent>
|
||||||
)}
|
|
||||||
</SidebarPaneContent>
|
{totalCount > filter.itemsPerPage && (
|
||||||
</SidebarPane>
|
<div className="pagination-footer">
|
||||||
|
<Pagination
|
||||||
|
itemsPerPage={filter.itemsPerPage}
|
||||||
|
currentPage={filter.currentPage}
|
||||||
|
totalItems={totalCount}
|
||||||
|
metadataByline={metadataByline}
|
||||||
|
onChangePage={setPage}
|
||||||
|
pagePopupPlacement="top"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SidebarPaneContent>
|
||||||
|
</SidebarPane>
|
||||||
|
</SidebarStateContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
</TaggerContext>
|
</TaggerContext>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import {
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faChevronUp,
|
faChevronUp,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} 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 { Button, Collapse, CollapseProps } from "react-bootstrap";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
|
|
@ -12,22 +12,27 @@ interface IProps {
|
||||||
text: React.ReactNode;
|
text: React.ReactNode;
|
||||||
collapseProps?: Partial<CollapseProps>;
|
collapseProps?: Partial<CollapseProps>;
|
||||||
outsideCollapse?: React.ReactNode;
|
outsideCollapse?: React.ReactNode;
|
||||||
onOpen?: () => void;
|
onOpenChanged?: (o: boolean) => void;
|
||||||
|
open?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CollapseButton: React.FC<React.PropsWithChildren<IProps>> = (
|
export const CollapseButton: React.FC<React.PropsWithChildren<IProps>> = (
|
||||||
props: React.PropsWithChildren<IProps>
|
props: React.PropsWithChildren<IProps>
|
||||||
) => {
|
) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(props.open ?? false);
|
||||||
|
|
||||||
function toggleOpen() {
|
function toggleOpen() {
|
||||||
const nv = !open;
|
const nv = !open;
|
||||||
setOpen(nv);
|
setOpen(nv);
|
||||||
if (props.onOpen && nv) {
|
props.onOpenChanged?.(nv);
|
||||||
props.onOpen();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.open !== undefined) {
|
||||||
|
setOpen(props.open);
|
||||||
|
}
|
||||||
|
}, [props.open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={props.className}>
|
<div className={props.className}>
|
||||||
<div className="collapse-header">
|
<div className="collapse-header">
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ import { Button, CollapseProps } from "react-bootstrap";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
import { faSliders } from "@fortawesome/free-solid-svg-icons";
|
import { faSliders } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
|
export type SidebarSectionStates = Record<string, boolean>;
|
||||||
|
|
||||||
// this needs to correspond to the CSS media query that overlaps the sidebar over content
|
// this needs to correspond to the CSS media query that overlaps the sidebar over content
|
||||||
const fixedSidebarMediaQuery = "only screen and (max-width: 767px)";
|
const fixedSidebarMediaQuery = "only screen and (max-width: 767px)";
|
||||||
|
|
@ -61,14 +64,35 @@ export const SidebarPaneContent: React.FC = ({ children }) => {
|
||||||
return <div className="sidebar-pane-content">{children}</div>;
|
return <div className="sidebar-pane-content">{children}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface IContext {
|
||||||
|
sectionOpen: SidebarSectionStates;
|
||||||
|
setSectionOpen: (section: string, open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarStateContext = React.createContext<IContext | null>(null);
|
||||||
|
|
||||||
export const SidebarSection: React.FC<
|
export const SidebarSection: React.FC<
|
||||||
PropsWithChildren<{
|
PropsWithChildren<{
|
||||||
text: React.ReactNode;
|
text: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
outsideCollapse?: React.ReactNode;
|
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<CollapseProps> = {
|
const collapseProps: Partial<CollapseProps> = {
|
||||||
mountOnEnter: true,
|
mountOnEnter: true,
|
||||||
unmountOnExit: true,
|
unmountOnExit: true,
|
||||||
|
|
@ -79,7 +103,8 @@ export const SidebarSection: React.FC<
|
||||||
collapseProps={collapseProps}
|
collapseProps={collapseProps}
|
||||||
text={text}
|
text={text}
|
||||||
outsideCollapse={outsideCollapse}
|
outsideCollapse={outsideCollapse}
|
||||||
onOpen={onOpen}
|
onOpenChanged={onOpenInternal}
|
||||||
|
open={openState}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</CollapseButton>
|
</CollapseButton>
|
||||||
|
|
@ -110,6 +135,7 @@ export function defaultShowSidebar() {
|
||||||
export function useSidebarState(view?: View) {
|
export function useSidebarState(view?: View) {
|
||||||
const [interfaceLocalForage, setInterfaceLocalForage] =
|
const [interfaceLocalForage, setInterfaceLocalForage] =
|
||||||
useInterfaceLocalForage();
|
useInterfaceLocalForage();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const { data: interfaceLocalForageData, loading } = interfaceLocalForage;
|
const { data: interfaceLocalForageData, loading } = interfaceLocalForage;
|
||||||
|
|
||||||
|
|
@ -118,6 +144,7 @@ export function useSidebarState(view?: View) {
|
||||||
}, [view, interfaceLocalForageData]);
|
}, [view, interfaceLocalForageData]);
|
||||||
|
|
||||||
const [showSidebar, setShowSidebar] = useState<boolean>();
|
const [showSidebar, setShowSidebar] = useState<boolean>();
|
||||||
|
const [sectionOpen, setSectionOpen] = useState<SidebarSectionStates>();
|
||||||
|
|
||||||
// set initial state once loading is done
|
// set initial state once loading is done
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -132,7 +159,17 @@ export function useSidebarState(view?: View) {
|
||||||
|
|
||||||
// only show sidebar by default on large screens
|
// only show sidebar by default on large screens
|
||||||
setShowSidebar(!!viewConfig.showSidebar && defaultShowSidebar());
|
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(
|
const onSetShowSidebar = useCallback(
|
||||||
(show: boolean | ((prevState: boolean | undefined) => boolean)) => {
|
(show: boolean | ((prevState: boolean | undefined) => boolean)) => {
|
||||||
|
|
@ -154,9 +191,28 @@ export function useSidebarState(view?: View) {
|
||||||
[showSidebar, setInterfaceLocalForage, view, viewConfig]
|
[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 {
|
return {
|
||||||
showSidebar: showSidebar ?? defaultShowSidebar(),
|
showSidebar: showSidebar ?? defaultShowSidebar(),
|
||||||
|
sectionOpen: sectionOpen || {},
|
||||||
setShowSidebar: onSetShowSidebar,
|
setShowSidebar: onSetShowSidebar,
|
||||||
|
setSectionOpen: onSetSectionOpen,
|
||||||
loading: showSidebar === undefined,
|
loading: showSidebar === undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue