mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Scene list cleanup (#6104)
* Generalise and cleanup list toolbar * Generalise ListResultsHeader * Fix padding on sub-pages
This commit is contained in:
parent
af76f4a24a
commit
c5bad48ece
9 changed files with 560 additions and 583 deletions
|
|
@ -8,11 +8,9 @@ import {
|
|||
IListFilterOperation,
|
||||
ListOperationButtons,
|
||||
} from "./ListOperationButtons";
|
||||
import { Button, ButtonGroup, ButtonToolbar } from "react-bootstrap";
|
||||
import { ButtonGroup, ButtonToolbar } from "react-bootstrap";
|
||||
import { View } from "./views";
|
||||
import { IListSelect, useFilterOperations } from "./util";
|
||||
import { SidebarIcon } from "../Shared/Sidebar";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
export interface IItemListOperation<T extends QueryResult> {
|
||||
text: string;
|
||||
|
|
@ -43,7 +41,6 @@ export interface IFilteredListToolbar {
|
|||
onDelete?: () => void;
|
||||
operations?: IListFilterOperation[];
|
||||
zoomable?: boolean;
|
||||
onToggleSidebar?: () => void;
|
||||
}
|
||||
|
||||
export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
||||
|
|
@ -56,9 +53,7 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
|||
onDelete,
|
||||
operations,
|
||||
zoomable = false,
|
||||
onToggleSidebar,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const filterOptions = filter.options;
|
||||
const { setDisplayMode, setZoom } = useFilterOperations({
|
||||
filter,
|
||||
|
|
@ -68,21 +63,6 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
|||
|
||||
return (
|
||||
<ButtonToolbar className="filtered-list-toolbar">
|
||||
<ButtonGroup>
|
||||
{onToggleSidebar && (
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
className="sidebar-toggle-button"
|
||||
onClick={onToggleSidebar}
|
||||
variant="secondary"
|
||||
title={intl.formatMessage({ id: "actions.sidebar.open" })}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup>
|
||||
{showEditFilter && (
|
||||
<ListFilter
|
||||
|
|
@ -90,7 +70,6 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
|||
filter={filter}
|
||||
openFilterDialog={() => showEditFilter()}
|
||||
view={view}
|
||||
withSidebar={!!onToggleSidebar}
|
||||
/>
|
||||
)}
|
||||
<ListOperationButtons
|
||||
|
|
|
|||
|
|
@ -324,7 +324,6 @@ interface IListFilterProps {
|
|||
filter: ListFilterModel;
|
||||
view?: View;
|
||||
openFilterDialog: () => void;
|
||||
withSidebar?: boolean;
|
||||
}
|
||||
|
||||
export const ListFilter: React.FC<IListFilterProps> = ({
|
||||
|
|
@ -332,7 +331,6 @@ export const ListFilter: React.FC<IListFilterProps> = ({
|
|||
filter,
|
||||
openFilterDialog,
|
||||
view,
|
||||
withSidebar,
|
||||
}) => {
|
||||
const filterOptions = filter.options;
|
||||
|
||||
|
|
@ -379,13 +377,10 @@ export const ListFilter: React.FC<IListFilterProps> = ({
|
|||
function render() {
|
||||
return (
|
||||
<>
|
||||
{!withSidebar && (
|
||||
<div className="d-flex">
|
||||
<SearchTermInput filter={filter} onFilterUpdate={onFilterUpdate} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!withSidebar && (
|
||||
<ButtonGroup className="mr-2">
|
||||
<SavedFilterDropdown
|
||||
filter={filter}
|
||||
|
|
@ -408,7 +403,6 @@ export const ListFilter: React.FC<IListFilterProps> = ({
|
|||
/>
|
||||
</OverlayTrigger>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
|
||||
<SortBySelect
|
||||
className="mr-2"
|
||||
|
|
|
|||
66
ui/v2.5/src/components/List/ListResultsHeader.tsx
Normal file
66
ui/v2.5/src/components/List/ListResultsHeader.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import React from "react";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { PaginationIndex } from "../List/Pagination";
|
||||
import { ButtonToolbar } from "react-bootstrap";
|
||||
import { ListViewOptions } from "../List/ListViewOptions";
|
||||
import { PageSizeSelector, SortBySelect } from "../List/ListFilter";
|
||||
import cx from "classnames";
|
||||
|
||||
export const ListResultsHeader: React.FC<{
|
||||
className?: string;
|
||||
loading: boolean;
|
||||
filter: ListFilterModel;
|
||||
totalCount: number;
|
||||
metadataByline?: React.ReactNode;
|
||||
onChangeFilter: (filter: ListFilterModel) => void;
|
||||
}> = ({
|
||||
className,
|
||||
loading,
|
||||
filter,
|
||||
totalCount,
|
||||
metadataByline,
|
||||
onChangeFilter,
|
||||
}) => {
|
||||
return (
|
||||
<ButtonToolbar className={cx(className, "list-results-header")}>
|
||||
<div>
|
||||
<PaginationIndex
|
||||
loading={loading}
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
currentPage={filter.currentPage}
|
||||
totalItems={totalCount}
|
||||
metadataByline={metadataByline}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<SortBySelect
|
||||
options={filter.options.sortByOptions}
|
||||
sortBy={filter.sortBy}
|
||||
sortDirection={filter.sortDirection}
|
||||
onChangeSortBy={(s) =>
|
||||
onChangeFilter(filter.setSortBy(s ?? undefined))
|
||||
}
|
||||
onChangeSortDirection={() =>
|
||||
onChangeFilter(filter.toggleSortDirection())
|
||||
}
|
||||
onReshuffleRandomSort={() =>
|
||||
onChangeFilter(filter.reshuffleRandomSort())
|
||||
}
|
||||
/>
|
||||
<PageSizeSelector
|
||||
pageSize={filter.itemsPerPage}
|
||||
setPageSize={(s) => onChangeFilter(filter.setPageSize(s))}
|
||||
/>
|
||||
<ListViewOptions
|
||||
displayMode={filter.displayMode}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
displayModeOptions={filter.options.displayModeOptions}
|
||||
onSetDisplayMode={(mode) =>
|
||||
onChangeFilter(filter.setDisplayMode(mode))
|
||||
}
|
||||
onSetZoom={(zoom) => onChangeFilter(filter.setZoom(zoom))}
|
||||
/>
|
||||
</div>
|
||||
</ButtonToolbar>
|
||||
);
|
||||
};
|
||||
120
ui/v2.5/src/components/List/ListToolbar.tsx
Normal file
120
ui/v2.5/src/components/List/ListToolbar.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FilterTags } from "../List/FilterTags";
|
||||
import cx from "classnames";
|
||||
import { Button, ButtonToolbar } from "react-bootstrap";
|
||||
import { FilterButton } from "../List/Filters/FilterButton";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { SearchTermInput } from "../List/ListFilter";
|
||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { SidebarToggleButton } from "../Shared/Sidebar";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
export const ToolbarFilterSection: React.FC<{
|
||||
filter: ListFilterModel;
|
||||
onToggleSidebar: () => void;
|
||||
onSetFilter: (filter: ListFilterModel) => void;
|
||||
onEditCriterion: (c?: Criterion) => void;
|
||||
onRemoveCriterion: (criterion: Criterion, valueIndex?: number) => void;
|
||||
onRemoveAllCriterion: () => void;
|
||||
onEditSearchTerm: () => void;
|
||||
onRemoveSearchTerm: () => void;
|
||||
}> = PatchComponent(
|
||||
"ToolbarFilterSection",
|
||||
({
|
||||
filter,
|
||||
onToggleSidebar,
|
||||
onSetFilter,
|
||||
onEditCriterion,
|
||||
onRemoveCriterion,
|
||||
onRemoveAllCriterion,
|
||||
onEditSearchTerm,
|
||||
onRemoveSearchTerm,
|
||||
}) => {
|
||||
const { criteria, searchTerm } = filter;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="search-container">
|
||||
<SearchTermInput filter={filter} onFilterUpdate={onSetFilter} />
|
||||
</div>
|
||||
<div className="filter-section">
|
||||
<FilterButton
|
||||
onClick={() => onEditCriterion()}
|
||||
count={criteria.length}
|
||||
/>
|
||||
<FilterTags
|
||||
searchTerm={searchTerm}
|
||||
criteria={criteria}
|
||||
onEditCriterion={onEditCriterion}
|
||||
onRemoveCriterion={onRemoveCriterion}
|
||||
onRemoveAll={onRemoveAllCriterion}
|
||||
onEditSearchTerm={onEditSearchTerm}
|
||||
onRemoveSearchTerm={onRemoveSearchTerm}
|
||||
truncateOnOverflow
|
||||
/>
|
||||
<SidebarToggleButton onClick={onToggleSidebar} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const ToolbarSelectionSection: React.FC<{
|
||||
selected: number;
|
||||
onToggleSidebar: () => void;
|
||||
onSelectAll: () => void;
|
||||
onSelectNone: () => void;
|
||||
}> = PatchComponent(
|
||||
"ToolbarSelectionSection",
|
||||
({ selected, onToggleSidebar, onSelectAll, onSelectNone }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<div className="selected-items-info">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="minimal"
|
||||
onClick={() => onSelectNone()}
|
||||
title={intl.formatMessage({ id: "actions.select_none" })}
|
||||
>
|
||||
<Icon icon={faTimes} />
|
||||
</Button>
|
||||
<span>{selected} selected</span>
|
||||
<Button variant="link" onClick={() => onSelectAll()}>
|
||||
<FormattedMessage id="actions.select_all" />
|
||||
</Button>
|
||||
<SidebarToggleButton onClick={onToggleSidebar} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// TODO - rename to FilteredListToolbar once all list components have been updated
|
||||
// TODO - and expose to plugins
|
||||
export const FilteredListToolbar2: React.FC<{
|
||||
className?: string;
|
||||
hasSelection: boolean;
|
||||
filterSection: React.ReactNode;
|
||||
selectionSection: React.ReactNode;
|
||||
operationSection: React.ReactNode;
|
||||
}> = ({
|
||||
className,
|
||||
hasSelection,
|
||||
filterSection,
|
||||
selectionSection,
|
||||
operationSection,
|
||||
}) => {
|
||||
return (
|
||||
<ButtonToolbar
|
||||
className={cx(className, "filtered-list-toolbar", {
|
||||
"has-selection": hasSelection,
|
||||
})}
|
||||
>
|
||||
{!hasSelection ? filterSection : selectionSection}
|
||||
<div className="filtered-list-toolbar-operations">{operationSection}</div>
|
||||
</ButtonToolbar>
|
||||
);
|
||||
};
|
||||
|
|
@ -1046,3 +1046,234 @@ input[type="range"].zoom-slider {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hide sidebar Edit Filter button on larger screens
|
||||
@include media-breakpoint-up(lg) {
|
||||
.sidebar .edit-filter-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// the following refers to the new FilteredListToolbar2 component
|
||||
// ensure the rules here don't conflict with the original filtered-list-toolbar above
|
||||
// TODO - replace with only .filtered-list-toolbar once all lists use the new toolbar
|
||||
.scene-list-toolbar {
|
||||
&.filtered-list-toolbar {
|
||||
align-items: center;
|
||||
background-color: $body-bg;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
row-gap: 1rem;
|
||||
|
||||
> div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-start;
|
||||
|
||||
&:last-child {
|
||||
flex-shrink: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.filtered-list-toolbar {
|
||||
flex-wrap: nowrap;
|
||||
gap: 1rem;
|
||||
// offset the main padding
|
||||
margin-top: -0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
position: sticky;
|
||||
top: $navbar-height;
|
||||
z-index: 10;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.selected-items-info .btn {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
// hide drop down menu items for play and create new
|
||||
// when the buttons are visible
|
||||
@include media-breakpoint-up(sm) {
|
||||
.scene-list-operations {
|
||||
.play-item,
|
||||
.create-new-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hide play and create new buttons on xs screens
|
||||
// show these in the drop down menu instead
|
||||
@include media-breakpoint-down(xs) {
|
||||
.play-button,
|
||||
.create-new-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-items-info,
|
||||
div.filter-section {
|
||||
border: 1px solid $secondary;
|
||||
border-radius: 0.25rem;
|
||||
flex-grow: 1;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar-toggle-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
border-right: 1px solid $secondary;
|
||||
display: block;
|
||||
margin-right: -0.5rem;
|
||||
min-width: calc($sidebar-width - 15px);
|
||||
padding-right: 10px;
|
||||
|
||||
.search-term-input {
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
|
||||
.clearable-text-field {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-tags {
|
||||
flex-grow: 1;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 0;
|
||||
|
||||
// account for filter button, and toggle sidebar buttons with gaps
|
||||
width: calc(100% - 70px - 1rem);
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
.sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .search-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@include media-breakpoint-down(md) {
|
||||
.sidebar-pane.hide-sidebar .filtered-list-toolbar .search-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// hide the filter icon button when sidebar is shown on smaller screens
|
||||
@include media-breakpoint-down(md) {
|
||||
.sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .filter-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// adjust the width of the filter-tags as well
|
||||
.sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .filter-tags {
|
||||
width: calc(100% - 35px - 0.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
// move the sidebar toggle to the left on xl viewports
|
||||
@include media-breakpoint-up(xl) {
|
||||
.filtered-list-toolbar .filter-section {
|
||||
.sidebar-toggle-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.filter-tags {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hide the search term tag item when the search box is visible
|
||||
@include media-breakpoint-up(lg) {
|
||||
// TODO - remove scene-list-toolbar when all lists use the new toolbar
|
||||
.scene-list-toolbar.filtered-list-toolbar
|
||||
.filter-tags
|
||||
.search-term-filter-tag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@include media-breakpoint-down(md) {
|
||||
// TODO - remove scene-list-toolbar when all lists use the new toolbar
|
||||
.sidebar-pane:not(.hide-sidebar)
|
||||
.scene-list-toolbar.filtered-list-toolbar
|
||||
.filter-tags
|
||||
.search-term-filter-tag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - remove scene-list-toolbar when all lists use the new toolbar
|
||||
.detail-body .scene-list-toolbar.filtered-list-toolbar {
|
||||
top: calc($sticky-detail-header-height + $navbar-height);
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#more-criteria-popover {
|
||||
box-shadow: 0 8px 10px 2px rgb(0 0 0 / 30%);
|
||||
max-width: 400px;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.list-results-header {
|
||||
align-items: center;
|
||||
background-color: $body-bg;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
> div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-start;
|
||||
|
||||
&:last-child {
|
||||
flex-shrink: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-results-header {
|
||||
flex-wrap: wrap-reverse;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.paginationIndex {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// center the header on smaller screens
|
||||
@include media-breakpoint-down(sm) {
|
||||
& > div,
|
||||
& > div:last-child {
|
||||
flex-basis: 100%;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ import {
|
|||
faPencil,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faSliders,
|
||||
faTimes,
|
||||
faTrash,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { SceneMergeModal } from "./SceneMergeDialog";
|
||||
|
|
@ -39,7 +37,6 @@ import {
|
|||
OperationDropdownItem,
|
||||
} from "../List/ListOperationButtons";
|
||||
import { useFilteredItemList } from "../List/ItemList";
|
||||
import { FilterTags } from "../List/FilterTags";
|
||||
import { Sidebar, SidebarPane, useSidebarState } from "../Shared/Sidebar";
|
||||
import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter";
|
||||
import { SidebarStudiosFilter } from "../List/Filters/StudiosFilter";
|
||||
|
|
@ -57,18 +54,16 @@ import {
|
|||
useFilteredSidebarKeybinds,
|
||||
} from "../List/Filters/FilterSidebar";
|
||||
import { PatchContainerComponent } from "src/patch";
|
||||
import { Pagination, PaginationIndex } from "../List/Pagination";
|
||||
import { Button, ButtonGroup, ButtonToolbar } from "react-bootstrap";
|
||||
import { FilterButton } from "../List/Filters/FilterButton";
|
||||
import { Pagination } from "../List/Pagination";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { ListViewOptions } from "../List/ListViewOptions";
|
||||
import {
|
||||
PageSizeSelector,
|
||||
SearchTermInput,
|
||||
SortBySelect,
|
||||
} from "../List/ListFilter";
|
||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||
import useFocus from "src/utils/focus";
|
||||
import {
|
||||
FilteredListToolbar2,
|
||||
ToolbarFilterSection,
|
||||
ToolbarSelectionSection,
|
||||
} from "../List/ListToolbar";
|
||||
import { ListResultsHeader } from "../List/ListResultsHeader";
|
||||
|
||||
function renderMetadataByline(result: GQL.FindScenesQueryResult) {
|
||||
const duration = result?.data?.findScenes?.duration;
|
||||
|
|
@ -340,38 +335,18 @@ interface IOperations {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
const ListToolbarContent: React.FC<{
|
||||
filter: ListFilterModel;
|
||||
items: GQL.SlimSceneDataFragment[];
|
||||
selectedIds: Set<string>;
|
||||
const SceneListOperations: React.FC<{
|
||||
items: number;
|
||||
hasSelection: boolean;
|
||||
operations: IOperations[];
|
||||
onToggleSidebar: () => void;
|
||||
onSetFilter: (filter: ListFilterModel) => void;
|
||||
onEditCriterion: (c?: Criterion) => void;
|
||||
onRemoveCriterion: (criterion: Criterion, valueIndex?: number) => void;
|
||||
onRemoveAllCriterion: () => void;
|
||||
onEditSearchTerm: () => void;
|
||||
onRemoveSearchTerm: () => void;
|
||||
onSelectAll: () => void;
|
||||
onSelectNone: () => void;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onPlay: () => void;
|
||||
onCreateNew: () => void;
|
||||
}> = ({
|
||||
filter,
|
||||
items,
|
||||
selectedIds,
|
||||
hasSelection,
|
||||
operations,
|
||||
onToggleSidebar,
|
||||
onSetFilter,
|
||||
onEditCriterion,
|
||||
onRemoveCriterion,
|
||||
onRemoveAllCriterion,
|
||||
onEditSearchTerm,
|
||||
onRemoveSearchTerm,
|
||||
onSelectAll,
|
||||
onSelectNone,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onPlay,
|
||||
|
|
@ -379,66 +354,10 @@ const ListToolbarContent: React.FC<{
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { criteria, searchTerm } = filter;
|
||||
const hasSelection = selectedIds.size > 0;
|
||||
|
||||
const sidebarToggle = (
|
||||
<Button
|
||||
className="minimal sidebar-toggle-button ignore-sidebar-outside-click"
|
||||
variant="secondary"
|
||||
onClick={() => onToggleSidebar()}
|
||||
title={intl.formatMessage({ id: "actions.sidebar.toggle" })}
|
||||
>
|
||||
<Icon icon={faSliders} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hasSelection && (
|
||||
<>
|
||||
<div className="search-container">
|
||||
<SearchTermInput filter={filter} onFilterUpdate={onSetFilter} />
|
||||
</div>
|
||||
<div className="filter-section">
|
||||
<FilterButton
|
||||
onClick={() => onEditCriterion()}
|
||||
count={criteria.length}
|
||||
/>
|
||||
<FilterTags
|
||||
searchTerm={searchTerm}
|
||||
criteria={criteria}
|
||||
onEditCriterion={onEditCriterion}
|
||||
onRemoveCriterion={onRemoveCriterion}
|
||||
onRemoveAll={onRemoveAllCriterion}
|
||||
onEditSearchTerm={onEditSearchTerm}
|
||||
onRemoveSearchTerm={onRemoveSearchTerm}
|
||||
truncateOnOverflow
|
||||
/>
|
||||
{sidebarToggle}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{hasSelection && (
|
||||
<div className="selected-items-info">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="minimal"
|
||||
onClick={() => onSelectNone()}
|
||||
title={intl.formatMessage({ id: "actions.select_none" })}
|
||||
>
|
||||
<Icon icon={faTimes} />
|
||||
</Button>
|
||||
<span>{selectedIds.size} selected</span>
|
||||
<Button variant="link" onClick={() => onSelectAll()}>
|
||||
<FormattedMessage id="actions.select_all" />
|
||||
</Button>
|
||||
{sidebarToggle}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<ButtonGroup>
|
||||
{!!items.length && (
|
||||
{!!items && (
|
||||
<Button
|
||||
className="play-button"
|
||||
variant="secondary"
|
||||
|
|
@ -495,58 +414,6 @@ const ListToolbarContent: React.FC<{
|
|||
</OperationDropdown>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ListResultsHeader: React.FC<{
|
||||
loading: boolean;
|
||||
filter: ListFilterModel;
|
||||
totalCount: number;
|
||||
metadataByline?: React.ReactNode;
|
||||
onChangeFilter: (filter: ListFilterModel) => void;
|
||||
}> = ({ loading, filter, totalCount, metadataByline, onChangeFilter }) => {
|
||||
return (
|
||||
<ButtonToolbar className="scene-list-header">
|
||||
<div>
|
||||
<PaginationIndex
|
||||
loading={loading}
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
currentPage={filter.currentPage}
|
||||
totalItems={totalCount}
|
||||
metadataByline={metadataByline}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<SortBySelect
|
||||
options={filter.options.sortByOptions}
|
||||
sortBy={filter.sortBy}
|
||||
sortDirection={filter.sortDirection}
|
||||
onChangeSortBy={(s) =>
|
||||
onChangeFilter(filter.setSortBy(s ?? undefined))
|
||||
}
|
||||
onChangeSortDirection={() =>
|
||||
onChangeFilter(filter.toggleSortDirection())
|
||||
}
|
||||
onReshuffleRandomSort={() =>
|
||||
onChangeFilter(filter.reshuffleRandomSort())
|
||||
}
|
||||
/>
|
||||
<PageSizeSelector
|
||||
pageSize={filter.itemsPerPage}
|
||||
setPageSize={(s) => onChangeFilter(filter.setPageSize(s))}
|
||||
/>
|
||||
<ListViewOptions
|
||||
displayMode={filter.displayMode}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
displayModeOptions={filter.options.displayModeOptions}
|
||||
onSetDisplayMode={(mode) =>
|
||||
onChangeFilter(filter.setDisplayMode(mode))
|
||||
}
|
||||
onSetZoom={(zoom) => onChangeFilter(filter.setZoom(zoom))}
|
||||
/>
|
||||
</div>
|
||||
</ButtonToolbar>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -823,19 +690,17 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
/>
|
||||
</Sidebar>
|
||||
<div>
|
||||
<ButtonToolbar
|
||||
className={cx("scene-list-toolbar", {
|
||||
"has-selection": hasSelection,
|
||||
})}
|
||||
>
|
||||
<ListToolbarContent
|
||||
<FilteredListToolbar2
|
||||
className="scene-list-toolbar"
|
||||
hasSelection={hasSelection}
|
||||
filterSection={
|
||||
<ToolbarFilterSection
|
||||
filter={filter}
|
||||
onSetFilter={setFilter}
|
||||
items={items}
|
||||
selectedIds={selectedIds}
|
||||
operations={otherOperations}
|
||||
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
||||
onEditCriterion={(c) => showEditFilter(c?.criterionOption.type)}
|
||||
onEditCriterion={(c) =>
|
||||
showEditFilter(c?.criterionOption.type)
|
||||
}
|
||||
onRemoveCriterion={removeCriterion}
|
||||
onRemoveAllCriterion={() => clearAllCriteria(true)}
|
||||
onEditSearchTerm={() => {
|
||||
|
|
@ -843,14 +708,28 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
setSearchFocus(true);
|
||||
}}
|
||||
onRemoveSearchTerm={() => setFilter(filter.clearSearchTerm())}
|
||||
/>
|
||||
}
|
||||
selectionSection={
|
||||
<ToolbarSelectionSection
|
||||
selected={selectedIds.size}
|
||||
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
||||
onSelectAll={() => onSelectAll()}
|
||||
onSelectNone={() => onSelectNone()}
|
||||
/>
|
||||
}
|
||||
operationSection={
|
||||
<SceneListOperations
|
||||
items={items.length}
|
||||
hasSelection={hasSelection}
|
||||
operations={otherOperations}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onCreateNew={onCreateNew}
|
||||
onPlay={onPlay}
|
||||
onCreateNew={onCreateNew}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ButtonToolbar>
|
||||
|
||||
<ListResultsHeader
|
||||
loading={cachedResult.loading}
|
||||
|
|
|
|||
|
|
@ -902,46 +902,6 @@ input[type="range"].blue-slider {
|
|||
}
|
||||
}
|
||||
|
||||
.scene-list .filtered-list-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 1rem;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
&:first-child {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
&:nth-child(2) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scene-list.hide-sidebar .sidebar-toggle-button {
|
||||
transition-delay: 0.1s;
|
||||
transition-duration: 0;
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
.scene-wall,
|
||||
.marker-wall {
|
||||
.wall-item {
|
||||
|
|
@ -998,214 +958,3 @@ input[type="range"].blue-slider {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scene-list-toolbar,
|
||||
.scene-list-header {
|
||||
align-items: center;
|
||||
background-color: $body-bg;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
> div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-start;
|
||||
|
||||
&:last-child {
|
||||
flex-shrink: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scene-list-toolbar {
|
||||
flex-wrap: nowrap;
|
||||
gap: 1rem;
|
||||
// offset the main padding
|
||||
margin-top: -0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
position: sticky;
|
||||
top: $navbar-height;
|
||||
z-index: 10;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.selected-items-info .btn {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
// hide drop down menu items for play and create new
|
||||
// when the buttons are visible
|
||||
@include media-breakpoint-up(sm) {
|
||||
.scene-list-operations {
|
||||
.play-item,
|
||||
.create-new-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hide play and create new buttons on xs screens
|
||||
// show these in the drop down menu instead
|
||||
@include media-breakpoint-down(xs) {
|
||||
.play-button,
|
||||
.create-new-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-items-info,
|
||||
> div.filter-section {
|
||||
border: 1px solid $secondary;
|
||||
border-radius: 0.25rem;
|
||||
flex-grow: 1;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
> div.filter-toolbar {
|
||||
border: 1px solid $secondary;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
.filter-button {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-toggle-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
border-right: 1px solid $secondary;
|
||||
display: block;
|
||||
margin-right: -0.5rem;
|
||||
min-width: calc($sidebar-width - 15px);
|
||||
padding-right: 10px;
|
||||
|
||||
.search-term-input {
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
|
||||
.clearable-text-field {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-tags {
|
||||
flex-grow: 1;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 0;
|
||||
|
||||
// account for filter button, and toggle sidebar buttons with gaps
|
||||
width: calc(100% - 70px - 1rem);
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
.sidebar-pane:not(.hide-sidebar) .scene-list-toolbar .search-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@include media-breakpoint-down(md) {
|
||||
.sidebar-pane.hide-sidebar .scene-list-toolbar .search-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// hide Edit Filter button on larger screens
|
||||
@include media-breakpoint-up(lg) {
|
||||
.scene-list .sidebar .edit-filter-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// hide the filter icon button when sidebar is shown on smaller screens
|
||||
@include media-breakpoint-down(md) {
|
||||
.sidebar-pane:not(.hide-sidebar) .scene-list-toolbar .filter-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// adjust the width of the filter-tags as well
|
||||
.sidebar-pane:not(.hide-sidebar) .scene-list-toolbar .filter-tags {
|
||||
width: calc(100% - 35px - 0.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
// move the sidebar toggle to the left on xl viewports
|
||||
@include media-breakpoint-up(xl) {
|
||||
.scene-list .scene-list-toolbar .filter-section {
|
||||
.sidebar-toggle-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.filter-tags {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hide the search term tag item when the search box is visible
|
||||
@include media-breakpoint-up(lg) {
|
||||
.scene-list-toolbar .filter-tags .search-term-filter-tag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@include media-breakpoint-down(md) {
|
||||
.sidebar-pane:not(.hide-sidebar)
|
||||
.scene-list-toolbar
|
||||
.filter-tags
|
||||
.search-term-filter-tag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scene-list-header {
|
||||
flex-wrap: wrap-reverse;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.paginationIndex {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// center the header on smaller screens
|
||||
@include media-breakpoint-down(sm) {
|
||||
& > div,
|
||||
& > div:last-child {
|
||||
flex-basis: 100%;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-body .scene-list-toolbar {
|
||||
top: calc($sticky-detail-header-height + $navbar-height);
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#more-criteria-popover {
|
||||
box-shadow: 0 8px 10px 2px rgb(0 0 0 / 30%);
|
||||
max-width: 400px;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import ScreenUtils, { useMediaQuery } from "src/utils/screen";
|
|||
import { IViewConfig, useInterfaceLocalForage } from "src/hooks/LocalForage";
|
||||
import { View } from "../List/views";
|
||||
import cx from "classnames";
|
||||
import { Button, ButtonToolbar, CollapseProps } from "react-bootstrap";
|
||||
import { Button, CollapseProps } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Icon } from "./Icon";
|
||||
import { faSliders } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const fixedSidebarMediaQuery = "only screen and (max-width: 1199px)";
|
||||
|
||||
|
|
@ -79,62 +81,19 @@ export const SidebarSection: React.FC<
|
|||
);
|
||||
};
|
||||
|
||||
export const SidebarIcon: React.FC = () => (
|
||||
<>
|
||||
{/* From: https://iconduck.com/icons/19707/sidebar
|
||||
MIT License
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE. */}
|
||||
<svg
|
||||
className="svg-inline--fa fa-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="9" y1="3" x2="9" y2="21" />
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
|
||||
export const SidebarToolbar: React.FC<{
|
||||
onClose?: () => void;
|
||||
}> = ({ onClose, children }) => {
|
||||
export const SidebarToggleButton: React.FC<{
|
||||
onClick: () => void;
|
||||
}> = ({ onClick }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<ButtonToolbar className="sidebar-toolbar">
|
||||
{onClose ? (
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="sidebar-close-button"
|
||||
className="minimal sidebar-toggle-button ignore-sidebar-outside-click"
|
||||
variant="secondary"
|
||||
title={intl.formatMessage({ id: "actions.sidebar.close" })}
|
||||
onClick={onClick}
|
||||
title={intl.formatMessage({ id: "actions.sidebar.toggle" })}
|
||||
>
|
||||
<SidebarIcon />
|
||||
<Icon icon={faSliders} />
|
||||
</Button>
|
||||
) : null}
|
||||
{children}
|
||||
</ButtonToolbar>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -941,11 +941,11 @@ $sticky-header-height: calc(50px + 3.3rem);
|
|||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-pane.hide-sidebar {
|
||||
> :nth-child(2) {
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue