Dependencies between rows in filtering (#3956)
* Fields constraint * Keep constraing on the channel level * Keep constraing on the channel level
This commit is contained in:
parent
09d254d50a
commit
23bb5976c6
8 changed files with 206 additions and 21 deletions
5
.changeset/smooth-pumas-judge.md
Normal file
5
.changeset/smooth-pumas-judge.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-dashboard": minor
|
||||
---
|
||||
|
||||
Constrains implementation for filter rows
|
|
@ -2,7 +2,7 @@
|
|||
import { InitialStateResponse } from "../API/InitialStateResponse";
|
||||
import { LeftOperand } from "../LeftOperandsProvider";
|
||||
import { UrlToken } from "./../ValueProvider/UrlToken";
|
||||
import { ConditionOptions } from "./ConditionOptions";
|
||||
import { ConditionOptions, StaticElementName } from "./ConditionOptions";
|
||||
import { ConditionSelected } from "./ConditionSelected";
|
||||
import { ItemOption } from "./ConditionValue";
|
||||
|
||||
|
@ -33,6 +33,16 @@ export class Condition {
|
|||
);
|
||||
}
|
||||
|
||||
public static emptyFromSlug(slug: StaticElementName) {
|
||||
const options = ConditionOptions.fromName(slug);
|
||||
|
||||
return new Condition(
|
||||
options,
|
||||
ConditionSelected.fromConditionItem(options.first()),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
public static emptyFromLeftOperand(operand: LeftOperand) {
|
||||
const options = ConditionOptions.fromName(operand.type);
|
||||
|
||||
|
|
43
src/components/ConditionalFilter/FilterElement/Constraint.ts
Normal file
43
src/components/ConditionalFilter/FilterElement/Constraint.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { CONSTRAINTS } from "../constants"
|
||||
import { StaticElementName } from "./ConditionOptions"
|
||||
import { FilterContainer, FilterElement } from "./FilterElement"
|
||||
|
||||
type DisabledScope = "left" | "right" | "condition"
|
||||
|
||||
export class Constraint {
|
||||
constructor(
|
||||
public dependsOn: string[],
|
||||
public disabled?: [DisabledScope],
|
||||
public removable?: boolean,
|
||||
) {}
|
||||
|
||||
public static getDependency (slug: string): StaticElementName | null {
|
||||
const fieldConstraint = Object.entries(CONSTRAINTS)
|
||||
.find(([_, v]) => v.dependsOn.includes(slug))
|
||||
|
||||
if (!fieldConstraint) return null
|
||||
|
||||
return fieldConstraint[0] as StaticElementName
|
||||
}
|
||||
|
||||
public static fromSlug(slug: string) {
|
||||
const constraintKey = Object.keys(CONSTRAINTS).find(key => key === slug)
|
||||
|
||||
if (!constraintKey) return null
|
||||
const fieldConstraint = CONSTRAINTS[constraintKey as keyof typeof CONSTRAINTS]
|
||||
|
||||
return new Constraint(
|
||||
fieldConstraint.dependsOn,
|
||||
fieldConstraint.disabled as [DisabledScope],
|
||||
fieldConstraint.removable
|
||||
)
|
||||
}
|
||||
|
||||
public existIn (container: FilterContainer) {
|
||||
return container.some((s) => {
|
||||
if (!FilterElement.isCompatible(s)) return false
|
||||
|
||||
return this.dependsOn.includes(s.value.value)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -4,9 +4,10 @@ import { RowType, STATIC_OPTIONS } from "../constants";
|
|||
import { LeftOperand } from "../LeftOperandsProvider";
|
||||
import { TokenType, UrlEntry, UrlToken } from "./../ValueProvider/UrlToken";
|
||||
import { Condition } from "./Condition";
|
||||
import { ConditionItem, ConditionOptions } from "./ConditionOptions";
|
||||
import { ConditionItem, ConditionOptions, StaticElementName } from "./ConditionOptions";
|
||||
import { ConditionSelected } from "./ConditionSelected";
|
||||
import { ConditionValue, ItemOption } from "./ConditionValue";
|
||||
import { Constraint } from "./Constraint";
|
||||
|
||||
class ExpressionValue {
|
||||
constructor(
|
||||
|
@ -15,6 +16,18 @@ class ExpressionValue {
|
|||
public type: string,
|
||||
) {}
|
||||
|
||||
public static fromSlug (slug: string) {
|
||||
const option = STATIC_OPTIONS.find(o => o.slug === slug)
|
||||
|
||||
if (!option) return ExpressionValue.emptyStatic()
|
||||
|
||||
return new ExpressionValue(
|
||||
option.slug,
|
||||
option.label,
|
||||
option.type,
|
||||
);
|
||||
}
|
||||
|
||||
public static fromLeftOperand(leftOperand: LeftOperand) {
|
||||
return new ExpressionValue(
|
||||
leftOperand.slug,
|
||||
|
@ -56,7 +69,14 @@ export class FilterElement {
|
|||
public value: ExpressionValue,
|
||||
public condition: Condition,
|
||||
public loading: boolean,
|
||||
) {}
|
||||
public constraint?: Constraint,
|
||||
) {
|
||||
const newConstraint = Constraint.fromSlug(this.value.value)
|
||||
|
||||
if (newConstraint) {
|
||||
this.constraint = newConstraint
|
||||
}
|
||||
}
|
||||
|
||||
public enableLoading() {
|
||||
this.loading = true;
|
||||
|
@ -125,6 +145,15 @@ export class FilterElement {
|
|||
return null;
|
||||
}
|
||||
|
||||
public setConstraint (constraint: Constraint) {
|
||||
this.constraint = constraint
|
||||
}
|
||||
|
||||
public clearConstraint () {
|
||||
this.constraint = undefined
|
||||
}
|
||||
|
||||
|
||||
public asUrlEntry(): UrlEntry {
|
||||
if (this.isAttribute()) {
|
||||
return UrlEntry.forAttribute(this.condition.selected, this.value.value);
|
||||
|
@ -133,6 +162,10 @@ export class FilterElement {
|
|||
return UrlEntry.forStatic(this.condition.selected, this.value.value);
|
||||
}
|
||||
|
||||
public static isCompatible(element: unknown): element is FilterElement {
|
||||
return typeof element !== "string" && !Array.isArray(element)
|
||||
}
|
||||
|
||||
public static fromValueEntry(valueEntry: any) {
|
||||
return new FilterElement(valueEntry.value, valueEntry.condition, false);
|
||||
}
|
||||
|
@ -145,6 +178,14 @@ export class FilterElement {
|
|||
);
|
||||
}
|
||||
|
||||
public static createStaticBySlug (slug: StaticElementName) {
|
||||
return new FilterElement(
|
||||
ExpressionValue.fromSlug(slug),
|
||||
Condition.emptyFromSlug(slug),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
public static fromUrlToken(token: UrlToken, response: InitialStateResponse) {
|
||||
if (token.isStatic()) {
|
||||
return new FilterElement(
|
||||
|
|
|
@ -3,6 +3,7 @@ import React from "react";
|
|||
|
||||
import { useConditionalFilterContext } from "./context";
|
||||
import { FilterContainer } from "./FilterElement";
|
||||
import { LeftOperand } from "./LeftOperandsProvider";
|
||||
import { useFilterContainer } from "./useFilterContainer";
|
||||
|
||||
interface FiltersAreaProps {
|
||||
|
@ -35,7 +36,7 @@ export const FiltersArea = ({ onConfirm }: FiltersAreaProps) => {
|
|||
}
|
||||
|
||||
if (event.type === "leftOperator.onChange") {
|
||||
updateLeftOperator(event.path, event.value);
|
||||
updateLeftOperator(event.path, event.value as LeftOperand);
|
||||
}
|
||||
|
||||
if (event.type === "condition.onChange") {
|
||||
|
|
|
@ -26,6 +26,15 @@ export const STATIC_CONDITIONS = {
|
|||
giftCard: [{ type: "select", label: "is", value: "input-1" }],
|
||||
};
|
||||
|
||||
|
||||
export const CONSTRAINTS = {
|
||||
channel: {
|
||||
dependsOn: ["price", "isVisibleInListing"],
|
||||
removable: true,
|
||||
disabled: ["left", "condition"]
|
||||
},
|
||||
}
|
||||
|
||||
export const STATIC_OPTIONS: LeftOperand[] = [
|
||||
{ value: "price", label: "Price", type: "price", slug: "price" },
|
||||
{ value: "category", label: "Category", type: "category", slug: "category" },
|
||||
|
|
|
@ -6,6 +6,49 @@ import { FilterValueProvider } from "./FilterValueProvider";
|
|||
type StateCallback = (el: FilterElement) => void;
|
||||
type Element = FilterContainer[number];
|
||||
|
||||
|
||||
const isFilterElement = (el: unknown): el is FilterElement => typeof el !== "string" && !Array.isArray(el)
|
||||
|
||||
const removeConstraint = (container: FilterContainer) => {
|
||||
return container.map((el) => {
|
||||
if (!isFilterElement(el)) return el
|
||||
|
||||
if (!el.constraint?.existIn(container)) {
|
||||
el.clearConstraint()
|
||||
}
|
||||
|
||||
return el
|
||||
})
|
||||
}
|
||||
|
||||
const calculateIndexesToRemove = (container: FilterContainer, position: number) => {
|
||||
const next = position + 1
|
||||
const previous = position - 1
|
||||
const indexTuple = [position]
|
||||
|
||||
if (typeof container[next] === "string") {
|
||||
indexTuple.push(next)
|
||||
|
||||
return indexTuple
|
||||
}
|
||||
|
||||
if (typeof container[previous] === "string") {
|
||||
indexTuple.push(previous)
|
||||
}
|
||||
|
||||
return indexTuple
|
||||
}
|
||||
|
||||
|
||||
const removeElement = (container: FilterContainer, position: number) => {
|
||||
const indexTuple = calculateIndexesToRemove(container, position)
|
||||
|
||||
const newContainer = container
|
||||
.filter((_, elIndex) => !indexTuple.includes(elIndex))
|
||||
|
||||
return removeConstraint(newContainer)
|
||||
}
|
||||
|
||||
export const useContainerState = (valueProvider: FilterValueProvider) => {
|
||||
const [value, setValue] = useState<FilterContainer>([]);
|
||||
|
||||
|
@ -15,17 +58,17 @@ export const useContainerState = (valueProvider: FilterValueProvider) => {
|
|||
}
|
||||
}, [valueProvider.loading]);
|
||||
|
||||
const isFilterElement = (
|
||||
const isFilterElementAtIndex = (
|
||||
elIndex: number,
|
||||
index: number,
|
||||
el: Element,
|
||||
): el is FilterElement => {
|
||||
return elIndex === index && typeof el !== "string" && !Array.isArray(el);
|
||||
return elIndex === index && isFilterElement(el);
|
||||
};
|
||||
|
||||
const updateFilterElement =
|
||||
(index: number, cb: StateCallback) => (el: Element, elIndex: number) => {
|
||||
if (isFilterElement(elIndex, index, el)) {
|
||||
if (isFilterElementAtIndex(elIndex, index, el)) {
|
||||
cb(el);
|
||||
}
|
||||
|
||||
|
@ -37,32 +80,48 @@ export const useContainerState = (valueProvider: FilterValueProvider) => {
|
|||
setValue(v => v.map(updateFilterElement(index, cb)));
|
||||
};
|
||||
|
||||
const updateBySlug = (slug: string, cb: StateCallback) => {
|
||||
setValue(v => v.map((el) => {
|
||||
if (isFilterElement(el) && el.value.value === slug) {
|
||||
cb(el)
|
||||
}
|
||||
|
||||
return el
|
||||
}))
|
||||
};
|
||||
|
||||
const removeAt = (position: string) => {
|
||||
const index = parseInt(position, 10);
|
||||
|
||||
if (value.length > 0) {
|
||||
setValue(v =>
|
||||
v.filter((_, elIndex) => ![index - 1, index].includes(elIndex)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(v => v.filter((_, elIndex) => ![index].includes(elIndex)));
|
||||
setValue(v => removeElement(v, index));
|
||||
};
|
||||
|
||||
const createEmpty = () => {
|
||||
const create = (element: FilterElement) => {
|
||||
const newValue: FilterContainer = [];
|
||||
|
||||
if (value.length > 0) {
|
||||
newValue.push("OR");
|
||||
newValue.push("AND");
|
||||
}
|
||||
|
||||
newValue.push(FilterElement.createEmpty());
|
||||
newValue.push(element);
|
||||
|
||||
setValue(v => v.concat(newValue));
|
||||
};
|
||||
|
||||
const exist = (slug: string) => {
|
||||
return value.some((entry) =>
|
||||
isFilterElement(entry) && entry.value.value === slug
|
||||
)
|
||||
}
|
||||
|
||||
const createEmpty = () => {
|
||||
create(FilterElement.createEmpty())
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
exist,
|
||||
updateBySlug,
|
||||
createEmpty,
|
||||
updateAt,
|
||||
removeAt,
|
||||
|
|
|
@ -2,23 +2,40 @@ import useDebounce from "@dashboard/hooks/useDebounce";
|
|||
|
||||
import { FilterAPIProvider } from "./API/FilterAPIProvider";
|
||||
import { useConditionalFilterContext } from "./context";
|
||||
import { FilterElement } from "./FilterElement";
|
||||
import { ConditionValue, ItemOption } from "./FilterElement/ConditionValue";
|
||||
import { LeftOperandsProvider } from "./LeftOperandsProvider";
|
||||
import { Constraint } from "./FilterElement/Constraint";
|
||||
import { LeftOperand, LeftOperandsProvider } from "./LeftOperandsProvider";
|
||||
|
||||
export const useFilterContainer = (
|
||||
apiProvider: FilterAPIProvider,
|
||||
leftOperandsProvider: LeftOperandsProvider,
|
||||
) => {
|
||||
const {
|
||||
containerState: { value, updateAt, removeAt, createEmpty },
|
||||
containerState: { value, updateAt, removeAt, createEmpty, create, exist, updateBySlug },
|
||||
} = useConditionalFilterContext();
|
||||
|
||||
const addEmpty = () => {
|
||||
createEmpty();
|
||||
};
|
||||
|
||||
const updateLeftOperator = (position: string, leftOperator: any) => {
|
||||
const updateLeftOperator = (position: string, leftOperator: LeftOperand) => {
|
||||
updateAt(position, el => el.updateLeftOperator(leftOperator));
|
||||
|
||||
const dependency = Constraint.getDependency(leftOperator.value)
|
||||
|
||||
if (!dependency) return
|
||||
|
||||
if (!exist(dependency)) {
|
||||
create(FilterElement.createStaticBySlug(dependency))
|
||||
return
|
||||
}
|
||||
|
||||
updateBySlug(dependency, (el) => {
|
||||
const newConstraint = Constraint.fromSlug(dependency)
|
||||
|
||||
if (newConstraint) el.setConstraint(newConstraint)
|
||||
})
|
||||
};
|
||||
|
||||
const updateLeftLoadingState = (position: string, loading: boolean) => {
|
||||
|
|
Loading…
Reference in a new issue