mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Add sticky selection toolbar (#6320)
This commit is contained in:
parent
d1ee64d36f
commit
1bc32a3099
2 changed files with 121 additions and 32 deletions
|
|
@ -8,11 +8,47 @@ import {
|
||||||
IListFilterOperation,
|
IListFilterOperation,
|
||||||
ListOperationButtons,
|
ListOperationButtons,
|
||||||
} from "./ListOperationButtons";
|
} from "./ListOperationButtons";
|
||||||
import { ButtonGroup, ButtonToolbar } from "react-bootstrap";
|
import { Button, ButtonGroup, ButtonToolbar } from "react-bootstrap";
|
||||||
import { View } from "./views";
|
import { View } from "./views";
|
||||||
import { IListSelect, useFilterOperations } from "./util";
|
import { IListSelect, useFilterOperations } from "./util";
|
||||||
import { SavedFilterDropdown } from "./SavedFilterList";
|
import { SavedFilterDropdown } from "./SavedFilterList";
|
||||||
import { FilterButton } from "./Filters/FilterButton";
|
import { FilterButton } from "./Filters/FilterButton";
|
||||||
|
import { Icon } from "../Shared/Icon";
|
||||||
|
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import cx from "classnames";
|
||||||
|
|
||||||
|
const SelectionSection: React.FC<{
|
||||||
|
filter: ListFilterModel;
|
||||||
|
selected: number;
|
||||||
|
onSelectAll: () => void;
|
||||||
|
onSelectNone: () => void;
|
||||||
|
}> = ({ selected, 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 className="selected-count">{selected}</span>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="minimal"
|
||||||
|
onClick={() => onSelectAll()}
|
||||||
|
title={intl.formatMessage({ id: "actions.select_all" })}
|
||||||
|
>
|
||||||
|
<Icon icon={faSquareCheck} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export interface IItemListOperation<T extends QueryResult> {
|
export interface IItemListOperation<T extends QueryResult> {
|
||||||
text: string;
|
text: string;
|
||||||
|
|
@ -62,9 +98,21 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
||||||
setFilter,
|
setFilter,
|
||||||
});
|
});
|
||||||
const { selectedIds, onSelectAll, onSelectNone } = listSelect;
|
const { selectedIds, onSelectAll, onSelectNone } = listSelect;
|
||||||
|
const hasSelection = selectedIds.size > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonToolbar className="filtered-list-toolbar">
|
<ButtonToolbar
|
||||||
|
className={cx("filtered-list-toolbar", { "has-selection": hasSelection })}
|
||||||
|
>
|
||||||
|
{hasSelection ? (
|
||||||
|
<SelectionSection
|
||||||
|
filter={filter}
|
||||||
|
selected={selectedIds.size}
|
||||||
|
onSelectAll={onSelectAll}
|
||||||
|
onSelectNone={onSelectNone}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<SearchTermInput filter={filter} onFilterUpdate={setFilter} />
|
<SearchTermInput filter={filter} onFilterUpdate={setFilter} />
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
|
|
@ -73,7 +121,10 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
||||||
onSetFilter={setFilter}
|
onSetFilter={setFilter}
|
||||||
view={view}
|
view={view}
|
||||||
/>
|
/>
|
||||||
<FilterButton onClick={() => showEditFilter()} count={filter.count()} />
|
<FilterButton
|
||||||
|
onClick={() => showEditFilter()}
|
||||||
|
count={filter.count()}
|
||||||
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<SortBySelect
|
<SortBySelect
|
||||||
|
|
@ -81,14 +132,20 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
||||||
sortDirection={filter.sortDirection}
|
sortDirection={filter.sortDirection}
|
||||||
options={filterOptions.sortByOptions}
|
options={filterOptions.sortByOptions}
|
||||||
onChangeSortBy={(e) => setFilter(filter.setSortBy(e ?? undefined))}
|
onChangeSortBy={(e) => setFilter(filter.setSortBy(e ?? undefined))}
|
||||||
onChangeSortDirection={() => setFilter(filter.toggleSortDirection())}
|
onChangeSortDirection={() =>
|
||||||
onReshuffleRandomSort={() => setFilter(filter.reshuffleRandomSort())}
|
setFilter(filter.toggleSortDirection())
|
||||||
|
}
|
||||||
|
onReshuffleRandomSort={() =>
|
||||||
|
setFilter(filter.reshuffleRandomSort())
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageSizeSelector
|
<PageSizeSelector
|
||||||
pageSize={filter.itemsPerPage}
|
pageSize={filter.itemsPerPage}
|
||||||
setPageSize={(size) => setFilter(filter.setPageSize(size))}
|
setPageSize={(size) => setFilter(filter.setPageSize(size))}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<ListOperationButtons
|
<ListOperationButtons
|
||||||
onSelectAll={onSelectAll}
|
onSelectAll={onSelectAll}
|
||||||
|
|
|
||||||
|
|
@ -918,9 +918,14 @@ input[type="range"].zoom-slider {
|
||||||
|
|
||||||
.filtered-list-toolbar {
|
.filtered-list-toolbar {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
background-color: $body-bg;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
// offset the main padding
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
|
||||||
& > .btn-group {
|
& > .btn-group {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
@ -1091,10 +1096,6 @@ input[type="range"].zoom-slider {
|
||||||
&.filtered-list-toolbar {
|
&.filtered-list-toolbar {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
// offset the main padding
|
|
||||||
margin-top: -0.5rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: $navbar-height;
|
top: $navbar-height;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
@ -1141,11 +1142,6 @@ input[type="range"].zoom-slider {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-items-info {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div:first-child,
|
> div:first-child,
|
||||||
> div:last-child {
|
> div:last-child {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -1433,3 +1429,39 @@ input[type="range"].zoom-slider {
|
||||||
.duration-preset {
|
.duration-preset {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-items-info {
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid $secondary;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-list-toolbar .selected-items-info {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-list-container > .filtered-list-toolbar.has-selection {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
position: sticky;
|
||||||
|
top: $navbar-height;
|
||||||
|
width: fit-content;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
@include media-breakpoint-down(xs) {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-body .filtered-list-toolbar.has-selection {
|
||||||
|
top: calc($sticky-detail-header-height + $navbar-height);
|
||||||
|
|
||||||
|
@include media-breakpoint-down(xs) {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue