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:
Patryk Andrzejewski 2023-07-20 16:53:00 +02:00 committed by GitHub
parent 09d254d50a
commit 23bb5976c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 206 additions and 21 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": minor
---
Constrains implementation for filter rows

View file

@ -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);

View 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)
})
}
}

View file

@ -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(

View file

@ -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") {

View file

@ -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" },

View file

@ -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,

View file

@ -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) => {