mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
UI filter builder (#3515)
* Add clear criteria button * Add count to filter button
This commit is contained in:
@@ -1,348 +0,0 @@
|
|||||||
import cloneDeep from "lodash-es/cloneDeep";
|
|
||||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
|
||||||
import { Button, Form, Modal } from "react-bootstrap";
|
|
||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
|
||||||
import {
|
|
||||||
DurationCriterion,
|
|
||||||
CriterionValue,
|
|
||||||
Criterion,
|
|
||||||
IHierarchicalLabeledIdCriterion,
|
|
||||||
NumberCriterion,
|
|
||||||
ILabeledIdCriterion,
|
|
||||||
DateCriterion,
|
|
||||||
TimestampCriterion,
|
|
||||||
} from "src/models/list-filter/criteria/criterion";
|
|
||||||
import {
|
|
||||||
NoneCriterion,
|
|
||||||
NoneCriterionOption,
|
|
||||||
} from "src/models/list-filter/criteria/none";
|
|
||||||
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
|
||||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
|
||||||
import {
|
|
||||||
criterionIsHierarchicalLabelValue,
|
|
||||||
criterionIsNumberValue,
|
|
||||||
criterionIsStashIDValue,
|
|
||||||
criterionIsDateValue,
|
|
||||||
criterionIsTimestampValue,
|
|
||||||
CriterionType,
|
|
||||||
} from "src/models/list-filter/types";
|
|
||||||
import { DurationFilter } from "./Filters/DurationFilter";
|
|
||||||
import { NumberFilter } from "./Filters/NumberFilter";
|
|
||||||
import { LabeledIdFilter } from "./Filters/LabeledIdFilter";
|
|
||||||
import { HierarchicalLabelValueFilter } from "./Filters/HierarchicalLabelValueFilter";
|
|
||||||
import { OptionsFilter } from "./Filters/OptionsFilter";
|
|
||||||
import { InputFilter } from "./Filters/InputFilter";
|
|
||||||
import { DateFilter } from "./Filters/DateFilter";
|
|
||||||
import { TimestampFilter } from "./Filters/TimestampFilter";
|
|
||||||
import { CountryCriterion } from "src/models/list-filter/criteria/country";
|
|
||||||
import { CountrySelect } from "../Shared/CountrySelect";
|
|
||||||
import { StashIDCriterion } from "src/models/list-filter/criteria/stash-ids";
|
|
||||||
import { StashIDFilter } from "./Filters/StashIDFilter";
|
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
|
||||||
import { RatingCriterion } from "../../models/list-filter/criteria/rating";
|
|
||||||
import { RatingFilter } from "./Filters/RatingFilter";
|
|
||||||
|
|
||||||
interface IAddFilterProps {
|
|
||||||
onAddCriterion: (
|
|
||||||
criterion: Criterion<CriterionValue>,
|
|
||||||
oldId?: string
|
|
||||||
) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
filterOptions: ListFilterOptions;
|
|
||||||
editingCriterion?: Criterion<CriterionValue>;
|
|
||||||
existingCriterions: Criterion<CriterionValue>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
|
||||||
onAddCriterion,
|
|
||||||
onCancel,
|
|
||||||
filterOptions,
|
|
||||||
editingCriterion,
|
|
||||||
existingCriterions,
|
|
||||||
}) => {
|
|
||||||
const defaultValue = useRef<string | number | undefined>();
|
|
||||||
|
|
||||||
const [criterion, setCriterion] = useState<Criterion<CriterionValue>>(
|
|
||||||
new NoneCriterion()
|
|
||||||
);
|
|
||||||
const { options, modifierOptions } = criterion.criterionOption;
|
|
||||||
|
|
||||||
const valueStage = useRef<CriterionValue>(criterion.value);
|
|
||||||
const { configuration: config } = useContext(ConfigurationContext);
|
|
||||||
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
// Configure if we are editing an existing criterion
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editingCriterion) {
|
|
||||||
setCriterion(makeCriteria(config));
|
|
||||||
} else {
|
|
||||||
setCriterion(editingCriterion);
|
|
||||||
}
|
|
||||||
}, [config, editingCriterion]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
valueStage.current = criterion.value;
|
|
||||||
}, [criterion]);
|
|
||||||
|
|
||||||
function onChangedCriteriaType(event: React.ChangeEvent<HTMLSelectElement>) {
|
|
||||||
const newCriterionType = event.target.value as CriterionType;
|
|
||||||
const newCriterion = makeCriteria(config, newCriterionType);
|
|
||||||
setCriterion(newCriterion);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChangedModifierSelect(
|
|
||||||
event: React.ChangeEvent<HTMLSelectElement>
|
|
||||||
) {
|
|
||||||
const newCriterion = cloneDeep(criterion);
|
|
||||||
newCriterion.modifier = event.target.value as CriterionModifier;
|
|
||||||
setCriterion(newCriterion);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onValueChanged(value: CriterionValue) {
|
|
||||||
const newCriterion = cloneDeep(criterion);
|
|
||||||
newCriterion.value = value;
|
|
||||||
setCriterion(newCriterion);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAddFilter() {
|
|
||||||
const oldId = editingCriterion ? editingCriterion.getId() : undefined;
|
|
||||||
onAddCriterion(criterion, oldId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const maybeRenderFilterPopoverContents = () => {
|
|
||||||
if (criterion.criterionOption.type === "none") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderModifier() {
|
|
||||||
if (modifierOptions.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Form.Control
|
|
||||||
as="select"
|
|
||||||
onChange={onChangedModifierSelect}
|
|
||||||
value={criterion.modifier}
|
|
||||||
className="btn-secondary"
|
|
||||||
>
|
|
||||||
{modifierOptions.map((c) => (
|
|
||||||
<option key={c.value} value={c.value}>
|
|
||||||
{c.label ? intl.formatMessage({ id: c.label }) : ""}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Form.Control>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSelect() {
|
|
||||||
// always show stashID filter
|
|
||||||
if (criterion instanceof StashIDCriterion) {
|
|
||||||
return (
|
|
||||||
<StashIDFilter
|
|
||||||
criterion={criterion}
|
|
||||||
onValueChanged={onValueChanged}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide the value select if the modifier is "IsNull" or "NotNull"
|
|
||||||
if (
|
|
||||||
criterion.modifier === CriterionModifier.IsNull ||
|
|
||||||
criterion.modifier === CriterionModifier.NotNull
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (criterion instanceof ILabeledIdCriterion) {
|
|
||||||
return (
|
|
||||||
<LabeledIdFilter
|
|
||||||
criterion={criterion}
|
|
||||||
onValueChanged={onValueChanged}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
|
||||||
return (
|
|
||||||
<HierarchicalLabelValueFilter
|
|
||||||
criterion={criterion}
|
|
||||||
onValueChanged={onValueChanged}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
options &&
|
|
||||||
!criterionIsHierarchicalLabelValue(criterion.value) &&
|
|
||||||
!criterionIsNumberValue(criterion.value) &&
|
|
||||||
!criterionIsStashIDValue(criterion.value) &&
|
|
||||||
!criterionIsDateValue(criterion.value) &&
|
|
||||||
!criterionIsTimestampValue(criterion.value) &&
|
|
||||||
!Array.isArray(criterion.value)
|
|
||||||
) {
|
|
||||||
defaultValue.current = criterion.value;
|
|
||||||
return (
|
|
||||||
<OptionsFilter
|
|
||||||
criterion={criterion}
|
|
||||||
onValueChanged={onValueChanged}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (criterion instanceof DurationCriterion) {
|
|
||||||
return (
|
|
||||||
<DurationFilter
|
|
||||||
criterion={criterion}
|
|
||||||
onValueChanged={onValueChanged}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (criterion instanceof DateCriterion) {
|
|
||||||
return (
|
|
||||||
<DateFilter criterion={criterion} onValueChanged={onValueChanged} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (criterion instanceof TimestampCriterion) {
|
|
||||||
return (
|
|
||||||
<TimestampFilter
|
|
||||||
criterion={criterion}
|
|
||||||
onValueChanged={onValueChanged}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (criterion instanceof NumberCriterion) {
|
|
||||||
return (
|
|
||||||
<NumberFilter criterion={criterion} onValueChanged={onValueChanged} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (criterion instanceof RatingCriterion) {
|
|
||||||
return (
|
|
||||||
<RatingFilter criterion={criterion} onValueChanged={onValueChanged} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
criterion instanceof CountryCriterion &&
|
|
||||||
(criterion.modifier === CriterionModifier.Equals ||
|
|
||||||
criterion.modifier === CriterionModifier.NotEquals)
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<CountrySelect
|
|
||||||
value={criterion.value}
|
|
||||||
onChange={(v) => onValueChanged(v)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<InputFilter criterion={criterion} onValueChanged={onValueChanged} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form.Group>{renderModifier()}</Form.Group>
|
|
||||||
{renderSelect()}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function maybeRenderFilterCriterion() {
|
|
||||||
if (!editingCriterion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form.Group>
|
|
||||||
<strong>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: editingCriterion.criterionOption.messageID,
|
|
||||||
})}
|
|
||||||
</strong>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeRenderFilterSelect() {
|
|
||||||
if (editingCriterion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const thisOptions = [NoneCriterionOption]
|
|
||||||
.concat(filterOptions.criterionOptions)
|
|
||||||
.filter(
|
|
||||||
(c) =>
|
|
||||||
!existingCriterions.find((ec) => ec.criterionOption.type === c.type)
|
|
||||||
)
|
|
||||||
.map((c) => {
|
|
||||||
return {
|
|
||||||
value: c.type,
|
|
||||||
text: intl.formatMessage({ id: c.messageID }),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.value === "none") return -1;
|
|
||||||
if (b.value === "none") return 1;
|
|
||||||
return a.text.localeCompare(b.text);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form.Group controlId="filter">
|
|
||||||
<Form.Label>
|
|
||||||
<FormattedMessage id="search_filter.name" />
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
as="select"
|
|
||||||
onChange={onChangedCriteriaType}
|
|
||||||
value={criterion.criterionOption.type}
|
|
||||||
className="btn-secondary"
|
|
||||||
>
|
|
||||||
{thisOptions.map((c) => (
|
|
||||||
<option key={c.value} value={c.value} disabled={c.value === "none"}>
|
|
||||||
{c.text}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValid() {
|
|
||||||
if (criterion.criterionOption.type === "none") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (criterion instanceof RatingCriterion) {
|
|
||||||
switch (criterion.modifier) {
|
|
||||||
case CriterionModifier.Equals:
|
|
||||||
case CriterionModifier.NotEquals:
|
|
||||||
case CriterionModifier.LessThan:
|
|
||||||
return !!criterion.value.value;
|
|
||||||
case CriterionModifier.Between:
|
|
||||||
case CriterionModifier.NotBetween:
|
|
||||||
return criterion.value.value < (criterion.value.value2 ?? 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = !editingCriterion
|
|
||||||
? intl.formatMessage({ id: "search_filter.add_filter" })
|
|
||||||
: intl.formatMessage({ id: "search_filter.update_filter" });
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal show onHide={() => onCancel()} className="add-filter-dialog">
|
|
||||||
<Modal.Header>{title}</Modal.Header>
|
|
||||||
<Modal.Body>
|
|
||||||
<div className="dialog-content">
|
|
||||||
{maybeRenderFilterSelect()}
|
|
||||||
{maybeRenderFilterCriterion()}
|
|
||||||
{maybeRenderFilterPopoverContents()}
|
|
||||||
</div>
|
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
<Button onClick={onAddFilter} disabled={!isValid()}>
|
|
||||||
{title}
|
|
||||||
</Button>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
218
ui/v2.5/src/components/List/CriterionEditor.tsx
Normal file
218
ui/v2.5/src/components/List/CriterionEditor.tsx
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
|
import React, { useCallback, useMemo } from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
|
import {
|
||||||
|
DurationCriterion,
|
||||||
|
CriterionValue,
|
||||||
|
Criterion,
|
||||||
|
IHierarchicalLabeledIdCriterion,
|
||||||
|
NumberCriterion,
|
||||||
|
ILabeledIdCriterion,
|
||||||
|
DateCriterion,
|
||||||
|
TimestampCriterion,
|
||||||
|
BooleanCriterion,
|
||||||
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import {
|
||||||
|
criterionIsHierarchicalLabelValue,
|
||||||
|
criterionIsNumberValue,
|
||||||
|
criterionIsStashIDValue,
|
||||||
|
criterionIsDateValue,
|
||||||
|
criterionIsTimestampValue,
|
||||||
|
} from "src/models/list-filter/types";
|
||||||
|
import { DurationFilter } from "./Filters/DurationFilter";
|
||||||
|
import { NumberFilter } from "./Filters/NumberFilter";
|
||||||
|
import { LabeledIdFilter } from "./Filters/LabeledIdFilter";
|
||||||
|
import { HierarchicalLabelValueFilter } from "./Filters/HierarchicalLabelValueFilter";
|
||||||
|
import { InputFilter } from "./Filters/InputFilter";
|
||||||
|
import { DateFilter } from "./Filters/DateFilter";
|
||||||
|
import { TimestampFilter } from "./Filters/TimestampFilter";
|
||||||
|
import { CountryCriterion } from "src/models/list-filter/criteria/country";
|
||||||
|
import { CountrySelect } from "../Shared/CountrySelect";
|
||||||
|
import { StashIDCriterion } from "src/models/list-filter/criteria/stash-ids";
|
||||||
|
import { StashIDFilter } from "./Filters/StashIDFilter";
|
||||||
|
import { RatingCriterion } from "../../models/list-filter/criteria/rating";
|
||||||
|
import { RatingFilter } from "./Filters/RatingFilter";
|
||||||
|
import { BooleanFilter } from "./Filters/BooleanFilter";
|
||||||
|
import { OptionsListFilter } from "./Filters/OptionsListFilter";
|
||||||
|
|
||||||
|
interface IGenericCriterionEditor {
|
||||||
|
criterion: Criterion<CriterionValue>;
|
||||||
|
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenericCriterionEditor: React.FC<IGenericCriterionEditor> = ({
|
||||||
|
criterion,
|
||||||
|
setCriterion,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const { options, modifierOptions } = criterion.criterionOption;
|
||||||
|
|
||||||
|
const onChangedModifierSelect = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const newCriterion = cloneDeep(criterion);
|
||||||
|
newCriterion.modifier = event.target.value as CriterionModifier;
|
||||||
|
setCriterion(newCriterion);
|
||||||
|
},
|
||||||
|
[criterion, setCriterion]
|
||||||
|
);
|
||||||
|
|
||||||
|
const modifierSelector = useMemo(() => {
|
||||||
|
if (!modifierOptions || modifierOptions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Control
|
||||||
|
as="select"
|
||||||
|
onChange={onChangedModifierSelect}
|
||||||
|
value={criterion.modifier}
|
||||||
|
className="btn-secondary modifier-selector"
|
||||||
|
>
|
||||||
|
{modifierOptions.map((c) => (
|
||||||
|
<option key={c.value} value={c.value}>
|
||||||
|
{c.label ? intl.formatMessage({ id: c.label }) : ""}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Form.Control>
|
||||||
|
);
|
||||||
|
}, [modifierOptions, onChangedModifierSelect, criterion.modifier, intl]);
|
||||||
|
|
||||||
|
const valueControl = useMemo(() => {
|
||||||
|
function onValueChanged(value: CriterionValue) {
|
||||||
|
const newCriterion = cloneDeep(criterion);
|
||||||
|
newCriterion.value = value;
|
||||||
|
setCriterion(newCriterion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// always show stashID filter
|
||||||
|
if (criterion instanceof StashIDCriterion) {
|
||||||
|
return (
|
||||||
|
<StashIDFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the value select if the modifier is "IsNull" or "NotNull"
|
||||||
|
if (
|
||||||
|
criterion.modifier === CriterionModifier.IsNull ||
|
||||||
|
criterion.modifier === CriterionModifier.NotNull
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (criterion instanceof ILabeledIdCriterion) {
|
||||||
|
return (
|
||||||
|
<LabeledIdFilter
|
||||||
|
criterion={criterion}
|
||||||
|
onValueChanged={onValueChanged}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
||||||
|
return (
|
||||||
|
<HierarchicalLabelValueFilter
|
||||||
|
criterion={criterion}
|
||||||
|
onValueChanged={onValueChanged}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options &&
|
||||||
|
!criterionIsHierarchicalLabelValue(criterion.value) &&
|
||||||
|
!criterionIsNumberValue(criterion.value) &&
|
||||||
|
!criterionIsStashIDValue(criterion.value) &&
|
||||||
|
!criterionIsDateValue(criterion.value) &&
|
||||||
|
!criterionIsTimestampValue(criterion.value) &&
|
||||||
|
!Array.isArray(criterion.value)
|
||||||
|
) {
|
||||||
|
// if (!modifierOptions || modifierOptions.length === 0) {
|
||||||
|
return (
|
||||||
|
<OptionsListFilter criterion={criterion} setCriterion={setCriterion} />
|
||||||
|
);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <OptionsFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
if (criterion instanceof DurationCriterion) {
|
||||||
|
return (
|
||||||
|
<DurationFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (criterion instanceof DateCriterion) {
|
||||||
|
return (
|
||||||
|
<DateFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (criterion instanceof TimestampCriterion) {
|
||||||
|
return (
|
||||||
|
<TimestampFilter
|
||||||
|
criterion={criterion}
|
||||||
|
onValueChanged={onValueChanged}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (criterion instanceof NumberCriterion) {
|
||||||
|
return (
|
||||||
|
<NumberFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (criterion instanceof RatingCriterion) {
|
||||||
|
return (
|
||||||
|
<RatingFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
criterion instanceof CountryCriterion &&
|
||||||
|
(criterion.modifier === CriterionModifier.Equals ||
|
||||||
|
criterion.modifier === CriterionModifier.NotEquals)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<CountrySelect
|
||||||
|
value={criterion.value}
|
||||||
|
onChange={(v) => onValueChanged(v)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<InputFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
|
);
|
||||||
|
}, [criterion, setCriterion, options]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{modifierSelector}
|
||||||
|
{valueControl}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ICriterionEditor {
|
||||||
|
criterion: Criterion<CriterionValue>;
|
||||||
|
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CriterionEditor: React.FC<ICriterionEditor> = ({
|
||||||
|
criterion,
|
||||||
|
setCriterion,
|
||||||
|
}) => {
|
||||||
|
const filterControl = useMemo(() => {
|
||||||
|
if (criterion instanceof BooleanCriterion) {
|
||||||
|
return (
|
||||||
|
<BooleanFilter criterion={criterion} setCriterion={setCriterion} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericCriterionEditor
|
||||||
|
criterion={criterion}
|
||||||
|
setCriterion={setCriterion}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [criterion, setCriterion]);
|
||||||
|
|
||||||
|
return <div className="criterion-editor">{filterControl}</div>;
|
||||||
|
};
|
||||||
340
ui/v2.5/src/components/List/EditFilterDialog.tsx
Normal file
340
ui/v2.5/src/components/List/EditFilterDialog.tsx
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Accordion, Button, Card, Modal } from "react-bootstrap";
|
||||||
|
import cx from "classnames";
|
||||||
|
import {
|
||||||
|
CriterionValue,
|
||||||
|
Criterion,
|
||||||
|
CriterionOption,
|
||||||
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
|
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { getFilterOptions } from "src/models/list-filter/factory";
|
||||||
|
import { FilterTags } from "./FilterTags";
|
||||||
|
import { CriterionEditor } from "./CriterionEditor";
|
||||||
|
import { Icon } from "../Shared/Icon";
|
||||||
|
import {
|
||||||
|
faChevronDown,
|
||||||
|
faChevronRight,
|
||||||
|
faTimes,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { useCompare, usePrevious } from "src/hooks/state";
|
||||||
|
import { CriterionType } from "src/models/list-filter/types";
|
||||||
|
|
||||||
|
interface ICriterionList {
|
||||||
|
criteria: string[];
|
||||||
|
currentCriterion?: Criterion<CriterionValue>;
|
||||||
|
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||||
|
criterionOptions: CriterionOption[];
|
||||||
|
selected?: CriterionOption;
|
||||||
|
optionSelected: (o?: CriterionOption) => void;
|
||||||
|
onRemoveCriterion: (c: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CriterionOptionList: React.FC<ICriterionList> = ({
|
||||||
|
criteria,
|
||||||
|
currentCriterion,
|
||||||
|
setCriterion,
|
||||||
|
criterionOptions,
|
||||||
|
selected,
|
||||||
|
optionSelected,
|
||||||
|
onRemoveCriterion,
|
||||||
|
}) => {
|
||||||
|
const prevCriterion = usePrevious(currentCriterion);
|
||||||
|
|
||||||
|
const scrolled = useRef(false);
|
||||||
|
|
||||||
|
const type = currentCriterion?.criterionOption.type;
|
||||||
|
const prevType = prevCriterion?.criterionOption.type;
|
||||||
|
|
||||||
|
const criteriaRefs = useMemo(() => {
|
||||||
|
const refs: Record<string, React.RefObject<HTMLDivElement>> = {};
|
||||||
|
criterionOptions.forEach((c) => {
|
||||||
|
refs[c.type] = React.createRef();
|
||||||
|
});
|
||||||
|
return refs;
|
||||||
|
}, [criterionOptions]);
|
||||||
|
|
||||||
|
function onSelect(k: string | null) {
|
||||||
|
if (!k) {
|
||||||
|
optionSelected(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const option = criterionOptions.find((c) => c.type === k);
|
||||||
|
|
||||||
|
if (option) {
|
||||||
|
optionSelected(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// scrolling to the current criterion doesn't work well when the
|
||||||
|
// dialog is already open, so limit to when we click on the
|
||||||
|
// criterion from the external tags
|
||||||
|
if (!scrolled.current && type && criteriaRefs[type]?.current) {
|
||||||
|
criteriaRefs[type].current!.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "start",
|
||||||
|
});
|
||||||
|
scrolled.current = true;
|
||||||
|
}
|
||||||
|
}, [currentCriterion, criteriaRefs, type]);
|
||||||
|
|
||||||
|
function getReleventCriterion(t: CriterionType) {
|
||||||
|
if (currentCriterion?.criterionOption.type === t) {
|
||||||
|
return currentCriterion;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prevCriterion;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeClicked(ev: React.MouseEvent, t: string) {
|
||||||
|
// needed to prevent the nav item from being selected
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
onRemoveCriterion(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IEditFilterProps {
|
||||||
|
filter: ListFilterModel;
|
||||||
|
editingCriterion?: string;
|
||||||
|
onApply: (filter: ListFilterModel) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
||||||
|
filter,
|
||||||
|
editingCriterion,
|
||||||
|
onApply,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const { configuration: config } = useContext(ConfigurationContext);
|
||||||
|
|
||||||
|
const [currentFilter, setCurrentFilter] = useState<ListFilterModel>(
|
||||||
|
cloneDeep(filter)
|
||||||
|
);
|
||||||
|
const [criterion, setCriterion] = useState<Criterion<CriterionValue>>();
|
||||||
|
|
||||||
|
const { criteria } = currentFilter;
|
||||||
|
|
||||||
|
const criteriaList = useMemo(() => {
|
||||||
|
return criteria.map((c) => c.criterionOption.type);
|
||||||
|
}, [criteria]);
|
||||||
|
|
||||||
|
const filterOptions = useMemo(() => {
|
||||||
|
return getFilterOptions(currentFilter.mode);
|
||||||
|
}, [currentFilter.mode]);
|
||||||
|
|
||||||
|
const criterionOptions = useMemo(() => {
|
||||||
|
const filteredOptions = filterOptions.criterionOptions.filter((o) => {
|
||||||
|
return o.type !== "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredOptions.sort((a, b) => {
|
||||||
|
return intl
|
||||||
|
.formatMessage({ id: a.messageID })
|
||||||
|
.localeCompare(intl.formatMessage({ id: b.messageID }));
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredOptions;
|
||||||
|
}, [intl, filterOptions.criterionOptions]);
|
||||||
|
|
||||||
|
const optionSelected = useCallback(
|
||||||
|
(option?: CriterionOption) => {
|
||||||
|
if (!option) {
|
||||||
|
setCriterion(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the existing criterion if present
|
||||||
|
const existing = criteria.find(
|
||||||
|
(c) => c.criterionOption.type === option.type
|
||||||
|
);
|
||||||
|
if (existing) {
|
||||||
|
setCriterion(existing);
|
||||||
|
} else {
|
||||||
|
const newCriterion = makeCriteria(config, option.type);
|
||||||
|
setCriterion(newCriterion);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[criteria, config]
|
||||||
|
);
|
||||||
|
|
||||||
|
const editingCriterionChanged = useCompare(editingCriterion);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingCriterionChanged && editingCriterion) {
|
||||||
|
const option = criterionOptions.find((c) => c.type === editingCriterion);
|
||||||
|
if (option) {
|
||||||
|
optionSelected(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
editingCriterion,
|
||||||
|
criterionOptions,
|
||||||
|
optionSelected,
|
||||||
|
editingCriterionChanged,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function replaceCriterion(c: Criterion<CriterionValue>) {
|
||||||
|
const newFilter = cloneDeep(currentFilter);
|
||||||
|
|
||||||
|
if (!c.isValid()) {
|
||||||
|
// remove from the filter if present
|
||||||
|
const newCriteria = criteria.filter((cc) => {
|
||||||
|
return cc.criterionOption.type !== c.criterionOption.type;
|
||||||
|
});
|
||||||
|
|
||||||
|
newFilter.criteria = newCriteria;
|
||||||
|
} else {
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
const newCriteria = criteria.map((cc) => {
|
||||||
|
if (cc.criterionOption.type === c.criterionOption.type) {
|
||||||
|
found = true;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
newCriteria.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
newFilter.criteria = newCriteria;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentFilter(newFilter);
|
||||||
|
setCriterion(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCriterion(c: Criterion<CriterionValue>) {
|
||||||
|
const newFilter = cloneDeep(currentFilter);
|
||||||
|
|
||||||
|
const newCriteria = criteria.filter((cc) => {
|
||||||
|
return cc.getId() !== c.getId();
|
||||||
|
});
|
||||||
|
|
||||||
|
newFilter.criteria = newCriteria;
|
||||||
|
|
||||||
|
setCurrentFilter(newFilter);
|
||||||
|
if (criterion?.getId() === c.getId()) {
|
||||||
|
optionSelected(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCriterionString(c: string) {
|
||||||
|
const cc = criteria.find((ccc) => ccc.criterionOption.type === c);
|
||||||
|
if (cc) {
|
||||||
|
removeCriterion(cc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClearAll() {
|
||||||
|
const newFilter = cloneDeep(currentFilter);
|
||||||
|
newFilter.criteria = [];
|
||||||
|
setCurrentFilter(newFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal show onHide={() => onCancel()} className="edit-filter-dialog">
|
||||||
|
<Modal.Header>
|
||||||
|
<FormattedMessage id="search_filter.edit_filter" />
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<div
|
||||||
|
className={cx("dialog-content", {
|
||||||
|
"criterion-selected": !!criterion,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<CriterionOptionList
|
||||||
|
criteria={criteriaList}
|
||||||
|
currentCriterion={criterion}
|
||||||
|
setCriterion={replaceCriterion}
|
||||||
|
criterionOptions={criterionOptions}
|
||||||
|
optionSelected={optionSelected}
|
||||||
|
selected={criterion?.criterionOption}
|
||||||
|
onRemoveCriterion={(c) => removeCriterionString(c)}
|
||||||
|
/>
|
||||||
|
{criteria.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<FilterTags
|
||||||
|
criteria={criteria}
|
||||||
|
onEditCriterion={(c) => optionSelected(c.criterionOption)}
|
||||||
|
onRemoveCriterion={(c) => removeCriterion(c)}
|
||||||
|
onRemoveAll={() => onClearAll()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant="secondary" onClick={() => onCancel()}>
|
||||||
|
<FormattedMessage id="actions.cancel" />
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => onApply(currentFilter)}>
|
||||||
|
<FormattedMessage id="actions.apply" />
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
Criterion,
|
Criterion,
|
||||||
CriterionValue,
|
CriterionValue,
|
||||||
} from "src/models/list-filter/criteria/criterion";
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
import { useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Icon } from "../Shared/Icon";
|
import { Icon } from "../Shared/Icon";
|
||||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
@@ -12,12 +12,14 @@ interface IFilterTagsProps {
|
|||||||
criteria: Criterion<CriterionValue>[];
|
criteria: Criterion<CriterionValue>[];
|
||||||
onEditCriterion: (c: Criterion<CriterionValue>) => void;
|
onEditCriterion: (c: Criterion<CriterionValue>) => void;
|
||||||
onRemoveCriterion: (c: Criterion<CriterionValue>) => void;
|
onRemoveCriterion: (c: Criterion<CriterionValue>) => void;
|
||||||
|
onRemoveAll: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilterTags: React.FC<IFilterTagsProps> = ({
|
export const FilterTags: React.FC<IFilterTagsProps> = ({
|
||||||
criteria,
|
criteria,
|
||||||
onEditCriterion,
|
onEditCriterion,
|
||||||
onRemoveCriterion,
|
onRemoveCriterion,
|
||||||
|
onRemoveAll,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
@@ -55,9 +57,26 @@ export const FilterTags: React.FC<IFilterTagsProps> = ({
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeRenderClearAll() {
|
||||||
|
if (criteria.length < 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="minimal"
|
||||||
|
className="clear-all-button"
|
||||||
|
onClick={() => onRemoveAll()}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="actions.clear" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="d-flex justify-content-center mb-2 wrap-tags">
|
<div className="d-flex justify-content-center mb-2 wrap-tags filter-tags">
|
||||||
{renderFilterTags()}
|
{renderFilterTags()}
|
||||||
|
{maybeRenderClearAll()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
45
ui/v2.5/src/components/List/Filters/BooleanFilter.tsx
Normal file
45
ui/v2.5/src/components/List/Filters/BooleanFilter.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
|
import React from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import { BooleanCriterion } from "src/models/list-filter/criteria/criterion";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
interface IBooleanFilter {
|
||||||
|
criterion: BooleanCriterion;
|
||||||
|
setCriterion: (c: BooleanCriterion) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BooleanFilter: React.FC<IBooleanFilter> = ({
|
||||||
|
criterion,
|
||||||
|
setCriterion,
|
||||||
|
}) => {
|
||||||
|
function onSelect(v: boolean) {
|
||||||
|
const c = cloneDeep(criterion);
|
||||||
|
if ((v && c.value === "true") || (!v && c.value === "false")) {
|
||||||
|
c.value = "";
|
||||||
|
} else {
|
||||||
|
c.value = v ? "true" : "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
setCriterion(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="boolean-filter">
|
||||||
|
<Form.Check
|
||||||
|
id={`${criterion.getId()}-true`}
|
||||||
|
onChange={() => onSelect(true)}
|
||||||
|
checked={criterion.value === "true"}
|
||||||
|
type="checkbox"
|
||||||
|
label={<FormattedMessage id="true" />}
|
||||||
|
/>
|
||||||
|
<Form.Check
|
||||||
|
id={`${criterion.getId()}-false`}
|
||||||
|
onChange={() => onSelect(false)}
|
||||||
|
checked={criterion.value === "false"}
|
||||||
|
type="checkbox"
|
||||||
|
label={<FormattedMessage id="false" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef } from "react";
|
import React from "react";
|
||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||||
@@ -16,18 +16,17 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const valueStage = useRef<IDateValue>(criterion.value);
|
const { value } = criterion;
|
||||||
|
|
||||||
function onChanged(
|
function onChanged(
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
property: "value" | "value2"
|
property: "value" | "value2"
|
||||||
) {
|
) {
|
||||||
const { value } = event.target;
|
const newValue = event.target.value;
|
||||||
valueStage.current[property] = value;
|
const valueCopy = { ...value };
|
||||||
}
|
|
||||||
|
|
||||||
function onBlurInput() {
|
valueCopy[property] = newValue;
|
||||||
onValueChanged(valueStage.current);
|
onValueChanged(valueCopy);
|
||||||
}
|
}
|
||||||
|
|
||||||
let equalsControl: JSX.Element | null = null;
|
let equalsControl: JSX.Element | null = null;
|
||||||
@@ -43,8 +42,7 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
|||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChanged(e, "value")
|
onChanged(e, "value")
|
||||||
}
|
}
|
||||||
onBlur={onBlurInput}
|
value={value?.value ?? ""}
|
||||||
defaultValue={criterion.value?.value ?? ""}
|
|
||||||
placeholder={
|
placeholder={
|
||||||
intl.formatMessage({ id: "criterion.value" }) + " (YYYY-MM-DD)"
|
intl.formatMessage({ id: "criterion.value" }) + " (YYYY-MM-DD)"
|
||||||
}
|
}
|
||||||
@@ -67,8 +65,7 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
|||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChanged(e, "value")
|
onChanged(e, "value")
|
||||||
}
|
}
|
||||||
onBlur={onBlurInput}
|
value={value?.value ?? ""}
|
||||||
defaultValue={criterion.value?.value ?? ""}
|
|
||||||
placeholder={
|
placeholder={
|
||||||
intl.formatMessage({ id: "criterion.greater_than" }) +
|
intl.formatMessage({ id: "criterion.greater_than" }) +
|
||||||
" (YYYY-MM-DD)"
|
" (YYYY-MM-DD)"
|
||||||
@@ -97,11 +94,10 @@ export const DateFilter: React.FC<IDateFilterProps> = ({
|
|||||||
: "value2"
|
: "value2"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onBlur={onBlurInput}
|
value={
|
||||||
defaultValue={
|
|
||||||
(criterion.modifier === CriterionModifier.LessThan
|
(criterion.modifier === CriterionModifier.LessThan
|
||||||
? criterion.value?.value
|
? value?.value
|
||||||
: criterion.value?.value2) ?? ""
|
: value?.value2) ?? ""
|
||||||
}
|
}
|
||||||
placeholder={
|
placeholder={
|
||||||
intl.formatMessage({ id: "criterion.less_than" }) + " (YYYY-MM-DD)"
|
intl.formatMessage({ id: "criterion.less_than" }) + " (YYYY-MM-DD)"
|
||||||
|
|||||||
28
ui/v2.5/src/components/List/Filters/FilterButton.tsx
Normal file
28
ui/v2.5/src/components/List/Filters/FilterButton.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Badge, Button } from "react-bootstrap";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { faFilter } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
|
|
||||||
|
interface IFilterButtonProps {
|
||||||
|
filter: ListFilterModel;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilterButton: React.FC<IFilterButtonProps> = ({
|
||||||
|
filter,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const count = useMemo(() => filter.count(), [filter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button variant="secondary" className="filter-button" onClick={onClick}>
|
||||||
|
<Icon icon={faFilter} />
|
||||||
|
{count ? (
|
||||||
|
<Badge pill variant="danger">
|
||||||
|
{count}
|
||||||
|
</Badge>
|
||||||
|
) : undefined}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -80,6 +80,7 @@ export const HierarchicalLabelValueFilter: React.FC<
|
|||||||
isMulti
|
isMulti
|
||||||
onSelect={onSelectionChanged}
|
onSelect={onSelectionChanged}
|
||||||
ids={criterion.value.items.map((labeled) => labeled.id)}
|
ids={criterion.value.items.map((labeled) => labeled.id)}
|
||||||
|
menuPortalTarget={document.body}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,15 @@ export const InputFilter: React.FC<IInputFilterProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<>
|
||||||
<Form.Control
|
<Form.Group>
|
||||||
className="btn-secondary"
|
<Form.Control
|
||||||
type={criterion.criterionOption.inputType}
|
className="btn-secondary"
|
||||||
onBlur={onChanged}
|
type={criterion.criterionOption.inputType}
|
||||||
defaultValue={criterion.value ? criterion.value.toString() : ""}
|
onChange={onChanged}
|
||||||
/>
|
value={criterion.value ? criterion.value.toString() : ""}
|
||||||
</Form.Group>
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const LabeledIdFilter: React.FC<ILabeledIdFilterProps> = ({
|
|||||||
isMulti
|
isMulti
|
||||||
onSelect={onSelectionChanged}
|
onSelect={onSelectionChanged}
|
||||||
ids={criterion.value.map((labeled) => labeled.id)}
|
ids={criterion.value.map((labeled) => labeled.id)}
|
||||||
|
menuPortalTarget={document.body}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { Form } from "react-bootstrap";
|
|||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||||
import { INumberValue } from "../../../models/list-filter/types";
|
import { INumberValue } from "../../../models/list-filter/types";
|
||||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
import { NumberCriterion } from "../../../models/list-filter/criteria/criterion";
|
||||||
|
|
||||||
interface IDurationFilterProps {
|
interface IDurationFilterProps {
|
||||||
criterion: Criterion<INumberValue>;
|
criterion: NumberCriterion;
|
||||||
onValueChanged: (value: INumberValue) => void;
|
onValueChanged: (value: INumberValue) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,12 +16,14 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const { value } = criterion;
|
||||||
|
|
||||||
function onChanged(
|
function onChanged(
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
property: "value" | "value2"
|
property: "value" | "value2"
|
||||||
) {
|
) {
|
||||||
const numericValue = parseInt(event.target.value, 10);
|
const numericValue = parseInt(event.target.value, 10);
|
||||||
const valueCopy = { ...criterion.value };
|
const valueCopy = { ...value };
|
||||||
|
|
||||||
valueCopy[property] = !Number.isNaN(numericValue) ? numericValue : 0;
|
valueCopy[property] = !Number.isNaN(numericValue) ? numericValue : 0;
|
||||||
onValueChanged(valueCopy);
|
onValueChanged(valueCopy);
|
||||||
@@ -40,7 +42,7 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
|
|||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChanged(e, "value")
|
onChanged(e, "value")
|
||||||
}
|
}
|
||||||
value={criterion.value?.value ?? ""}
|
value={value?.value ?? ""}
|
||||||
placeholder={intl.formatMessage({ id: "criterion.value" })}
|
placeholder={intl.formatMessage({ id: "criterion.value" })}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -61,7 +63,7 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
|
|||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChanged(e, "value")
|
onChanged(e, "value")
|
||||||
}
|
}
|
||||||
value={criterion.value?.value ?? ""}
|
value={value?.value ?? ""}
|
||||||
placeholder={intl.formatMessage({ id: "criterion.greater_than" })}
|
placeholder={intl.formatMessage({ id: "criterion.greater_than" })}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -89,8 +91,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
|
|||||||
}
|
}
|
||||||
value={
|
value={
|
||||||
(criterion.modifier === CriterionModifier.LessThan
|
(criterion.modifier === CriterionModifier.LessThan
|
||||||
? criterion.value?.value
|
? value?.value
|
||||||
: criterion.value?.value2) ?? ""
|
: value?.value2) ?? ""
|
||||||
}
|
}
|
||||||
placeholder={intl.formatMessage({ id: "criterion.less_than" })}
|
placeholder={intl.formatMessage({ id: "criterion.less_than" })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
@@ -18,16 +18,13 @@ export const OptionsFilter: React.FC<IOptionsFilterProps> = ({
|
|||||||
onValueChanged(event.target.value);
|
onValueChanged(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = criterion.criterionOption.options ?? [];
|
const options = useMemo(() => {
|
||||||
|
const ret = criterion.criterionOption.options?.slice() ?? [];
|
||||||
|
|
||||||
if (
|
ret.unshift("");
|
||||||
options &&
|
|
||||||
(criterion.value === undefined ||
|
return ret;
|
||||||
criterion.value === "" ||
|
}, [criterion.criterionOption.options]);
|
||||||
typeof criterion.value === "number")
|
|
||||||
) {
|
|
||||||
onValueChanged(options[0].toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
@@ -39,7 +36,7 @@ export const OptionsFilter: React.FC<IOptionsFilterProps> = ({
|
|||||||
>
|
>
|
||||||
{options.map((c) => (
|
{options.map((c) => (
|
||||||
<option key={c.toString()} value={c.toString()}>
|
<option key={c.toString()} value={c.toString()}>
|
||||||
{c}
|
{c ? c : "---"}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
|||||||
45
ui/v2.5/src/components/List/Filters/OptionsListFilter.tsx
Normal file
45
ui/v2.5/src/components/List/Filters/OptionsListFilter.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
|
import React from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import {
|
||||||
|
CriterionValue,
|
||||||
|
Criterion,
|
||||||
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
|
|
||||||
|
interface IOptionsListFilter {
|
||||||
|
criterion: Criterion<CriterionValue>;
|
||||||
|
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OptionsListFilter: React.FC<IOptionsListFilter> = ({
|
||||||
|
criterion,
|
||||||
|
setCriterion,
|
||||||
|
}) => {
|
||||||
|
function onSelect(v: string) {
|
||||||
|
const c = cloneDeep(criterion);
|
||||||
|
if (c.value === v) {
|
||||||
|
c.value = "";
|
||||||
|
} else {
|
||||||
|
c.value = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCriterion(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { options } = criterion.criterionOption;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="option-list-filter">
|
||||||
|
{options?.map((o) => (
|
||||||
|
<Form.Check
|
||||||
|
id={`${criterion.getId()}-${o.toString()}`}
|
||||||
|
key={o.toString()}
|
||||||
|
onChange={() => onSelect(o.toString())}
|
||||||
|
checked={criterion.value === o.toString()}
|
||||||
|
type="checkbox"
|
||||||
|
label={o.toString()}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -15,6 +15,7 @@ export const StashIDFilter: React.FC<IStashIDFilterProps> = ({
|
|||||||
onValueChanged,
|
onValueChanged,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { value } = criterion;
|
||||||
|
|
||||||
function onEndpointChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
function onEndpointChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
onValueChanged({
|
onValueChanged({
|
||||||
@@ -35,8 +36,8 @@ export const StashIDFilter: React.FC<IStashIDFilterProps> = ({
|
|||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
onBlur={onEndpointChanged}
|
onChange={onEndpointChanged}
|
||||||
defaultValue={criterion.value ? criterion.value.endpoint : ""}
|
value={value ? value.endpoint : ""}
|
||||||
placeholder={intl.formatMessage({ id: "stash_id_endpoint" })}
|
placeholder={intl.formatMessage({ id: "stash_id_endpoint" })}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -45,8 +46,8 @@ export const StashIDFilter: React.FC<IStashIDFilterProps> = ({
|
|||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
onBlur={onStashIDChanged}
|
onChange={onStashIDChanged}
|
||||||
defaultValue={criterion.value ? criterion.value.stashID : ""}
|
value={value ? value.stashID : ""}
|
||||||
placeholder={intl.formatMessage({ id: "stash_id" })}
|
placeholder={intl.formatMessage({ id: "stash_id" })}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef } from "react";
|
import React from "react";
|
||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||||
@@ -16,18 +16,17 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const valueStage = useRef<ITimestampValue>(criterion.value);
|
const { value } = criterion;
|
||||||
|
|
||||||
function onChanged(
|
function onChanged(
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
property: "value" | "value2"
|
property: "value" | "value2"
|
||||||
) {
|
) {
|
||||||
const { value } = event.target;
|
const newValue = event.target.value;
|
||||||
valueStage.current[property] = value;
|
const valueCopy = { ...value };
|
||||||
}
|
|
||||||
|
|
||||||
function onBlurInput() {
|
valueCopy[property] = newValue;
|
||||||
onValueChanged(valueStage.current);
|
onValueChanged(valueCopy);
|
||||||
}
|
}
|
||||||
|
|
||||||
let equalsControl: JSX.Element | null = null;
|
let equalsControl: JSX.Element | null = null;
|
||||||
@@ -43,11 +42,10 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
|||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChanged(e, "value")
|
onChanged(e, "value")
|
||||||
}
|
}
|
||||||
onBlur={onBlurInput}
|
value={value?.value ?? ""}
|
||||||
defaultValue={criterion.value?.value ?? ""}
|
|
||||||
placeholder={
|
placeholder={
|
||||||
intl.formatMessage({ id: "criterion.value" }) +
|
intl.formatMessage({ id: "criterion.value" }) +
|
||||||
" (YYYY-MM-DD HH-MM)"
|
" (YYYY-MM-DD HH:MM)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -68,11 +66,10 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
|||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChanged(e, "value")
|
onChanged(e, "value")
|
||||||
}
|
}
|
||||||
onBlur={onBlurInput}
|
value={value?.value ?? ""}
|
||||||
defaultValue={criterion.value?.value ?? ""}
|
|
||||||
placeholder={
|
placeholder={
|
||||||
intl.formatMessage({ id: "criterion.greater_than" }) +
|
intl.formatMessage({ id: "criterion.greater_than" }) +
|
||||||
" (YYYY-MM-DD HH-MM)"
|
" (YYYY-MM-DD HH:MM)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -98,15 +95,14 @@ export const TimestampFilter: React.FC<ITimestampFilterProps> = ({
|
|||||||
: "value2"
|
: "value2"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onBlur={onBlurInput}
|
value={
|
||||||
defaultValue={
|
|
||||||
(criterion.modifier === CriterionModifier.LessThan
|
(criterion.modifier === CriterionModifier.LessThan
|
||||||
? criterion.value?.value
|
? value?.value
|
||||||
: criterion.value?.value2) ?? ""
|
: value?.value2) ?? ""
|
||||||
}
|
}
|
||||||
placeholder={
|
placeholder={
|
||||||
intl.formatMessage({ id: "criterion.less_than" }) +
|
intl.formatMessage({ id: "criterion.less_than" }) +
|
||||||
" (YYYY-MM-DD HH-MM)"
|
" (YYYY-MM-DD HH:MM)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { ConfigurationContext } from "src/hooks/Config";
|
|||||||
import { getFilterOptions } from "src/models/list-filter/factory";
|
import { getFilterOptions } from "src/models/list-filter/factory";
|
||||||
import { useFindDefaultFilter } from "src/core/StashService";
|
import { useFindDefaultFilter } from "src/core/StashService";
|
||||||
import { Pagination, PaginationIndex } from "./Pagination";
|
import { Pagination, PaginationIndex } from "./Pagination";
|
||||||
import { AddFilterDialog } from "./AddFilterDialog";
|
import { EditFilterDialog } from "src/components/List/EditFilterDialog";
|
||||||
import { ListFilter } from "./ListFilter";
|
import { ListFilter } from "./ListFilter";
|
||||||
import { FilterTags } from "./FilterTags";
|
import { FilterTags } from "./FilterTags";
|
||||||
import { ListViewOptions } from "./ListViewOptions";
|
import { ListViewOptions } from "./ListViewOptions";
|
||||||
@@ -140,7 +140,6 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
|
|||||||
onChangePage: _onChangePage,
|
onChangePage: _onChangePage,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
persistState,
|
persistState,
|
||||||
filterDialog,
|
|
||||||
zoomable,
|
zoomable,
|
||||||
selectable,
|
selectable,
|
||||||
otherOperations,
|
otherOperations,
|
||||||
@@ -154,9 +153,10 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
|
|||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [lastClickedId, setLastClickedId] = useState<string>();
|
const [lastClickedId, setLastClickedId] = useState<string>();
|
||||||
|
|
||||||
const [editingCriterion, setEditingCriterion] =
|
const [editingCriterion, setEditingCriterion] = useState<
|
||||||
useState<Criterion<CriterionValue>>();
|
string | undefined
|
||||||
const [newCriterion, setNewCriterion] = useState(false);
|
>();
|
||||||
|
const [showEditFilter, setShowEditFilter] = useState(false);
|
||||||
|
|
||||||
const result = useResult(filter);
|
const result = useResult(filter);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
@@ -193,7 +193,7 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
|
|||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("f", () => setNewCriterion(true));
|
Mousetrap.bind("f", () => setShowEditFilter(true));
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind("f");
|
Mousetrap.unbind("f");
|
||||||
@@ -432,40 +432,6 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
|
|||||||
updateFilter(newFilter);
|
updateFilter(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAddCriterion(
|
|
||||||
criterion: Criterion<CriterionValue>,
|
|
||||||
oldId?: string
|
|
||||||
) {
|
|
||||||
const newFilter = cloneDeep(filter);
|
|
||||||
|
|
||||||
// Find if we are editing an existing criteria, then modify that. Or create a new one.
|
|
||||||
const existingIndex = newFilter.criteria.findIndex((c) => {
|
|
||||||
// If we modified an existing criterion, then look for the old id.
|
|
||||||
const id = oldId || criterion.getId();
|
|
||||||
return c.getId() === id;
|
|
||||||
});
|
|
||||||
if (existingIndex === -1) {
|
|
||||||
newFilter.criteria.push(criterion);
|
|
||||||
} else {
|
|
||||||
newFilter.criteria[existingIndex] = criterion;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove duplicate modifiers
|
|
||||||
newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {
|
|
||||||
return arr.map((mapObj) => mapObj.getId()).indexOf(obj.getId()) === pos;
|
|
||||||
});
|
|
||||||
|
|
||||||
newFilter.currentPage = 1;
|
|
||||||
updateFilter(newFilter);
|
|
||||||
setEditingCriterion(undefined);
|
|
||||||
setNewCriterion(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCancelAddCriterion() {
|
|
||||||
setEditingCriterion(undefined);
|
|
||||||
setNewCriterion(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRemoveCriterion(removedCriterion: Criterion<CriterionValue>) {
|
function onRemoveCriterion(removedCriterion: Criterion<CriterionValue>) {
|
||||||
const newFilter = cloneDeep(filter);
|
const newFilter = cloneDeep(filter);
|
||||||
newFilter.criteria = newFilter.criteria.filter(
|
newFilter.criteria = newFilter.criteria.filter(
|
||||||
@@ -475,10 +441,22 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
|
|||||||
updateFilter(newFilter);
|
updateFilter(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCriteria(c: Criterion<CriterionValue>[]) {
|
function onClearAllCriteria() {
|
||||||
const newFilter = cloneDeep(filter);
|
const newFilter = cloneDeep(filter);
|
||||||
newFilter.criteria = c.slice();
|
newFilter.criteria = [];
|
||||||
setNewCriterion(false);
|
newFilter.currentPage = 1;
|
||||||
|
updateFilter(newFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onApplyEditFilter(f: ListFilterModel) {
|
||||||
|
setShowEditFilter(false);
|
||||||
|
setEditingCriterion(undefined);
|
||||||
|
updateFilter(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancelEditFilter() {
|
||||||
|
setShowEditFilter(false);
|
||||||
|
setEditingCriterion(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -488,8 +466,7 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
|
|||||||
onFilterUpdate={updateFilter}
|
onFilterUpdate={updateFilter}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
filterOptions={filterOptions}
|
filterOptions={filterOptions}
|
||||||
openFilterDialog={() => setNewCriterion(true)}
|
openFilterDialog={() => setShowEditFilter(true)}
|
||||||
filterDialogOpen={newCriterion}
|
|
||||||
persistState={persistState}
|
persistState={persistState}
|
||||||
/>
|
/>
|
||||||
<ListOperationButtons
|
<ListOperationButtons
|
||||||
@@ -510,21 +487,18 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
|
|||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
<FilterTags
|
<FilterTags
|
||||||
criteria={filter.criteria}
|
criteria={filter.criteria}
|
||||||
onEditCriterion={(c) => setEditingCriterion(c)}
|
onEditCriterion={(c) => setEditingCriterion(c.criterionOption.type)}
|
||||||
onRemoveCriterion={onRemoveCriterion}
|
onRemoveCriterion={onRemoveCriterion}
|
||||||
|
onRemoveAll={() => onClearAllCriteria()}
|
||||||
/>
|
/>
|
||||||
{(newCriterion || editingCriterion) && !filterDialog && (
|
{(showEditFilter || editingCriterion) && (
|
||||||
<AddFilterDialog
|
<EditFilterDialog
|
||||||
filterOptions={filterOptions}
|
filter={filter}
|
||||||
onAddCriterion={onAddCriterion}
|
onApply={onApplyEditFilter}
|
||||||
onCancel={onCancelAddCriterion}
|
onCancel={onCancelEditFilter}
|
||||||
editingCriterion={editingCriterion}
|
editingCriterion={editingCriterion}
|
||||||
existingCriterions={filter.criteria}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{newCriterion &&
|
|
||||||
filterDialog &&
|
|
||||||
filterDialog(filter.criteria, (c) => updateCriteria(c))}
|
|
||||||
{isEditDialogOpen &&
|
{isEditDialogOpen &&
|
||||||
renderEditDialog &&
|
renderEditDialog &&
|
||||||
renderEditDialog(getSelectedData(items, selectedIds), (applied) =>
|
renderEditDialog(getSelectedData(items, selectedIds), (applied) =>
|
||||||
|
|||||||
@@ -34,17 +34,16 @@ import {
|
|||||||
faCaretDown,
|
faCaretDown,
|
||||||
faCaretUp,
|
faCaretUp,
|
||||||
faCheck,
|
faCheck,
|
||||||
faFilter,
|
|
||||||
faRandom,
|
faRandom,
|
||||||
faTimes,
|
faTimes,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FilterButton } from "./Filters/FilterButton";
|
||||||
import { useDebounce } from "src/hooks/debounce";
|
import { useDebounce } from "src/hooks/debounce";
|
||||||
|
|
||||||
interface IListFilterProps {
|
interface IListFilterProps {
|
||||||
onFilterUpdate: (newFilter: ListFilterModel) => void;
|
onFilterUpdate: (newFilter: ListFilterModel) => void;
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
filterOptions: ListFilterOptions;
|
filterOptions: ListFilterOptions;
|
||||||
filterDialogOpen?: boolean;
|
|
||||||
persistState?: PersistanceLevel;
|
persistState?: PersistanceLevel;
|
||||||
openFilterDialog: () => void;
|
openFilterDialog: () => void;
|
||||||
}
|
}
|
||||||
@@ -55,7 +54,6 @@ export const ListFilter: React.FC<IListFilterProps> = ({
|
|||||||
onFilterUpdate,
|
onFilterUpdate,
|
||||||
filter,
|
filter,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
filterDialogOpen,
|
|
||||||
openFilterDialog,
|
openFilterDialog,
|
||||||
persistState,
|
persistState,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -289,13 +287,7 @@ export const ListFilter: React.FC<IListFilterProps> = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<FilterButton onClick={() => openFilterDialog()} filter={filter} />
|
||||||
variant="secondary"
|
|
||||||
onClick={() => openFilterDialog()}
|
|
||||||
active={filterDialogOpen}
|
|
||||||
>
|
|
||||||
<Icon icon={faFilter} />
|
|
||||||
</Button>
|
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -116,3 +116,83 @@ input[type="range"].zoom-slider {
|
|||||||
.rating-filter .and-divider {
|
.rating-filter .and-divider {
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-filter-dialog {
|
||||||
|
.modal-body {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tags {
|
||||||
|
border-top: 1px solid rgb(16 22 26 / 40%);
|
||||||
|
padding: 1rem 1rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.criterion-list {
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
max-height: 550px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 1px solid rgb(16 22 26 / 40%);
|
||||||
|
box-shadow: none;
|
||||||
|
margin: 0 0 -1px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-criterion-button {
|
||||||
|
border: 0;
|
||||||
|
color: $danger;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-filter-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifier-selector {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
// to accommodate for caret
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tags .clear-all-button {
|
||||||
|
color: $text-color;
|
||||||
|
// to match filter pills
|
||||||
|
line-height: 16px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-button {
|
||||||
|
.fa-icon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
right: -3px;
|
||||||
|
|
||||||
|
// button group has a z-index of 1
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
* Added toggleable favorite button to Performer cards. ([#3369](https://github.com/stashapp/stash/pull/3369))
|
* Added toggleable favorite button to Performer cards. ([#3369](https://github.com/stashapp/stash/pull/3369))
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Overhauled filtering interface to allow setting filter criteria from a single dialog. ([#3515](https://github.com/stashapp/stash/pull/3515))
|
||||||
* Removed upper limit on page size. ([#3544](https://github.com/stashapp/stash/pull/3544))
|
* Removed upper limit on page size. ([#3544](https://github.com/stashapp/stash/pull/3544))
|
||||||
* Anonymise task now obfuscates Marker titles. ([#3542](https://github.com/stashapp/stash/pull/3542))
|
* Anonymise task now obfuscates Marker titles. ([#3542](https://github.com/stashapp/stash/pull/3542))
|
||||||
* Improved Images wall view layout and added Interface settings to adjust the layout. ([#3511](https://github.com/stashapp/stash/pull/3511))
|
* Improved Images wall view layout and added Interface settings to adjust the layout. ([#3511](https://github.com/stashapp/stash/pull/3511))
|
||||||
|
|||||||
@@ -32,3 +32,18 @@ export function useInitialState<T>(
|
|||||||
|
|
||||||
return [value, setValue, setInitialValue];
|
return [value, setValue, setInitialValue];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// useCompare is a hook that returns true if the value has changed since the last render.
|
||||||
|
export function useCompare<T>(val: T) {
|
||||||
|
const prevVal = usePrevious(val);
|
||||||
|
return prevVal !== val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// usePrevious is a hook that returns the previous value of a variable.
|
||||||
|
export function usePrevious<T>(value: T) {
|
||||||
|
const ref = React.useRef<T>();
|
||||||
|
React.useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
}, [value]);
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
|||||||
@@ -286,11 +286,15 @@ div.react-select__control {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.react-select__menu-portal {
|
||||||
|
z-index: 1600;
|
||||||
|
}
|
||||||
|
|
||||||
div.react-select__menu,
|
div.react-select__menu,
|
||||||
div.dropdown-menu {
|
div.dropdown-menu {
|
||||||
background-color: $secondary;
|
background-color: $secondary;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
z-index: 16;
|
z-index: 1600;
|
||||||
|
|
||||||
.react-select__option,
|
.react-select__option,
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
|
|||||||
@@ -1039,7 +1039,7 @@
|
|||||||
"scenes": "Scenes",
|
"scenes": "Scenes",
|
||||||
"scenes_updated_at": "Scene Updated At",
|
"scenes_updated_at": "Scene Updated At",
|
||||||
"search_filter": {
|
"search_filter": {
|
||||||
"add_filter": "Add Filter",
|
"edit_filter": "Edit Filter",
|
||||||
"name": "Filter",
|
"name": "Filter",
|
||||||
"saved_filters": "Saved filters",
|
"saved_filters": "Saved filters",
|
||||||
"update_filter": "Update Filter"
|
"update_filter": "Update Filter"
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ export abstract class Criterion<V extends CriterionValue> {
|
|||||||
this._value = newValue;
|
this._value = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValid(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract getLabelValue(intl: IntlShape): string;
|
public abstract getLabelValue(intl: IntlShape): string;
|
||||||
|
|
||||||
constructor(type: CriterionOption, value: V) {
|
constructor(type: CriterionOption, value: V) {
|
||||||
@@ -227,6 +231,14 @@ export class StringCriterion extends Criterion<string> {
|
|||||||
public getLabelValue(_intl: IntlShape) {
|
public getLabelValue(_intl: IntlShape) {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValid(): boolean {
|
||||||
|
return (
|
||||||
|
this.modifier === CriterionModifier.IsNull ||
|
||||||
|
this.modifier === CriterionModifier.NotNull ||
|
||||||
|
this.value.length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MandatoryStringCriterionOption extends CriterionOption {
|
export class MandatoryStringCriterionOption extends CriterionOption {
|
||||||
@@ -284,6 +296,10 @@ export class BooleanCriterion extends StringCriterion {
|
|||||||
protected toCriterionInput(): boolean {
|
protected toCriterionInput(): boolean {
|
||||||
return this.value === "true";
|
return this.value === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValid() {
|
||||||
|
return this.value === "true" || this.value === "false";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBooleanCriterionOption(
|
export function createBooleanCriterionOption(
|
||||||
@@ -375,7 +391,7 @@ export class NumberCriterion extends Criterion<INumberValue> {
|
|||||||
protected toCriterionInput(): IntCriterionInput {
|
protected toCriterionInput(): IntCriterionInput {
|
||||||
return {
|
return {
|
||||||
modifier: this.modifier,
|
modifier: this.modifier,
|
||||||
value: this.value.value,
|
value: this.value.value ?? 0,
|
||||||
value2: this.value.value2,
|
value2: this.value.value2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -392,8 +408,32 @@ export class NumberCriterion extends Criterion<INumberValue> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValid(): boolean {
|
||||||
|
if (
|
||||||
|
this.modifier === CriterionModifier.IsNull ||
|
||||||
|
this.modifier === CriterionModifier.NotNull
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value, value2 } = this.value;
|
||||||
|
if (value === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
value2 === undefined &&
|
||||||
|
(this.modifier === CriterionModifier.Between ||
|
||||||
|
this.modifier === CriterionModifier.NotBetween)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
super(type, { value: 0, value2: undefined });
|
super(type, { value: undefined, value2: undefined });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +479,17 @@ export class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValid(): boolean {
|
||||||
|
if (
|
||||||
|
this.modifier === CriterionModifier.IsNull ||
|
||||||
|
this.modifier === CriterionModifier.NotNull
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.value.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
super(type, []);
|
super(type, []);
|
||||||
}
|
}
|
||||||
@@ -463,6 +514,17 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
|||||||
return `${labels} (+${this.value.depth > 0 ? this.value.depth : "all"})`;
|
return `${labels} (+${this.value.depth > 0 ? this.value.depth : "all"})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValid(): boolean {
|
||||||
|
if (
|
||||||
|
this.modifier === CriterionModifier.IsNull ||
|
||||||
|
this.modifier === CriterionModifier.NotNull
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.value.items.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
const value: IHierarchicalLabelValue = {
|
const value: IHierarchicalLabelValue = {
|
||||||
items: [],
|
items: [],
|
||||||
@@ -502,13 +564,13 @@ export function createMandatoryNumberCriterionOption(
|
|||||||
|
|
||||||
export class DurationCriterion extends Criterion<INumberValue> {
|
export class DurationCriterion extends Criterion<INumberValue> {
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
super(type, { value: 0, value2: undefined });
|
super(type, { value: undefined, value2: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected toCriterionInput(): IntCriterionInput {
|
protected toCriterionInput(): IntCriterionInput {
|
||||||
return {
|
return {
|
||||||
modifier: this.modifier,
|
modifier: this.modifier,
|
||||||
value: this.value.value,
|
value: this.value.value ?? 0,
|
||||||
value2: this.value.value2,
|
value2: this.value.value2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -517,15 +579,39 @@ export class DurationCriterion extends Criterion<INumberValue> {
|
|||||||
return this.modifier === CriterionModifier.Between ||
|
return this.modifier === CriterionModifier.Between ||
|
||||||
this.modifier === CriterionModifier.NotBetween
|
this.modifier === CriterionModifier.NotBetween
|
||||||
? `${DurationUtils.secondsToString(
|
? `${DurationUtils.secondsToString(
|
||||||
this.value.value
|
this.value.value ?? 0
|
||||||
)} ${DurationUtils.secondsToString(this.value.value2 ?? 0)}`
|
)} ${DurationUtils.secondsToString(this.value.value2 ?? 0)}`
|
||||||
: this.modifier === CriterionModifier.GreaterThan ||
|
: this.modifier === CriterionModifier.GreaterThan ||
|
||||||
this.modifier === CriterionModifier.LessThan ||
|
this.modifier === CriterionModifier.LessThan ||
|
||||||
this.modifier === CriterionModifier.Equals ||
|
this.modifier === CriterionModifier.Equals ||
|
||||||
this.modifier === CriterionModifier.NotEquals
|
this.modifier === CriterionModifier.NotEquals
|
||||||
? DurationUtils.secondsToString(this.value.value)
|
? DurationUtils.secondsToString(this.value.value ?? 0)
|
||||||
: "?";
|
: "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValid(): boolean {
|
||||||
|
if (
|
||||||
|
this.modifier === CriterionModifier.IsNull ||
|
||||||
|
this.modifier === CriterionModifier.NotNull
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value, value2 } = this.value;
|
||||||
|
if (value === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
value2 === undefined &&
|
||||||
|
(this.modifier === CriterionModifier.Between ||
|
||||||
|
this.modifier === CriterionModifier.NotBetween)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PhashDuplicateCriterion extends StringCriterion {
|
export class PhashDuplicateCriterion extends StringCriterion {
|
||||||
@@ -592,6 +678,30 @@ export class DateCriterion extends Criterion<IDateValue> {
|
|||||||
: `${value}`;
|
: `${value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValid(): boolean {
|
||||||
|
if (
|
||||||
|
this.modifier === CriterionModifier.IsNull ||
|
||||||
|
this.modifier === CriterionModifier.NotNull
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value, value2 } = this.value;
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!value2 &&
|
||||||
|
(this.modifier === CriterionModifier.Between ||
|
||||||
|
this.modifier === CriterionModifier.NotBetween)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
super(type, { value: "", value2: undefined });
|
super(type, { value: "", value2: undefined });
|
||||||
}
|
}
|
||||||
@@ -662,6 +772,30 @@ export class TimestampCriterion extends Criterion<ITimestampValue> {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValid(): boolean {
|
||||||
|
if (
|
||||||
|
this.modifier === CriterionModifier.IsNull ||
|
||||||
|
this.modifier === CriterionModifier.NotNull
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value, value2 } = this.value;
|
||||||
|
if (!value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!value2 &&
|
||||||
|
(this.modifier === CriterionModifier.Between ||
|
||||||
|
this.modifier === CriterionModifier.NotBetween)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
super(type, { value: "", value2: undefined });
|
super(type, { value: "", value2: undefined });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class RatingCriterion extends Criterion<INumberValue> {
|
|||||||
protected toCriterionInput(): IntCriterionInput {
|
protected toCriterionInput(): IntCriterionInput {
|
||||||
return {
|
return {
|
||||||
modifier: this.modifier,
|
modifier: this.modifier,
|
||||||
value: this.value.value,
|
value: this.value.value ?? 0,
|
||||||
value2: this.value.value2,
|
value2: this.value.value2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,4 +103,12 @@ export class StashIDCriterion extends Criterion<IStashIDValue> {
|
|||||||
}
|
}
|
||||||
return JSON.stringify(encodedCriterion);
|
return JSON.stringify(encodedCriterion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isValid(): boolean {
|
||||||
|
return (
|
||||||
|
this.modifier === CriterionModifier.IsNull ||
|
||||||
|
this.modifier === CriterionModifier.NotNull ||
|
||||||
|
this.value.stashID.length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,16 @@ export class ListFilterModel {
|
|||||||
return Object.assign(new ListFilterModel(this.mode, this.config), this);
|
return Object.assign(new ListFilterModel(this.mode, this.config), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns the number of filters applied
|
||||||
|
public count() {
|
||||||
|
const count = this.criteria.length;
|
||||||
|
if (this.searchTerm) {
|
||||||
|
return count + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
public configureFromDecodedParams(params: IDecodedParams) {
|
public configureFromDecodedParams(params: IDecodedParams) {
|
||||||
if (params.perPage !== undefined) {
|
if (params.perPage !== undefined) {
|
||||||
this.itemsPerPage = params.perPage;
|
this.itemsPerPage = params.perPage;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export interface IHierarchicalLabelValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface INumberValue {
|
export interface INumberValue {
|
||||||
value: number;
|
value: number | undefined;
|
||||||
value2: number | undefined;
|
value2: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user