Pinned Filters (#3675)

* Pinned Filters // Add the ability to pin filters in the new filter dialog

* Pinned Filters // Prevent overlap with x

* Pinned Filters // Pills in the button show up correctly now...

* Pinned Filters // Maximum height for mobile view

* Pinned Filters // Save in config.yml

* Style changes and minor fixes

* Pinned Filters // Increase divider space

---------

Co-authored-by: DingDongSoLong4 <99329275+DingDongSoLong4@users.noreply.github.com>
This commit is contained in:
yoshnopa 2023-04-25 21:40:28 +02:00 committed by GitHub
parent 3bc5caa6de
commit 8d3f632d4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 160 additions and 45 deletions

View file

@ -26,18 +26,24 @@ import {
faChevronDown,
faChevronRight,
faTimes,
faThumbtack,
} from "@fortawesome/free-solid-svg-icons";
import { useCompare, usePrevious } from "src/hooks/state";
import { CriterionType } from "src/models/list-filter/types";
import { useToast } from "src/hooks/Toast";
import { useConfigureUI } from "src/core/StashService";
import { IUIConfig } from "src/core/config";
interface ICriterionList {
criteria: string[];
currentCriterion?: Criterion<CriterionValue>;
setCriterion: (c: Criterion<CriterionValue>) => void;
criterionOptions: CriterionOption[];
pinnedCriterionOptions: CriterionOption[];
selected?: CriterionOption;
optionSelected: (o?: CriterionOption) => void;
onRemoveCriterion: (c: string) => void;
onTogglePin: (c: CriterionOption) => void;
}
const CriterionOptionList: React.FC<ICriterionList> = ({
@ -45,9 +51,11 @@ const CriterionOptionList: React.FC<ICriterionList> = ({
currentCriterion,
setCriterion,
criterionOptions,
pinnedCriterionOptions,
selected,
optionSelected,
onRemoveCriterion,
onTogglePin,
}) => {
const prevCriterion = usePrevious(currentCriterion);
@ -61,15 +69,22 @@ const CriterionOptionList: React.FC<ICriterionList> = ({
criterionOptions.forEach((c) => {
refs[c.type] = React.createRef();
});
pinnedCriterionOptions.forEach((c) => {
refs[c.type] = React.createRef();
});
return refs;
}, [criterionOptions]);
}, [criterionOptions, pinnedCriterionOptions]);
function onSelect(k: string | null) {
if (!k) {
optionSelected(undefined);
return;
}
const option = criterionOptions.find((c) => c.type === k);
let option = criterionOptions.find((c) => c.type === k);
if (!option) {
option = pinnedCriterionOptions.find((c) => c.type === k);
}
if (option) {
optionSelected(option);
@ -104,47 +119,71 @@ const CriterionOptionList: React.FC<ICriterionList> = ({
onRemoveCriterion(t);
}
function togglePin(ev: React.MouseEvent, c: CriterionOption) {
// needed to prevent the nav item from being selected
ev.stopPropagation();
ev.preventDefault();
onTogglePin(c);
}
function renderCard(c: CriterionOption, isPin: boolean) {
return (
<Card key={c.type} data-type={c.type} ref={criteriaRefs[c.type]!}>
<Accordion.Toggle eventKey={c.type} as={Card.Header}>
<span className="mr-auto">
<Icon
className="collapse-icon fa-fw"
icon={type === c.type ? faChevronDown : faChevronRight}
/>
<FormattedMessage id={c.messageID} />
</span>
{criteria.some((cc) => c.type === cc) && (
<Button
className="remove-criterion-button"
variant="minimal"
onClick={(e) => removeClicked(e, c.type)}
>
<Icon icon={faTimes} />
</Button>
)}
<Button
className="pin-criterion-button"
variant="minimal"
onClick={(e) => togglePin(e, c)}
>
<Icon icon={faThumbtack} className={isPin ? "" : "tilted"} />
</Button>
</Accordion.Toggle>
<Accordion.Collapse eventKey={c.type}>
{(type === c.type && currentCriterion) ||
(prevType === c.type && prevCriterion) ? (
<Card.Body>
<CriterionEditor
criterion={getReleventCriterion(c.type)!}
setCriterion={setCriterion}
/>
</Card.Body>
) : (
<Card.Body></Card.Body>
)}
</Accordion.Collapse>
</Card>
);
}
return (
<Accordion
className="criterion-list"
activeKey={selected?.type}
onSelect={onSelect}
>
{criterionOptions.map((c) => (
<Card key={c.type} data-type={c.type} ref={criteriaRefs[c.type]!}>
<Accordion.Toggle eventKey={c.type} as={Card.Header}>
<span>
<Icon
className="collapse-icon fa-fw"
icon={type === c.type ? faChevronDown : faChevronRight}
/>
<FormattedMessage id={c.messageID} />
</span>
{criteria.some((cc) => c.type === cc) && (
<Button
className="remove-criterion-button"
variant="minimal"
onClick={(e) => removeClicked(e, c.type)}
>
<Icon icon={faTimes} />
</Button>
)}
</Accordion.Toggle>
<Accordion.Collapse eventKey={c.type}>
{(type === c.type && currentCriterion) ||
(prevType === c.type && prevCriterion) ? (
<Card.Body>
<CriterionEditor
criterion={getReleventCriterion(c.type)!}
setCriterion={setCriterion}
/>
</Card.Body>
) : (
<Card.Body></Card.Body>
)}
</Accordion.Collapse>
</Card>
))}
{pinnedCriterionOptions.length !== 0 && (
<>
{pinnedCriterionOptions.map((c) => renderCard(c, true))}
<div className="pinned-criterion-divider" />
</>
)}
{criterionOptions.map((c) => renderCard(c, false))}
</Accordion>
);
};
@ -162,9 +201,10 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
onApply,
onCancel,
}) => {
const Toast = useToast();
const intl = useIntl();
const { configuration: config } = useContext(ConfigurationContext);
const { configuration } = useContext(ConfigurationContext);
const [currentFilter, setCurrentFilter] = useState<ListFilterModel>(
cloneDeep(filter)
@ -209,11 +249,27 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
if (existing) {
setCriterion(existing);
} else {
const newCriterion = makeCriteria(config, option.type);
const newCriterion = makeCriteria(configuration, option.type);
setCriterion(newCriterion);
}
},
[criteria, config]
[criteria, configuration]
);
const ui = (configuration?.ui ?? {}) as IUIConfig;
const [saveUI] = useConfigureUI();
const pinnedFilters = useMemo(
() => ui.pinnedFilters?.[currentFilter.mode.toLowerCase()] ?? [],
[currentFilter.mode, ui.pinnedFilters]
);
const pinnedElements = useMemo(
() => criterionOptions.filter((c) => pinnedFilters.includes(c.messageID)),
[pinnedFilters, criterionOptions]
);
const unpinnedElements = useMemo(
() => criterionOptions.filter((c) => !pinnedFilters.includes(c.messageID)),
[pinnedFilters, criterionOptions]
);
const editingCriterionChanged = useCompare(editingCriterion);
@ -232,6 +288,40 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
editingCriterionChanged,
]);
async function updatePinnedFilters(filters: string[]) {
const currentMode = currentFilter.mode.toLowerCase();
try {
await saveUI({
variables: {
input: {
...configuration?.ui,
pinnedFilters: {
...ui.pinnedFilters,
[currentMode]: filters,
},
},
},
});
} catch (e) {
Toast.error(e);
}
}
async function onTogglePinFilter(f: CriterionOption) {
try {
const existing = pinnedFilters.find((name) => name === f.messageID);
if (existing) {
await updatePinnedFilters(
pinnedFilters.filter((name) => name !== f.messageID)
);
} else {
await updatePinnedFilters([...pinnedFilters, f.messageID]);
}
} catch (err) {
Toast.error(err);
}
}
function replaceCriterion(c: Criterion<CriterionValue>) {
const newFilter = cloneDeep(currentFilter);
@ -309,10 +399,12 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
criteria={criteriaList}
currentCriterion={criterion}
setCriterion={replaceCriterion}
criterionOptions={criterionOptions}
criterionOptions={unpinnedElements}
pinnedCriterionOptions={pinnedElements}
optionSelected={optionSelected}
selected={criterion?.criterionOption}
onRemoveCriterion={(c) => removeCriterionString(c)}
onTogglePin={(c) => onTogglePinFilter(c)}
/>
{criteria.length > 0 && (
<div>

View file

@ -134,6 +134,10 @@ input[type="range"].zoom-slider {
max-height: 550px;
overflow-y: auto;
.pinned-criterion-divider {
padding-bottom: 2.5rem;
}
.card {
border: 1px solid rgb(16 22 26 / 40%);
box-shadow: none;
@ -147,15 +151,25 @@ input[type="range"].zoom-slider {
.card-header {
cursor: pointer;
display: flex;
justify-content: space-between;
}
}
.btn {
border: 0;
padding-bottom: 0;
padding-top: 0;
}
.pin-criterion-button {
color: $text_color;
&:hover svg {
transform: rotate(0);
}
}
.remove-criterion-button {
border: 0;
color: $danger;
padding-bottom: 0;
padding-top: 0;
}
}
@ -196,3 +210,7 @@ input[type="range"].zoom-slider {
z-index: 2;
}
}
.tilted {
transform: rotate(45deg);
}

View file

@ -25,6 +25,9 @@ export interface ICustomFilter extends ITypename {
direction: SortDirectionEnum;
}
// NOTE: This value cannot be more defined, because the generated enum it depends upon is UpperCase, which leads to errors on saving
export type PinnedFilters = Record<string, Array<string>>;
export type FrontPageContent = ISavedFilterRow | ICustomFilter;
export const defaultMaxOptionsShown = 200;
@ -55,6 +58,8 @@ export interface IUIConfig {
imageWallOptions?: ImageWallOptions;
lastNoteSeen?: number;
pinnedFilters?: PinnedFilters;
}
function recentlyReleased(