mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Custom fields filter UI (#5632)
* Refactor criteria and criterion options * Add custom fields filtering in UI
This commit is contained in:
@@ -29,7 +29,7 @@ import { sortByRelevance } from "src/utils/query";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
import { PatchComponent, PatchFunction } from "src/patch";
|
||||
import {
|
||||
Criterion,
|
||||
ModifierCriterion,
|
||||
CriterionValue,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { PathCriterion } from "src/models/list-filter/criteria/path";
|
||||
@@ -46,7 +46,7 @@ type Option = SelectOption<Gallery>;
|
||||
type ExtraGalleryProps = {
|
||||
hoverPlacement?: Placement;
|
||||
excludeIds?: string[];
|
||||
extraCriteria?: Array<Criterion<CriterionValue>>;
|
||||
extraCriteria?: Array<ModifierCriterion<CriterionValue>>;
|
||||
};
|
||||
|
||||
type FindGalleriesResult = Awaited<
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
DurationCriterion,
|
||||
CriterionValue,
|
||||
Criterion,
|
||||
ModifierCriterion,
|
||||
IHierarchicalLabeledIdCriterion,
|
||||
NumberCriterion,
|
||||
ILabeledIdCriterion,
|
||||
DateCriterion,
|
||||
TimestampCriterion,
|
||||
BooleanCriterion,
|
||||
Criterion,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { useIntl } from "react-intl";
|
||||
import {
|
||||
criterionIsHierarchicalLabelValue,
|
||||
criterionIsNumberValue,
|
||||
@@ -45,21 +44,21 @@ import { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
||||
import TagsFilter from "./Filters/TagsFilter";
|
||||
import { PhashCriterion } from "src/models/list-filter/criteria/phash";
|
||||
import { PhashFilter } from "./Filters/PhashFilter";
|
||||
import cx from "classnames";
|
||||
import { PathCriterion } from "src/models/list-filter/criteria/path";
|
||||
import { ModifierSelectorButtons } from "./ModifierSelect";
|
||||
import { CustomFieldsCriterion } from "src/models/list-filter/criteria/custom-fields";
|
||||
import { CustomFieldsFilter } from "./Filters/CustomFieldsFilter";
|
||||
|
||||
interface IGenericCriterionEditor {
|
||||
criterion: Criterion<CriterionValue>;
|
||||
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||
criterion: ModifierCriterion<CriterionValue>;
|
||||
setCriterion: (c: ModifierCriterion<CriterionValue>) => void;
|
||||
}
|
||||
|
||||
const GenericCriterionEditor: React.FC<IGenericCriterionEditor> = ({
|
||||
criterion,
|
||||
setCriterion,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { options, modifierOptions } = criterion.criterionOption;
|
||||
const { options, modifierOptions } = criterion.modifierCriterionOption();
|
||||
|
||||
const showModifierSelector = useMemo(() => {
|
||||
if (
|
||||
@@ -97,26 +96,17 @@ const GenericCriterionEditor: React.FC<IGenericCriterionEditor> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group className="modifier-options">
|
||||
{modifierOptions.map((m) => (
|
||||
<Button
|
||||
className={cx("modifier-option", {
|
||||
selected: criterion.modifier === m,
|
||||
})}
|
||||
key={m}
|
||||
onClick={() => onChangedModifierSelect(m)}
|
||||
>
|
||||
{Criterion.getModifierLabel(intl, m)}
|
||||
</Button>
|
||||
))}
|
||||
</Form.Group>
|
||||
<ModifierSelectorButtons
|
||||
options={modifierOptions}
|
||||
value={criterion.modifier}
|
||||
onChanged={onChangedModifierSelect}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
showModifierSelector,
|
||||
modifierOptions,
|
||||
onChangedModifierSelect,
|
||||
criterion.modifier,
|
||||
intl,
|
||||
]);
|
||||
|
||||
const valueControl = useMemo(() => {
|
||||
@@ -268,8 +258,8 @@ const GenericCriterionEditor: React.FC<IGenericCriterionEditor> = ({
|
||||
};
|
||||
|
||||
interface ICriterionEditor {
|
||||
criterion: Criterion<CriterionValue>;
|
||||
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||
criterion: Criterion;
|
||||
setCriterion: (c: Criterion) => void;
|
||||
}
|
||||
|
||||
export const CriterionEditor: React.FC<ICriterionEditor> = ({
|
||||
@@ -283,12 +273,22 @@ export const CriterionEditor: React.FC<ICriterionEditor> = ({
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericCriterionEditor
|
||||
criterion={criterion}
|
||||
setCriterion={setCriterion}
|
||||
/>
|
||||
);
|
||||
if (criterion instanceof CustomFieldsCriterion) {
|
||||
return (
|
||||
<CustomFieldsFilter criterion={criterion} setCriterion={setCriterion} />
|
||||
);
|
||||
}
|
||||
|
||||
if (criterion instanceof ModifierCriterion) {
|
||||
return (
|
||||
<GenericCriterionEditor
|
||||
criterion={criterion}
|
||||
setCriterion={setCriterion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [criterion, setCriterion]);
|
||||
|
||||
return <div className="criterion-editor">{filterControl}</div>;
|
||||
|
||||
@@ -10,7 +10,6 @@ import React, {
|
||||
import { Accordion, Button, Card, Form, Modal } from "react-bootstrap";
|
||||
import cx from "classnames";
|
||||
import {
|
||||
CriterionValue,
|
||||
Criterion,
|
||||
CriterionOption,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
@@ -38,8 +37,8 @@ import ScreenUtils from "src/utils/screen";
|
||||
|
||||
interface ICriterionList {
|
||||
criteria: string[];
|
||||
currentCriterion?: Criterion<CriterionValue>;
|
||||
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||
currentCriterion?: Criterion;
|
||||
setCriterion: (c: Criterion) => void;
|
||||
criterionOptions: CriterionOption[];
|
||||
pinnedCriterionOptions: CriterionOption[];
|
||||
selected?: CriterionOption;
|
||||
@@ -228,7 +227,7 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
||||
const [currentFilter, setCurrentFilter] = useState<ListFilterModel>(
|
||||
cloneDeep(filter)
|
||||
);
|
||||
const [criterion, setCriterion] = useState<Criterion<CriterionValue>>();
|
||||
const [criterion, setCriterion] = useState<Criterion>();
|
||||
|
||||
const [searchRef, setSearchFocus] = useFocusOnce(!ScreenUtils.isTouch());
|
||||
|
||||
@@ -364,7 +363,7 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
function replaceCriterion(c: Criterion<CriterionValue>) {
|
||||
function replaceCriterion(c: Criterion) {
|
||||
const newFilter = cloneDeep(currentFilter);
|
||||
|
||||
if (!c.isValid()) {
|
||||
@@ -397,18 +396,26 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
||||
setCriterion(c);
|
||||
}
|
||||
|
||||
function removeCriterion(c: Criterion<CriterionValue>) {
|
||||
const newFilter = cloneDeep(currentFilter);
|
||||
function removeCriterion(c: Criterion, valueIndex?: number) {
|
||||
if (valueIndex !== undefined) {
|
||||
setCurrentFilter(
|
||||
currentFilter.removeCustomFieldCriterion(
|
||||
c.criterionOption.type,
|
||||
valueIndex
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const newFilter = cloneDeep(currentFilter);
|
||||
const newCriteria = criteria.filter((cc) => {
|
||||
return cc.getId() !== c.getId();
|
||||
});
|
||||
|
||||
const newCriteria = criteria.filter((cc) => {
|
||||
return cc.getId() !== c.getId();
|
||||
});
|
||||
newFilter.criteria = newCriteria;
|
||||
|
||||
newFilter.criteria = newCriteria;
|
||||
|
||||
setCurrentFilter(newFilter);
|
||||
if (criterion?.getId() === c.getId()) {
|
||||
optionSelected(undefined);
|
||||
setCurrentFilter(newFilter);
|
||||
if (criterion?.getId() === c.getId()) {
|
||||
optionSelected(undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +469,7 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
||||
<FilterTags
|
||||
criteria={criteria}
|
||||
onEditCriterion={(c) => optionSelected(c.criterionOption)}
|
||||
onRemoveCriterion={(c) => removeCriterion(c)}
|
||||
onRemoveCriterion={removeCriterion}
|
||||
onRemoveAll={() => onClearAll()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,50 @@
|
||||
import React from "react";
|
||||
import { Badge, Button } from "react-bootstrap";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionValue,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { Badge, BadgeProps, Button } from "react-bootstrap";
|
||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { BsPrefixProps, ReplaceProps } from "react-bootstrap/esm/helpers";
|
||||
import { CustomFieldsCriterion } from "src/models/list-filter/criteria/custom-fields";
|
||||
|
||||
type TagItemProps = PropsWithChildren<
|
||||
ReplaceProps<"span", BsPrefixProps<"span"> & BadgeProps>
|
||||
>;
|
||||
|
||||
export const TagItem: React.FC<TagItemProps> = (props) => {
|
||||
const { children } = props;
|
||||
return (
|
||||
<Badge className="tag-item" variant="secondary" {...props}>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
export const FilterTag: React.FC<{
|
||||
label: React.ReactNode;
|
||||
onClick: React.MouseEventHandler<HTMLSpanElement>;
|
||||
onRemove: React.MouseEventHandler<HTMLElement>;
|
||||
}> = ({ label, onClick, onRemove }) => {
|
||||
return (
|
||||
<TagItem onClick={onClick}>
|
||||
{label}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={(e) => {
|
||||
onRemove(e);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Icon icon={faTimes} />
|
||||
</Button>
|
||||
</TagItem>
|
||||
);
|
||||
};
|
||||
|
||||
interface IFilterTagsProps {
|
||||
criteria: Criterion<CriterionValue>[];
|
||||
onEditCriterion: (c: Criterion<CriterionValue>) => void;
|
||||
onRemoveCriterion: (c: Criterion<CriterionValue>) => void;
|
||||
criteria: Criterion[];
|
||||
onEditCriterion: (c: Criterion) => void;
|
||||
onRemoveCriterion: (c: Criterion, valueIndex?: number) => void;
|
||||
onRemoveAll: () => void;
|
||||
}
|
||||
|
||||
@@ -24,59 +57,62 @@ export const FilterTags: React.FC<IFilterTagsProps> = ({
|
||||
const intl = useIntl();
|
||||
|
||||
function onRemoveCriterionTag(
|
||||
criterion: Criterion<CriterionValue>,
|
||||
$event: React.MouseEvent<HTMLElement, MouseEvent>
|
||||
criterion: Criterion,
|
||||
$event: React.MouseEvent<HTMLElement, MouseEvent>,
|
||||
valueIndex?: number
|
||||
) {
|
||||
if (!criterion) {
|
||||
return;
|
||||
}
|
||||
onRemoveCriterion(criterion);
|
||||
onRemoveCriterion(criterion, valueIndex);
|
||||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
function onClickCriterionTag(criterion: Criterion<CriterionValue>) {
|
||||
function onClickCriterionTag(criterion: Criterion) {
|
||||
onEditCriterion(criterion);
|
||||
}
|
||||
|
||||
function renderFilterTags() {
|
||||
return criteria.map((criterion) => (
|
||||
<Badge
|
||||
className="tag-item"
|
||||
variant="secondary"
|
||||
key={criterion.getId()}
|
||||
onClick={() => onClickCriterionTag(criterion)}
|
||||
>
|
||||
{criterion.getLabel(intl)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={($event) => onRemoveCriterionTag(criterion, $event)}
|
||||
>
|
||||
<Icon icon={faTimes} />
|
||||
</Button>
|
||||
</Badge>
|
||||
));
|
||||
}
|
||||
|
||||
function maybeRenderClearAll() {
|
||||
if (criteria.length < 3) {
|
||||
return;
|
||||
function renderFilterTags(criterion: Criterion) {
|
||||
if (
|
||||
criterion instanceof CustomFieldsCriterion &&
|
||||
criterion.value.length > 1
|
||||
) {
|
||||
return criterion.value.map((value, index) => {
|
||||
return (
|
||||
<FilterTag
|
||||
key={index}
|
||||
label={criterion.getValueLabel(intl, value)}
|
||||
onClick={() => onClickCriterionTag(criterion)}
|
||||
onRemove={($event) =>
|
||||
onRemoveCriterionTag(criterion, $event, index)
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="minimal"
|
||||
className="clear-all-button"
|
||||
onClick={() => onRemoveAll()}
|
||||
>
|
||||
<FormattedMessage id="actions.clear" />
|
||||
</Button>
|
||||
<FilterTag
|
||||
key={criterion.getId()}
|
||||
label={criterion.getLabel(intl)}
|
||||
onClick={() => onClickCriterionTag(criterion)}
|
||||
onRemove={($event) => onRemoveCriterionTag(criterion, $event)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="d-flex justify-content-center mb-2 wrap-tags filter-tags">
|
||||
{renderFilterTags()}
|
||||
{maybeRenderClearAll()}
|
||||
{criteria.map(renderFilterTags)}
|
||||
{criteria.length >= 3 && (
|
||||
<Button
|
||||
variant="minimal"
|
||||
className="clear-all-button"
|
||||
onClick={() => onRemoveAll()}
|
||||
>
|
||||
<FormattedMessage id="actions.clear" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
312
ui/v2.5/src/components/List/Filters/CustomFieldsFilter.tsx
Normal file
312
ui/v2.5/src/components/List/Filters/CustomFieldsFilter.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { CustomFieldsCriterion } from "src/models/list-filter/criteria/custom-fields";
|
||||
import { Button, Col, Form, Row } from "react-bootstrap";
|
||||
import {
|
||||
CriterionModifier,
|
||||
CustomFieldCriterionInput,
|
||||
} from "src/core/generated-graphql";
|
||||
import { cloneDeep } from "@apollo/client/utilities";
|
||||
import { ModifierSelect } from "../ModifierSelect";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { faCheck, faPencil, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FilterTag } from "../FilterTags";
|
||||
import { ModifierCriterion } from "src/models/list-filter/criteria/criterion";
|
||||
|
||||
interface ICustomFieldCriterionEditor {
|
||||
criterion?: CustomFieldCriterionInput;
|
||||
setCriterion: (c: CustomFieldCriterionInput) => void;
|
||||
cancel: () => void;
|
||||
editing?: boolean;
|
||||
}
|
||||
|
||||
function getValue(v: string) {
|
||||
// if the value is numeric, convert it to a number
|
||||
const num = Number(v);
|
||||
if (!isNaN(num)) {
|
||||
return num;
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
const CustomFieldCriterionEditor: React.FC<ICustomFieldCriterionEditor> = ({
|
||||
criterion,
|
||||
setCriterion,
|
||||
editing = false,
|
||||
cancel,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [field, setField] = React.useState(criterion?.field ?? "");
|
||||
const [value, setValue] = React.useState(criterion?.value);
|
||||
const [modifier, setModifier] = React.useState(
|
||||
criterion?.modifier ?? CriterionModifier.Equals
|
||||
);
|
||||
|
||||
const firstValue = value && value.length > 0 ? (value[0] as string) : "";
|
||||
const secondValue = value && value.length > 1 ? (value[1] as string) : "";
|
||||
|
||||
useEffect(() => {
|
||||
setField((criterion?.field as string) ?? "");
|
||||
setValue(criterion?.value ?? []);
|
||||
setModifier(criterion?.modifier ?? CriterionModifier.Equals);
|
||||
}, [criterion]);
|
||||
|
||||
function setFirstValue(v: string) {
|
||||
// convert to numeric if possible
|
||||
const nv = getValue(v);
|
||||
|
||||
if (
|
||||
modifier === CriterionModifier.Between ||
|
||||
modifier === CriterionModifier.NotBetween
|
||||
) {
|
||||
setValue([nv, secondValue]);
|
||||
} else {
|
||||
setValue([nv]);
|
||||
}
|
||||
}
|
||||
|
||||
function setSecondValue(v: string) {
|
||||
setValue([firstValue, getValue(v)]);
|
||||
}
|
||||
|
||||
function onChangeModifier(m: CriterionModifier) {
|
||||
setModifier(m);
|
||||
if (m === CriterionModifier.IsNull || m === CriterionModifier.NotNull) {
|
||||
setValue(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
setCriterion({
|
||||
field,
|
||||
value,
|
||||
modifier,
|
||||
});
|
||||
}
|
||||
|
||||
const firstPlaceholder =
|
||||
modifier === CriterionModifier.Between ||
|
||||
modifier === CriterionModifier.NotBetween
|
||||
? intl.formatMessage({ id: "criterion.greater_than" })
|
||||
: intl.formatMessage({ id: "custom_fields.value" });
|
||||
|
||||
const hasTwoValues =
|
||||
modifier === CriterionModifier.Between ||
|
||||
modifier === CriterionModifier.NotBetween;
|
||||
|
||||
return (
|
||||
<Form.Group className="custom-field-filter">
|
||||
<div>
|
||||
<Row noGutters>
|
||||
<Col xs={6}>
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
placeholder={intl.formatMessage({ id: "custom_fields.field" })}
|
||||
onChange={(e) => setField(e.target.value)}
|
||||
value={field}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<ModifierSelect
|
||||
value={modifier}
|
||||
onChanged={(m) => onChangeModifier(m)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row noGutters>
|
||||
{modifier !== CriterionModifier.IsNull &&
|
||||
modifier !== CriterionModifier.NotNull && (
|
||||
<Col xs={hasTwoValues ? 6 : 12}>
|
||||
<Form.Control
|
||||
placeholder={firstPlaceholder}
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e) => setFirstValue(e.target.value)}
|
||||
value={firstValue}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
{(modifier === CriterionModifier.Between ||
|
||||
modifier === CriterionModifier.NotBetween) && (
|
||||
<Col xs={6}>
|
||||
<Form.Control
|
||||
placeholder={intl.formatMessage({ id: "criterion.less_than" })}
|
||||
className="btn-secondary"
|
||||
type="text"
|
||||
onChange={(e) => setSecondValue(e.target.value)}
|
||||
value={secondValue}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
<div className="custom-field-filter-buttons">
|
||||
<Button variant="success" onClick={() => onConfirm()} disabled={!field}>
|
||||
<Icon icon={faCheck} />
|
||||
</Button>
|
||||
{editing && (
|
||||
<Button variant="secondary" onClick={() => cancel()}>
|
||||
<Icon icon={faTimes} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
function valueToString(value: unknown[] | undefined | null) {
|
||||
if (!value) return "";
|
||||
return value.map((v) => v as string).join(", ");
|
||||
}
|
||||
|
||||
const CustomFieldFilterTag: React.FC<{
|
||||
criterion: CustomFieldCriterionInput;
|
||||
editing?: boolean;
|
||||
onEditCriterion: () => void;
|
||||
onRemoveCriterion: () => void;
|
||||
}> = ({ criterion, editing, onEditCriterion, onRemoveCriterion }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const label = useMemo(() => {
|
||||
const { field, modifier, value } = criterion;
|
||||
const modifierString = ModifierCriterion.getModifierLabel(intl, modifier);
|
||||
|
||||
const str = intl.formatMessage(
|
||||
{ id: "criterion_modifier.format_string" },
|
||||
{
|
||||
criterion: field,
|
||||
modifierString,
|
||||
valueString: valueToString(value),
|
||||
}
|
||||
);
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<span>
|
||||
<Icon icon={faPencil} />
|
||||
{str}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{str}</>;
|
||||
}, [criterion, editing, intl]);
|
||||
|
||||
return (
|
||||
<FilterTag
|
||||
label={label}
|
||||
onClick={onEditCriterion}
|
||||
onRemove={onRemoveCriterion}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomFieldsCriteriaPills: React.FC<{
|
||||
criteria: CustomFieldCriterionInput[];
|
||||
editIndex?: number;
|
||||
onEditCriterion: (index: number) => void;
|
||||
onRemoveCriterion: (index: number) => void;
|
||||
}> = ({ criteria, editIndex, onEditCriterion, onRemoveCriterion }) => {
|
||||
return (
|
||||
<div className="d-flex justify-content-center mb-2 wrap-tags filter-tags">
|
||||
{criteria.map((c, index) => (
|
||||
<CustomFieldFilterTag
|
||||
key={index}
|
||||
editing={index === editIndex}
|
||||
criterion={c}
|
||||
onEditCriterion={() => onEditCriterion(index)}
|
||||
onRemoveCriterion={() => onRemoveCriterion(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICustomFieldsFilter {
|
||||
criterion: CustomFieldsCriterion;
|
||||
setCriterion: (c: CustomFieldsCriterion) => void;
|
||||
}
|
||||
|
||||
function initCriterion(
|
||||
criterion: CustomFieldsCriterion
|
||||
): CustomFieldsCriterion {
|
||||
return cloneDeep(criterion);
|
||||
}
|
||||
|
||||
function createNewCriterion(): CustomFieldCriterionInput {
|
||||
return {
|
||||
field: "",
|
||||
value: [],
|
||||
modifier: CriterionModifier.Equals,
|
||||
};
|
||||
}
|
||||
|
||||
export const CustomFieldsFilter: React.FC<ICustomFieldsFilter> = ({
|
||||
criterion,
|
||||
setCriterion,
|
||||
}) => {
|
||||
const [localCriterion, setLocalCriterion] = React.useState(
|
||||
initCriterion(criterion)
|
||||
);
|
||||
|
||||
const [editCriterion, setEditCriterion] = useState(createNewCriterion());
|
||||
const editIndex = useMemo(
|
||||
() => localCriterion.value.indexOf(editCriterion),
|
||||
[localCriterion, editCriterion]
|
||||
);
|
||||
|
||||
function updateCriteria(newCriteria: CustomFieldCriterionInput[]) {
|
||||
// update the parent - filter out invalid criteria
|
||||
const validCriteria = newCriteria.filter((c) => c.field !== "");
|
||||
const newValue = cloneDeep(criterion);
|
||||
newValue.value = validCriteria;
|
||||
setCriterion(newValue);
|
||||
}
|
||||
|
||||
function onChange(nv: CustomFieldCriterionInput) {
|
||||
const newValue = cloneDeep(localCriterion);
|
||||
|
||||
// if the criterion is new, add it to the list
|
||||
if (editIndex === -1) {
|
||||
newValue.value.push(nv);
|
||||
} else {
|
||||
newValue.value[editIndex] = nv;
|
||||
}
|
||||
|
||||
setLocalCriterion(newValue);
|
||||
updateCriteria(newValue.value);
|
||||
setEditCriterion(createNewCriterion());
|
||||
}
|
||||
|
||||
function onRemove(index: number) {
|
||||
const c = cloneDeep(localCriterion);
|
||||
c.value.splice(index, 1);
|
||||
setLocalCriterion(c);
|
||||
updateCriteria(c.value);
|
||||
if (index === editIndex) {
|
||||
setEditCriterion(createNewCriterion());
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<CustomFieldCriterionEditor
|
||||
criterion={editCriterion}
|
||||
editing={editCriterion.field !== ""}
|
||||
setCriterion={onChange}
|
||||
cancel={() => setEditCriterion(createNewCriterion())}
|
||||
/>
|
||||
<CustomFieldsCriteriaPills
|
||||
criteria={localCriterion.value}
|
||||
editIndex={editIndex !== -1 ? editIndex : undefined}
|
||||
onEditCriterion={(index) =>
|
||||
setEditCriterion(localCriterion.value[index])
|
||||
}
|
||||
onRemoveCriterion={(index) => onRemove(index)}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
@@ -3,11 +3,11 @@ import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||
import { IDateValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { ModifierCriterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface IDateFilterProps {
|
||||
criterion: Criterion<IDateValue>;
|
||||
criterion: ModifierCriterion<IDateValue>;
|
||||
onValueChanged: (value: IDateValue) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useIntl } from "react-intl";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { DurationInput } from "src/components/Shared/DurationInput";
|
||||
import { INumberValue } from "src/models/list-filter/types";
|
||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { ModifierCriterion } from "src/models/list-filter/criteria/criterion";
|
||||
|
||||
interface IDurationFilterProps {
|
||||
criterion: Criterion<INumberValue>;
|
||||
criterion: ModifierCriterion<INumberValue>;
|
||||
onValueChanged: (value: INumberValue) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,19 @@ import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { defineMessages, MessageDescriptor, useIntl } from "react-intl";
|
||||
import { FilterSelect, SelectObject } from "src/components/Shared/Select";
|
||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { ModifierCriterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { IHierarchicalLabelValue } from "src/models/list-filter/types";
|
||||
import { NumberField } from "src/utils/form";
|
||||
|
||||
interface IHierarchicalLabelValueFilterProps {
|
||||
criterion: Criterion<IHierarchicalLabelValue>;
|
||||
criterion: ModifierCriterion<IHierarchicalLabelValue>;
|
||||
onValueChanged: (value: IHierarchicalLabelValue) => void;
|
||||
}
|
||||
|
||||
export const HierarchicalLabelValueFilter: React.FC<
|
||||
IHierarchicalLabelValueFilterProps
|
||||
> = ({ criterion, onValueChanged }) => {
|
||||
const { criterionOption } = criterion;
|
||||
const criterionOption = criterion.modifierCriterionOption();
|
||||
const { type, inputType } = criterionOption;
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import {
|
||||
Criterion,
|
||||
ModifierCriterion,
|
||||
CriterionValue,
|
||||
} from "../../../models/list-filter/criteria/criterion";
|
||||
|
||||
interface IInputFilterProps {
|
||||
criterion: Criterion<CriterionValue>;
|
||||
criterion: ModifierCriterion<CriterionValue>;
|
||||
onValueChanged: (value: string) => void;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const InputFilter: React.FC<IInputFilterProps> = ({
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type={criterion.criterionOption.inputType}
|
||||
type={criterion.modifierCriterionOption().inputType}
|
||||
onChange={onChanged}
|
||||
value={criterion.value ? criterion.value.toString() : ""}
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,11 @@ import { Form } from "react-bootstrap";
|
||||
import { FilterSelect, SelectObject } from "src/components/Shared/Select";
|
||||
import { objectTitle } from "src/core/files";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { ModifierCriterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { ILabeledId } from "src/models/list-filter/types";
|
||||
|
||||
interface ILabeledIdFilterProps {
|
||||
criterion: Criterion<ILabeledId[]>;
|
||||
criterion: ModifierCriterion<ILabeledId[]>;
|
||||
onValueChanged: (value: ILabeledId[]) => void;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const LabeledIdFilter: React.FC<ILabeledIdFilterProps> = ({
|
||||
criterion,
|
||||
onValueChanged,
|
||||
}) => {
|
||||
const { criterionOption } = criterion;
|
||||
const criterionOption = criterion.modifierCriterionOption();
|
||||
const { inputType } = criterionOption;
|
||||
|
||||
if (
|
||||
|
||||
@@ -3,12 +3,12 @@ import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import {
|
||||
CriterionValue,
|
||||
Criterion,
|
||||
ModifierCriterion,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
|
||||
interface IOptionsFilter {
|
||||
criterion: Criterion<CriterionValue>;
|
||||
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||
criterion: ModifierCriterion<CriterionValue>;
|
||||
setCriterion: (c: ModifierCriterion<CriterionValue>) => void;
|
||||
}
|
||||
|
||||
export const OptionFilter: React.FC<IOptionsFilter> = ({
|
||||
@@ -26,7 +26,7 @@ export const OptionFilter: React.FC<IOptionsFilter> = ({
|
||||
setCriterion(c);
|
||||
}
|
||||
|
||||
const { options } = criterion.criterionOption;
|
||||
const { options } = criterion.modifierCriterionOption();
|
||||
|
||||
return (
|
||||
<div className="option-list-filter">
|
||||
@@ -45,8 +45,8 @@ export const OptionFilter: React.FC<IOptionsFilter> = ({
|
||||
};
|
||||
|
||||
interface IOptionsListFilter {
|
||||
criterion: Criterion<CriterionValue>;
|
||||
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||
criterion: ModifierCriterion<CriterionValue>;
|
||||
setCriterion: (c: ModifierCriterion<CriterionValue>) => void;
|
||||
}
|
||||
|
||||
export const OptionListFilter: React.FC<IOptionsListFilter> = ({
|
||||
@@ -65,7 +65,7 @@ export const OptionListFilter: React.FC<IOptionsListFilter> = ({
|
||||
setCriterion(c);
|
||||
}
|
||||
|
||||
const { options } = criterion.criterionOption;
|
||||
const { options } = criterion.modifierCriterionOption();
|
||||
const value = criterion.value as string[];
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,12 +4,12 @@ import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import {
|
||||
Criterion,
|
||||
ModifierCriterion,
|
||||
CriterionValue,
|
||||
} from "../../../models/list-filter/criteria/criterion";
|
||||
|
||||
interface IInputFilterProps {
|
||||
criterion: Criterion<CriterionValue>;
|
||||
criterion: ModifierCriterion<CriterionValue>;
|
||||
onValueChanged: (value: string) => void;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const PathFilter: React.FC<IInputFilterProps> = ({
|
||||
{regex ? (
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type={criterion.criterionOption.inputType}
|
||||
type={criterion.modifierCriterionOption().inputType}
|
||||
onChange={(v) => onValueChanged(v.target.value)}
|
||||
value={criterion.value ? criterion.value.toString() : ""}
|
||||
/>
|
||||
|
||||
@@ -2,12 +2,12 @@ import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { IPhashDistanceValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { ModifierCriterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { NumberField } from "src/utils/form";
|
||||
|
||||
interface IPhashFilterProps {
|
||||
criterion: Criterion<IPhashDistanceValue>;
|
||||
criterion: ModifierCriterion<IPhashDistanceValue>;
|
||||
onValueChanged: (value: IPhashDistanceValue) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||
import { INumberValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { ModifierCriterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
|
||||
interface IRatingFilterProps {
|
||||
criterion: Criterion<INumberValue>;
|
||||
criterion: ModifierCriterion<INumberValue>;
|
||||
onValueChanged: (value: INumberValue) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "src/models/list-filter/types";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import {
|
||||
Criterion,
|
||||
ModifierCriterion,
|
||||
IHierarchicalLabeledIdCriterion,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { defineMessages, MessageDescriptor, useIntl } from "react-intl";
|
||||
@@ -316,7 +316,7 @@ const SelectableFilter: React.FC<ISelectableFilter> = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface IObjectsFilter<T extends Criterion<ILabeledValueListValue>> {
|
||||
interface IObjectsFilter<T extends ModifierCriterion<ILabeledValueListValue>> {
|
||||
criterion: T;
|
||||
setCriterion: (criterion: T) => void;
|
||||
useResults: (query: string) => { results: ILabeledId[]; loading: boolean };
|
||||
@@ -324,7 +324,7 @@ interface IObjectsFilter<T extends Criterion<ILabeledValueListValue>> {
|
||||
}
|
||||
|
||||
export const ObjectsFilter = <
|
||||
T extends Criterion<ILabeledValueListValue | IHierarchicalLabelValue>
|
||||
T extends ModifierCriterion<ILabeledValueListValue | IHierarchicalLabelValue>
|
||||
>({
|
||||
criterion,
|
||||
setCriterion,
|
||||
@@ -426,9 +426,10 @@ export const ObjectsFilter = <
|
||||
|
||||
// if excludes is not a valid modifierOption then we can use `value.excluded`
|
||||
const canExclude =
|
||||
criterion.criterionOption.modifierOptions.find(
|
||||
(m) => m === CriterionModifier.Excludes
|
||||
) === undefined;
|
||||
criterion
|
||||
.modifierCriterionOption()
|
||||
.modifierOptions.find((m) => m === CriterionModifier.Excludes) ===
|
||||
undefined;
|
||||
|
||||
return (
|
||||
<SelectableFilter
|
||||
|
||||
@@ -2,11 +2,11 @@ import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { IStashIDValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { ModifierCriterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
|
||||
interface IStashIDFilterProps {
|
||||
criterion: Criterion<IStashIDValue>;
|
||||
criterion: ModifierCriterion<IStashIDValue>;
|
||||
onValueChanged: (value: IStashIDValue) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||
import { ITimestampValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { ModifierCriterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
|
||||
interface ITimestampFilterProps {
|
||||
criterion: Criterion<ITimestampValue>;
|
||||
criterion: ModifierCriterion<ITimestampValue>;
|
||||
onValueChanged: (value: ITimestampValue) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,7 @@ import React, {
|
||||
} from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { QueryResult } from "@apollo/client";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionValue,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { EditFilterDialog } from "src/components/List/EditFilterDialog";
|
||||
import { FilterTags } from "./FilterTags";
|
||||
@@ -220,8 +217,19 @@ export const ItemList = <T extends QueryResult, E extends IHasID>(
|
||||
result.refetch();
|
||||
}
|
||||
|
||||
function onRemoveCriterion(removedCriterion: Criterion<CriterionValue>) {
|
||||
updateFilter(filter.removeCriterion(removedCriterion.criterionOption.type));
|
||||
function onRemoveCriterion(removedCriterion: Criterion, valueIndex?: number) {
|
||||
if (valueIndex === undefined) {
|
||||
updateFilter(
|
||||
filter.removeCriterion(removedCriterion.criterionOption.type)
|
||||
);
|
||||
} else {
|
||||
updateFilter(
|
||||
filter.removeCustomFieldCriterion(
|
||||
removedCriterion.criterionOption.type,
|
||||
valueIndex
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onClearAllCriteria() {
|
||||
|
||||
72
ui/v2.5/src/components/List/ModifierSelect.tsx
Normal file
72
ui/v2.5/src/components/List/ModifierSelect.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { ModifierCriterion } from "src/models/list-filter/criteria/criterion";
|
||||
import cx from "classnames";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
const defaultOptions = [
|
||||
CriterionModifier.IsNull,
|
||||
CriterionModifier.NotNull,
|
||||
CriterionModifier.Equals,
|
||||
CriterionModifier.NotEquals,
|
||||
CriterionModifier.Includes,
|
||||
CriterionModifier.Excludes,
|
||||
CriterionModifier.GreaterThan,
|
||||
CriterionModifier.LessThan,
|
||||
CriterionModifier.Between,
|
||||
CriterionModifier.NotBetween,
|
||||
];
|
||||
|
||||
interface IModifierSelect {
|
||||
options?: CriterionModifier[];
|
||||
value: CriterionModifier;
|
||||
onChanged: (m: CriterionModifier) => void;
|
||||
}
|
||||
|
||||
export const ModifierSelectorButtons: React.FC<IModifierSelect> = ({
|
||||
options = defaultOptions,
|
||||
value,
|
||||
onChanged,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Form.Group className="modifier-options">
|
||||
{options.map((m) => (
|
||||
<Button
|
||||
className={cx("modifier-option", {
|
||||
selected: value === m,
|
||||
})}
|
||||
key={m}
|
||||
onClick={() => onChanged(m)}
|
||||
>
|
||||
{ModifierCriterion.getModifierLabel(intl, m)}
|
||||
</Button>
|
||||
))}
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModifierSelect: React.FC<IModifierSelect> = ({
|
||||
options = defaultOptions,
|
||||
value,
|
||||
onChanged,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={(e) => onChanged(e.target.value as CriterionModifier)}
|
||||
value={value}
|
||||
className="btn-secondary modifier-selector"
|
||||
>
|
||||
{options.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{ModifierCriterion.getModifierLabel(intl, m)}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
);
|
||||
};
|
||||
@@ -67,7 +67,7 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||
mode: filter.mode,
|
||||
name,
|
||||
find_filter: filterCopy.makeFindFilter(),
|
||||
object_filter: filterCopy.makeSavedFilter(),
|
||||
object_filter: filterCopy.makeFilter(),
|
||||
ui_options: filterCopy.makeSavedUIOptions(),
|
||||
},
|
||||
},
|
||||
@@ -142,7 +142,7 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||
value: {
|
||||
mode: filter.mode,
|
||||
find_filter: filterCopy.makeFindFilter(),
|
||||
object_filter: filterCopy.makeSavedFilter(),
|
||||
object_filter: filterCopy.makeFilter(),
|
||||
ui_options: filterCopy.makeSavedUIOptions(),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -587,3 +587,29 @@ input[type="range"].zoom-slider {
|
||||
.search-term-input {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.custom-field-filter {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
> div:first-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.custom-field-filter-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 0.25rem;
|
||||
|
||||
.btn {
|
||||
border-radius: 0.2rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
&:first-child {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Icon } from "../Shared/Icon";
|
||||
import { TagLink } from "../Shared/TagLink";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import {
|
||||
Criterion,
|
||||
ModifierCriterion,
|
||||
CriterionValue,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { PopoverCountButton } from "../Shared/PopoverCountButton";
|
||||
@@ -25,10 +25,10 @@ import ScreenUtils from "src/utils/screen";
|
||||
import { FavoriteIcon } from "../Shared/FavoriteIcon";
|
||||
|
||||
export interface IPerformerCardExtraCriteria {
|
||||
scenes?: Criterion<CriterionValue>[];
|
||||
images?: Criterion<CriterionValue>[];
|
||||
galleries?: Criterion<CriterionValue>[];
|
||||
groups?: Criterion<CriterionValue>[];
|
||||
scenes?: ModifierCriterion<CriterionValue>[];
|
||||
images?: ModifierCriterion<CriterionValue>[];
|
||||
galleries?: ModifierCriterion<CriterionValue>[];
|
||||
groups?: ModifierCriterion<CriterionValue>[];
|
||||
performer?: ILabeledId;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import { sortByRelevance } from "src/utils/query";
|
||||
import { objectTitle } from "src/core/files";
|
||||
import { PatchComponent, PatchFunction } from "src/patch";
|
||||
import {
|
||||
Criterion,
|
||||
ModifierCriterion,
|
||||
CriterionValue,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { TruncatedText } from "../Shared/TruncatedText";
|
||||
@@ -45,7 +45,7 @@ type Option = SelectOption<Scene>;
|
||||
type ExtraSceneProps = {
|
||||
hoverPlacement?: Placement;
|
||||
excludeIds?: string[];
|
||||
extraCriteria?: Array<Criterion<CriterionValue>>;
|
||||
extraCriteria?: Array<ModifierCriterion<CriterionValue>>;
|
||||
};
|
||||
|
||||
type FindScenesResult = Awaited<
|
||||
|
||||
@@ -856,6 +856,8 @@
|
||||
},
|
||||
"custom": "Custom",
|
||||
"custom_fields": {
|
||||
"criteria_format_string": "{criterion} (custom field) {modifierString} {valueString}",
|
||||
"criteria_format_string_others": "{criterion} (custom field) {modifierString} {valueString} (+{others} others)",
|
||||
"field": "Field",
|
||||
"title": "Custom Fields",
|
||||
"value": "Value"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { languageMap, valueToCode } from "src/utils/caption";
|
||||
import { CriterionOption, StringCriterion } from "./criterion";
|
||||
import { ModifierCriterionOption, StringCriterion } from "./criterion";
|
||||
|
||||
const languageStrings = Array.from(languageMap.values());
|
||||
|
||||
export const CaptionsCriterionOption = new CriterionOption({
|
||||
export const CaptionsCriterionOption = new ModifierCriterionOption({
|
||||
messageID: "captions",
|
||||
type: "captions",
|
||||
modifierOptions: [
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
CriterionModifier,
|
||||
} from "src/core/generated-graphql";
|
||||
import { circumcisedStrings, stringToCircumcised } from "src/utils/circumcised";
|
||||
import { CriterionOption, MultiStringCriterion } from "./criterion";
|
||||
import { ModifierCriterionOption, MultiStringCriterion } from "./criterion";
|
||||
|
||||
export const CircumcisedCriterionOption = new CriterionOption({
|
||||
export const CircumcisedCriterionOption = new ModifierCriterionOption({
|
||||
messageID: "circumcised",
|
||||
type: "circumcised",
|
||||
modifierOptions: [
|
||||
|
||||
@@ -57,10 +57,41 @@ const modifierMessageIDs = {
|
||||
[CriterionModifier.NotBetween]: "criterion_modifier.not_between",
|
||||
};
|
||||
|
||||
// V = criterion value type
|
||||
export abstract class Criterion<V extends CriterionValue> {
|
||||
export abstract class Criterion {
|
||||
public criterionOption: CriterionOption;
|
||||
|
||||
constructor(type: CriterionOption) {
|
||||
this.criterionOption = type;
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public clone() {
|
||||
const ret = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
|
||||
ret.cloneValues();
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected cloneValues() {}
|
||||
|
||||
public abstract getLabel(intl: IntlShape): string;
|
||||
|
||||
public getId(): string {
|
||||
return `${this.criterionOption.type}`;
|
||||
}
|
||||
|
||||
public abstract toJSON(): string;
|
||||
|
||||
public abstract applyToCriterionInput(input: Record<string, unknown>): void;
|
||||
public abstract setFromSavedCriterion(criterion: unknown): void;
|
||||
}
|
||||
|
||||
// V = criterion value type
|
||||
export abstract class ModifierCriterion<
|
||||
V extends CriterionValue
|
||||
> extends Criterion {
|
||||
protected _modifier!: CriterionModifier;
|
||||
public get modifier(): CriterionModifier {
|
||||
return this._modifier;
|
||||
@@ -83,12 +114,16 @@ export abstract class Criterion<V extends CriterionValue> {
|
||||
|
||||
protected abstract getLabelValue(intl: IntlShape): string;
|
||||
|
||||
constructor(type: CriterionOption, value: V) {
|
||||
this.criterionOption = type;
|
||||
constructor(type: ModifierCriterionOption, value: V) {
|
||||
super(type);
|
||||
this.modifier = type.defaultModifier;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public modifierCriterionOption() {
|
||||
return this.criterionOption as ModifierCriterionOption;
|
||||
}
|
||||
|
||||
public clone() {
|
||||
const ret = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
|
||||
ret.cloneValues();
|
||||
@@ -106,7 +141,10 @@ export abstract class Criterion<V extends CriterionValue> {
|
||||
}
|
||||
|
||||
public getLabel(intl: IntlShape): string {
|
||||
const modifierString = Criterion.getModifierLabel(intl, this.modifier);
|
||||
const modifierString = ModifierCriterion.getModifierLabel(
|
||||
intl,
|
||||
this.modifier
|
||||
);
|
||||
let valueString = "";
|
||||
|
||||
if (
|
||||
@@ -126,10 +164,6 @@ export abstract class Criterion<V extends CriterionValue> {
|
||||
);
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return `${this.criterionOption.type}-${this.modifier.toString()}`; // TODO add values?
|
||||
}
|
||||
|
||||
public toJSON() {
|
||||
let encodedCriterion;
|
||||
if (
|
||||
@@ -157,14 +191,11 @@ export abstract class Criterion<V extends CriterionValue> {
|
||||
this.modifier = criterion.modifier;
|
||||
}
|
||||
|
||||
public toCriterionInput(): unknown {
|
||||
return {
|
||||
value: this.value,
|
||||
modifier: this.modifier,
|
||||
};
|
||||
public applyToCriterionInput(input: Record<string, unknown>) {
|
||||
input[this.criterionOption.type] = this.toCriterionInput();
|
||||
}
|
||||
|
||||
public toSavedCriterion(): ISavedCriterion<V> {
|
||||
public toCriterionInput(): unknown {
|
||||
return {
|
||||
value: this.value,
|
||||
modifier: this.modifier,
|
||||
@@ -185,44 +216,31 @@ export type InputType =
|
||||
| "galleries"
|
||||
| undefined;
|
||||
|
||||
interface ICriterionOptionsParams {
|
||||
type MakeCriterionFn = (
|
||||
o: CriterionOption,
|
||||
config?: ConfigDataFragment
|
||||
) => Criterion;
|
||||
|
||||
interface ICriterionOptionParams {
|
||||
messageID: string;
|
||||
type: CriterionType;
|
||||
inputType?: InputType;
|
||||
modifierOptions?: CriterionModifier[];
|
||||
defaultModifier?: CriterionModifier;
|
||||
options?: Option[];
|
||||
makeCriterion: MakeCriterionFn;
|
||||
hidden?: boolean;
|
||||
makeCriterion: (
|
||||
o: CriterionOption,
|
||||
config?: ConfigDataFragment
|
||||
) => Criterion<CriterionValue>;
|
||||
}
|
||||
|
||||
export class CriterionOption {
|
||||
public readonly messageID: string;
|
||||
public readonly type: CriterionType;
|
||||
public readonly modifierOptions: CriterionModifier[];
|
||||
public readonly defaultModifier: CriterionModifier;
|
||||
public readonly options: Option[] | undefined;
|
||||
public readonly inputType: InputType;
|
||||
public readonly messageID: string;
|
||||
public readonly makeCriterionFn: MakeCriterionFn;
|
||||
|
||||
// used for legacy criteria that are not shown in the UI
|
||||
public readonly hidden: boolean = false;
|
||||
|
||||
public readonly makeCriterionFn: (
|
||||
o: CriterionOption,
|
||||
config?: ConfigDataFragment
|
||||
) => Criterion<CriterionValue>;
|
||||
|
||||
constructor(options: ICriterionOptionsParams) {
|
||||
this.messageID = options.messageID;
|
||||
constructor(options: ICriterionOptionParams) {
|
||||
this.type = options.type;
|
||||
this.modifierOptions = options.modifierOptions ?? [];
|
||||
this.defaultModifier = options.defaultModifier ?? CriterionModifier.Equals;
|
||||
this.options = options.options;
|
||||
this.inputType = options.inputType;
|
||||
this.hidden = options.hidden ?? false;
|
||||
this.messageID = options.messageID;
|
||||
this.makeCriterionFn = options.makeCriterion;
|
||||
this.hidden = options.hidden ?? false;
|
||||
}
|
||||
|
||||
public makeCriterion(config?: ConfigDataFragment) {
|
||||
@@ -230,13 +248,35 @@ export class CriterionOption {
|
||||
}
|
||||
}
|
||||
|
||||
export class ILabeledIdCriterionOption extends CriterionOption {
|
||||
interface IModifierCriterionOptionParams extends ICriterionOptionParams {
|
||||
inputType?: InputType;
|
||||
modifierOptions?: CriterionModifier[];
|
||||
defaultModifier?: CriterionModifier;
|
||||
options?: Option[];
|
||||
}
|
||||
|
||||
export class ModifierCriterionOption extends CriterionOption {
|
||||
public readonly modifierOptions: CriterionModifier[];
|
||||
public readonly defaultModifier: CriterionModifier;
|
||||
public readonly options: Option[] | undefined;
|
||||
public readonly inputType: InputType;
|
||||
|
||||
constructor(options: IModifierCriterionOptionParams) {
|
||||
super(options);
|
||||
this.modifierOptions = options.modifierOptions ?? [];
|
||||
this.defaultModifier = options.defaultModifier ?? CriterionModifier.Equals;
|
||||
this.options = options.options;
|
||||
this.inputType = options.inputType;
|
||||
}
|
||||
}
|
||||
|
||||
export class ILabeledIdCriterionOption extends ModifierCriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
includeAll: boolean,
|
||||
inputType: InputType,
|
||||
makeCriterion?: () => Criterion<CriterionValue>
|
||||
makeCriterion?: () => ModifierCriterion<CriterionValue>
|
||||
) {
|
||||
const modifierOptions = [
|
||||
CriterionModifier.Includes,
|
||||
@@ -264,8 +304,8 @@ export class ILabeledIdCriterionOption extends CriterionOption {
|
||||
}
|
||||
}
|
||||
|
||||
export class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
|
||||
constructor(type: CriterionOption, value: ILabeledId[] = []) {
|
||||
export class ILabeledIdCriterion extends ModifierCriterion<ILabeledId[]> {
|
||||
constructor(type: ModifierCriterionOption, value: ILabeledId[] = []) {
|
||||
super(type, value);
|
||||
}
|
||||
|
||||
@@ -296,9 +336,9 @@ export class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabelValue> {
|
||||
export class IHierarchicalLabeledIdCriterion extends ModifierCriterion<IHierarchicalLabelValue> {
|
||||
constructor(
|
||||
type: CriterionOption,
|
||||
type: ModifierCriterionOption,
|
||||
value: IHierarchicalLabelValue = {
|
||||
items: [],
|
||||
excluded: [],
|
||||
@@ -346,14 +386,16 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
||||
};
|
||||
}
|
||||
|
||||
const modifierOptions =
|
||||
(this.criterionOption as ModifierCriterionOption).modifierOptions ?? [];
|
||||
|
||||
// if the previous modifier was excludes, replace it with the equivalent includes criterion
|
||||
// this is what is done on the backend
|
||||
// only replace if excludes is not a valid modifierOption
|
||||
if (
|
||||
modifier === CriterionModifier.Excludes &&
|
||||
this.criterionOption.modifierOptions.find(
|
||||
(m) => m === CriterionModifier.Excludes
|
||||
) === undefined
|
||||
modifierOptions.find((m) => m === CriterionModifier.Excludes) ===
|
||||
undefined
|
||||
) {
|
||||
this.modifier = CriterionModifier.Includes;
|
||||
this.value.excluded = [...this.value.excluded, ...this.value.items];
|
||||
@@ -407,7 +449,10 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
||||
|
||||
public getLabel(intl: IntlShape): string {
|
||||
let id = "criterion_modifier.format_string";
|
||||
let modifierString = Criterion.getModifierLabel(intl, this.modifier);
|
||||
let modifierString = ModifierCriterion.getModifierLabel(
|
||||
intl,
|
||||
this.modifier
|
||||
);
|
||||
let valueString = "";
|
||||
let excludedString = "";
|
||||
|
||||
@@ -419,7 +464,7 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
||||
|
||||
if (this.value.excluded && this.value.excluded.length > 0) {
|
||||
if (this.value.items.length === 0) {
|
||||
modifierString = Criterion.getModifierLabel(
|
||||
modifierString = ModifierCriterion.getModifierLabel(
|
||||
intl,
|
||||
CriterionModifier.Excludes
|
||||
);
|
||||
@@ -448,11 +493,11 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
||||
}
|
||||
}
|
||||
|
||||
export class StringCriterionOption extends CriterionOption {
|
||||
export class StringCriterionOption extends ModifierCriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
makeCriterion?: () => Criterion<CriterionValue>
|
||||
makeCriterion?: () => ModifierCriterion<CriterionValue>
|
||||
) {
|
||||
super({
|
||||
messageID,
|
||||
@@ -483,7 +528,7 @@ export function createStringCriterionOption(
|
||||
return new StringCriterionOption(messageID ?? type, type);
|
||||
}
|
||||
|
||||
export class MandatoryStringCriterionOption extends CriterionOption {
|
||||
export class MandatoryStringCriterionOption extends ModifierCriterionOption {
|
||||
constructor(messageID: string, value: CriterionType) {
|
||||
super({
|
||||
messageID,
|
||||
@@ -510,8 +555,8 @@ export function createMandatoryStringCriterionOption(
|
||||
return new MandatoryStringCriterionOption(messageID ?? value, value);
|
||||
}
|
||||
|
||||
export class StringCriterion extends Criterion<string> {
|
||||
constructor(type: CriterionOption) {
|
||||
export class StringCriterion extends ModifierCriterion<string> {
|
||||
constructor(type: ModifierCriterionOption) {
|
||||
super(type, "");
|
||||
}
|
||||
|
||||
@@ -528,8 +573,8 @@ export class StringCriterion extends Criterion<string> {
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class MultiStringCriterion extends Criterion<string[]> {
|
||||
constructor(type: CriterionOption, value: string[] = []) {
|
||||
export abstract class MultiStringCriterion extends ModifierCriterion<string[]> {
|
||||
constructor(type: ModifierCriterionOption, value: string[] = []) {
|
||||
super(type, value);
|
||||
}
|
||||
|
||||
@@ -550,11 +595,11 @@ export abstract class MultiStringCriterion extends Criterion<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export class BooleanCriterionOption extends CriterionOption {
|
||||
export class BooleanCriterionOption extends ModifierCriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
makeCriterion?: () => Criterion<CriterionValue>
|
||||
makeCriterion?: () => ModifierCriterion<CriterionValue>
|
||||
) {
|
||||
super({
|
||||
messageID,
|
||||
@@ -586,11 +631,11 @@ export class BooleanCriterion extends StringCriterion {
|
||||
}
|
||||
}
|
||||
|
||||
export class StringBooleanCriterionOption extends CriterionOption {
|
||||
export class StringBooleanCriterionOption extends ModifierCriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
makeCriterion?: () => Criterion<CriterionValue>
|
||||
makeCriterion?: () => ModifierCriterion<CriterionValue>
|
||||
) {
|
||||
super({
|
||||
messageID,
|
||||
@@ -613,7 +658,7 @@ export class StringBooleanCriterion extends StringCriterion {
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberCriterionOption extends CriterionOption {
|
||||
export class NumberCriterionOption extends ModifierCriterionOption {
|
||||
constructor(messageID: string, value: CriterionType) {
|
||||
super({
|
||||
messageID,
|
||||
@@ -642,11 +687,11 @@ export function createNumberCriterionOption(
|
||||
return new NumberCriterionOption(messageID ?? value, value);
|
||||
}
|
||||
|
||||
export class NullNumberCriterionOption extends CriterionOption {
|
||||
export class NullNumberCriterionOption extends ModifierCriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
makeCriterion?: () => Criterion<CriterionValue>
|
||||
makeCriterion?: MakeCriterionFn
|
||||
) {
|
||||
super({
|
||||
messageID,
|
||||
@@ -677,11 +722,11 @@ export function createNullNumberCriterionOption(
|
||||
return new NullNumberCriterionOption(messageID ?? value, value);
|
||||
}
|
||||
|
||||
export class MandatoryNumberCriterionOption extends CriterionOption {
|
||||
export class MandatoryNumberCriterionOption extends ModifierCriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
makeCriterion?: () => Criterion<CriterionValue>
|
||||
makeCriterion?: () => ModifierCriterion<CriterionValue>
|
||||
) {
|
||||
super({
|
||||
messageID,
|
||||
@@ -710,8 +755,8 @@ export function createMandatoryNumberCriterionOption(
|
||||
return new MandatoryNumberCriterionOption(messageID ?? value, value);
|
||||
}
|
||||
|
||||
export class NumberCriterion extends Criterion<INumberValue> {
|
||||
constructor(type: CriterionOption) {
|
||||
export class NumberCriterion extends ModifierCriterion<INumberValue> {
|
||||
constructor(type: ModifierCriterionOption) {
|
||||
super(type, { value: undefined, value2: undefined });
|
||||
}
|
||||
|
||||
@@ -805,8 +850,8 @@ export function createNullDurationCriterionOption(
|
||||
return new NullDurationCriterionOption(messageID ?? value, value);
|
||||
}
|
||||
|
||||
export class DurationCriterion extends Criterion<INumberValue> {
|
||||
constructor(type: CriterionOption) {
|
||||
export class DurationCriterion extends ModifierCriterion<INumberValue> {
|
||||
constructor(type: ModifierCriterionOption) {
|
||||
super(type, { value: undefined, value2: undefined });
|
||||
}
|
||||
|
||||
@@ -860,7 +905,7 @@ export class DurationCriterion extends Criterion<INumberValue> {
|
||||
}
|
||||
}
|
||||
|
||||
export class DateCriterionOption extends CriterionOption {
|
||||
export class DateCriterionOption extends ModifierCriterionOption {
|
||||
constructor(messageID: string, value: CriterionType) {
|
||||
super({
|
||||
messageID,
|
||||
@@ -886,8 +931,8 @@ export function createDateCriterionOption(value: CriterionType) {
|
||||
return new DateCriterionOption(value, value);
|
||||
}
|
||||
|
||||
export class DateCriterion extends Criterion<IDateValue> {
|
||||
constructor(type: CriterionOption) {
|
||||
export class DateCriterion extends ModifierCriterion<IDateValue> {
|
||||
constructor(type: ModifierCriterionOption) {
|
||||
super(type, { value: "", value2: undefined });
|
||||
}
|
||||
|
||||
@@ -943,7 +988,7 @@ export class DateCriterion extends Criterion<IDateValue> {
|
||||
}
|
||||
}
|
||||
|
||||
export class TimestampCriterionOption extends CriterionOption {
|
||||
export class TimestampCriterionOption extends ModifierCriterionOption {
|
||||
constructor(messageID: string, value: CriterionType) {
|
||||
super({
|
||||
messageID,
|
||||
@@ -967,7 +1012,7 @@ export function createTimestampCriterionOption(value: CriterionType) {
|
||||
return new TimestampCriterionOption(value, value);
|
||||
}
|
||||
|
||||
export class MandatoryTimestampCriterionOption extends CriterionOption {
|
||||
export class MandatoryTimestampCriterionOption extends ModifierCriterionOption {
|
||||
constructor(messageID: string, value: CriterionType) {
|
||||
super({
|
||||
messageID,
|
||||
@@ -989,8 +1034,8 @@ export function createMandatoryTimestampCriterionOption(value: CriterionType) {
|
||||
return new MandatoryTimestampCriterionOption(value, value);
|
||||
}
|
||||
|
||||
export class TimestampCriterion extends Criterion<ITimestampValue> {
|
||||
constructor(type: CriterionOption) {
|
||||
export class TimestampCriterion extends ModifierCriterion<ITimestampValue> {
|
||||
constructor(type: ModifierCriterionOption) {
|
||||
super(type, { value: "", value2: undefined });
|
||||
}
|
||||
|
||||
|
||||
109
ui/v2.5/src/models/list-filter/criteria/custom-fields.ts
Normal file
109
ui/v2.5/src/models/list-filter/criteria/custom-fields.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { IntlShape } from "react-intl";
|
||||
import { Criterion, CriterionOption, ModifierCriterion } from "./criterion";
|
||||
import {
|
||||
CriterionModifier,
|
||||
CustomFieldCriterionInput,
|
||||
} from "src/core/generated-graphql";
|
||||
import { cloneDeep } from "@apollo/client/utilities";
|
||||
|
||||
export const CustomFieldsCriterionOption = new CriterionOption({
|
||||
type: "custom_fields",
|
||||
messageID: "custom_fields.title",
|
||||
makeCriterion: () => new CustomFieldsCriterion(),
|
||||
});
|
||||
|
||||
export class CustomFieldsCriterion extends Criterion {
|
||||
public value: CustomFieldCriterionInput[] = [];
|
||||
|
||||
constructor() {
|
||||
super(CustomFieldsCriterionOption);
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return this.value.length > 0;
|
||||
}
|
||||
|
||||
public applyToCriterionInput(input: Record<string, unknown>): void {
|
||||
input.custom_fields = cloneDeep(this.value);
|
||||
}
|
||||
|
||||
public getLabel(intl: IntlShape): string {
|
||||
// show first criterion
|
||||
if (this.value.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const first = this.value[0];
|
||||
let messageID;
|
||||
let valueString = "";
|
||||
|
||||
if (
|
||||
first.modifier !== CriterionModifier.IsNull &&
|
||||
first.modifier !== CriterionModifier.NotNull &&
|
||||
(first.value?.length ?? 0) > 0
|
||||
) {
|
||||
valueString = (first.value![0] as string) ?? "";
|
||||
}
|
||||
|
||||
const modifierString = ModifierCriterion.getModifierLabel(
|
||||
intl,
|
||||
first.modifier
|
||||
);
|
||||
const opts = {
|
||||
criterion: first.field,
|
||||
modifierString,
|
||||
valueString,
|
||||
others: "",
|
||||
};
|
||||
|
||||
if (this.value.length === 1) {
|
||||
messageID = "custom_fields.criteria_format_string";
|
||||
} else {
|
||||
messageID = "custom_fields.criteria_format_string_others";
|
||||
opts.others = (this.value.length - 1).toString();
|
||||
}
|
||||
|
||||
return intl.formatMessage({ id: messageID }, opts);
|
||||
}
|
||||
|
||||
public getValueLabel(intl: IntlShape, v: CustomFieldCriterionInput): string {
|
||||
let valueString = "";
|
||||
|
||||
if (
|
||||
v.modifier !== CriterionModifier.IsNull &&
|
||||
v.modifier !== CriterionModifier.NotNull &&
|
||||
(v.value?.length ?? 0) > 0
|
||||
) {
|
||||
valueString = (v.value![0] as string) ?? "";
|
||||
}
|
||||
|
||||
const modifierString = ModifierCriterion.getModifierLabel(intl, v.modifier);
|
||||
const opts = {
|
||||
criterion: v.field,
|
||||
modifierString,
|
||||
valueString,
|
||||
others: "",
|
||||
};
|
||||
|
||||
return intl.formatMessage(
|
||||
{ id: "custom_fields.criteria_format_string" },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
public toJSON(): string {
|
||||
const encodedCriterion = {
|
||||
type: this.criterionOption.type,
|
||||
value: this.value,
|
||||
};
|
||||
return JSON.stringify(encodedCriterion);
|
||||
}
|
||||
|
||||
public setFromSavedCriterion(criterion: {
|
||||
type: string;
|
||||
value: CustomFieldCriterionInput[];
|
||||
}): void {
|
||||
const { value } = criterion;
|
||||
this.value = cloneDeep(value);
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
} from "src/core/generated-graphql";
|
||||
import { genderStrings, stringToGender } from "src/utils/gender";
|
||||
import {
|
||||
CriterionOption,
|
||||
ModifierCriterionOption,
|
||||
ISavedCriterion,
|
||||
MultiStringCriterion,
|
||||
} from "./criterion";
|
||||
|
||||
export const GenderCriterionOption = new CriterionOption({
|
||||
export const GenderCriterionOption = new ModifierCriterionOption({
|
||||
messageID: "gender",
|
||||
type: "gender",
|
||||
options: genderStrings,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { CriterionOption, IHierarchicalLabeledIdCriterion } from "./criterion";
|
||||
import {
|
||||
ModifierCriterionOption,
|
||||
IHierarchicalLabeledIdCriterion,
|
||||
} from "./criterion";
|
||||
import { CriterionType } from "../types";
|
||||
|
||||
const inputType = "groups";
|
||||
@@ -13,7 +16,7 @@ const modifierOptions = [
|
||||
|
||||
const defaultModifier = CriterionModifier.Includes;
|
||||
|
||||
class BaseGroupsCriterionOption extends CriterionOption {
|
||||
class BaseGroupsCriterionOption extends ModifierCriterionOption {
|
||||
constructor(messageID: string, type: CriterionType) {
|
||||
super({
|
||||
messageID,
|
||||
@@ -44,7 +47,7 @@ export const SubGroupsCriterionOption = new BaseGroupsCriterionOption(
|
||||
);
|
||||
|
||||
// redirects to GroupsCriterion
|
||||
export const LegacyMoviesCriterionOption = new CriterionOption({
|
||||
export const LegacyMoviesCriterionOption = new ModifierCriterionOption({
|
||||
messageID: "groups",
|
||||
type: "movies",
|
||||
modifierOptions,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { CriterionType } from "../types";
|
||||
import { CriterionOption, StringCriterion, Option } from "./criterion";
|
||||
import { ModifierCriterionOption, StringCriterion, Option } from "./criterion";
|
||||
|
||||
export class IsMissingCriterion extends StringCriterion {
|
||||
public toCriterionInput(): string {
|
||||
@@ -8,7 +8,7 @@ export class IsMissingCriterion extends StringCriterion {
|
||||
}
|
||||
}
|
||||
|
||||
class IsMissingCriterionOption extends CriterionOption {
|
||||
class IsMissingCriterionOption extends ModifierCriterionOption {
|
||||
constructor(messageID: string, type: CriterionType, options: Option[]) {
|
||||
super({
|
||||
messageID,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { orientationStrings, stringToOrientation } from "src/utils/orientation";
|
||||
import { CriterionType } from "../types";
|
||||
import { CriterionOption, MultiStringCriterion } from "./criterion";
|
||||
import { ModifierCriterionOption, MultiStringCriterion } from "./criterion";
|
||||
import {
|
||||
OrientationCriterionInput,
|
||||
OrientationEnum,
|
||||
@@ -16,7 +16,7 @@ export class OrientationCriterion extends MultiStringCriterion {
|
||||
}
|
||||
}
|
||||
|
||||
class BaseOrientationCriterionOption extends CriterionOption {
|
||||
class BaseOrientationCriterionOption extends ModifierCriterionOption {
|
||||
constructor(value: CriterionType) {
|
||||
super({
|
||||
messageID: value,
|
||||
|
||||
@@ -5,7 +5,11 @@ import {
|
||||
MultiCriterionInput,
|
||||
} from "src/core/generated-graphql";
|
||||
import { ILabeledId, ILabeledValueListValue } from "../types";
|
||||
import { Criterion, CriterionOption, ISavedCriterion } from "./criterion";
|
||||
import {
|
||||
ModifierCriterion,
|
||||
ModifierCriterionOption,
|
||||
ISavedCriterion,
|
||||
} from "./criterion";
|
||||
|
||||
const modifierOptions = [
|
||||
CriterionModifier.IncludesAll,
|
||||
@@ -19,7 +23,7 @@ const defaultModifier = CriterionModifier.IncludesAll;
|
||||
|
||||
const inputType = "performers";
|
||||
|
||||
export const PerformersCriterionOption = new CriterionOption({
|
||||
export const PerformersCriterionOption = new ModifierCriterionOption({
|
||||
messageID: "performers",
|
||||
type: "performers",
|
||||
modifierOptions,
|
||||
@@ -28,7 +32,7 @@ export const PerformersCriterionOption = new CriterionOption({
|
||||
makeCriterion: () => new PerformersCriterion(),
|
||||
});
|
||||
|
||||
export class PerformersCriterion extends Criterion<ILabeledValueListValue> {
|
||||
export class PerformersCriterion extends ModifierCriterion<ILabeledValueListValue> {
|
||||
constructor() {
|
||||
super(PerformersCriterionOption, { items: [], excluded: [] });
|
||||
}
|
||||
@@ -116,7 +120,10 @@ export class PerformersCriterion extends Criterion<ILabeledValueListValue> {
|
||||
|
||||
public getLabel(intl: IntlShape): string {
|
||||
let id = "criterion_modifier.format_string";
|
||||
let modifierString = Criterion.getModifierLabel(intl, this.modifier);
|
||||
let modifierString = ModifierCriterion.getModifierLabel(
|
||||
intl,
|
||||
this.modifier
|
||||
);
|
||||
let valueString = "";
|
||||
let excludedString = "";
|
||||
|
||||
@@ -128,7 +135,7 @@ export class PerformersCriterion extends Criterion<ILabeledValueListValue> {
|
||||
|
||||
if (this.value.excluded && this.value.excluded.length > 0) {
|
||||
if (this.value.items.length === 0) {
|
||||
modifierString = Criterion.getModifierLabel(
|
||||
modifierString = ModifierCriterion.getModifierLabel(
|
||||
intl,
|
||||
CriterionModifier.Excludes
|
||||
);
|
||||
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
import { IPhashDistanceValue } from "../types";
|
||||
import {
|
||||
BooleanCriterionOption,
|
||||
Criterion,
|
||||
CriterionOption,
|
||||
ModifierCriterion,
|
||||
ModifierCriterionOption,
|
||||
StringCriterion,
|
||||
} from "./criterion";
|
||||
|
||||
export const PhashCriterionOption = new CriterionOption({
|
||||
export const PhashCriterionOption = new ModifierCriterionOption({
|
||||
messageID: "media_info.phash",
|
||||
type: "phash_distance",
|
||||
inputType: "text",
|
||||
@@ -24,7 +24,7 @@ export const PhashCriterionOption = new CriterionOption({
|
||||
makeCriterion: () => new PhashCriterion(),
|
||||
});
|
||||
|
||||
export class PhashCriterion extends Criterion<IPhashDistanceValue> {
|
||||
export class PhashCriterion extends ModifierCriterion<IPhashDistanceValue> {
|
||||
constructor() {
|
||||
super(PhashCriterionOption, { value: "", distance: 0 });
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
IntCriterionInput,
|
||||
} from "src/core/generated-graphql";
|
||||
import { INumberValue } from "../types";
|
||||
import { Criterion, CriterionOption } from "./criterion";
|
||||
import { ModifierCriterion, ModifierCriterionOption } from "./criterion";
|
||||
|
||||
const modifierOptions = [
|
||||
CriterionModifier.Equals,
|
||||
@@ -27,7 +27,7 @@ function getRatingSystemOptions(config?: ConfigDataFragment) {
|
||||
return config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions;
|
||||
}
|
||||
|
||||
export const RatingCriterionOption = new CriterionOption({
|
||||
export const RatingCriterionOption = new ModifierCriterionOption({
|
||||
messageID: "rating",
|
||||
type: "rating100",
|
||||
modifierOptions,
|
||||
@@ -37,7 +37,7 @@ export const RatingCriterionOption = new CriterionOption({
|
||||
inputType: "number",
|
||||
});
|
||||
|
||||
export class RatingCriterion extends Criterion<INumberValue> {
|
||||
export class RatingCriterion extends ModifierCriterion<INumberValue> {
|
||||
ratingSystem: RatingSystemOptions;
|
||||
|
||||
constructor(ratingSystem: RatingSystemOptions) {
|
||||
|
||||
@@ -5,16 +5,16 @@ import {
|
||||
import { stringToResolution, resolutionStrings } from "src/utils/resolution";
|
||||
import { CriterionType } from "../types";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionOption,
|
||||
ModifierCriterion,
|
||||
ModifierCriterionOption,
|
||||
CriterionValue,
|
||||
StringCriterion,
|
||||
} from "./criterion";
|
||||
|
||||
class BaseResolutionCriterionOption extends CriterionOption {
|
||||
class BaseResolutionCriterionOption extends ModifierCriterionOption {
|
||||
constructor(
|
||||
value: CriterionType,
|
||||
makeCriterion: () => Criterion<CriterionValue>
|
||||
makeCriterion: () => ModifierCriterion<CriterionValue>
|
||||
) {
|
||||
super({
|
||||
messageID: value,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
CriterionOption,
|
||||
ModifierCriterionOption,
|
||||
ILabeledIdCriterion,
|
||||
ILabeledIdCriterionOption,
|
||||
} from "./criterion";
|
||||
@@ -28,7 +28,7 @@ const modifierOptions = [
|
||||
|
||||
const defaultModifier = CriterionModifier.Includes;
|
||||
|
||||
export const MarkersScenesCriterionOption = new CriterionOption({
|
||||
export const MarkersScenesCriterionOption = new ModifierCriterionOption({
|
||||
messageID: "scenes",
|
||||
type: "scenes",
|
||||
modifierOptions,
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
StashIdCriterionInput,
|
||||
} from "src/core/generated-graphql";
|
||||
import { IStashIDValue } from "../types";
|
||||
import { Criterion, CriterionOption } from "./criterion";
|
||||
import { ModifierCriterion, ModifierCriterionOption } from "./criterion";
|
||||
|
||||
export const StashIDCriterionOption = new CriterionOption({
|
||||
export const StashIDCriterionOption = new ModifierCriterionOption({
|
||||
messageID: "stash_id",
|
||||
type: "stash_id_endpoint",
|
||||
modifierOptions: [
|
||||
@@ -19,7 +19,7 @@ export const StashIDCriterionOption = new CriterionOption({
|
||||
makeCriterion: () => new StashIDCriterion(),
|
||||
});
|
||||
|
||||
export class StashIDCriterion extends Criterion<IStashIDValue> {
|
||||
export class StashIDCriterion extends ModifierCriterion<IStashIDValue> {
|
||||
constructor() {
|
||||
super(StashIDCriterionOption, {
|
||||
endpoint: "",
|
||||
@@ -56,7 +56,10 @@ export class StashIDCriterion extends Criterion<IStashIDValue> {
|
||||
}
|
||||
|
||||
public getLabel(intl: IntlShape): string {
|
||||
const modifierString = Criterion.getModifierLabel(intl, this.modifier);
|
||||
const modifierString = ModifierCriterion.getModifierLabel(
|
||||
intl,
|
||||
this.modifier
|
||||
);
|
||||
let valueString = "";
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
CriterionOption,
|
||||
ModifierCriterionOption,
|
||||
IHierarchicalLabeledIdCriterion,
|
||||
ILabeledIdCriterion,
|
||||
ILabeledIdCriterionOption,
|
||||
@@ -15,7 +15,7 @@ const modifierOptions = [
|
||||
const defaultModifier = CriterionModifier.Includes;
|
||||
const inputType = "studios";
|
||||
|
||||
export const StudiosCriterionOption = new CriterionOption({
|
||||
export const StudiosCriterionOption = new ModifierCriterionOption({
|
||||
messageID: "studios",
|
||||
type: "studios",
|
||||
modifierOptions,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { CriterionOption, IHierarchicalLabeledIdCriterion } from "./criterion";
|
||||
import {
|
||||
ModifierCriterionOption,
|
||||
IHierarchicalLabeledIdCriterion,
|
||||
} from "./criterion";
|
||||
import { CriterionType } from "../types";
|
||||
|
||||
const defaultModifierOptions = [
|
||||
@@ -20,7 +23,7 @@ const withoutEqualsModifierOptions = [
|
||||
const defaultModifier = CriterionModifier.IncludesAll;
|
||||
const inputType = "tags";
|
||||
|
||||
class BaseTagsCriterionOption extends CriterionOption {
|
||||
class BaseTagsCriterionOption extends ModifierCriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
type: CriterionType,
|
||||
|
||||
@@ -11,13 +11,9 @@ import {
|
||||
ISavedCriterion,
|
||||
} from "./criteria/criterion";
|
||||
import { getFilterOptions } from "./factory";
|
||||
import {
|
||||
CriterionType,
|
||||
DisplayMode,
|
||||
SavedObjectFilter,
|
||||
SavedUIOptions,
|
||||
} from "./types";
|
||||
import { CriterionType, DisplayMode, SavedUIOptions } from "./types";
|
||||
import { ListFilterOptions } from "./filter-options";
|
||||
import { CustomFieldsCriterion } from "./criteria/custom-fields";
|
||||
|
||||
interface IDecodedParams {
|
||||
perPage?: number;
|
||||
@@ -60,7 +56,7 @@ export class ListFilterModel {
|
||||
public sortBy?: string;
|
||||
public displayMode: DisplayMode = DEFAULT_PARAMS.displayMode;
|
||||
public zoomIndex: number = 1;
|
||||
public criteria: Array<Criterion<CriterionValue>> = [];
|
||||
public criteria: Array<Criterion> = [];
|
||||
public randomSeed = -1;
|
||||
private defaultZoomIndex: number = 1;
|
||||
|
||||
@@ -446,15 +442,7 @@ export class ListFilterModel {
|
||||
public makeFilter() {
|
||||
const output: Record<string, unknown> = {};
|
||||
for (const c of this.criteria) {
|
||||
output[c.criterionOption.type] = c.toCriterionInput();
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
public makeSavedFilter() {
|
||||
const output: SavedObjectFilter = {};
|
||||
for (const c of this.criteria) {
|
||||
output[c.criterionOption.type] = c.toSavedCriterion();
|
||||
c.applyToCriterionInput(output);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
@@ -488,6 +476,20 @@ export class ListFilterModel {
|
||||
return ret;
|
||||
}
|
||||
|
||||
public removeCustomFieldCriterion(type: CriterionType, index: number) {
|
||||
const ret = this.clone();
|
||||
const c = ret.criteria.find((cc) => cc.criterionOption.type === type);
|
||||
|
||||
if (!c) return ret;
|
||||
|
||||
if (c instanceof CustomFieldsCriterion) {
|
||||
const newCriteria = c.value.filter((_, i) => i !== index);
|
||||
c.value = newCriteria;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public setPageSize(pageSize: number) {
|
||||
const ret = this.clone();
|
||||
ret.itemsPerPage = pageSize;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ListFilterOptions } from "./filter-options";
|
||||
import { CriterionType, DisplayMode } from "./types";
|
||||
import { CountryCriterionOption } from "./criteria/country";
|
||||
import { RatingCriterionOption } from "./criteria/rating";
|
||||
import { CustomFieldsCriterionOption } from "./criteria/custom-fields";
|
||||
|
||||
const defaultSortBy = "name";
|
||||
const sortByOptions = [
|
||||
@@ -108,6 +109,7 @@ const criterionOptions = [
|
||||
createDateCriterionOption("death_date"),
|
||||
createMandatoryTimestampCriterionOption("created_at"),
|
||||
createMandatoryTimestampCriterionOption("updated_at"),
|
||||
CustomFieldsCriterionOption,
|
||||
];
|
||||
export const PerformerListFilterOptions = new ListFilterOptions(
|
||||
defaultSortBy,
|
||||
|
||||
@@ -220,4 +220,5 @@ export type CriterionType =
|
||||
| "photographer"
|
||||
| "disambiguation"
|
||||
| "has_chapters"
|
||||
| "sort_name";
|
||||
| "sort_name"
|
||||
| "custom_fields";
|
||||
|
||||
@@ -19,11 +19,12 @@ import {
|
||||
SubGroupsCriterionOption,
|
||||
} from "src/models/list-filter/criteria/groups";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionOption,
|
||||
ModifierCriterion,
|
||||
ModifierCriterionOption,
|
||||
CriterionValue,
|
||||
StringCriterion,
|
||||
createStringCriterionOption,
|
||||
Criterion,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { GalleriesCriterion } from "src/models/list-filter/criteria/galleries";
|
||||
import { PhashCriterion } from "src/models/list-filter/criteria/phash";
|
||||
@@ -33,10 +34,7 @@ import { galleryTitle } from "src/core/galleries";
|
||||
import { MarkersScenesCriterion } from "src/models/list-filter/criteria/scenes";
|
||||
import { objectTitle } from "src/core/files";
|
||||
|
||||
function addExtraCriteria(
|
||||
dest: Criterion<CriterionValue>[],
|
||||
src?: Criterion<CriterionValue>[]
|
||||
) {
|
||||
function addExtraCriteria(dest: Criterion[], src?: Criterion[]) {
|
||||
if (src && src.length > 0) {
|
||||
dest.push(...src);
|
||||
}
|
||||
@@ -45,7 +43,7 @@ function addExtraCriteria(
|
||||
const makePerformerScenesUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>,
|
||||
extraPerformer?: ILabeledId,
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
extraCriteria?: ModifierCriterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Scenes, undefined);
|
||||
@@ -66,7 +64,7 @@ const makePerformerScenesUrl = (
|
||||
const makePerformerImagesUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>,
|
||||
extraPerformer?: ILabeledId,
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
extraCriteria?: ModifierCriterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Images, undefined);
|
||||
@@ -93,7 +91,7 @@ export interface INamedObject {
|
||||
const makePerformerGalleriesUrl = (
|
||||
performer: INamedObject,
|
||||
extraPerformer?: ILabeledId,
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
extraCriteria?: ModifierCriterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Galleries, undefined);
|
||||
@@ -114,7 +112,7 @@ const makePerformerGalleriesUrl = (
|
||||
const makePerformerGroupsUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>,
|
||||
extraPerformer?: ILabeledId,
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
extraCriteria?: ModifierCriterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Groups, undefined);
|
||||
@@ -346,7 +344,7 @@ const makeScenesPHashMatchUrl = (phash: GQL.Maybe<string> | undefined) => {
|
||||
|
||||
const makeGalleryImagesUrl = (
|
||||
gallery: Partial<GQL.GalleryDataFragment | GQL.SlimGalleryDataFragment>,
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
extraCriteria?: ModifierCriterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!gallery.id) return "#";
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Images, undefined);
|
||||
@@ -357,7 +355,7 @@ const makeGalleryImagesUrl = (
|
||||
return `/images?${filter.makeQueryParameters()}`;
|
||||
};
|
||||
|
||||
function stringEqualsCriterion(option: CriterionOption, value: string) {
|
||||
function stringEqualsCriterion(option: ModifierCriterionOption, value: string) {
|
||||
const criterion = new StringCriterion(option);
|
||||
criterion.modifier = GQL.CriterionModifier.Equals;
|
||||
criterion.value = value;
|
||||
|
||||
Reference in New Issue
Block a user