Experimental filters: add support for constrains on UI (#3969)

This commit is contained in:
Krzysztof Żuraw 2023-07-21 11:26:48 +02:00 committed by GitHub
parent 23bb5976c6
commit 5a6c255004
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1081 additions and 669 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---
Experimental filters: add support for constrains in MacawUI

1495
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -34,7 +34,7 @@
"@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/styles": "^4.11.4", "@material-ui/styles": "^4.11.4",
"@reach/auto-id": "^0.16.0", "@reach/auto-id": "^0.16.0",
"@saleor/macaw-ui": "0.8.0-pre.106", "@saleor/macaw-ui": "0.8.0-pre.107",
"@saleor/sdk": "0.6.0", "@saleor/sdk": "0.6.0",
"@sentry/react": "^6.0.0", "@sentry/react": "^6.0.0",
"@types/faker": "^5.1.6", "@types/faker": "^5.1.6",

View file

@ -1,4 +1,4 @@
import { _ExperimentalFilters, Box, Divider } from "@saleor/macaw-ui/next"; import { Box, Divider, Skeleton } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
export const LoadingFiltersArea = () => ( export const LoadingFiltersArea = () => (
@ -11,16 +11,16 @@ export const LoadingFiltersArea = () => (
flexDirection="column" flexDirection="column"
> >
<Box display="flex" flexDirection="column" gap={3} height="100%"> <Box display="flex" flexDirection="column" gap={3} height="100%">
<_ExperimentalFilters.Skeleton height={7} /> <Skeleton height={7} />
<_ExperimentalFilters.Skeleton height={7} /> <Skeleton height={7} />
<_ExperimentalFilters.Skeleton height={7} /> <Skeleton height={7} />
</Box> </Box>
<Divider /> <Divider />
<Box display="flex" gap={4} justifyContent="space-between"> <Box display="flex" gap={4} justifyContent="space-between">
<_ExperimentalFilters.Skeleton height={7} __width="60px" /> <Skeleton height={7} __width="60px" />
<Box display="flex" gap={3}> <Box display="flex" gap={3}>
<_ExperimentalFilters.Skeleton height={7} __width="60px" /> <Skeleton height={7} __width="60px" />
<_ExperimentalFilters.Skeleton height={7} __width="60px" /> <Skeleton height={7} __width="60px" />
</Box> </Box>
</Box> </Box>
</Box> </Box>

View file

@ -17,7 +17,7 @@ export const STATIC_CONDITIONS = {
channel: [{ type: "select", label: "is", value: "input-5" }], channel: [{ type: "select", label: "is", value: "input-5" }],
productType: [ productType: [
{ type: "combobox", label: "is", value: "input-1" }, { type: "combobox", label: "is", value: "input-1" },
{ type: "multiselect", label: "in", value: "input-2" } { type: "multiselect", label: "in", value: "input-2" },
], ],
isAvailable: [{ type: "select", label: "is", value: "input-1" }], isAvailable: [{ type: "select", label: "is", value: "input-1" }],
isPublished: [{ type: "select", label: "is", value: "input-1" }], isPublished: [{ type: "select", label: "is", value: "input-1" }],
@ -26,14 +26,13 @@ export const STATIC_CONDITIONS = {
giftCard: [{ type: "select", label: "is", value: "input-1" }], giftCard: [{ type: "select", label: "is", value: "input-1" }],
}; };
export const CONSTRAINTS = { export const CONSTRAINTS = {
channel: { channel: {
dependsOn: ["price", "isVisibleInListing"], dependsOn: ["price", "isVisibleInListing", "isAvailable", "isPublished"],
removable: true, removable: false,
disabled: ["left", "condition"] 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" },
@ -45,12 +44,42 @@ export const STATIC_OPTIONS: LeftOperand[] = [
slug: "collection", slug: "collection",
}, },
{ value: "channel", label: "Channel", type: "channel", slug: "channel" }, { value: "channel", label: "Channel", type: "channel", slug: "channel" },
{ value: "productType", label: "Product Type", type: "productType", slug: "productType" }, {
{ value: "isAvailable", label: "Is available", type: "isAvailable", slug: "isAvailable" }, value: "productType",
{ value: "isPublished", label: "Is published", type: "isPublished", slug: "isPublished" }, label: "Product Type",
{ value: "isVisibleInListing", label: "Visible in listing", type: "isVisibleInListing", slug: "isVisibleInListing" }, type: "productType",
{ value: "hasCategory", label: "Has category", type: "hasCategory", slug: "hasCategory" }, slug: "productType",
{ value: "giftCard", label: "Has giftcard", type: "giftCard", slug: "giftCard" }, },
{
value: "isAvailable",
label: "Is available",
type: "isAvailable",
slug: "isAvailable",
},
{
value: "isPublished",
label: "Is published",
type: "isPublished",
slug: "isPublished",
},
{
value: "isVisibleInListing",
label: "Visible in listing",
type: "isVisibleInListing",
slug: "isVisibleInListing",
},
{
value: "hasCategory",
label: "Has category",
type: "hasCategory",
slug: "hasCategory",
},
{
value: "giftCard",
label: "Has giftcard",
type: "giftCard",
slug: "giftCard",
},
]; ];
export const ATTRIBUTE_INPUT_TYPE_CONDITIONS = { export const ATTRIBUTE_INPUT_TYPE_CONDITIONS = {
@ -69,38 +98,42 @@ export const ATTRIBUTE_INPUT_TYPE_CONDITIONS = {
}; };
export const getAtributeInputType = (item: ConditionItem | null) => { export const getAtributeInputType = (item: ConditionItem | null) => {
const result = Object.entries(ATTRIBUTE_INPUT_TYPE_CONDITIONS) const result = Object.entries(ATTRIBUTE_INPUT_TYPE_CONDITIONS).find(
.find(([_, value]) => ([_, value]) =>
value.find(entry => entry.type === item?.type && entry.label === item.label) value.find(
) entry => entry.type === item?.type && entry.label === item.label,
),
);
return result && result[0] return result && result[0];
} };
export type RowType = keyof typeof STATIC_CONDITIONS | "attribute";
export type RowType = keyof typeof STATIC_CONDITIONS | "attribute"
export const booleanOptionTrue = (type?: string) => ({ export const booleanOptionTrue = (type?: string) => ({
label: "Yes", label: "Yes",
value: "true", value: "true",
slug: "true", slug: "true",
...({ type }) ...{ type },
}) });
export const booleanOptionFalse = (type?: string) => ({ export const booleanOptionFalse = (type?: string) => ({
label: "No", label: "No",
value: "false", value: "false",
slug: "false", slug: "false",
...({ type }) ...{ type },
}) });
export const createBooleanOptions = (type?: string): ItemOption[] => [ export const createBooleanOptions = (type?: string): ItemOption[] => [
booleanOptionTrue(type), booleanOptionTrue(type),
booleanOptionFalse(type) booleanOptionFalse(type),
] ];
export const createBoleanOption = (flag: boolean, type?: string): ItemOption => { export const createBoleanOption = (
if (flag) return booleanOptionTrue(type) flag: boolean,
type?: string,
): ItemOption => {
if (flag) return booleanOptionTrue(type);
return booleanOptionFalse(type) return booleanOptionFalse(type);
} };

View file

@ -1,111 +1,134 @@
import { AttributeInput,DecimalFilterInput, GlobalIdFilterInput, ProductWhereInput } from "@dashboard/graphql"; import {
AttributeInput,
DecimalFilterInput,
GlobalIdFilterInput,
ProductWhereInput,
} from "@dashboard/graphql";
import { FilterContainer } from "./FilterElement"; import { FilterContainer } from "./FilterElement";
import { ConditionSelected } from "./FilterElement/ConditionSelected"; import { ConditionSelected } from "./FilterElement/ConditionSelected";
import { isItemOption, isItemOptionArray, isTuple } from "./FilterElement/ConditionValue"; import {
isItemOption,
isItemOptionArray,
isTuple,
} from "./FilterElement/ConditionValue";
type StaticQueryPart = type StaticQueryPart =
| string | string
| GlobalIdFilterInput | GlobalIdFilterInput
| boolean | boolean
| DecimalFilterInput | DecimalFilterInput;
const createStaticQueryPart = (
selected: ConditionSelected,
): StaticQueryPart => {
if (!selected.conditionValue) return "";
const createStaticQueryPart = (selected: ConditionSelected): StaticQueryPart => { const { label } = selected.conditionValue;
if (!selected.conditionValue) return "" const { value } = selected;
const { label } = selected.conditionValue
const { value } = selected
if (label === "lower") { if (label === "lower") {
return { range: { lte: value } } return { range: { lte: value } };
} }
if (label === "greater") { if (label === "greater") {
return { range: { gte: value } } return { range: { gte: value } };
} }
if (isTuple(value) && label === "between") { if (isTuple(value) && label === "between") {
const [lte, gte] = value const [lte, gte] = value;
return { range: { lte, gte } } return { range: { lte, gte } };
} }
if (isItemOption(value)) { if (isItemOption(value)) {
return { eq: value.value } return { eq: value.value };
} }
if (isItemOptionArray(value)) { if (isItemOptionArray(value)) {
return { oneOf: value.map(x => x.value) } return { oneOf: value.map(x => x.value) };
} }
if (typeof value === "string") { if (typeof value === "string") {
return { eq: value } return { eq: value };
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
return { eq: value } return { eq: value };
} }
return value return value;
} };
const createAttributeQueryPart = (attributeSlug: string, selected: ConditionSelected): AttributeInput => { const createAttributeQueryPart = (
if (!selected.conditionValue) return { slug: attributeSlug } attributeSlug: string,
selected: ConditionSelected,
): AttributeInput => {
if (!selected.conditionValue) return { slug: attributeSlug };
const { label } = selected.conditionValue const { label } = selected.conditionValue;
const { value } = selected const { value } = selected;
if (label === "lower" && typeof value === "string") { if (label === "lower" && typeof value === "string") {
return { slug: attributeSlug, valuesRange: { lte: parseFloat(value) } } return { slug: attributeSlug, valuesRange: { lte: parseFloat(value) } };
} }
if (label === "greater" && typeof value === "string") { if (label === "greater" && typeof value === "string") {
return { slug: attributeSlug, valuesRange: { gte: parseFloat(value) } } return { slug: attributeSlug, valuesRange: { gte: parseFloat(value) } };
} }
if (isTuple(value) && label === "between") { if (isTuple(value) && label === "between") {
const [lte, gte] = value const [lte, gte] = value;
return { slug: attributeSlug, valuesRange: { lte: parseFloat(lte), gte: parseFloat(gte) } } return {
slug: attributeSlug,
valuesRange: { lte: parseFloat(lte), gte: parseFloat(gte) },
};
} }
if (isItemOption(value)) { if (isItemOption(value)) {
return { slug: attributeSlug, values: [value.value] } return { slug: attributeSlug, values: [value.value] };
} }
if (isItemOptionArray(value)) { if (isItemOptionArray(value)) {
return {slug: attributeSlug, values: value.map(x => x.value) } return { slug: attributeSlug, values: value.map(x => x.value) };
} }
if (typeof value === "string") { if (typeof value === "string") {
return { slug: attributeSlug, values: [value] } return { slug: attributeSlug, values: [value] };
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
return { slug: attributeSlug, values: value } return { slug: attributeSlug, values: value };
} }
if (value === "true" || value === "false") { if (value === "true" || value === "false") {
return { slug: attributeSlug, boolean: value } return { slug: attributeSlug, boolean: value };
} }
return value return value;
} };
type ProductQueryVars = ProductWhereInput & { channel?: { eq: string } };
export const createProductQueryVariables = (
export const createProductQueryVariables = (value: FilterContainer): ProductWhereInput => { value: FilterContainer,
return value.reduce((p, c) => { ): ProductQueryVars => {
if (typeof c === "string" || Array.isArray(c)) return p return value.reduce(
(p, c) => {
if (typeof c === "string" || Array.isArray(c)) return p;
if (c.isStatic()) { if (c.isStatic()) {
p[c.value.value as keyof ProductWhereInput] = createStaticQueryPart(c.condition.selected) p[c.value.value as keyof ProductWhereInput] = createStaticQueryPart(
c.condition.selected,
);
} }
if (c.isAttribute()) { if (c.isAttribute()) {
p.attributes!.push( p.attributes!.push(
createAttributeQueryPart(c.value.value, c.condition.selected) createAttributeQueryPart(c.value.value, c.condition.selected),
) );
} }
return p return p;
}, { attributes: [] } as ProductWhereInput) },
} { attributes: [] } as ProductWhereInput,
);
};

View file

@ -290,6 +290,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
filterContainer: valueProvider.value, filterContainer: valueProvider.value,
queryParams: params, queryParams: params,
isChannelSelected: !!selectedChannel, isChannelSelected: !!selectedChannel,
channelSlug: selectedChannel?.slug,
}); });
const sort = getSortQueryVariables(params, !!selectedChannel); const sort = getSortQueryVariables(params, !!selectedChannel);
@ -300,7 +301,6 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
...paginationState, ...paginationState,
...filterVariables, ...filterVariables,
sort, sort,
channel: selectedChannel?.slug,
}), }),
[params, settings.rowNumber, valueProvider.value], [params, settings.rowNumber, valueProvider.value],
); );

View file

@ -61,6 +61,7 @@ import {
ProductListUrlQueryParams, ProductListUrlQueryParams,
} from "../../urls"; } from "../../urls";
import { getProductGiftCardFilterParam } from "./utils"; import { getProductGiftCardFilterParam } from "./utils";
export const PRODUCT_FILTERS_KEY = "productPresets"; export const PRODUCT_FILTERS_KEY = "productPresets";
function getAttributeFilterParamType(inputType: AttributeInputTypeEnum) { function getAttributeFilterParamType(inputType: AttributeInputTypeEnum) {
@ -496,16 +497,27 @@ export const getFilterVariables = ({
filterContainer, filterContainer,
queryParams, queryParams,
isChannelSelected, isChannelSelected,
channelSlug,
}: { }: {
isProductListingPageFiltersFlagEnabled: boolean; isProductListingPageFiltersFlagEnabled: boolean;
filterContainer: FilterContainer; filterContainer: FilterContainer;
queryParams: ProductListUrlFilters; queryParams: ProductListUrlFilters;
isChannelSelected: boolean; isChannelSelected: boolean;
channelSlug: string | undefined;
}) => { }) => {
if (isProductListingPageFiltersFlagEnabled) { if (isProductListingPageFiltersFlagEnabled) {
const queryVars = createProductQueryVariables(filterContainer); const queryVars = createProductQueryVariables(filterContainer);
return { where: queryVars, search: queryParams.query }; const { channel, ...where } = queryVars;
return {
where,
search: queryParams.query,
channel: channel?.eq,
};
} }
return { filter: getLegacyFilterVariables(queryParams, isChannelSelected) }; return {
filter: getLegacyFilterVariables(queryParams, isChannelSelected),
channel: channelSlug,
};
}; };