Skip to content

Commit 0b63d06

Browse files
committed
Merge remote-tracking branch 'origin/trunk' into feat/promotions-milestone-2
Signed-off-by: tedraykov <[email protected]>
2 parents 00e099f + 494b86f commit 0b63d06

File tree

12 files changed

+327
-35
lines changed

12 files changed

+327
-35
lines changed

src/components/TextField/InputWithLabel.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ export const InputWithLabel = forwardRef((
2222
InputProps,
2323
inputProps,
2424
placeholder,
25-
name
26-
}: TextFieldProps,
25+
name,
26+
ariaLabel
27+
}: TextFieldProps & {ariaLabel?: string},
2728
ref
2829
) => {
2930
const labelId = useRef(uniqueId("label")).current;
@@ -45,7 +46,7 @@ export const InputWithLabel = forwardRef((
4546
error={error}
4647
disabled={disabled}
4748
ref={ref}
48-
inputProps={{ ...inputProps, "aria-labelledby": labelId }}
49+
inputProps={{ ...inputProps, "aria-labelledby": labelId, "aria-label": ariaLabel }}
4950
placeholder={placeholder}
5051
name={name}
5152
{...InputProps}

src/graphql/generates.ts

+29
Original file line numberDiff line numberDiff line change
@@ -9066,6 +9066,13 @@ export type GetViewerQueryVariables = Exact<{ [key: string]: never; }>;
90669066

90679067
export type GetViewerQuery = { __typename?: 'Query', viewer?: { __typename?: 'Account', _id: string, firstName?: string | null, language?: string | null, lastName?: string | null, name?: string | null, primaryEmailAddress: any, adminUIShops?: Array<{ __typename?: 'Shop', _id: string, name: string, slug?: string | null, shopType?: string | null, language: string, brandAssets?: { __typename?: 'ShopBrandAssets', navbarBrandImage?: { __typename?: 'ImageSizes', large?: string | null } | null } | null, storefrontUrls?: { __typename?: 'StorefrontUrls', storefrontHomeUrl?: string | null } | null, shopLogoUrls?: { __typename?: 'ShopLogoUrls', primaryShopLogoUrl?: string | null } | null, currency: { __typename?: 'Currency', _id: string, code: string, format: string, symbol: string } } | null> | null, groups?: { __typename?: 'GroupConnection', nodes?: Array<{ __typename?: 'Group', _id: string, name: string, permissions?: Array<string | null> | null } | null> | null } | null } | null };
90689068

9069+
export type GetIntrospectSchemaQueryVariables = Exact<{
9070+
schemaName: Scalars['String'];
9071+
}>;
9072+
9073+
9074+
export type GetIntrospectSchemaQuery = { __typename?: 'Query', introspectSchema: { __typename?: 'IntrospectSchemaPayload', schemaName: string, schema?: any | null } };
9075+
90699076
export type CreateShopMutationVariables = Exact<{
90709077
input: CreateShopInput;
90719078
}>;
@@ -9644,6 +9651,28 @@ export const useGetViewerQuery = <
96449651
fetcher<GetViewerQuery, GetViewerQueryVariables>(client, GetViewerDocument, variables, headers),
96459652
options
96469653
);
9654+
export const GetIntrospectSchemaDocument = `
9655+
query getIntrospectSchema($schemaName: String!) {
9656+
introspectSchema(schemaName: $schemaName) {
9657+
schemaName
9658+
schema
9659+
}
9660+
}
9661+
`;
9662+
export const useGetIntrospectSchemaQuery = <
9663+
TData = GetIntrospectSchemaQuery,
9664+
TError = unknown
9665+
>(
9666+
client: GraphQLClient,
9667+
variables: GetIntrospectSchemaQueryVariables,
9668+
options?: UseQueryOptions<GetIntrospectSchemaQuery, TError, TData>,
9669+
headers?: RequestInit['headers']
9670+
) =>
9671+
useQuery<GetIntrospectSchemaQuery, TError, TData>(
9672+
['getIntrospectSchema', variables],
9673+
fetcher<GetIntrospectSchemaQuery, GetIntrospectSchemaQueryVariables>(client, GetIntrospectSchemaDocument, variables, headers),
9674+
options
9675+
);
96479676
export const CreateShopDocument = `
96489677
mutation createShop($input: CreateShopInput!) {
96499678
createShop(input: $input) {
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { startCase } from "lodash-es";
2+
import { useMemo } from "react";
3+
4+
import { SelectOptionType } from "types/common";
5+
import { useGetIntrospectSchemaQuery } from "@graphql/generates";
6+
import { client } from "@graphql/graphql-request-client";
7+
import { FieldProperty, SchemaProperties, Type } from "types/schema";
8+
9+
export type FieldPropertySelectOption = SelectOptionType & FieldProperty
10+
11+
export const isObjectType = (fieldProperty: FieldProperty): fieldProperty is FieldProperty<Type.Object> => fieldProperty.type === Type.Object;
12+
13+
export const isArrayType = (fieldProperty: FieldProperty): fieldProperty is FieldProperty<Type.Array> => fieldProperty.type === Type.Array;
14+
15+
const normalizeSchemaProperties = ({ schemaProperties = {}, filterFn, prependFieldName }:
16+
{ schemaProperties?: SchemaProperties,
17+
filterFn?: (property: FieldPropertySelectOption) => boolean,
18+
prependFieldName?: string
19+
}) : FieldPropertySelectOption[] => {
20+
const normalizedResults = Object.entries(schemaProperties).flatMap(([field, fieldProperty]) => {
21+
if (isObjectType(fieldProperty) && Object.keys(fieldProperty.properties).length) {
22+
return normalizeSchemaProperties({ schemaProperties: fieldProperty.properties, filterFn, prependFieldName: field });
23+
}
24+
if (isArrayType(fieldProperty) && isObjectType(fieldProperty.items[0])) {
25+
return normalizeSchemaProperties({ schemaProperties: fieldProperty.items[0].properties, filterFn, prependFieldName: field });
26+
}
27+
return [{ label: startCase(prependFieldName ? `${prependFieldName} ${field}` : field), value: fieldProperty.path, ...fieldProperty }];
28+
});
29+
30+
return filterFn ? normalizedResults.filter(filterFn) : normalizedResults;
31+
};
32+
33+
export const useIntrospectSchema = ({ schemaName, filterFn, enabled }:
34+
{schemaName: string, filterFn?: (property: FieldProperty) => boolean, enabled: boolean}) => {
35+
const { data, isLoading } = useGetIntrospectSchemaQuery(client, { schemaName }, { enabled });
36+
37+
const schemaProperties = useMemo(() => {
38+
const properties = data?.introspectSchema.schema.properties;
39+
return normalizeSchemaProperties({ schemaProperties: properties, filterFn });
40+
}, [data, filterFn]);
41+
42+
return { schemaProperties, originalSchema: data?.introspectSchema.schema.properties, isLoading };
43+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
query getIntrospectSchema($schemaName: String!) {
2+
introspectSchema(schemaName: $schemaName) {
3+
schemaName
4+
schema
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { QueryClient, QueryClientProvider } from "react-query";
2+
import { cartItemProperties } from "@mocks/handlers/introspectSchemaHandlers";
3+
4+
import { renderHook, waitFor } from "@utils/testUtils";
5+
6+
import { useIntrospectSchema } from ".";
7+
8+
const client = new QueryClient();
9+
const wrapper = ({ children }: {children: JSX.Element}) => <QueryClientProvider client={client}>{children}</QueryClientProvider>;
10+
11+
describe("useIntrospectSchema", () => {
12+
it("should return normalize schema data", async () => {
13+
const { result } = renderHook(() => useIntrospectSchema({ schemaName: "CartItem", enabled: true }), { wrapper });
14+
await waitFor(() => {
15+
expect(result.current.schemaProperties).toEqual([
16+
{ label: "Product Id", value: "$.productId", ...cartItemProperties.productId },
17+
{ label: "Price Type", value: "$.priceType", ...cartItemProperties.priceType },
18+
{ label: "Product Tag Ids", value: "$.productTagIds", ...cartItemProperties.productTagIds },
19+
{ label: "Parcel Containers", value: "$.parcel.containers", ...cartItemProperties.parcel.properties.containers },
20+
{ label: "Parcel Length", value: "$.parcel.length", ...cartItemProperties.parcel.properties.length },
21+
{ label: "Attributes Label", value: "$.attributes.[0].label", ...cartItemProperties.attributes.items[0].properties.label },
22+
{ label: "Attributes Value", value: "$.attributes.[0].value", ...cartItemProperties.attributes.items[0].properties.value }
23+
]);
24+
});
25+
});
26+
});

src/mocks/handlers/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { handlers as transactionalEmailHandlers } from "./transactionalEmailHand
66
import { handlers as checkoutSettingsHandlers } from "./checkoutSettingsHandlers";
77
import { handlers as customersHandlersHandlers } from "./customersHandlers";
88
import { handlers as promotionsHandlersHandlers } from "./promotionsHandlers";
9+
import { handlers as introspectSchemaHandlers } from "./introspectSchemaHandlers";
910

1011
export const handlers = [
1112
...shippingMethodsHandlers, ...accountHandlers, ...userHandlers, ...shopSettingsHandlers, ...transactionalEmailHandlers, ...checkoutSettingsHandlers,
12-
...customersHandlersHandlers, ...promotionsHandlersHandlers
13+
...customersHandlersHandlers, ...promotionsHandlersHandlers, ...introspectSchemaHandlers
1314
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { graphql } from "msw";
2+
3+
export const mockCartItemSchema = {
4+
schema: {
5+
properties: {
6+
productId: {
7+
type: "string",
8+
path: "$.productId"
9+
},
10+
priceType: {
11+
type: "string",
12+
enum: [
13+
"full",
14+
"clearance",
15+
"sale"
16+
],
17+
path: "$.priceType"
18+
},
19+
productTagIds: {
20+
type: "array",
21+
items: [
22+
{
23+
type: "string",
24+
path: "$.productTagIds.[0]"
25+
}
26+
],
27+
additionalItems: false,
28+
path: "$.productTagIds"
29+
},
30+
parcel: {
31+
type: "object",
32+
properties: {
33+
containers: {
34+
type: "string",
35+
path: "$.parcel.containers"
36+
},
37+
length: {
38+
type: "number",
39+
path: "$.parcel.length"
40+
}
41+
},
42+
required: [],
43+
additionalProperties: false,
44+
path: "$.parcel"
45+
},
46+
attributes: {
47+
type: "array",
48+
items: [
49+
{
50+
type: "object",
51+
properties: {
52+
label: {
53+
type: "string",
54+
path: "$.attributes.[0].label"
55+
},
56+
value: {
57+
type: "string",
58+
path: "$.attributes.[0].value"
59+
}
60+
},
61+
required: [
62+
"label"
63+
],
64+
additionalProperties: false,
65+
path: "$.attributes.[0]"
66+
}
67+
],
68+
additionalItems: false,
69+
path: "$.attributes"
70+
}
71+
}
72+
},
73+
schemaName: "CartItem"
74+
};
75+
76+
export const cartItemProperties = mockCartItemSchema.schema.properties;
77+
const getIntrospectSchemaHandler = graphql.query("getIntrospectSchema", (req, res, ctx) =>
78+
res(ctx.data({ introspectSchema: mockCartItemSchema })));
79+
80+
81+
export const handlers = [
82+
getIntrospectSchemaHandler
83+
];
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,92 @@
11
import Stack from "@mui/material/Stack";
2-
import { FastField } from "formik";
2+
import { FastField, Field, getIn, useFormikContext } from "formik";
33
import { AutocompleteRenderInputParams } from "@mui/material/Autocomplete";
44
import Typography from "@mui/material/Typography";
5-
import { memo } from "react";
5+
import { SyntheticEvent, useRef } from "react";
6+
import * as Yup from "yup";
67

78
import { SelectField } from "@components/SelectField";
8-
import { CONDITION_OPERATORS, CONDITION_PROPERTIES_OPTIONS, OPERATOR_OPTIONS } from "../constants";
9+
import { CONDITION_OPERATORS, OPERATOR_OPTIONS } from "../constants";
910
import { InputWithLabel } from "@components/TextField";
1011
import { AutocompleteField } from "@components/AutocompleteField";
12+
import { FieldPropertySelectOption } from "@hooks/useIntrospectSchema";
13+
import { Promotion } from "types/promotions";
14+
import { SelectOptionType } from "types/common";
15+
import { Type } from "types/schema";
1116

1217
type ConditionFieldProps = {
1318
name: string
1419
index: number
1520
operator: string
21+
schemaProperties: FieldPropertySelectOption[]
22+
isLoadingSchema: boolean
1623
}
1724

18-
export const ConditionField = memo(({ name, index, operator }: ConditionFieldProps) =>
19-
(
25+
26+
export const ConditionField = ({ name, index, operator, isLoadingSchema, schemaProperties }: ConditionFieldProps) => {
27+
const { setFieldValue, values } = useFormikContext<Promotion>();
28+
29+
const selectedProperty = useRef(schemaProperties.find((option) => option.value === getIn(values, `${name}.path`)) || null);
30+
const availableValueOptions = useRef<string[]>([]);
31+
32+
const onPathChange = (_: SyntheticEvent, selectedOption: FieldPropertySelectOption | null) => {
33+
setFieldValue(`${name}.path`, selectedOption ? selectedOption.value : null);
34+
selectedProperty.current = selectedOption;
35+
36+
if (selectedOption?.type === Type.Boolean) {
37+
availableValueOptions.current = ["true", "false"];
38+
} else if (selectedOption?.enum) {
39+
availableValueOptions.current = selectedOption.enum;
40+
}
41+
};
42+
43+
const getOptionLabel = (optionValue: string | FieldPropertySelectOption) => {
44+
if (typeof optionValue === "string") return schemaProperties.find((option) => option.value === optionValue)?.label || "";
45+
return optionValue.label;
46+
};
47+
48+
const validateConditionValue = (selectedValues: string[]) => {
49+
let error;
50+
if (selectedProperty.current?.type === Type.Number || selectedProperty.current?.type === Type.Integer) {
51+
if (selectedValues.some((value) => !Yup.number().isValidSync(value))) {
52+
error = "Please enter number values";
53+
}
54+
if (typeof selectedProperty.current?.minimum !== "undefined"
55+
&& selectedValues.some((value) => !Yup.number().min(Number(selectedProperty.current?.minimum)).isValidSync(value))) {
56+
error = `All values must be greater than or equal to ${selectedProperty.current.minimum}`;
57+
}
58+
}
59+
if (selectedProperty.current?.format === Type.DateTime && selectedValues.some((value) => !Yup.date().isValidSync(value))) {
60+
error = "Please enter valid date time values";
61+
}
62+
return error;
63+
};
64+
65+
return (
2066
<Stack direction="row" gap={1} alignItems="center" pl={1}>
2167
<Stack flexBasis="30px">
2268
{index > 0 ? <Typography color="grey.700" variant="caption">
2369
{CONDITION_OPERATORS[operator]?.fieldPrefix?.toUpperCase()}
2470
</Typography> : null}
2571
</Stack>
2672
<Stack sx={{ flexDirection: { sm: "column", md: "row" }, gap: { sm: 0, md: 3 } }} flexGrow={1}>
27-
<FastField
28-
component={SelectField}
73+
<Field
2974
name={`${name}.path`}
30-
placeholder="Property"
31-
ariaLabel="Property"
32-
hiddenLabel
33-
options={CONDITION_PROPERTIES_OPTIONS}
34-
displayEmpty
75+
component={AutocompleteField}
76+
options={schemaProperties}
77+
loading={isLoadingSchema}
78+
isOptionEqualToValue={(option: SelectOptionType, value: string) => (value ? option.value === value : false)}
79+
onChange={onPathChange}
80+
getOptionLabel={getOptionLabel}
81+
renderInput={(params: AutocompleteRenderInputParams) => (
82+
<InputWithLabel
83+
{...params}
84+
name={`${name}.path`}
85+
placeholder="Property"
86+
ariaLabel="Property"
87+
hiddenLabel
88+
/>
89+
)}
3590
/>
3691
<FastField
3792
component={SelectField}
@@ -42,23 +97,24 @@ export const ConditionField = memo(({ name, index, operator }: ConditionFieldPro
4297
placeholder="Operator"
4398
displayEmpty
4499
/>
45-
<FastField
100+
<Field
46101
component={AutocompleteField}
102+
validate={validateConditionValue}
47103
name={`${name}.value`}
48-
freeSolo
104+
freeSolo={!availableValueOptions.current.length}
49105
multiple
50-
options={[]}
106+
options={availableValueOptions.current}
51107
autoSelect
52108
renderInput={(params: AutocompleteRenderInputParams) => (
53109
<InputWithLabel
54110
{...params}
55-
name="value"
111+
name={`${name}.value`}
56112
placeholder="Enter Values"
57113
hiddenLabel
58114
/>
59115
)}
60116
/>
61117
</Stack>
62118
</Stack>
63-
));
64-
119+
);
120+
};

0 commit comments

Comments
 (0)