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 { InitialStateResponse } from "../API/InitialStateResponse";
|
||||||
import { LeftOperand } from "../LeftOperandsProvider";
|
import { LeftOperand } from "../LeftOperandsProvider";
|
||||||
import { UrlToken } from "./../ValueProvider/UrlToken";
|
import { UrlToken } from "./../ValueProvider/UrlToken";
|
||||||
import { ConditionOptions } from "./ConditionOptions";
|
import { ConditionOptions, StaticElementName } from "./ConditionOptions";
|
||||||
import { ConditionSelected } from "./ConditionSelected";
|
import { ConditionSelected } from "./ConditionSelected";
|
||||||
import { ItemOption } from "./ConditionValue";
|
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) {
|
public static emptyFromLeftOperand(operand: LeftOperand) {
|
||||||
const options = ConditionOptions.fromName(operand.type);
|
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 { LeftOperand } from "../LeftOperandsProvider";
|
||||||
import { TokenType, UrlEntry, UrlToken } from "./../ValueProvider/UrlToken";
|
import { TokenType, UrlEntry, UrlToken } from "./../ValueProvider/UrlToken";
|
||||||
import { Condition } from "./Condition";
|
import { Condition } from "./Condition";
|
||||||
import { ConditionItem, ConditionOptions } from "./ConditionOptions";
|
import { ConditionItem, ConditionOptions, StaticElementName } from "./ConditionOptions";
|
||||||
import { ConditionSelected } from "./ConditionSelected";
|
import { ConditionSelected } from "./ConditionSelected";
|
||||||
import { ConditionValue, ItemOption } from "./ConditionValue";
|
import { ConditionValue, ItemOption } from "./ConditionValue";
|
||||||
|
import { Constraint } from "./Constraint";
|
||||||
|
|
||||||
class ExpressionValue {
|
class ExpressionValue {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -15,6 +16,18 @@ class ExpressionValue {
|
||||||
public type: string,
|
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) {
|
public static fromLeftOperand(leftOperand: LeftOperand) {
|
||||||
return new ExpressionValue(
|
return new ExpressionValue(
|
||||||
leftOperand.slug,
|
leftOperand.slug,
|
||||||
|
@ -56,7 +69,14 @@ export class FilterElement {
|
||||||
public value: ExpressionValue,
|
public value: ExpressionValue,
|
||||||
public condition: Condition,
|
public condition: Condition,
|
||||||
public loading: boolean,
|
public loading: boolean,
|
||||||
) {}
|
public constraint?: Constraint,
|
||||||
|
) {
|
||||||
|
const newConstraint = Constraint.fromSlug(this.value.value)
|
||||||
|
|
||||||
|
if (newConstraint) {
|
||||||
|
this.constraint = newConstraint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enableLoading() {
|
public enableLoading() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
@ -125,6 +145,15 @@ export class FilterElement {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setConstraint (constraint: Constraint) {
|
||||||
|
this.constraint = constraint
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearConstraint () {
|
||||||
|
this.constraint = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public asUrlEntry(): UrlEntry {
|
public asUrlEntry(): UrlEntry {
|
||||||
if (this.isAttribute()) {
|
if (this.isAttribute()) {
|
||||||
return UrlEntry.forAttribute(this.condition.selected, this.value.value);
|
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);
|
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) {
|
public static fromValueEntry(valueEntry: any) {
|
||||||
return new FilterElement(valueEntry.value, valueEntry.condition, false);
|
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) {
|
public static fromUrlToken(token: UrlToken, response: InitialStateResponse) {
|
||||||
if (token.isStatic()) {
|
if (token.isStatic()) {
|
||||||
return new FilterElement(
|
return new FilterElement(
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React from "react";
|
||||||
|
|
||||||
import { useConditionalFilterContext } from "./context";
|
import { useConditionalFilterContext } from "./context";
|
||||||
import { FilterContainer } from "./FilterElement";
|
import { FilterContainer } from "./FilterElement";
|
||||||
|
import { LeftOperand } from "./LeftOperandsProvider";
|
||||||
import { useFilterContainer } from "./useFilterContainer";
|
import { useFilterContainer } from "./useFilterContainer";
|
||||||
|
|
||||||
interface FiltersAreaProps {
|
interface FiltersAreaProps {
|
||||||
|
@ -35,7 +36,7 @@ export const FiltersArea = ({ onConfirm }: FiltersAreaProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "leftOperator.onChange") {
|
if (event.type === "leftOperator.onChange") {
|
||||||
updateLeftOperator(event.path, event.value);
|
updateLeftOperator(event.path, event.value as LeftOperand);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "condition.onChange") {
|
if (event.type === "condition.onChange") {
|
||||||
|
|
|
@ -26,6 +26,15 @@ export const STATIC_CONDITIONS = {
|
||||||
giftCard: [{ type: "select", label: "is", value: "input-1" }],
|
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[] = [
|
export const STATIC_OPTIONS: LeftOperand[] = [
|
||||||
{ value: "price", label: "Price", type: "price", slug: "price" },
|
{ value: "price", label: "Price", type: "price", slug: "price" },
|
||||||
{ value: "category", label: "Category", type: "category", slug: "category" },
|
{ value: "category", label: "Category", type: "category", slug: "category" },
|
||||||
|
|
|
@ -6,6 +6,49 @@ import { FilterValueProvider } from "./FilterValueProvider";
|
||||||
type StateCallback = (el: FilterElement) => void;
|
type StateCallback = (el: FilterElement) => void;
|
||||||
type Element = FilterContainer[number];
|
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) => {
|
export const useContainerState = (valueProvider: FilterValueProvider) => {
|
||||||
const [value, setValue] = useState<FilterContainer>([]);
|
const [value, setValue] = useState<FilterContainer>([]);
|
||||||
|
|
||||||
|
@ -15,17 +58,17 @@ export const useContainerState = (valueProvider: FilterValueProvider) => {
|
||||||
}
|
}
|
||||||
}, [valueProvider.loading]);
|
}, [valueProvider.loading]);
|
||||||
|
|
||||||
const isFilterElement = (
|
const isFilterElementAtIndex = (
|
||||||
elIndex: number,
|
elIndex: number,
|
||||||
index: number,
|
index: number,
|
||||||
el: Element,
|
el: Element,
|
||||||
): el is FilterElement => {
|
): el is FilterElement => {
|
||||||
return elIndex === index && typeof el !== "string" && !Array.isArray(el);
|
return elIndex === index && isFilterElement(el);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFilterElement =
|
const updateFilterElement =
|
||||||
(index: number, cb: StateCallback) => (el: Element, elIndex: number) => {
|
(index: number, cb: StateCallback) => (el: Element, elIndex: number) => {
|
||||||
if (isFilterElement(elIndex, index, el)) {
|
if (isFilterElementAtIndex(elIndex, index, el)) {
|
||||||
cb(el);
|
cb(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,32 +80,48 @@ export const useContainerState = (valueProvider: FilterValueProvider) => {
|
||||||
setValue(v => v.map(updateFilterElement(index, cb)));
|
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 removeAt = (position: string) => {
|
||||||
const index = parseInt(position, 10);
|
const index = parseInt(position, 10);
|
||||||
|
|
||||||
if (value.length > 0) {
|
setValue(v => removeElement(v, index));
|
||||||
setValue(v =>
|
|
||||||
v.filter((_, elIndex) => ![index - 1, index].includes(elIndex)),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue(v => v.filter((_, elIndex) => ![index].includes(elIndex)));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createEmpty = () => {
|
const create = (element: FilterElement) => {
|
||||||
const newValue: FilterContainer = [];
|
const newValue: FilterContainer = [];
|
||||||
|
|
||||||
if (value.length > 0) {
|
if (value.length > 0) {
|
||||||
newValue.push("OR");
|
newValue.push("AND");
|
||||||
}
|
}
|
||||||
|
|
||||||
newValue.push(FilterElement.createEmpty());
|
newValue.push(element);
|
||||||
|
|
||||||
setValue(v => v.concat(newValue));
|
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 {
|
return {
|
||||||
|
create,
|
||||||
|
exist,
|
||||||
|
updateBySlug,
|
||||||
createEmpty,
|
createEmpty,
|
||||||
updateAt,
|
updateAt,
|
||||||
removeAt,
|
removeAt,
|
||||||
|
|
|
@ -2,23 +2,40 @@ import useDebounce from "@dashboard/hooks/useDebounce";
|
||||||
|
|
||||||
import { FilterAPIProvider } from "./API/FilterAPIProvider";
|
import { FilterAPIProvider } from "./API/FilterAPIProvider";
|
||||||
import { useConditionalFilterContext } from "./context";
|
import { useConditionalFilterContext } from "./context";
|
||||||
|
import { FilterElement } from "./FilterElement";
|
||||||
import { ConditionValue, ItemOption } from "./FilterElement/ConditionValue";
|
import { ConditionValue, ItemOption } from "./FilterElement/ConditionValue";
|
||||||
import { LeftOperandsProvider } from "./LeftOperandsProvider";
|
import { Constraint } from "./FilterElement/Constraint";
|
||||||
|
import { LeftOperand, LeftOperandsProvider } from "./LeftOperandsProvider";
|
||||||
|
|
||||||
export const useFilterContainer = (
|
export const useFilterContainer = (
|
||||||
apiProvider: FilterAPIProvider,
|
apiProvider: FilterAPIProvider,
|
||||||
leftOperandsProvider: LeftOperandsProvider,
|
leftOperandsProvider: LeftOperandsProvider,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
containerState: { value, updateAt, removeAt, createEmpty },
|
containerState: { value, updateAt, removeAt, createEmpty, create, exist, updateBySlug },
|
||||||
} = useConditionalFilterContext();
|
} = useConditionalFilterContext();
|
||||||
|
|
||||||
const addEmpty = () => {
|
const addEmpty = () => {
|
||||||
createEmpty();
|
createEmpty();
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateLeftOperator = (position: string, leftOperator: any) => {
|
const updateLeftOperator = (position: string, leftOperator: LeftOperand) => {
|
||||||
updateAt(position, el => el.updateLeftOperator(leftOperator));
|
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) => {
|
const updateLeftLoadingState = (position: string, loading: boolean) => {
|
||||||
|
|
Loading…
Reference in a new issue