From 8d3f632d4c0266fa9d887ffc41177aa26745da4f Mon Sep 17 00:00:00 2001 From: yoshnopa <40072150+yoshnopa@users.noreply.github.com> Date: Tue, 25 Apr 2023 21:40:28 +0200 Subject: [PATCH] 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> --- .../src/components/List/EditFilterDialog.tsx | 174 +++++++++++++----- ui/v2.5/src/components/List/styles.scss | 26 ++- ui/v2.5/src/core/config.ts | 5 + 3 files changed, 160 insertions(+), 45 deletions(-) diff --git a/ui/v2.5/src/components/List/EditFilterDialog.tsx b/ui/v2.5/src/components/List/EditFilterDialog.tsx index 7097bc1d8..4e52f1259 100644 --- a/ui/v2.5/src/components/List/EditFilterDialog.tsx +++ b/ui/v2.5/src/components/List/EditFilterDialog.tsx @@ -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; setCriterion: (c: Criterion) => void; criterionOptions: CriterionOption[]; + pinnedCriterionOptions: CriterionOption[]; selected?: CriterionOption; optionSelected: (o?: CriterionOption) => void; onRemoveCriterion: (c: string) => void; + onTogglePin: (c: CriterionOption) => void; } const CriterionOptionList: React.FC = ({ @@ -45,9 +51,11 @@ const CriterionOptionList: React.FC = ({ currentCriterion, setCriterion, criterionOptions, + pinnedCriterionOptions, selected, optionSelected, onRemoveCriterion, + onTogglePin, }) => { const prevCriterion = usePrevious(currentCriterion); @@ -61,15 +69,22 @@ const CriterionOptionList: React.FC = ({ 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 = ({ 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 ( + + + + + + + {criteria.some((cc) => c.type === cc) && ( + + )} + + + + {(type === c.type && currentCriterion) || + (prevType === c.type && prevCriterion) ? ( + + + + ) : ( + + )} + + + ); + } + return ( - {criterionOptions.map((c) => ( - - - - - - - {criteria.some((cc) => c.type === cc) && ( - - )} - - - {(type === c.type && currentCriterion) || - (prevType === c.type && prevCriterion) ? ( - - - - ) : ( - - )} - - - ))} + {pinnedCriterionOptions.length !== 0 && ( + <> + {pinnedCriterionOptions.map((c) => renderCard(c, true))} +
+ + )} + {criterionOptions.map((c) => renderCard(c, false))} ); }; @@ -162,9 +201,10 @@ export const EditFilterDialog: React.FC = ({ onApply, onCancel, }) => { + const Toast = useToast(); const intl = useIntl(); - const { configuration: config } = useContext(ConfigurationContext); + const { configuration } = useContext(ConfigurationContext); const [currentFilter, setCurrentFilter] = useState( cloneDeep(filter) @@ -209,11 +249,27 @@ export const EditFilterDialog: React.FC = ({ 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 = ({ 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) { const newFilter = cloneDeep(currentFilter); @@ -309,10 +399,12 @@ export const EditFilterDialog: React.FC = ({ 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 && (
diff --git a/ui/v2.5/src/components/List/styles.scss b/ui/v2.5/src/components/List/styles.scss index c8fcb4bc4..e86199e6b 100644 --- a/ui/v2.5/src/components/List/styles.scss +++ b/ui/v2.5/src/components/List/styles.scss @@ -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); +} diff --git a/ui/v2.5/src/core/config.ts b/ui/v2.5/src/core/config.ts index 024914c94..8ca489bf3 100644 --- a/ui/v2.5/src/core/config.ts +++ b/ui/v2.5/src/core/config.ts @@ -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>; + export type FrontPageContent = ISavedFilterRow | ICustomFilter; export const defaultMaxOptionsShown = 200; @@ -55,6 +58,8 @@ export interface IUIConfig { imageWallOptions?: ImageWallOptions; lastNoteSeen?: number; + + pinnedFilters?: PinnedFilters; } function recentlyReleased(