diff --git a/.changeset/sweet-otters-roll.md b/.changeset/sweet-otters-roll.md new file mode 100644 index 00000000000..b5dfb463dc9 --- /dev/null +++ b/.changeset/sweet-otters-roll.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Now it's possible to filter orders by its metadata. diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 310d46016f0..4c55a0a5dad 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -2495,6 +2495,9 @@ "context": "transaction reference", "string": "Transaction reference" }, + "EcglP9": { + "string": "Key" + }, "Ediw6/": { "context": "button label", "string": "Assign references" @@ -2928,6 +2931,9 @@ "GsBRWL": { "string": "Languages" }, + "GufXy5": { + "string": "Value" + }, "GuihaP": { "context": "order transaction refund summary label", "string": "Shipping" diff --git a/src/components/ConditionalFilter/API/OrderFilterAPIProvider.tsx b/src/components/ConditionalFilter/API/OrderFilterAPIProvider.tsx index f804c8de32a..6547b935a07 100644 --- a/src/components/ConditionalFilter/API/OrderFilterAPIProvider.tsx +++ b/src/components/ConditionalFilter/API/OrderFilterAPIProvider.tsx @@ -93,6 +93,10 @@ const createAPIHandler = ( return new NoopValuesHandler([]); } + if (rowType === "metadata") { + return new NoopValuesHandler([]); + } + throw new Error(`Unknown filter element: "${rowType}"`); }; diff --git a/src/components/ConditionalFilter/API/initialState/helpers.ts b/src/components/ConditionalFilter/API/initialState/helpers.ts index 258126f9945..4457d7e7ff0 100644 --- a/src/components/ConditionalFilter/API/initialState/helpers.ts +++ b/src/components/ConditionalFilter/API/initialState/helpers.ts @@ -131,7 +131,6 @@ export const createInitialOrderState = (data: InitialOrderAPIResponse[]) => isPreorder: createBooleanOptions(), giftCardBought: createBooleanOptions(), giftCardUsed: createBooleanOptions(), - customer: [], ids: [], created: "", updatedAt: "", diff --git a/src/components/ConditionalFilter/API/initialState/orders/InitialOrderState.test.ts b/src/components/ConditionalFilter/API/initialState/orders/InitialOrderState.test.ts index 57da8c0e931..5917383a9ae 100644 --- a/src/components/ConditionalFilter/API/initialState/orders/InitialOrderState.test.ts +++ b/src/components/ConditionalFilter/API/initialState/orders/InitialOrderState.test.ts @@ -31,34 +31,6 @@ describe("ConditionalFilter / API / Orders / InitialOrderState", () => { expect(result).toEqual(expectedOutput); }); - it("should filter by customer", () => { - // Arrange - const initialOrderState = InitialOrderStateResponse.empty(); - - initialOrderState.customer = [ - { - label: "Customer", - slug: "customer", - value: "test", - }, - ]; - - const token = UrlToken.fromUrlEntry(new UrlEntry("s0.customer", "test")); - const expectedOutput = [ - { - label: "Customer", - slug: "customer", - value: "test", - }, - ]; - - // Act - const result = initialOrderState.filterByUrlToken(token); - - // Assert - expect(result).toEqual(expectedOutput); - }); - it("should filter by click and collect", () => { // Arrange const initialOrderState = InitialOrderStateResponse.empty(); diff --git a/src/components/ConditionalFilter/API/initialState/orders/InitialOrderState.ts b/src/components/ConditionalFilter/API/initialState/orders/InitialOrderState.ts index 2f1064274c7..082e2650a03 100644 --- a/src/components/ConditionalFilter/API/initialState/orders/InitialOrderState.ts +++ b/src/components/ConditionalFilter/API/initialState/orders/InitialOrderState.ts @@ -11,13 +11,11 @@ export interface InitialOrderState { isPreorder: ItemOption[]; giftCardBought: ItemOption[]; giftCardUsed: ItemOption[]; - customer: ItemOption[]; created: string | string[]; updatedAt: string | string[]; ids: ItemOption[]; } -const isTextInput = (name: string) => ["customer"].includes(name); const isDateField = (name: string) => ["created", "updatedAt"].includes(name); export class InitialOrderStateResponse implements InitialOrderState { @@ -31,7 +29,6 @@ export class InitialOrderStateResponse implements InitialOrderState { public isPreorder: ItemOption[] = [], public giftCardBought: ItemOption[] = [], public giftCardUsed: ItemOption[] = [], - public customer: ItemOption[] = [], public created: string | string[] = [], public updatedAt: string | string[] = [], public ids: ItemOption[] = [], @@ -48,8 +45,8 @@ export class InitialOrderStateResponse implements InitialOrderState { const entry = this.getEntryByName(token.name); - if (isTextInput(token.name)) { - return entry; + if (!token.isLoadable()) { + return [token.value] as string[]; } return (entry as ItemOption[]).filter(({ slug }) => slug && token.value.includes(slug)); @@ -75,8 +72,6 @@ export class InitialOrderStateResponse implements InitialOrderState { return this.giftCardBought; case "giftCardUsed": return this.giftCardUsed; - case "customer": - return this.customer; case "ids": return this.ids; default: diff --git a/src/components/ConditionalFilter/API/initialState/orders/useInitialOrderState.ts b/src/components/ConditionalFilter/API/initialState/orders/useInitialOrderState.ts index f24e44349c2..df134647877 100644 --- a/src/components/ConditionalFilter/API/initialState/orders/useInitialOrderState.ts +++ b/src/components/ConditionalFilter/API/initialState/orders/useInitialOrderState.ts @@ -17,14 +17,6 @@ import { createInitialOrderState } from "../helpers"; import { InitialOrderAPIResponse } from "../types"; import { InitialOrderStateResponse } from "./InitialOrderState"; -const getCustomer = (customer: string[]) => { - if (Array.isArray(customer) && customer.length > 0) { - return customer.at(-1) ?? ""; - } - - return ""; -}; - const mapIDsToOptions = (ids: string[]) => ids.map(id => ({ type: "ids", @@ -53,7 +45,6 @@ export const useInitialOrderState = (): InitialOrderAPIState => { paymentStatus, status, authorizeStatus, - customer, ids, }: OrderFetchingParams) => { if (channels.length > 0) { @@ -93,14 +84,6 @@ export const useInitialOrderState = (): InitialOrderAPIState => { status: await statusInit.fetch(), authorizeStatus: await authorizeStatusInit.fetch(), chargeStatus: await chargeStatusInit.fetch(), - customer: [ - { - type: "customer", - label: "Customer", - value: getCustomer(customer), - slug: "customer", - }, - ], ids: mapIDsToOptions(ids), }; @@ -115,7 +98,6 @@ export const useInitialOrderState = (): InitialOrderAPIState => { initialState.isClickAndCollect, initialState.giftCardBought, initialState.giftCardUsed, - initialState.customer, initialState.created, initialState.updatedAt, initialState.ids, diff --git a/src/components/ConditionalFilter/FilterElement/Condition.ts b/src/components/ConditionalFilter/FilterElement/Condition.ts index 35655331368..b1e9abc4f7f 100644 --- a/src/components/ConditionalFilter/FilterElement/Condition.ts +++ b/src/components/ConditionalFilter/FilterElement/Condition.ts @@ -57,7 +57,6 @@ export class Condition { const isMultiSelect = selectedOption?.type === "multiselect" && valueItems.length > 0; const isBulkSelect = selectedOption?.type === "bulkselect" && valueItems.length > 0; const isDate = ["created", "updatedAt", "startDate", "endDate"].includes(token.name); - const value = isMultiSelect || isDate || isBulkSelect ? valueItems : valueItems[0]; if (!selectedOption) { diff --git a/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts b/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts index cb0df633794..694d5dca2f7 100644 --- a/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts +++ b/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts @@ -14,7 +14,7 @@ export class ConditionSelected { return ( this.value === "" || (isItemOptionArray(this.value) && this.value.length === 0) || - (isTuple(this.value) && this.value.includes("")) + (isTuple(this.value) && this.value.every(el => el === "")) ); } diff --git a/src/components/ConditionalFilter/UI/MetadataInput.tsx b/src/components/ConditionalFilter/UI/MetadataInput.tsx new file mode 100644 index 00000000000..76fc18fc193 --- /dev/null +++ b/src/components/ConditionalFilter/UI/MetadataInput.tsx @@ -0,0 +1,73 @@ +import { Box, Input } from "@saleor/macaw-ui-next"; +import React from "react"; +import { useIntl } from "react-intl"; + +import { metadataInputMessages } from "../intl"; +import { FilterEventEmitter } from "./EventEmitter"; +import { DoubleTextOperator } from "./types"; + +interface MetadataInputProps { + index: number; + selected: DoubleTextOperator; + emitter: FilterEventEmitter; + error: boolean; + disabled: boolean; +} + +export const MetadataInput = ({ + index, + selected, + emitter, + error, + disabled, +}: MetadataInputProps) => { + const intl = useIntl(); + + return ( + + { + emitter.changeRightOperator(index, [e.target.value, selected.value[1]]); + }} + onFocus={() => { + emitter.focusRightOperator(index); + }} + onBlur={() => { + emitter.blurRightOperator(index); + }} + error={error} + placeholder={intl.formatMessage(metadataInputMessages.keyPlaceholder)} + disabled={disabled} + /> + + { + emitter.changeRightOperator(index, [selected.value[0], e.target.value]); + }} + onFocus={() => { + emitter.focusRightOperator(index); + }} + onBlur={() => { + emitter.blurRightOperator(index); + }} + error={error} + placeholder={intl.formatMessage(metadataInputMessages.valuePlaceholder)} + disabled={disabled} + /> + + ); +}; diff --git a/src/components/ConditionalFilter/UI/RightOperator.tsx b/src/components/ConditionalFilter/UI/RightOperator.tsx index d108a371f49..c1dd5aca754 100644 --- a/src/components/ConditionalFilter/UI/RightOperator.tsx +++ b/src/components/ConditionalFilter/UI/RightOperator.tsx @@ -9,6 +9,7 @@ import React from "react"; import BulkSelect from "./BulkSelect"; import { FilterEventEmitter } from "./EventEmitter"; +import { MetadataInput } from "./MetadataInput"; import { isBulkSelect, isCombobox, @@ -16,6 +17,7 @@ import { isDateRange, isDateTime, isDateTimeRange, + isDoubleText, isMultiselect, isNumberInput, isNumberRange, @@ -261,5 +263,17 @@ export const RightOperator = ({ ); } + if (isDoubleText(selected)) { + return ( + + ); + } + return ; }; diff --git a/src/components/ConditionalFilter/UI/operators.ts b/src/components/ConditionalFilter/UI/operators.ts index 2c3412c458e..50498686919 100644 --- a/src/components/ConditionalFilter/UI/operators.ts +++ b/src/components/ConditionalFilter/UI/operators.ts @@ -3,6 +3,7 @@ import { ComboboxOperator, DateOperator, DateTimeOperator, + DoubleTextOperator, InputOperator, MultiselectOperator, NumberRangeOperator, @@ -42,3 +43,6 @@ export const isDateRange = (value: SelectedOperator): value is DateOperator => export const isDateTimeRange = (value: SelectedOperator): value is DateTimeOperator => value.conditionValue?.type === "datetime.range"; + +export const isDoubleText = (value: SelectedOperator): value is DoubleTextOperator => + value.conditionValue?.type === "text.double"; diff --git a/src/components/ConditionalFilter/UI/types.ts b/src/components/ConditionalFilter/UI/types.ts index 9afcf2aa848..39df8a68355 100644 --- a/src/components/ConditionalFilter/UI/types.ts +++ b/src/components/ConditionalFilter/UI/types.ts @@ -33,6 +33,7 @@ type ConditionOptionTypes = ConditionOption< | "datetime" | "date.range" | "datetime.range" + | "text.double" >; export interface Row { @@ -60,7 +61,8 @@ export type SelectedOperator = | DateOperator | DateTimeOperator | DateRangeOperator - | DateTimeRangeOperator; + | DateTimeRangeOperator + | DoubleTextOperator; export interface InputOperator { value: string | RightOperatorOption; @@ -119,6 +121,11 @@ export interface DateTimeRangeOperator { conditionValue: ConditionOption<"datetime.range"> | null; } +export interface DoubleTextOperator { + value: [string, string]; + conditionValue: ConditionOption<"text.double"> | null; +} + export interface FilterEvent extends Event { detail?: | RowAddData diff --git a/src/components/ConditionalFilter/ValueProvider/UrlToken.ts b/src/components/ConditionalFilter/ValueProvider/UrlToken.ts index 564474c5c63..27291100ceb 100644 --- a/src/components/ConditionalFilter/ValueProvider/UrlToken.ts +++ b/src/components/ConditionalFilter/ValueProvider/UrlToken.ts @@ -16,7 +16,6 @@ const ORDER_STATICS = [ "isPreorder", "isClickAndCollect", "channels", - "customer", "ids", ]; diff --git a/src/components/ConditionalFilter/constants.ts b/src/components/ConditionalFilter/constants.ts index b9dab9eba80..b3084e64327 100644 --- a/src/components/ConditionalFilter/constants.ts +++ b/src/components/ConditionalFilter/constants.ts @@ -111,6 +111,13 @@ export const STATIC_CONDITIONS = { value: "input-1", }, ], + metadata: [ + { + type: "text.double", + label: "is", + value: "input-1", + }, + ], }; export const CONSTRAINTS = { @@ -258,6 +265,12 @@ export const STATIC_ORDER_OPTIONS: LeftOperand[] = [ type: "customer", slug: "customer", }, + { + value: "metadata", + label: "Metadata", + type: "metadata", + slug: "metadata", + }, ]; export const STATIC_OPTIONS = [ diff --git a/src/components/ConditionalFilter/controlsType.ts b/src/components/ConditionalFilter/controlsType.ts index 565ffcc0605..1f769677468 100644 --- a/src/components/ConditionalFilter/controlsType.ts +++ b/src/components/ConditionalFilter/controlsType.ts @@ -11,6 +11,7 @@ export const CONTROL_DEFAULTS = { datetime: "", "date.range": ["", ""] as [string, string], "datetime.range": ["", ""] as [string, string], + "text.double": ["", ""] as [string, string], }; export const getDefaultByControlName = (name: string): ConditionValue => diff --git a/src/components/ConditionalFilter/intl.ts b/src/components/ConditionalFilter/intl.ts index 99e98ee9858..64f4b20389f 100644 --- a/src/components/ConditionalFilter/intl.ts +++ b/src/components/ConditionalFilter/intl.ts @@ -42,3 +42,14 @@ export const leftOperatorsMessages = defineMessages({ defaultMessage: "Is giftcard", }, }); + +export const metadataInputMessages = defineMessages({ + keyPlaceholder: { + id: "EcglP9", + defaultMessage: "Key", + }, + valuePlaceholder: { + id: "GufXy5", + defaultMessage: "Value", + }, +}); diff --git a/src/components/ConditionalFilter/queryVariables.ts b/src/components/ConditionalFilter/queryVariables.ts index 412006d39c6..5cdbf379187 100644 --- a/src/components/ConditionalFilter/queryVariables.ts +++ b/src/components/ConditionalFilter/queryVariables.ts @@ -5,7 +5,6 @@ import { DateTimeRangeInput, DecimalFilterInput, GlobalIdFilterInput, - OrderFilterInput, ProductWhereInput, PromotionWhereInput, } from "@dashboard/graphql"; @@ -133,6 +132,11 @@ const createAttributeQueryPart = ( type ProductQueryVars = ProductWhereInput & { channel?: { eq: string } }; +/* + Map to ProductQueryVars as long as it does not have "where" filter - it would use mostly same keys. +*/ +export type OrderQueryVars = ProductQueryVars & { created?: DateTimeRangeInput | DateRangeInput }; + export const createProductQueryVariables = (value: FilterContainer): ProductQueryVars => { return value.reduce((p, c) => { if (typeof c === "string" || Array.isArray(c)) return p; @@ -163,23 +167,35 @@ export const createDiscountsQueryVariables = (value: FilterContainer): Promotion }; export const createOrderQueryVariables = (value: FilterContainer) => { - return value.reduce((p, c) => { + return value.reduce((p: OrderQueryVars, c) => { if (typeof c === "string" || Array.isArray(c)) { return p; } + if (c.value.type === "metadata") { + p.metadata = p.metadata || []; + + const [key, value] = c.condition.selected.value as [string, string]; + + p.metadata.push({ key, value }); + + return p; + } + if (c.value.type === "updatedAt" || c.value.type === "created") { p[c.value.value as "updatedAt" | "created"] = createStaticQueryPart(c.condition.selected) as | DateTimeRangeInput | DateRangeInput; + + return p; } if (c.isStatic()) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - seems to be a bug in TS, works fine in 5.4.5 - p[c.value.value] = createStaticQueryPart(c.condition.selected); + p[c.value.value as keyof OrderQueryVars] = createStaticQueryPart(c.condition.selected); + + return p; } return p; - }, {} as OrderFilterInput); + }, {} as OrderQueryVars); }; diff --git a/src/index.css b/src/index.css index 286a7f284b3..2b701cf4129 100644 --- a/src/index.css +++ b/src/index.css @@ -74,3 +74,8 @@ body { .noBorder { border: none; } + + +.conditional-metadata label { + border: none; +} \ No newline at end of file diff --git a/src/orders/views/OrderList/OrderList.tsx b/src/orders/views/OrderList/OrderList.tsx index b264d518a05..c3ec9ca4959 100644 --- a/src/orders/views/OrderList/OrderList.tsx +++ b/src/orders/views/OrderList/OrderList.tsx @@ -116,7 +116,7 @@ export const OrderList: React.FC = ({ params }) => { filter: filterVariables, sort: getSortQueryVariables(params), }), - [params, settings.rowNumber, valueProvider.value, paginationState], + [params, settings.rowNumber, valueProvider.value], ); const { data } = useOrderListQuery({ displayLoader: true, diff --git a/src/orders/views/OrderList/filters.test.ts b/src/orders/views/OrderList/filters.test.ts index f8c71d07283..70560055672 100644 --- a/src/orders/views/OrderList/filters.test.ts +++ b/src/orders/views/OrderList/filters.test.ts @@ -18,7 +18,7 @@ describe("Filtering URL params", () => { it("should not be empty object if params given", () => { // Arrange const params = new URLSearchParams( - "0%5Bs2.status%5D%5B0%5D=FULFILLED&0%5Bs2.status%5D%5B1%5D=CANCELED&1=AND&2%5Bs0.customer%5D=customer&3=AND&4%5Bs0.isClickAndCollect%5D=false", + "0%5Bs2.status%5D%5B0%5D=FULFILLED&0%5Bs2.status%5D%5B1%5D=CANCELED&1=AND&2%5Bs0.customer%5D=test&3=AND&4%5Bs0.isClickAndCollect%5D=false", ); const tokenizedUrl = new TokenArray(params.toString()); const initialOrderState = InitialOrderStateResponse.empty(); @@ -40,13 +40,6 @@ describe("Filtering URL params", () => { value: "UNCONFIRMED", }, ]; - initialOrderState.customer = [ - { - label: "Customer", - slug: "customer", - value: "test", - }, - ]; initialOrderState.isClickAndCollect = [ { label: "No", @@ -67,4 +60,24 @@ describe("Filtering URL params", () => { expect(filterVariables.status).toEqual(["FULFILLED", "CANCELED"]); expect(filterVariables.isClickAndCollect).toBe(false); }); + + it("should filter by the metadata", () => { + // Arrange + const params = new URLSearchParams( + "0%5Bs0.metadata%5D%5B0%5D=key1&0%5Bs0.metadata%5D%5B1%5D=value1&1=AND&2%5Bs0.metadata%5D%5B0%5D=key2&2%5Bs0.metadata%5D%5B1%5D=value2&asc=false&sort=number", + ); + const tokenizedUrl = new TokenArray(params.toString()); + + // Act + const filterVariables = getFilterVariables( + {}, + tokenizedUrl.asFilterValuesFromResponse(InitialOrderStateResponse.empty()), + ); + + // Assert + expect(filterVariables.metadata).toEqual([ + { key: "key1", value: "value1" }, + { key: "key2", value: "value2" }, + ]); + }); }); diff --git a/src/orders/views/OrderList/filters.ts b/src/orders/views/OrderList/filters.ts index 5e8405c5358..a1095dca374 100644 --- a/src/orders/views/OrderList/filters.ts +++ b/src/orders/views/OrderList/filters.ts @@ -1,6 +1,9 @@ // @ts-strict-ignore import { FilterContainer } from "@dashboard/components/ConditionalFilter/FilterElement"; -import { createOrderQueryVariables } from "@dashboard/components/ConditionalFilter/queryVariables"; +import { + createOrderQueryVariables, + OrderQueryVars, +} from "@dashboard/components/ConditionalFilter/queryVariables"; import { OrderFilterInput, OrderStatusFilter, PaymentChargeStatusEnum } from "@dashboard/graphql"; import { findInEnum, parseBoolean } from "@dashboard/misc"; import { @@ -36,7 +39,7 @@ export const ORDER_FILTERS_KEY = "orderFiltersPresets"; const whereInputTypes = ["oneOf", "eq", "range", "gte", "lte"]; const orderBooleanFilters = ["isClickAndCollect", "isPreorder"]; -const _whereToLegacyVariables = (where: OrderFilterInput) => { +const _whereToLegacyVariables = (where: OrderQueryVars) => { return where ? Object.keys(where).reduce((acc, key) => { if (typeof where[key] === "object") { @@ -62,6 +65,10 @@ const _whereToLegacyVariables = (where: OrderFilterInput) => { acc[key] = where[key]; } + if (Array.isArray(where[key])) { + acc[key] = where[key]; + } + return acc; }, {}) : {};