mirror of
https://github.com/stashapp/stash.git
synced 2025-12-09 01:44:52 +01:00
Fix saved filter UI bugs (#4394)
* Fix missing intl strings * Fix saved filter list z-index * Fix saved filter list double scrollbar * Display error inside filter list * Filter out nonexistent saved filter rows in FrontPageConfig
This commit is contained in:
parent
0cdea209bb
commit
9bd36408ee
6 changed files with 59 additions and 46 deletions
|
|
@ -3,11 +3,7 @@ import { FormattedMessage, IntlShape, useIntl } from "react-intl";
|
||||||
import { useFindSavedFilters } from "src/core/StashService";
|
import { useFindSavedFilters } from "src/core/StashService";
|
||||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||||
import { Button, Form, Modal } from "react-bootstrap";
|
import { Button, Form, Modal } from "react-bootstrap";
|
||||||
import {
|
import * as GQL from "src/core/generated-graphql";
|
||||||
FilterMode,
|
|
||||||
FindSavedFiltersQuery,
|
|
||||||
SavedFilter,
|
|
||||||
} from "src/core/generated-graphql";
|
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import {
|
import {
|
||||||
IUIConfig,
|
IUIConfig,
|
||||||
|
|
@ -21,24 +17,25 @@ import {
|
||||||
interface IAddSavedFilterModalProps {
|
interface IAddSavedFilterModalProps {
|
||||||
onClose: (content?: FrontPageContent) => void;
|
onClose: (content?: FrontPageContent) => void;
|
||||||
existingSavedFilterIDs: string[];
|
existingSavedFilterIDs: string[];
|
||||||
candidates: FindSavedFiltersQuery;
|
candidates: GQL.FindSavedFiltersQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilterModeToMessageID = {
|
const FilterModeToMessageID = {
|
||||||
[FilterMode.Galleries]: "galleries",
|
[GQL.FilterMode.Galleries]: "galleries",
|
||||||
[FilterMode.Images]: "images",
|
[GQL.FilterMode.Images]: "images",
|
||||||
[FilterMode.Movies]: "movies",
|
[GQL.FilterMode.Movies]: "movies",
|
||||||
[FilterMode.Performers]: "performers",
|
[GQL.FilterMode.Performers]: "performers",
|
||||||
[FilterMode.SceneMarkers]: "markers",
|
[GQL.FilterMode.SceneMarkers]: "markers",
|
||||||
[FilterMode.Scenes]: "scenes",
|
[GQL.FilterMode.Scenes]: "scenes",
|
||||||
[FilterMode.Studios]: "studios",
|
[GQL.FilterMode.Studios]: "studios",
|
||||||
[FilterMode.Tags]: "tags",
|
[GQL.FilterMode.Tags]: "tags",
|
||||||
};
|
};
|
||||||
|
|
||||||
function filterTitle(intl: IntlShape, f: Pick<SavedFilter, "mode" | "name">) {
|
type SavedFilter = Pick<GQL.SavedFilter, "id" | "mode" | "name">;
|
||||||
return `${intl.formatMessage({ id: FilterModeToMessageID[f.mode] })}: ${
|
|
||||||
f.name
|
function filterTitle(intl: IntlShape, f: SavedFilter) {
|
||||||
}`;
|
const typeMessage = intl.formatMessage({ id: FilterModeToMessageID[f.mode] });
|
||||||
|
return `${typeMessage}: ${f.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddContentModal: React.FC<IAddSavedFilterModalProps> = ({
|
const AddContentModal: React.FC<IAddSavedFilterModalProps> = ({
|
||||||
|
|
@ -98,7 +95,7 @@ const AddContentModal: React.FC<IAddSavedFilterModalProps> = ({
|
||||||
.filter((f) => {
|
.filter((f) => {
|
||||||
// markers not currently supported
|
// markers not currently supported
|
||||||
return (
|
return (
|
||||||
f.mode !== FilterMode.SceneMarkers &&
|
f.mode !== GQL.FilterMode.SceneMarkers &&
|
||||||
!existingSavedFilterIDs.includes(f.id)
|
!existingSavedFilterIDs.includes(f.id)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
@ -232,7 +229,7 @@ const AddContentModal: React.FC<IAddSavedFilterModalProps> = ({
|
||||||
|
|
||||||
interface IFilterRowProps {
|
interface IFilterRowProps {
|
||||||
content: FrontPageContent;
|
content: FrontPageContent;
|
||||||
allSavedFilters: Pick<SavedFilter, "id" | "mode" | "name">[];
|
allSavedFilters: SavedFilter[];
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,10 +239,9 @@ const ContentRow: React.FC<IFilterRowProps> = (props: IFilterRowProps) => {
|
||||||
function title() {
|
function title() {
|
||||||
switch (props.content.__typename) {
|
switch (props.content.__typename) {
|
||||||
case "SavedFilter":
|
case "SavedFilter":
|
||||||
|
const savedFilterId = String(props.content.savedFilterId);
|
||||||
const savedFilter = props.allSavedFilters.find(
|
const savedFilter = props.allSavedFilters.find(
|
||||||
(f) =>
|
(f) => f.id === savedFilterId
|
||||||
f.id ===
|
|
||||||
(props.content as ISavedFilterRow).savedFilterId?.toString()
|
|
||||||
);
|
);
|
||||||
if (!savedFilter) return "";
|
if (!savedFilter) return "";
|
||||||
return filterTitle(intl, savedFilter);
|
return filterTitle(intl, savedFilter);
|
||||||
|
|
@ -302,7 +298,18 @@ export const FrontPageConfig: React.FC<IFrontPageConfigProps> = ({
|
||||||
|
|
||||||
const frontPageContent = getFrontPageContent(ui);
|
const frontPageContent = getFrontPageContent(ui);
|
||||||
if (frontPageContent) {
|
if (frontPageContent) {
|
||||||
setCurrentContent(frontPageContent);
|
setCurrentContent(
|
||||||
|
// filter out rows where the saved filter no longer exists
|
||||||
|
frontPageContent.filter((r) => {
|
||||||
|
if (r.__typename === "SavedFilter") {
|
||||||
|
const savedFilterId = String(r.savedFilterId);
|
||||||
|
return allFilters.findSavedFilters.some(
|
||||||
|
(f) => f.id === savedFilterId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [allFilters, ui]);
|
}, [allFilters, ui]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export const PhashFilter: React.FC<IPhashFilterProps> = ({
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
onChange={valueChanged}
|
onChange={valueChanged}
|
||||||
value={value ? value.value : ""}
|
value={value ? value.value : ""}
|
||||||
placeholder={intl.formatMessage({ id: "phash" })}
|
placeholder={intl.formatMessage({ id: "media_info.phash" })}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
{criterion.modifier !== CriterionModifier.IsNull &&
|
{criterion.modifier !== CriterionModifier.IsNull &&
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
|
|
@ -39,7 +39,6 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const { data, error, loading, refetch } = useFindSavedFilters(filter.mode);
|
const { data, error, loading, refetch } = useFindSavedFilters(filter.mode);
|
||||||
const oldError = useRef(error);
|
|
||||||
|
|
||||||
const [filterName, setFilterName] = useState("");
|
const [filterName, setFilterName] = useState("");
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
@ -56,14 +55,6 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||||
|
|
||||||
const savedFilters = data?.findSavedFilters ?? [];
|
const savedFilters = data?.findSavedFilters ?? [];
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (error && error !== oldError.current) {
|
|
||||||
Toast.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
oldError.current = error;
|
|
||||||
}, [error, Toast, oldError]);
|
|
||||||
|
|
||||||
async function onSaveFilter(name: string, id?: string) {
|
async function onSaveFilter(name: string, id?: string) {
|
||||||
const filterCopy = filter.clone();
|
const filterCopy = filter.clone();
|
||||||
|
|
||||||
|
|
@ -285,6 +276,8 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSavedFilters() {
|
function renderSavedFilters() {
|
||||||
|
if (error) return <h6 className="text-center">{error.message}</h6>;
|
||||||
|
|
||||||
if (loading || saving) {
|
if (loading || saving) {
|
||||||
return (
|
return (
|
||||||
<div className="loading">
|
<div className="loading">
|
||||||
|
|
@ -311,20 +304,22 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||||
function maybeRenderSetDefaultButton() {
|
function maybeRenderSetDefaultButton() {
|
||||||
if (persistState === PersistanceLevel.ALL) {
|
if (persistState === PersistanceLevel.ALL) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<div className="mt-1">
|
||||||
className="set-as-default-button"
|
<Button
|
||||||
variant="secondary"
|
className="set-as-default-button"
|
||||||
size="sm"
|
variant="secondary"
|
||||||
onClick={() => onSetDefaultFilter()}
|
size="sm"
|
||||||
>
|
onClick={() => onSetDefaultFilter()}
|
||||||
{intl.formatMessage({ id: "actions.set_as_default" })}
|
>
|
||||||
</Button>
|
{intl.formatMessage({ id: "actions.set_as_default" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
{maybeRenderDeleteAlert()}
|
{maybeRenderDeleteAlert()}
|
||||||
{maybeRenderOverwriteAlert()}
|
{maybeRenderOverwriteAlert()}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
|
|
@ -359,6 +354,6 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
{renderSavedFilters()}
|
{renderSavedFilters()}
|
||||||
{maybeRenderSetDefaultButton()}
|
{maybeRenderSetDefaultButton()}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,14 @@ input[type="range"].zoom-slider {
|
||||||
.saved-filter-list-menu {
|
.saved-filter-list-menu {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
|
||||||
|
&.dropdown-menu.show {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.set-as-default-button {
|
.set-as-default-button {
|
||||||
float: right;
|
float: right;
|
||||||
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LoadingIndicator {
|
.LoadingIndicator {
|
||||||
|
|
@ -94,12 +100,17 @@ input[type="range"].zoom-slider {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: inline;
|
display: inline;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
padding-left: 1.25rem;
|
||||||
padding-right: 0.25rem;
|
padding-right: 0.25rem;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group {
|
.btn-group {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-button {
|
.delete-button {
|
||||||
|
|
|
||||||
|
|
@ -660,7 +660,6 @@ div.react-select__menu,
|
||||||
div.dropdown-menu {
|
div.dropdown-menu {
|
||||||
background-color: $secondary;
|
background-color: $secondary;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
z-index: 1600;
|
|
||||||
|
|
||||||
.react-select__option,
|
.react-select__option,
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
|
|
|
||||||
|
|
@ -921,6 +921,7 @@
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"wall": "Wall"
|
"wall": "Wall"
|
||||||
},
|
},
|
||||||
|
"distance": "Distance",
|
||||||
"donate": "Donate",
|
"donate": "Donate",
|
||||||
"dupe_check": {
|
"dupe_check": {
|
||||||
"description": "Levels below 'Exact' can take longer to calculate. False positives might also be returned on lower accuracy levels.",
|
"description": "Levels below 'Exact' can take longer to calculate. False positives might also be returned on lower accuracy levels.",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue