Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/trunk' into feat/promotions-mile…
Browse files Browse the repository at this point in the history
…stone-2

Signed-off-by: tedraykov <[email protected]>
  • Loading branch information
tedraykov committed Aug 14, 2023
2 parents 00e099f + 494b86f commit 0b63d06
Show file tree
Hide file tree
Showing 12 changed files with 327 additions and 35 deletions.
7 changes: 4 additions & 3 deletions src/components/TextField/InputWithLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ export const InputWithLabel = forwardRef((
InputProps,
inputProps,
placeholder,
name
}: TextFieldProps,
name,
ariaLabel
}: TextFieldProps & {ariaLabel?: string},
ref
) => {
const labelId = useRef(uniqueId("label")).current;
Expand All @@ -45,7 +46,7 @@ export const InputWithLabel = forwardRef((
error={error}
disabled={disabled}
ref={ref}
inputProps={{ ...inputProps, "aria-labelledby": labelId }}
inputProps={{ ...inputProps, "aria-labelledby": labelId, "aria-label": ariaLabel }}
placeholder={placeholder}
name={name}
{...InputProps}
Expand Down
29 changes: 29 additions & 0 deletions src/graphql/generates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9066,6 +9066,13 @@ export type GetViewerQueryVariables = Exact<{ [key: string]: never; }>;

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 };

export type GetIntrospectSchemaQueryVariables = Exact<{
schemaName: Scalars['String'];
}>;


export type GetIntrospectSchemaQuery = { __typename?: 'Query', introspectSchema: { __typename?: 'IntrospectSchemaPayload', schemaName: string, schema?: any | null } };

export type CreateShopMutationVariables = Exact<{
input: CreateShopInput;
}>;
Expand Down Expand Up @@ -9644,6 +9651,28 @@ export const useGetViewerQuery = <
fetcher<GetViewerQuery, GetViewerQueryVariables>(client, GetViewerDocument, variables, headers),
options
);
export const GetIntrospectSchemaDocument = `
query getIntrospectSchema($schemaName: String!) {
introspectSchema(schemaName: $schemaName) {
schemaName
schema
}
}
`;
export const useGetIntrospectSchemaQuery = <
TData = GetIntrospectSchemaQuery,
TError = unknown
>(
client: GraphQLClient,
variables: GetIntrospectSchemaQueryVariables,
options?: UseQueryOptions<GetIntrospectSchemaQuery, TError, TData>,
headers?: RequestInit['headers']
) =>
useQuery<GetIntrospectSchemaQuery, TError, TData>(
['getIntrospectSchema', variables],
fetcher<GetIntrospectSchemaQuery, GetIntrospectSchemaQueryVariables>(client, GetIntrospectSchemaDocument, variables, headers),
options
);
export const CreateShopDocument = `
mutation createShop($input: CreateShopInput!) {
createShop(input: $input) {
Expand Down
43 changes: 43 additions & 0 deletions src/hooks/useIntrospectSchema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { startCase } from "lodash-es";
import { useMemo } from "react";

import { SelectOptionType } from "types/common";
import { useGetIntrospectSchemaQuery } from "@graphql/generates";
import { client } from "@graphql/graphql-request-client";
import { FieldProperty, SchemaProperties, Type } from "types/schema";

export type FieldPropertySelectOption = SelectOptionType & FieldProperty

export const isObjectType = (fieldProperty: FieldProperty): fieldProperty is FieldProperty<Type.Object> => fieldProperty.type === Type.Object;

export const isArrayType = (fieldProperty: FieldProperty): fieldProperty is FieldProperty<Type.Array> => fieldProperty.type === Type.Array;

const normalizeSchemaProperties = ({ schemaProperties = {}, filterFn, prependFieldName }:
{ schemaProperties?: SchemaProperties,
filterFn?: (property: FieldPropertySelectOption) => boolean,
prependFieldName?: string
}) : FieldPropertySelectOption[] => {
const normalizedResults = Object.entries(schemaProperties).flatMap(([field, fieldProperty]) => {
if (isObjectType(fieldProperty) && Object.keys(fieldProperty.properties).length) {
return normalizeSchemaProperties({ schemaProperties: fieldProperty.properties, filterFn, prependFieldName: field });
}
if (isArrayType(fieldProperty) && isObjectType(fieldProperty.items[0])) {
return normalizeSchemaProperties({ schemaProperties: fieldProperty.items[0].properties, filterFn, prependFieldName: field });
}
return [{ label: startCase(prependFieldName ? `${prependFieldName} ${field}` : field), value: fieldProperty.path, ...fieldProperty }];
});

return filterFn ? normalizedResults.filter(filterFn) : normalizedResults;
};

export const useIntrospectSchema = ({ schemaName, filterFn, enabled }:
{schemaName: string, filterFn?: (property: FieldProperty) => boolean, enabled: boolean}) => {
const { data, isLoading } = useGetIntrospectSchemaQuery(client, { schemaName }, { enabled });

const schemaProperties = useMemo(() => {
const properties = data?.introspectSchema.schema.properties;
return normalizeSchemaProperties({ schemaProperties: properties, filterFn });
}, [data, filterFn]);

return { schemaProperties, originalSchema: data?.introspectSchema.schema.properties, isLoading };
};
6 changes: 6 additions & 0 deletions src/hooks/useIntrospectSchema/introspectSchema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
query getIntrospectSchema($schemaName: String!) {
introspectSchema(schemaName: $schemaName) {
schemaName
schema
}
}
26 changes: 26 additions & 0 deletions src/hooks/useIntrospectSchema/useIntrospectSchema.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { QueryClient, QueryClientProvider } from "react-query";
import { cartItemProperties } from "@mocks/handlers/introspectSchemaHandlers";

import { renderHook, waitFor } from "@utils/testUtils";

import { useIntrospectSchema } from ".";

const client = new QueryClient();
const wrapper = ({ children }: {children: JSX.Element}) => <QueryClientProvider client={client}>{children}</QueryClientProvider>;

describe("useIntrospectSchema", () => {
it("should return normalize schema data", async () => {
const { result } = renderHook(() => useIntrospectSchema({ schemaName: "CartItem", enabled: true }), { wrapper });
await waitFor(() => {
expect(result.current.schemaProperties).toEqual([
{ label: "Product Id", value: "$.productId", ...cartItemProperties.productId },
{ label: "Price Type", value: "$.priceType", ...cartItemProperties.priceType },
{ label: "Product Tag Ids", value: "$.productTagIds", ...cartItemProperties.productTagIds },
{ label: "Parcel Containers", value: "$.parcel.containers", ...cartItemProperties.parcel.properties.containers },
{ label: "Parcel Length", value: "$.parcel.length", ...cartItemProperties.parcel.properties.length },
{ label: "Attributes Label", value: "$.attributes.[0].label", ...cartItemProperties.attributes.items[0].properties.label },
{ label: "Attributes Value", value: "$.attributes.[0].value", ...cartItemProperties.attributes.items[0].properties.value }
]);
});
});
});
3 changes: 2 additions & 1 deletion src/mocks/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { handlers as transactionalEmailHandlers } from "./transactionalEmailHand
import { handlers as checkoutSettingsHandlers } from "./checkoutSettingsHandlers";
import { handlers as customersHandlersHandlers } from "./customersHandlers";
import { handlers as promotionsHandlersHandlers } from "./promotionsHandlers";
import { handlers as introspectSchemaHandlers } from "./introspectSchemaHandlers";

export const handlers = [
...shippingMethodsHandlers, ...accountHandlers, ...userHandlers, ...shopSettingsHandlers, ...transactionalEmailHandlers, ...checkoutSettingsHandlers,
...customersHandlersHandlers, ...promotionsHandlersHandlers
...customersHandlersHandlers, ...promotionsHandlersHandlers, ...introspectSchemaHandlers
];
83 changes: 83 additions & 0 deletions src/mocks/handlers/introspectSchemaHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { graphql } from "msw";

export const mockCartItemSchema = {
schema: {
properties: {
productId: {
type: "string",
path: "$.productId"
},
priceType: {
type: "string",
enum: [
"full",
"clearance",
"sale"
],
path: "$.priceType"
},
productTagIds: {
type: "array",
items: [
{
type: "string",
path: "$.productTagIds.[0]"
}
],
additionalItems: false,
path: "$.productTagIds"
},
parcel: {
type: "object",
properties: {
containers: {
type: "string",
path: "$.parcel.containers"
},
length: {
type: "number",
path: "$.parcel.length"
}
},
required: [],
additionalProperties: false,
path: "$.parcel"
},
attributes: {
type: "array",
items: [
{
type: "object",
properties: {
label: {
type: "string",
path: "$.attributes.[0].label"
},
value: {
type: "string",
path: "$.attributes.[0].value"
}
},
required: [
"label"
],
additionalProperties: false,
path: "$.attributes.[0]"
}
],
additionalItems: false,
path: "$.attributes"
}
}
},
schemaName: "CartItem"
};

export const cartItemProperties = mockCartItemSchema.schema.properties;
const getIntrospectSchemaHandler = graphql.query("getIntrospectSchema", (req, res, ctx) =>
res(ctx.data({ introspectSchema: mockCartItemSchema })));


export const handlers = [
getIntrospectSchemaHandler
];
92 changes: 74 additions & 18 deletions src/pages/Promotions/Details/ConditionField.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,92 @@
import Stack from "@mui/material/Stack";
import { FastField } from "formik";
import { FastField, Field, getIn, useFormikContext } from "formik";
import { AutocompleteRenderInputParams } from "@mui/material/Autocomplete";
import Typography from "@mui/material/Typography";
import { memo } from "react";
import { SyntheticEvent, useRef } from "react";
import * as Yup from "yup";

import { SelectField } from "@components/SelectField";
import { CONDITION_OPERATORS, CONDITION_PROPERTIES_OPTIONS, OPERATOR_OPTIONS } from "../constants";
import { CONDITION_OPERATORS, OPERATOR_OPTIONS } from "../constants";
import { InputWithLabel } from "@components/TextField";
import { AutocompleteField } from "@components/AutocompleteField";
import { FieldPropertySelectOption } from "@hooks/useIntrospectSchema";
import { Promotion } from "types/promotions";
import { SelectOptionType } from "types/common";
import { Type } from "types/schema";

type ConditionFieldProps = {
name: string
index: number
operator: string
schemaProperties: FieldPropertySelectOption[]
isLoadingSchema: boolean
}

export const ConditionField = memo(({ name, index, operator }: ConditionFieldProps) =>
(

export const ConditionField = ({ name, index, operator, isLoadingSchema, schemaProperties }: ConditionFieldProps) => {
const { setFieldValue, values } = useFormikContext<Promotion>();

const selectedProperty = useRef(schemaProperties.find((option) => option.value === getIn(values, `${name}.path`)) || null);
const availableValueOptions = useRef<string[]>([]);

const onPathChange = (_: SyntheticEvent, selectedOption: FieldPropertySelectOption | null) => {
setFieldValue(`${name}.path`, selectedOption ? selectedOption.value : null);
selectedProperty.current = selectedOption;

if (selectedOption?.type === Type.Boolean) {
availableValueOptions.current = ["true", "false"];
} else if (selectedOption?.enum) {
availableValueOptions.current = selectedOption.enum;
}
};

const getOptionLabel = (optionValue: string | FieldPropertySelectOption) => {
if (typeof optionValue === "string") return schemaProperties.find((option) => option.value === optionValue)?.label || "";
return optionValue.label;
};

const validateConditionValue = (selectedValues: string[]) => {
let error;
if (selectedProperty.current?.type === Type.Number || selectedProperty.current?.type === Type.Integer) {
if (selectedValues.some((value) => !Yup.number().isValidSync(value))) {
error = "Please enter number values";
}
if (typeof selectedProperty.current?.minimum !== "undefined"
&& selectedValues.some((value) => !Yup.number().min(Number(selectedProperty.current?.minimum)).isValidSync(value))) {
error = `All values must be greater than or equal to ${selectedProperty.current.minimum}`;
}
}
if (selectedProperty.current?.format === Type.DateTime && selectedValues.some((value) => !Yup.date().isValidSync(value))) {
error = "Please enter valid date time values";
}
return error;
};

return (
<Stack direction="row" gap={1} alignItems="center" pl={1}>
<Stack flexBasis="30px">
{index > 0 ? <Typography color="grey.700" variant="caption">
{CONDITION_OPERATORS[operator]?.fieldPrefix?.toUpperCase()}
</Typography> : null}
</Stack>
<Stack sx={{ flexDirection: { sm: "column", md: "row" }, gap: { sm: 0, md: 3 } }} flexGrow={1}>
<FastField
component={SelectField}
<Field
name={`${name}.path`}
placeholder="Property"
ariaLabel="Property"
hiddenLabel
options={CONDITION_PROPERTIES_OPTIONS}
displayEmpty
component={AutocompleteField}
options={schemaProperties}
loading={isLoadingSchema}
isOptionEqualToValue={(option: SelectOptionType, value: string) => (value ? option.value === value : false)}
onChange={onPathChange}
getOptionLabel={getOptionLabel}
renderInput={(params: AutocompleteRenderInputParams) => (
<InputWithLabel
{...params}
name={`${name}.path`}
placeholder="Property"
ariaLabel="Property"
hiddenLabel
/>
)}
/>
<FastField
component={SelectField}
Expand All @@ -42,23 +97,24 @@ export const ConditionField = memo(({ name, index, operator }: ConditionFieldPro
placeholder="Operator"
displayEmpty
/>
<FastField
<Field
component={AutocompleteField}
validate={validateConditionValue}
name={`${name}.value`}
freeSolo
freeSolo={!availableValueOptions.current.length}
multiple
options={[]}
options={availableValueOptions.current}
autoSelect
renderInput={(params: AutocompleteRenderInputParams) => (
<InputWithLabel
{...params}
name="value"
name={`${name}.value`}
placeholder="Enter Values"
hiddenLabel
/>
)}
/>
</Stack>
</Stack>
));

);
};
Loading

0 comments on commit 0b63d06

Please sign in to comment.