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;
}, {})
: {};