Scene list toolbar style update (#6215)

* Add saved filter button to toolbar
* Rearrange and add portal target
* Only overlap sidebar on sm viewports
* Hide dropdown button on smaller viewports when sidebar open
* Center operations during selection
* Restyle results header
* Add classname for sidebar pane content
* Move sidebar toggle to left during scene selection
This commit is contained in:
WithoutPants 2025-10-31 14:29:01 +11:00 committed by GitHub
parent fb7bd89834
commit 299e1ac1f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 235 additions and 82 deletions

View file

@ -16,22 +16,28 @@ import {
faTrash, faTrash,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import cx from "classnames"; import cx from "classnames";
import { createPortal } from "react-dom";
export const OperationDropdown: React.FC< export const OperationDropdown: React.FC<
PropsWithChildren<{ PropsWithChildren<{
className?: string; className?: string;
menuPortalTarget?: HTMLElement;
}> }>
> = ({ className, children }) => { > = ({ className, menuPortalTarget, children }) => {
if (!children) return null; if (!children) return null;
const menu = (
<Dropdown.Menu className="bg-secondary text-white">
{children}
</Dropdown.Menu>
);
return ( return (
<Dropdown className={className} as={ButtonGroup}> <Dropdown className={className} as={ButtonGroup}>
<Dropdown.Toggle variant="secondary" id="more-menu"> <Dropdown.Toggle variant="secondary" id="more-menu">
<Icon icon={faEllipsisH} /> <Icon icon={faEllipsisH} />
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu className="bg-secondary text-white"> {menuPortalTarget ? createPortal(menu, menuPortalTarget) : menu}
{children}
</Dropdown.Menu>
</Dropdown> </Dropdown>
); );
}; };

View file

@ -23,15 +23,6 @@ export const ListResultsHeader: React.FC<{
}) => { }) => {
return ( return (
<ButtonToolbar className={cx(className, "list-results-header")}> <ButtonToolbar className={cx(className, "list-results-header")}>
<div>
<PaginationIndex
loading={loading}
itemsPerPage={filter.itemsPerPage}
currentPage={filter.currentPage}
totalItems={totalCount}
metadataByline={metadataByline}
/>
</div>
<div> <div>
<SortBySelect <SortBySelect
options={filter.options.sortByOptions} options={filter.options.sortByOptions}
@ -61,6 +52,16 @@ export const ListResultsHeader: React.FC<{
onSetZoom={(zoom) => onChangeFilter(filter.setZoom(zoom))} onSetZoom={(zoom) => onChangeFilter(filter.setZoom(zoom))}
/> />
</div> </div>
<div className="pagination-index-container">
<PaginationIndex
loading={loading}
itemsPerPage={filter.itemsPerPage}
currentPage={filter.currentPage}
totalItems={totalCount}
metadataByline={metadataByline}
/>
</div>
<div className="empty-space"></div>
</ButtonToolbar> </ButtonToolbar>
); );
}; };

View file

@ -4,13 +4,15 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { faTimes } from "@fortawesome/free-solid-svg-icons"; import { faTimes } from "@fortawesome/free-solid-svg-icons";
import { FilterTags } from "../List/FilterTags"; import { FilterTags } from "../List/FilterTags";
import cx from "classnames"; import cx from "classnames";
import { Button, ButtonToolbar } from "react-bootstrap"; import { Button, ButtonGroup, ButtonToolbar } from "react-bootstrap";
import { FilterButton } from "../List/Filters/FilterButton"; import { FilterButton } from "../List/Filters/FilterButton";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { SearchTermInput } from "../List/ListFilter"; import { SearchTermInput } from "../List/ListFilter";
import { Criterion } from "src/models/list-filter/criteria/criterion"; import { Criterion } from "src/models/list-filter/criteria/criterion";
import { SidebarToggleButton } from "../Shared/Sidebar"; import { SidebarToggleButton } from "../Shared/Sidebar";
import { PatchComponent } from "src/patch"; import { PatchComponent } from "src/patch";
import { SavedFilterDropdown } from "./SavedFilterList";
import { View } from "./views";
export const ToolbarFilterSection: React.FC<{ export const ToolbarFilterSection: React.FC<{
filter: ListFilterModel; filter: ListFilterModel;
@ -21,6 +23,7 @@ export const ToolbarFilterSection: React.FC<{
onRemoveAllCriterion: () => void; onRemoveAllCriterion: () => void;
onEditSearchTerm: () => void; onEditSearchTerm: () => void;
onRemoveSearchTerm: () => void; onRemoveSearchTerm: () => void;
view?: View;
}> = PatchComponent( }> = PatchComponent(
"ToolbarFilterSection", "ToolbarFilterSection",
({ ({
@ -32,6 +35,7 @@ export const ToolbarFilterSection: React.FC<{
onRemoveAllCriterion, onRemoveAllCriterion,
onEditSearchTerm, onEditSearchTerm,
onRemoveSearchTerm, onRemoveSearchTerm,
view,
}) => { }) => {
const { criteria, searchTerm } = filter; const { criteria, searchTerm } = filter;
@ -41,10 +45,19 @@ export const ToolbarFilterSection: React.FC<{
<SearchTermInput filter={filter} onFilterUpdate={onSetFilter} /> <SearchTermInput filter={filter} onFilterUpdate={onSetFilter} />
</div> </div>
<div className="filter-section"> <div className="filter-section">
<ButtonGroup>
<SidebarToggleButton onClick={onToggleSidebar} />
<SavedFilterDropdown
filter={filter}
onSetFilter={onSetFilter}
view={view}
menuPortalTarget={document.body}
/>
<FilterButton <FilterButton
onClick={() => onEditCriterion()} onClick={() => onEditCriterion()}
count={criteria.length} count={criteria.length}
/> />
</ButtonGroup>
<FilterTags <FilterTags
searchTerm={searchTerm} searchTerm={searchTerm}
criteria={criteria} criteria={criteria}
@ -55,7 +68,6 @@ export const ToolbarFilterSection: React.FC<{
onRemoveSearchTerm={onRemoveSearchTerm} onRemoveSearchTerm={onRemoveSearchTerm}
truncateOnOverflow truncateOnOverflow
/> />
<SidebarToggleButton onClick={onToggleSidebar} />
</div> </div>
</> </>
); );
@ -65,15 +77,18 @@ export const ToolbarFilterSection: React.FC<{
export const ToolbarSelectionSection: React.FC<{ export const ToolbarSelectionSection: React.FC<{
selected: number; selected: number;
onToggleSidebar: () => void; onToggleSidebar: () => void;
operations?: React.ReactNode;
onSelectAll: () => void; onSelectAll: () => void;
onSelectNone: () => void; onSelectNone: () => void;
}> = PatchComponent( }> = PatchComponent(
"ToolbarSelectionSection", "ToolbarSelectionSection",
({ selected, onToggleSidebar, onSelectAll, onSelectNone }) => { ({ selected, onToggleSidebar, operations, onSelectAll, onSelectNone }) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
<div className="toolbar-selection-section">
<div className="selected-items-info"> <div className="selected-items-info">
<SidebarToggleButton onClick={onToggleSidebar} />
<Button <Button
variant="secondary" variant="secondary"
className="minimal" className="minimal"
@ -86,7 +101,9 @@ export const ToolbarSelectionSection: React.FC<{
<Button variant="link" onClick={() => onSelectAll()}> <Button variant="link" onClick={() => onSelectAll()}>
<FormattedMessage id="actions.select_all" /> <FormattedMessage id="actions.select_all" />
</Button> </Button>
<SidebarToggleButton onClick={onToggleSidebar} /> </div>
{operations}
<div className="empty-space" />
</div> </div>
); );
} }
@ -114,7 +131,11 @@ export const FilteredListToolbar2: React.FC<{
})} })}
> >
{!hasSelection ? filterSection : selectionSection} {!hasSelection ? filterSection : selectionSection}
<div className="filtered-list-toolbar-operations">{operationSection}</div> {!hasSelection ? (
<div className="filtered-list-toolbar-operations">
{operationSection}
</div>
) : null}
</ButtonToolbar> </ButtonToolbar>
); );
}; };

View file

@ -31,6 +31,7 @@ import { AlertModal } from "../Shared/Alert";
import cx from "classnames"; import cx from "classnames";
import { TruncatedInlineText } from "../Shared/TruncatedText"; import { TruncatedInlineText } from "../Shared/TruncatedText";
import { OperationButton } from "../Shared/OperationButton"; import { OperationButton } from "../Shared/OperationButton";
import { createPortal } from "react-dom";
const ExistingSavedFilterList: React.FC<{ const ExistingSavedFilterList: React.FC<{
name: string; name: string;
@ -243,6 +244,7 @@ interface ISavedFilterListProps {
filter: ListFilterModel; filter: ListFilterModel;
onSetFilter: (f: ListFilterModel) => void; onSetFilter: (f: ListFilterModel) => void;
view?: View; view?: View;
menuPortalTarget?: Element | DocumentFragment;
} }
export const SavedFilterList: React.FC<ISavedFilterListProps> = ({ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
@ -841,8 +843,15 @@ export const SavedFilterDropdown: React.FC<ISavedFilterListProps> = (props) => {
)); ));
SavedFilterDropdownRef.displayName = "SavedFilterDropdown"; SavedFilterDropdownRef.displayName = "SavedFilterDropdown";
const menu = (
<Dropdown.Menu
as={SavedFilterDropdownRef}
className="saved-filter-list-menu"
/>
);
return ( return (
<Dropdown as={ButtonGroup}> <Dropdown as={ButtonGroup} className="saved-filter-dropdown">
<OverlayTrigger <OverlayTrigger
placement="top" placement="top"
overlay={ overlay={
@ -855,10 +864,9 @@ export const SavedFilterDropdown: React.FC<ISavedFilterListProps> = (props) => {
<Icon icon={faBookmark} /> <Icon icon={faBookmark} />
</Dropdown.Toggle> </Dropdown.Toggle>
</OverlayTrigger> </OverlayTrigger>
<Dropdown.Menu {props.menuPortalTarget
as={SavedFilterDropdownRef} ? createPortal(menu, props.menuPortalTarget)
className="saved-filter-list-menu" : menu}
/>
</Dropdown> </Dropdown>
); );
}; };

View file

@ -1055,7 +1055,7 @@ input[type="range"].zoom-slider {
} }
// hide sidebar Edit Filter button on larger screens // hide sidebar Edit Filter button on larger screens
@include media-breakpoint-up(lg) { @include media-breakpoint-up(md) {
.sidebar .edit-filter-button { .sidebar .edit-filter-button {
display: none; display: none;
} }
@ -1071,6 +1071,7 @@ input[type="range"].zoom-slider {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
margin-bottom: 0;
row-gap: 1rem; row-gap: 1rem;
> div { > div {
@ -1101,10 +1102,6 @@ input[type="range"].zoom-slider {
top: 0; top: 0;
} }
.selected-items-info .btn {
margin-right: 0.5rem;
}
// hide drop down menu items for play and create new // hide drop down menu items for play and create new
// when the buttons are visible // when the buttons are visible
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
@ -1125,7 +1122,7 @@ input[type="range"].zoom-slider {
} }
} }
.selected-items-info, .toolbar-selection-section,
div.filter-section { div.filter-section {
border: 1px solid $secondary; border: 1px solid $secondary;
border-radius: 0.25rem; border-radius: 0.25rem;
@ -1133,13 +1130,69 @@ input[type="range"].zoom-slider {
overflow-x: hidden; overflow-x: hidden;
} }
div.toolbar-selection-section {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
.sidebar-toggle-button { .sidebar-toggle-button {
margin-left: auto; margin-right: 0.5rem;
}
.selected-items-info {
align-items: center;
display: flex;
}
> div:first-child,
> div:last-child {
flex: 1;
}
> div:last-child {
display: flex;
justify-content: flex-end;
}
.scene-list-operations {
display: flex;
}
// on smaller viewports move the operation buttons to the right
@include media-breakpoint-down(md) {
div.scene-list-operations {
flex: 1;
justify-content: flex-end;
order: 3;
}
> div:last-child {
flex: 0;
order: 2;
}
}
}
// on larger viewports, move the operation buttons to the center
@include media-breakpoint-up(lg) {
div.toolbar-selection-section div.scene-list-operations {
justify-content: center;
> .btn-group {
gap: 0.5rem;
}
}
div.toolbar-selection-section .empty-space {
flex: 1;
order: 3;
}
} }
.search-container { .search-container {
border-right: 1px solid $secondary; border-right: 1px solid $secondary;
display: block; display: flex;
margin-right: -0.5rem; margin-right: -0.5rem;
min-width: calc($sidebar-width - 15px); min-width: calc($sidebar-width - 15px);
padding-right: 10px; padding-right: 10px;
@ -1175,22 +1228,28 @@ input[type="range"].zoom-slider {
} }
} }
@include media-breakpoint-up(xl) { // hide the search box in the toolbar when sidebar is shown on larger screens
// larger screens don't overlap the sidebar
@include media-breakpoint-up(md) {
.sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .search-container { .sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .search-container {
display: none; display: none;
} }
} }
// hide the search box when sidebar is hidden on smaller screens
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
.sidebar-pane.hide-sidebar .filtered-list-toolbar .search-container { .sidebar-pane.hide-sidebar .filtered-list-toolbar .search-container {
display: none; display: none;
} }
} }
// hide the filter icon button when sidebar is shown on smaller screens // hide the filter and saved filters icon buttons when sidebar is shown on smaller screens
@include media-breakpoint-down(md) { @include media-breakpoint-down(sm) {
.sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .filter-button { .sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar {
.filter-button,
.saved-filter-dropdown {
display: none; display: none;
} }
}
// adjust the width of the filter-tags as well // adjust the width of the filter-tags as well
.sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .filter-tags { .sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .filter-tags {
@ -1198,8 +1257,8 @@ input[type="range"].zoom-slider {
} }
} }
// move the sidebar toggle to the left on xl viewports // move the sidebar toggle to the left on larger viewports
@include media-breakpoint-up(xl) { @include media-breakpoint-up(md) {
.filtered-list-toolbar .filter-section { .filtered-list-toolbar .filter-section {
.sidebar-toggle-button { .sidebar-toggle-button {
margin-left: 0; margin-left: 0;
@ -1249,14 +1308,18 @@ input[type="range"].zoom-slider {
align-items: center; align-items: center;
background-color: $body-bg; background-color: $body-bg;
display: flex; display: flex;
justify-content: space-between;
> div { > div {
align-items: center; align-items: center;
display: flex; display: flex;
flex: 1;
gap: 0.5rem; gap: 0.5rem;
justify-content: flex-start; justify-content: flex-start;
&.pagination-index-container {
justify-content: center;
}
&:last-child { &:last-child {
flex-shrink: 0; flex-shrink: 0;
justify-content: flex-end; justify-content: flex-end;
@ -1265,18 +1328,55 @@ input[type="range"].zoom-slider {
} }
.list-results-header { .list-results-header {
flex-wrap: wrap-reverse; gap: 0.25rem;
gap: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
.paginationIndex { .paginationIndex {
margin: 0; margin: 0;
} }
// move pagination info to right on medium screens
@include media-breakpoint-down(md) {
& > .empty-space {
flex: 0;
}
& > div.pagination-index-container {
justify-content: flex-end;
order: 3;
}
}
// center the header on smaller screens // center the header on smaller screens
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
& > div, & > div,
& > div:last-child { & > div.pagination-index-container {
flex-basis: 100%;
justify-content: center;
margin-left: auto;
margin-right: auto;
}
}
}
// sidebar visible styling
.sidebar-pane:not(.hide-sidebar) .list-results-header {
// move pagination info to right on medium screens when sidebar
@include media-breakpoint-down(lg) {
& > .empty-space {
flex: 0;
}
& > div.pagination-index-container {
justify-content: flex-end;
order: 3;
}
}
// center the header on smaller screens when sidebar is visible
@include media-breakpoint-down(md) {
& > div,
& > div.pagination-index-container {
flex-basis: 100%; flex-basis: 100%;
justify-content: center; justify-content: center;
margin-left: auto; margin-left: auto;

View file

@ -37,7 +37,12 @@ import {
OperationDropdownItem, OperationDropdownItem,
} from "../List/ListOperationButtons"; } from "../List/ListOperationButtons";
import { useFilteredItemList } from "../List/ItemList"; import { useFilteredItemList } from "../List/ItemList";
import { Sidebar, SidebarPane, useSidebarState } from "../Shared/Sidebar"; import {
Sidebar,
SidebarPane,
SidebarPaneContent,
useSidebarState,
} from "../Shared/Sidebar";
import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter"; import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter";
import { SidebarStudiosFilter } from "../List/Filters/StudiosFilter"; import { SidebarStudiosFilter } from "../List/Filters/StudiosFilter";
import { PerformersCriterionOption } from "src/models/list-filter/criteria/performers"; import { PerformersCriterionOption } from "src/models/list-filter/criteria/performers";
@ -355,7 +360,7 @@ const SceneListOperations: React.FC<{
const intl = useIntl(); const intl = useIntl();
return ( return (
<div> <div className="scene-list-operations">
<ButtonGroup> <ButtonGroup>
{!!items && ( {!!items && (
<Button <Button
@ -396,7 +401,10 @@ const SceneListOperations: React.FC<{
</> </>
)} )}
<OperationDropdown className="scene-list-operations"> <OperationDropdown
className="scene-list-operations"
menuPortalTarget={document.body}
>
{operations.map((o) => { {operations.map((o) => {
if (o.isDisplayed && !o.isDisplayed()) { if (o.isDisplayed && !o.isDisplayed()) {
return null; return null;
@ -666,6 +674,18 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
// render // render
if (filterLoading || sidebarStateLoading) return null; if (filterLoading || sidebarStateLoading) return null;
const operations = (
<SceneListOperations
items={items.length}
hasSelection={hasSelection}
operations={otherOperations}
onEdit={onEdit}
onDelete={onDelete}
onPlay={onPlay}
onCreateNew={onCreateNew}
/>
);
return ( return (
<TaggerContext> <TaggerContext>
<div <div
@ -689,7 +709,7 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
focus={searchFocus} focus={searchFocus}
/> />
</Sidebar> </Sidebar>
<div> <SidebarPaneContent>
<FilteredListToolbar2 <FilteredListToolbar2
className="scene-list-toolbar" className="scene-list-toolbar"
hasSelection={hasSelection} hasSelection={hasSelection}
@ -708,6 +728,7 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
setSearchFocus(true); setSearchFocus(true);
}} }}
onRemoveSearchTerm={() => setFilter(filter.clearSearchTerm())} onRemoveSearchTerm={() => setFilter(filter.clearSearchTerm())}
view={view}
/> />
} }
selectionSection={ selectionSection={
@ -716,19 +737,10 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
onToggleSidebar={() => setShowSidebar(!showSidebar)} onToggleSidebar={() => setShowSidebar(!showSidebar)}
onSelectAll={() => onSelectAll()} onSelectAll={() => onSelectAll()}
onSelectNone={() => onSelectNone()} onSelectNone={() => onSelectNone()}
operations={operations}
/> />
} }
operationSection={ operationSection={operations}
<SceneListOperations
items={items.length}
hasSelection={hasSelection}
operations={otherOperations}
onEdit={onEdit}
onDelete={onDelete}
onPlay={onPlay}
onCreateNew={onCreateNew}
/>
}
/> />
<ListResultsHeader <ListResultsHeader
@ -761,7 +773,7 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
/> />
</div> </div>
)} )}
</div> </SidebarPaneContent>
</SidebarPane> </SidebarPane>
</div> </div>
</TaggerContext> </TaggerContext>

View file

@ -16,7 +16,8 @@ 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";
const fixedSidebarMediaQuery = "only screen and (max-width: 1199px)"; // this needs to correspond to the CSS media query that overlaps the sidebar over content
const fixedSidebarMediaQuery = "only screen and (max-width: 767px)";
export const Sidebar: React.FC< export const Sidebar: React.FC<
PropsWithChildren<{ PropsWithChildren<{
@ -56,6 +57,10 @@ export const SidebarPane: React.FC<
); );
}; };
export const SidebarPaneContent: React.FC = ({ children }) => {
return <div className="sidebar-pane-content">{children}</div>;
};
export const SidebarSection: React.FC< export const SidebarSection: React.FC<
PropsWithChildren<{ PropsWithChildren<{
text: React.ReactNode; text: React.ReactNode;
@ -87,7 +92,7 @@ export const SidebarToggleButton: React.FC<{
const intl = useIntl(); const intl = useIntl();
return ( return (
<Button <Button
className="minimal sidebar-toggle-button ignore-sidebar-outside-click" className="sidebar-toggle-button ignore-sidebar-outside-click"
variant="secondary" variant="secondary"
onClick={onClick} onClick={onClick}
title={intl.formatMessage({ id: "actions.sidebar.toggle" })} title={intl.formatMessage({ id: "actions.sidebar.toggle" })}

View file

@ -805,7 +805,7 @@ button.btn.favorite-button {
} }
} }
@include media-breakpoint-up(xl) { @include media-breakpoint-up(md) {
transition: margin-left 0.1s; transition: margin-left 0.1s;
&:not(.hide-sidebar) { &:not(.hide-sidebar) {
@ -910,12 +910,12 @@ $sticky-header-height: calc(50px + 3.3rem);
} }
// on smaller viewports we want the sidebar to overlap content // on smaller viewports we want the sidebar to overlap content
@include media-breakpoint-down(lg) { @include media-breakpoint-down(sm) {
.sidebar-pane:not(.hide-sidebar) .sidebar { .sidebar-pane:not(.hide-sidebar) .sidebar {
margin-right: -$sidebar-width; margin-right: -$sidebar-width;
} }
.sidebar-pane > :nth-child(2) { .sidebar-pane > .sidebar-pane-content {
transition: none; transition: none;
} }
} }
@ -935,7 +935,7 @@ $sticky-header-height: calc(50px + 3.3rem);
display: none; display: none;
} }
} }
@include media-breakpoint-up(xl) { @include media-breakpoint-up(md) {
.sidebar-pane:not(.hide-sidebar) { .sidebar-pane:not(.hide-sidebar) {
> :nth-child(2) { > :nth-child(2) {
margin-left: 0; margin-left: 0;