mirror of
https://github.com/stashapp/stash.git
synced 2026-01-23 16:43:39 +01:00
Add Invert Selection feature to list toolbars (#6491)
This commit is contained in:
parent
0fa132cf60
commit
211f06963e
9 changed files with 87 additions and 29 deletions
|
|
@ -76,7 +76,8 @@ const Toolbar: React.FC<IFilteredListToolbar> = ({
|
|||
onDelete,
|
||||
operations,
|
||||
}) => {
|
||||
const { getSelected, onSelectAll, onSelectNone } = useListContext();
|
||||
const { getSelected, onSelectAll, onSelectNone, onInvertSelection } =
|
||||
useListContext();
|
||||
const { filter, setFilter } = useFilter();
|
||||
|
||||
return (
|
||||
|
|
@ -91,6 +92,7 @@ const Toolbar: React.FC<IFilteredListToolbar> = ({
|
|||
<ListOperationButtons
|
||||
onSelectAll={onSelectAll}
|
||||
onSelectNone={onSelectNone}
|
||||
onInvertSelection={onInvertSelection}
|
||||
itemsSelected={getSelected().length > 0}
|
||||
otherOperations={operations}
|
||||
onEdit={onEdit}
|
||||
|
|
|
|||
|
|
@ -99,13 +99,15 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
|||
filter,
|
||||
setFilter,
|
||||
});
|
||||
const { selectedIds, onSelectAll, onSelectNone } = listSelect;
|
||||
const { selectedIds, onSelectAll, onSelectNone, onInvertSelection } =
|
||||
listSelect;
|
||||
const hasSelection = selectedIds.size > 0;
|
||||
|
||||
const renderOperations = operationComponent ?? (
|
||||
<ListOperationButtons
|
||||
onSelectAll={onSelectAll}
|
||||
onSelectNone={onSelectNone}
|
||||
onInvertSelection={onInvertSelection}
|
||||
otherOperations={operations}
|
||||
itemsSelected={selectedIds.size > 0}
|
||||
onEdit={onEdit}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export function useFilteredItemList<
|
|||
const { result, items, totalCount, pages } = queryResult;
|
||||
|
||||
const listSelect = useListSelect(items);
|
||||
const { onSelectAll, onSelectNone } = listSelect;
|
||||
const { onSelectAll, onSelectNone, onInvertSelection } = listSelect;
|
||||
|
||||
const modalState = useModal();
|
||||
const { showModal, closeModal } = modalState;
|
||||
|
|
@ -99,6 +99,7 @@ export function useFilteredItemList<
|
|||
onChangePage: setPage,
|
||||
onSelectAll,
|
||||
onSelectNone,
|
||||
onInvertSelection,
|
||||
pages,
|
||||
showEditFilter,
|
||||
});
|
||||
|
|
@ -164,6 +165,7 @@ export const ItemList = <T extends QueryResult, E extends IHasID, M = unknown>(
|
|||
onSelectChange,
|
||||
onSelectAll,
|
||||
onSelectNone,
|
||||
onInvertSelection,
|
||||
} = listSelect;
|
||||
|
||||
// scroll to the top of the page when the page changes
|
||||
|
|
@ -212,6 +214,7 @@ export const ItemList = <T extends QueryResult, E extends IHasID, M = unknown>(
|
|||
onChangePage,
|
||||
onSelectAll,
|
||||
onSelectNone,
|
||||
onInvertSelection,
|
||||
pages,
|
||||
showEditFilter,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export interface IListFilterOperation {
|
|||
interface IListOperationButtonsProps {
|
||||
onSelectAll?: () => void;
|
||||
onSelectNone?: () => void;
|
||||
onInvertSelection?: () => void;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
itemsSelected?: boolean;
|
||||
|
|
@ -72,6 +73,7 @@ interface IListOperationButtonsProps {
|
|||
export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
|
||||
onSelectAll,
|
||||
onSelectNone,
|
||||
onInvertSelection,
|
||||
onEdit,
|
||||
onDelete,
|
||||
itemsSelected,
|
||||
|
|
@ -82,6 +84,7 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
|
|||
useEffect(() => {
|
||||
Mousetrap.bind("s a", () => onSelectAll?.());
|
||||
Mousetrap.bind("s n", () => onSelectNone?.());
|
||||
Mousetrap.bind("s i", () => onInvertSelection?.());
|
||||
|
||||
Mousetrap.bind("e", () => {
|
||||
if (itemsSelected) {
|
||||
|
|
@ -98,10 +101,18 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
|
|||
return () => {
|
||||
Mousetrap.unbind("s a");
|
||||
Mousetrap.unbind("s n");
|
||||
Mousetrap.unbind("s i");
|
||||
Mousetrap.unbind("e");
|
||||
Mousetrap.unbind("d d");
|
||||
};
|
||||
});
|
||||
}, [
|
||||
onSelectAll,
|
||||
onSelectNone,
|
||||
onInvertSelection,
|
||||
itemsSelected,
|
||||
onEdit,
|
||||
onDelete,
|
||||
]);
|
||||
|
||||
const buttons = useMemo(() => {
|
||||
const ret = (otherOperations ?? []).filter((o) => {
|
||||
|
|
@ -185,7 +196,25 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
const options = [renderSelectAll(), renderSelectNone()].filter((o) => o);
|
||||
function renderInvertSelection() {
|
||||
if (onInvertSelection) {
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key="invert-selection"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onInvertSelection?.()}
|
||||
>
|
||||
<FormattedMessage id="actions.invert_selection" />
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const options = [
|
||||
renderSelectAll(),
|
||||
renderSelectNone(),
|
||||
renderInvertSelection(),
|
||||
].filter((o) => o);
|
||||
|
||||
if (otherOperations) {
|
||||
otherOperations
|
||||
|
|
@ -219,7 +248,7 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
|
|||
{options.length > 0 ? options : undefined}
|
||||
</OperationDropdown>
|
||||
);
|
||||
}, [otherOperations, onSelectAll, onSelectNone]);
|
||||
}, [otherOperations, onSelectAll, onSelectNone, onInvertSelection]);
|
||||
|
||||
// don't render anything if there are no buttons or operations
|
||||
if (buttons.length === 0 && !moreDropdown) {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ const emptyState: IListContextState = {
|
|||
onSelectChange: () => {},
|
||||
onSelectAll: () => {},
|
||||
onSelectNone: () => {},
|
||||
onInvertSelection: () => {},
|
||||
items: [],
|
||||
hasSelection: false,
|
||||
selectedItems: [],
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ export function useListKeyboardShortcuts(props: {
|
|||
pages?: number;
|
||||
onSelectAll?: () => void;
|
||||
onSelectNone?: () => void;
|
||||
onInvertSelection?: () => void;
|
||||
}) {
|
||||
const {
|
||||
currentPage,
|
||||
|
|
@ -237,6 +238,7 @@ export function useListKeyboardShortcuts(props: {
|
|||
pages = 0,
|
||||
onSelectAll,
|
||||
onSelectNone,
|
||||
onInvertSelection,
|
||||
} = props;
|
||||
|
||||
// set up hotkeys
|
||||
|
|
@ -298,12 +300,14 @@ export function useListKeyboardShortcuts(props: {
|
|||
useEffect(() => {
|
||||
Mousetrap.bind("s a", () => onSelectAll?.());
|
||||
Mousetrap.bind("s n", () => onSelectNone?.());
|
||||
Mousetrap.bind("s i", () => onInvertSelection?.());
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("s a");
|
||||
Mousetrap.unbind("s n");
|
||||
Mousetrap.unbind("s i");
|
||||
};
|
||||
}, [onSelectAll, onSelectNone]);
|
||||
}, [onSelectAll, onSelectNone, onInvertSelection]);
|
||||
}
|
||||
|
||||
export function useListSelect<T extends IHasID = IHasID>(items: T[]) {
|
||||
|
|
@ -420,6 +424,14 @@ export function useListSelect<T extends IHasID = IHasID>(items: T[]) {
|
|||
setLastClickedId(undefined);
|
||||
}
|
||||
|
||||
function onInvertSelection() {
|
||||
setItemsSelected((prevSelected) => {
|
||||
const selectedSet = new Set(prevSelected.map((item) => item.id));
|
||||
return items.filter((item) => !selectedSet.has(item.id));
|
||||
});
|
||||
setLastClickedId(undefined);
|
||||
}
|
||||
|
||||
// TODO - this is for backwards compatibility
|
||||
const getSelected = useCallback(() => itemsSelected, [itemsSelected]);
|
||||
|
||||
|
|
@ -433,6 +445,7 @@ export function useListSelect<T extends IHasID = IHasID>(items: T[]) {
|
|||
onSelectChange,
|
||||
onSelectAll,
|
||||
onSelectNone,
|
||||
onInvertSelection,
|
||||
hasSelection,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -522,6 +522,7 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
onSelectChange,
|
||||
onSelectAll,
|
||||
onSelectNone,
|
||||
onInvertSelection,
|
||||
hasSelection,
|
||||
} = listSelect;
|
||||
|
||||
|
|
@ -539,6 +540,27 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
setShowSidebar,
|
||||
});
|
||||
|
||||
const onCloseEditDelete = useCloseEditDelete({
|
||||
closeModal,
|
||||
onSelectNone,
|
||||
result,
|
||||
});
|
||||
|
||||
const onEdit = useCallback(() => {
|
||||
showModal(
|
||||
<EditScenesDialog selected={selectedItems} onClose={onCloseEditDelete} />
|
||||
);
|
||||
}, [showModal, selectedItems, onCloseEditDelete]);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
showModal(
|
||||
<DeleteScenesDialog
|
||||
selected={selectedItems}
|
||||
onClose={onCloseEditDelete}
|
||||
/>
|
||||
);
|
||||
}, [showModal, selectedItems, onCloseEditDelete]);
|
||||
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("e", () => {
|
||||
if (hasSelection) {
|
||||
|
|
@ -556,18 +578,12 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
Mousetrap.unbind("e");
|
||||
Mousetrap.unbind("d d");
|
||||
};
|
||||
});
|
||||
}, [onSelectAll, onSelectNone, hasSelection, onEdit, onDelete]);
|
||||
useZoomKeybinds({
|
||||
zoomIndex: filter.zoomIndex,
|
||||
onChangeZoom: (zoom) => setFilter(filter.setZoom(zoom)),
|
||||
});
|
||||
|
||||
const onCloseEditDelete = useCloseEditDelete({
|
||||
closeModal,
|
||||
onSelectNone,
|
||||
result,
|
||||
});
|
||||
|
||||
const metadataByline = useMemo(() => {
|
||||
if (cachedResult.loading) return null;
|
||||
|
||||
|
|
@ -636,21 +652,6 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
);
|
||||
}
|
||||
|
||||
function onEdit() {
|
||||
showModal(
|
||||
<EditScenesDialog selected={selectedItems} onClose={onCloseEditDelete} />
|
||||
);
|
||||
}
|
||||
|
||||
function onDelete() {
|
||||
showModal(
|
||||
<DeleteScenesDialog
|
||||
selected={selectedItems}
|
||||
onClose={onCloseEditDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const otherOperations = [
|
||||
{
|
||||
text: intl.formatMessage({ id: "actions.play" }),
|
||||
|
|
@ -677,6 +678,11 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
onClick: () => onSelectNone(),
|
||||
isDisplayed: () => hasSelection,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({ id: "actions.invert_selection" }),
|
||||
onClick: () => onInvertSelection(),
|
||||
isDisplayed: () => totalCount > 0,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({ id: "actions.play_random" }),
|
||||
onClick: playRandom,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
| `Ctrl + End` | Go to last page of results |
|
||||
| `s a` | Select all on page |
|
||||
| `s n` | Unselect all |
|
||||
| `s i` | Invert selection |
|
||||
| `e` | Edit selected |
|
||||
| `d d` | Delete selected |
|
||||
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"select_entity": "Select {entityType}",
|
||||
"select_folders": "Select folders",
|
||||
"select_none": "Select None",
|
||||
"invert_selection": "Invert Selection",
|
||||
"selective_auto_tag": "Selective Auto Tag",
|
||||
"selective_clean": "Selective Clean",
|
||||
"selective_scan": "Selective Scan",
|
||||
|
|
|
|||
Loading…
Reference in a new issue