Webhook query dry run (#2960)

This commit is contained in:
Bartłomiej Wiaduch 2023-01-30 13:06:54 +01:00 committed by GitHub
parent 7b8ececc3d
commit 6dbb8e4ecc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 3262 additions and 3158 deletions

File diff suppressed because it is too large Load diff

View file

@ -51,6 +51,10 @@
"context": "tabel column header",
"string": "Total"
},
"+RffqY": {
"context": "Dry run dialog object title",
"string": "Select object type to perform dry run on provided query"
},
"+RjQjs": {
"context": "return button",
"string": "Return / Replace order"
@ -390,6 +394,10 @@
"context": "selected customer channel subtitle",
"string": "Customer will be sent the gift card code via this channels email address"
},
"0u9Ng0": {
"context": "Dry run no objects found",
"string": "No objects found in the provided query"
},
"0vsMRq": {
"context": "delete custom app",
"string": "Deleting this app, you will delete all the data and webhooks regarding this app."
@ -800,6 +808,10 @@
"context": "open full-screen",
"string": "Open"
},
"48dMqY": {
"context": "Dry run trigger button",
"string": "Run"
},
"4B32Ba": {
"context": "delete page",
"string": "Are you sure you want to delete {title}?"
@ -993,6 +1005,10 @@
"context": "tabel column header",
"string": "Price"
},
"5b0Boq": {
"context": "Dry run items list default message",
"string": "Choose the object"
},
"5bJ26s": {
"string": "Successfully created page type"
},
@ -1591,6 +1607,10 @@
"context": "sale status",
"string": "Active"
},
"ApNw0L": {
"context": "Dry run objects unavailable",
"string": "The following objects are currently not available for dry run:"
},
"AqHafs": {
"context": "provided email input placeholder",
"string": "Provided email address"
@ -3651,6 +3671,10 @@
"context": "modal button url upload",
"string": "Upload URL"
},
"Q2us5X": {
"context": "Dry run icon label",
"string": "Dry run"
},
"Q47ovw": {
"context": "activate app",
"string": "Are you sure you want to activate this app? Activating will start gathering events."
@ -4767,6 +4791,10 @@
"context": "header",
"string": "Edit Authorization Field"
},
"Xz/sDf": {
"context": "Dry run items list item",
"string": "Item:"
},
"Y1B0PN": {
"context": "error message",
"string": "There's no payment associated with the order"
@ -4962,6 +4990,10 @@
"context": "subsection header",
"string": "Shipping Address"
},
"ZepEWY": {
"context": "Dry run item",
"string": "Item"
},
"Zg0dRo": {
"context": "dialog description",
"string": "You have changed customer assigned to this order. What would you like to do with the shipping address?"
@ -5647,6 +5679,10 @@
"fhksPD": {
"string": "Number of Orders"
},
"fi9Qa/": {
"context": "Dry run dialog header",
"string": "Dry run"
},
"fjPWOA": {
"context": "header",
"string": "Customer Overview"
@ -7472,6 +7508,10 @@
"ubmFc8": {
"string": "Install"
},
"uccjUM": {
"context": "Dry run objects",
"string": "Objects"
},
"ud0w8h": {
"context": "number of postal code ranges",
"string": "{number} postal code ranges"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
import { MockedProvider, MockedResponse } from "@apollo/client/testing";
import { WebhookEventTypeAsyncEnum } from "@dashboard/graphql";
import { ThemeProvider } from "@saleor/macaw-ui";
import productsMocks from "@test/mocks/products";
import { render, screen } from "@testing-library/react";
import React from "react";
import DryRun from "./DryRun";
const mocks: MockedResponse[] = [...productsMocks];
jest.mock("react-intl", () => ({
useIntl: jest.fn(() => ({
formatMessage: jest.fn(x => x.defaultMessage),
})),
defineMessages: jest.fn(x => x),
}));
describe("DryRun", () => {
it("Dialog is available on the webhook page", async () => {
// Arrange
const props = {
query: "",
showDialog: true,
setShowDialog: jest.fn(),
asyncEvents: [] as WebhookEventTypeAsyncEnum[],
setResult: jest.fn(),
};
// Act
render(
<MockedProvider mocks={mocks} addTypename={false}>
<ThemeProvider>
<DryRun {...props} />
</ThemeProvider>
</MockedProvider>,
);
// Assert
expect(screen.queryByTestId("dry-run")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,171 @@
import Grid from "@dashboard/components/Grid";
import { useStyles } from "@dashboard/custom-apps/components/WebhookEvents/styles";
import {
useTriggerWebhookDryRunMutation,
WebhookEventTypeAsyncEnum,
} from "@dashboard/graphql";
import {
capitalize,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
Typography,
} from "@material-ui/core";
import {
Alert,
Button,
DialogHeader,
List,
ListBody,
ListHeader,
ListItem,
ListItemCell,
} from "@saleor/macaw-ui";
import React, { Dispatch, SetStateAction, useState } from "react";
import { useIntl } from "react-intl";
import DryRunItemsList from "../DryRunItemsList/DryRunItemsList";
import { messages } from "./messages";
import { getObjects } from "./utils";
interface DryRunProps {
query: string;
showDialog: boolean;
setShowDialog: Dispatch<SetStateAction<boolean>>;
asyncEvents: WebhookEventTypeAsyncEnum[];
setResult: Dispatch<SetStateAction<string>>;
}
export const DryRun: React.FC<DryRunProps> = ({
setResult,
showDialog,
setShowDialog,
query,
}: DryRunProps) => {
const intl = useIntl();
const classes = useStyles();
const [objectId, setObjectId] = useState<string | null>(null);
const [triggerWebhookDryRun] = useTriggerWebhookDryRunMutation();
const availableObjects = getObjects(query);
const unavailableObjects = getObjects(query, false);
const [object, setObject] = useState<string | null>(null);
const dryRun = async () => {
const { data } = await triggerWebhookDryRun({
variables: { objectId, query },
});
setResult(JSON.stringify(JSON.parse(data.webhookDryRun.payload), null, 2));
closeDialog();
};
const closeDialog = () => {
setShowDialog(false);
setObjectId(null);
setObject(null);
setShowDialog(false);
};
if (!showDialog) {
return <></>;
}
return (
<Dialog open={showDialog} fullWidth maxWidth="md" data-test-id="dry-run">
<DialogHeader onClose={closeDialog}>
{intl.formatMessage(messages.header)}
</DialogHeader>
<DialogContent style={{ overflow: "scroll" }}>
<DialogContentText>
{intl.formatMessage(messages.selectObject)}
</DialogContentText>
{!!unavailableObjects.length && (
<Alert variant="warning" close={false}>
<Typography>
{intl.formatMessage(messages.unavailableObjects)}
&nbsp;
<strong>{unavailableObjects.join(", ")}</strong>
</Typography>
</Alert>
)}
<Grid variant="uniform">
<div className={classes.objectsWrapper}>
<List gridTemplate={["1fr 50px"]}>
<ListHeader>
<ListItem className={classes.listHeader}>
<ListItemCell className={classes.listItemCell}>
{intl.formatMessage(messages.objects)}
</ListItemCell>
<ListItemCell></ListItemCell>
</ListItem>
</ListHeader>
<ListBody className={classes.listBody}>
{!availableObjects.length && (
<Typography>
{intl.formatMessage(messages.noObjects)}
</Typography>
)}
{availableObjects.map((object, idx) => (
<ListItem
key={idx}
className={classes.listItem}
onClick={() =>
setObject(object.split(" ").join("_").toUpperCase())
}
>
<ListItemCell className={classes.listItemCell}>
<strong>
{capitalize(object.replaceAll("_", " ").toLowerCase())}
</strong>
</ListItemCell>
<ListItemCell></ListItemCell>
</ListItem>
))}
</ListBody>
</List>
</div>
<div className={classes.eventsWrapper}>
{object ? (
<DryRunItemsList
setObjectId={setObjectId}
objectId={objectId}
object={object}
/>
) : (
<>
<ListHeader>
<ListItem className={classes.listHeader}>
<ListItemCell className={classes.listItemCell}>
{intl.formatMessage(messages.item)}
</ListItemCell>
</ListItem>
</ListHeader>
<ListBody className={classes.listBody}>
<Typography>
{intl.formatMessage(messages.itemsDefaultMessage)}
</Typography>
</ListBody>
</>
)}
</div>
</Grid>
</DialogContent>
<DialogActions>
<Button
color="primary"
variant="primary"
onClick={dryRun}
disabled={!availableObjects.length}
>
{intl.formatMessage(messages.run)}
</Button>
</DialogActions>
</Dialog>
);
};
DryRun.displayName = "DryRun";
export default DryRun;

View file

@ -0,0 +1,2 @@
export { default } from "./DryRun";
export * from "./DryRun";

View file

@ -0,0 +1,45 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
header: {
id: "fi9Qa/",
defaultMessage: "Dry run",
description: "Dry run dialog header",
},
selectObject: {
id: "+RffqY",
defaultMessage: "Select object type to perform dry run on provided query",
description: "Dry run dialog object title",
},
unavailableObjects: {
id: "ApNw0L",
defaultMessage:
"The following objects are currently not available for dry run:",
description: "Dry run objects unavailable",
},
objects: {
id: "uccjUM",
defaultMessage: "Objects",
description: "Dry run objects",
},
noObjects: {
id: "0u9Ng0",
defaultMessage: "No objects found in the provided query",
description: "Dry run no objects found",
},
item: {
id: "ZepEWY",
defaultMessage: "Item",
description: "Dry run item",
},
itemsDefaultMessage: {
id: "5b0Boq",
defaultMessage: "Choose the object",
description: "Dry run items list default message",
},
run: {
id: "48dMqY",
defaultMessage: "Run",
description: "Dry run trigger button",
},
});

View file

@ -0,0 +1,13 @@
import { gql } from "@apollo/client";
export const triggerWebhookDryRun = gql`
mutation TriggerWebhookDryRun($objectId: ID!, $query: String!) {
webhookDryRun(objectId: $objectId, query: $query) {
payload
errors {
field
message
}
}
}
`;

View file

@ -0,0 +1,59 @@
import { AsyncWebhookTypes } from "@dashboard/custom-apps/components/WebhookEvents";
import { InlineFragmentNode, ObjectFieldNode, parse, visit } from "graphql";
import uniq from "lodash/uniq";
import { ExcludedDocumentMap } from "../DryRunItemsList/utils";
const getEventsFromQuery = (query: string) => {
if (query.length === 0) {
return [];
}
try {
const ast = parse(query);
const events: string[] = [];
visit(ast, {
SelectionSet(node, _key, parent) {
if ((parent as ObjectFieldNode).name?.value === "event") {
const queryEvents = node.selections.map(
selection =>
(selection as InlineFragmentNode).typeCondition.name.value,
);
queryEvents.map(event => events.push(event));
}
},
});
return events;
} catch {
return [];
}
};
export const getObjects = (query: string, available = true) => {
const queryEvents = getEventsFromQuery(query);
return uniq(
queryEvents.map(event => {
const object = event.split(/(?=[A-Z])/).slice(0, -1);
if (
Object.keys(AsyncWebhookTypes)
.filter(object =>
available
? !Object.keys(ExcludedDocumentMap).includes(object.toUpperCase())
: Object.keys(ExcludedDocumentMap).includes(object.toUpperCase()),
)
.includes(object.join("_").toUpperCase())
) {
return object.join(" ");
}
return event
.split(/(?=[A-Z])/)
.slice(0, -2)
.join(" ");
}),
).filter(object => object.length > 0);
};

View file

@ -0,0 +1,39 @@
import { MockedProvider, MockedResponse } from "@apollo/client/testing";
import { ThemeProvider } from "@saleor/macaw-ui";
import { productsMocks } from "@test/mocks/products";
import { render, screen } from "@testing-library/react";
import React from "react";
import DryRunItemsList from "./DryRunItemsList";
const mocks: MockedResponse[] = [...productsMocks];
jest.mock("react-intl", () => ({
useIntl: jest.fn(() => ({
formatMessage: jest.fn(x => x.defaultMessage),
})),
defineMessages: jest.fn(x => x),
}));
describe("DryRunItemsList", () => {
it("is available on the webhook page", async () => {
// Arrange
const props = {
objectId: null,
setObjectId: jest.fn(),
object: "PRODUCT",
};
// Act
render(
<MockedProvider mocks={mocks} addTypename={false}>
<ThemeProvider>
<DryRunItemsList {...props} />
</ThemeProvider>
</MockedProvider>,
);
// Assert
expect(screen.queryByTestId("dry-run-items-list")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,106 @@
import Skeleton from "@dashboard/components/Skeleton";
import { useStyles } from "@dashboard/custom-apps/components/WebhookEvents/styles";
import { useQuery } from "@dashboard/hooks/graphql";
import { mapEdgesToItems } from "@dashboard/utils/maps";
import { Radio } from "@material-ui/core";
import {
List,
ListBody,
ListHeader,
ListItem,
ListItemCell,
useListWidths,
} from "@saleor/macaw-ui";
import camelCase from "lodash/camelCase";
import React from "react";
import { useIntl } from "react-intl";
import Avatar from "../TableCellAvatar/Avatar";
import { messages } from "./messages";
import { DocumentMap, TData, TVariables } from "./utils";
interface DryRunItemsListProps {
objectId: string;
setObjectId: React.Dispatch<any>;
object: string;
}
const DryRunItemsList = (props: DryRunItemsListProps) => {
const intl = useIntl();
const classes = useStyles();
const { checkbox } = useListWidths();
const { object, objectId, setObjectId } = props;
const objectDocument = DocumentMap[object];
const objectCollection =
objectDocument.collection ?? camelCase(`${object.toLowerCase()}s`);
const { data, loading } = useQuery<TData, TVariables>(
objectDocument.document,
{
displayLoader: true,
variables: objectDocument.variables,
},
);
return (
<List
gridTemplate={["1fr", checkbox, checkbox]}
data-test-id="dry-run-items-list"
>
<ListHeader>
<ListItem className={classes.listHeader}>
<ListItemCell className={classes.listItemCell}>
{intl.formatMessage(messages.item)}
&nbsp;
{objectDocument.collection
?.split(/(?=[A-Z])/)
.map(item => item.toLowerCase())
.join(" ")}
&nbsp;
{objectDocument.displayedAttribute}
</ListItemCell>
</ListItem>
</ListHeader>
<ListBody className={classes.listBody}>
{loading ? (
<ListItem className={classes.listItem}>
<ListItemCell className={classes.listItemCell}>
<Skeleton />
</ListItemCell>
<ListItemCell>
<Skeleton />
</ListItemCell>
<ListItemCell>
<Skeleton />
</ListItemCell>
</ListItem>
) : (
(mapEdgesToItems<any>(data[objectCollection]) || []).map(
(item, idx) => (
<ListItem
className={classes.listItem}
key={idx}
onClick={() => setObjectId(item.id)}
>
<ListItemCell className={classes.listItemCell}>
{item.name ||
item[objectDocument.displayedAttribute] ||
item.id ||
item.__typename}
</ListItemCell>
<ListItemCell>
{item.thumbnail && <Avatar thumbnail={item.thumbnail?.url} />}
</ListItemCell>
<ListItemCell>
<Radio checked={item.id === objectId} />
</ListItemCell>
</ListItem>
),
)
)}
</ListBody>
</List>
);
};
DryRunItemsList.displayName = "DryRunItemsList";
export default DryRunItemsList;

View file

@ -0,0 +1,2 @@
export { default } from "./DryRunItemsList";
export * from "./DryRunItemsList";

View file

@ -0,0 +1,9 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
item: {
id: "Xz/sDf",
defaultMessage: "Item:",
description: "Dry run items list item",
},
});

View file

@ -0,0 +1,30 @@
import { gql } from "@apollo/client";
export const checkouts = gql`
query CheckoutList($first: Int, $after: String, $last: Int, $before: String) {
checkouts(before: $before, after: $after, first: $first, last: $last) {
edges {
cursor
node {
id
created
}
}
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
}
}
`;
export const channels = gql`
query ChannelList {
channels {
id
name
}
}
`;

View file

@ -0,0 +1,262 @@
import {
AppsListDocument,
AppsListQuery,
AppsListQueryVariables,
AttributeListDocument,
AttributeListQuery,
AttributeListQueryVariables,
CategoryDetailsQuery,
CategoryDetailsQueryVariables,
ChannelListDocument,
CheckoutListDocument,
CheckoutListQuery,
CheckoutListQueryVariables,
CollectionListDocument,
CollectionListQuery,
CollectionListQueryVariables,
CustomerAddressesDocument,
CustomerAddressesQuery,
CustomerAddressesQueryVariables,
CustomerDetailsQuery,
CustomerDetailsQueryVariables,
GiftCardListDocument,
GiftCardListQuery,
GiftCardListQueryVariables,
ListCustomersDocument,
ListCustomersQuery,
ListCustomersQueryVariables,
MenuListDocument,
MenuListQuery,
MenuListQueryVariables,
OrderFulfillDataDocument,
OrderFulfillDataQuery,
OrderFulfillDataQueryVariables,
OrderListDocument,
OrderListQuery,
OrderListQueryVariables,
PageListDocument,
PageListQuery,
PageListQueryVariables,
ProductListDocument,
ProductListQuery,
ProductListQueryVariables,
ProductVariantListDocument,
ProductVariantListQuery,
ProductVariantListQueryVariables,
RootCategoriesDocument,
RootCategoriesQuery,
RootCategoriesQueryVariables,
SaleListDocument,
SaleListQuery,
SaleListQueryVariables,
ShippingZonesDocument,
StaffListDocument,
StaffListQuery,
StaffListQueryVariables,
VoucherListDocument,
VoucherListQuery,
VoucherListQueryVariables,
WarehouseListDocument,
WarehouseListQuery,
WarehouseListQueryVariables,
} from "@dashboard/graphql";
import { DocumentNode } from "graphql";
const DefaultVariables = {
first: 100,
};
export type TData =
| ProductListQuery
| OrderListQuery
| GiftCardListQuery
| CustomerAddressesQuery
| AppsListQuery
| AttributeListQuery
| CategoryDetailsQuery
| CheckoutListQuery
| GiftCardListQuery
| CollectionListQuery
| CustomerDetailsQuery
| OrderFulfillDataQuery
| ListCustomersQuery
| MenuListQuery
| OrderListQuery
| PageListQuery
| ProductListQuery
| ProductVariantListQuery
| RootCategoriesQuery
| SaleListQuery
| StaffListQuery
| VoucherListQuery
| WarehouseListQuery;
export type TVariables =
| ProductListQueryVariables
| OrderListQueryVariables
| GiftCardListQueryVariables
| CustomerAddressesQueryVariables
| AppsListQueryVariables
| AttributeListQueryVariables
| CategoryDetailsQueryVariables
| ListCustomersQueryVariables
| CheckoutListQueryVariables
| GiftCardListQueryVariables
| CollectionListQueryVariables
| CustomerDetailsQueryVariables
| OrderFulfillDataQueryVariables
| MenuListQueryVariables
| OrderListQueryVariables
| PageListQueryVariables
| ProductListQueryVariables
| ProductVariantListQueryVariables
| RootCategoriesQueryVariables
| SaleListQueryVariables
| StaffListQueryVariables
| VoucherListQueryVariables
| WarehouseListQueryVariables;
interface Document {
document: DocumentNode;
variables: TVariables;
collection?: string;
displayedAttribute?: string;
}
export const DocumentMap: Record<string, Document> = {
APP: {
document: AppsListDocument,
variables: DefaultVariables,
displayedAttribute: "name",
},
ATTRIBUTE: {
document: AttributeListDocument,
variables: DefaultVariables,
displayedAttribute: "name",
},
CATEGORY: {
document: RootCategoriesDocument,
variables: DefaultVariables,
collection: "categories",
displayedAttribute: "name",
},
GIFT_CARD: {
document: GiftCardListDocument,
variables: DefaultVariables,
displayedAttribute: "last4CodeChars",
},
CHECKOUT: {
document: CheckoutListDocument,
variables: DefaultVariables,
displayedAttribute: "id",
},
COLLECTION: {
document: CollectionListDocument,
variables: DefaultVariables,
displayedAttribute: "name",
},
CUSTOMER: {
document: ListCustomersDocument,
variables: DefaultVariables,
displayedAttribute: "email",
// TODO inverted name
},
INVOICE: {
document: OrderListDocument,
variables: DefaultVariables,
collection: "orders",
displayedAttribute: "number",
},
MENU: {
document: MenuListDocument,
variables: DefaultVariables,
displayedAttribute: "name",
},
ORDER: {
document: OrderListDocument,
variables: DefaultVariables,
displayedAttribute: "number",
},
PAGE: {
document: PageListDocument,
variables: DefaultVariables,
displayedAttribute: "title",
},
PRODUCT: {
document: ProductListDocument,
variables: {
first: 100,
hasChannel: true,
hasSelectedAttributes: true,
},
displayedAttribute: "name",
},
SALE: {
document: SaleListDocument,
variables: DefaultVariables,
displayedAttribute: "name",
},
SHIPPING_PRICE: {
document: ShippingZonesDocument,
variables: DefaultVariables,
collection: "shippingZones",
displayedAttribute: "name",
},
SHIPPING_ZONE: {
document: ShippingZonesDocument,
variables: DefaultVariables,
displayedAttribute: "name",
},
STAFF: {
document: StaffListDocument,
variables: DefaultVariables,
collection: "staffUsers",
displayedAttribute: "email",
},
VOUCHER: {
document: VoucherListDocument,
variables: DefaultVariables,
displayedAttribute: "code",
},
WAREHOUSE: {
document: WarehouseListDocument,
variables: DefaultVariables,
displayedAttribute: "name",
},
};
// Documents which require parent object or can't be handled ATM
//
export const ExcludedDocumentMap: Record<string, Document> = {
ADDRESS: {
document: CustomerAddressesDocument,
variables: {
// USER ID REQUIRED
first: 100,
},
},
// it's not a countable collection
CHANNEL: {
document: ChannelListDocument,
variables: {},
},
FULFILLMENT: {
document: OrderFulfillDataDocument,
variables: {
// ORDER ID REQUIRED
first: 100,
},
},
PRODUCT_VARIANT: {
document: ProductVariantListDocument,
variables: {
// PRODUCT ID REQUIRED
first: 100,
},
},
TRANSLATION: {
document: null,
variables: {},
},
};

View file

@ -1,7 +1,9 @@
import { WebhookEventTypeAsyncEnum } from "@dashboard/graphql";
import {
CopyIcon,
GraphiQLProvider,
GraphiQLProviderProps,
PlayIcon,
PrettifyIcon,
QueryEditor,
ToolbarButton,
@ -18,8 +20,16 @@ import {
WriteableEditorProps,
} from "@graphiql/react";
import clsx from "clsx";
import React, { ComponentType, PropsWithChildren, ReactNode } from "react";
import React, {
ComponentType,
PropsWithChildren,
ReactNode,
useState,
} from "react";
import { useIntl } from "react-intl";
import DryRun from "../DryRun";
import { messages } from "./messages";
import {
useDashboardTheme,
useEditorStyles,
@ -68,7 +78,7 @@ export function GraphiQL({
visiblePlugin,
defaultHeaders,
...props
}: GraphiQLProps) {
}: GraphiQLProps & { asyncEvents: WebhookEventTypeAsyncEnum[] }) {
// Ensure props are correct
if (typeof fetcher !== "function") {
throw new TypeError(
@ -76,6 +86,9 @@ export function GraphiQL({
);
}
const [showDialog, setShowDialog] = useState(false);
const [result, setResult] = useState("");
return (
<GraphiQLProvider
getDefaultFieldNames={getDefaultFieldNames}
@ -106,7 +119,19 @@ export function GraphiQL({
validationRules={validationRules}
variables={variables}
>
<GraphiQLInterface {...props} />
<GraphiQLInterface
{...props}
showDialog={showDialog}
setShowDialog={setShowDialog}
result={result}
/>
<DryRun
showDialog={showDialog}
setShowDialog={setShowDialog}
query={query}
setResult={setResult}
asyncEvents={props.asyncEvents}
/>
</GraphiQLProvider>
);
}
@ -127,9 +152,13 @@ export type GraphiQLInterfaceProps = WriteableEditorProps &
defaultEditorToolsVisibility?: boolean | "variables" | "headers";
isHeadersEditorEnabled?: boolean;
toolbar?: GraphiQLToolbarConfig;
showDialog?: boolean;
setShowDialog?: React.Dispatch<React.SetStateAction<boolean>>;
result?: string;
};
export function GraphiQLInterface(props: GraphiQLInterfaceProps) {
const intl = useIntl();
const editorContext = useEditorContext({ nonNull: true });
const pluginContext = usePluginContext();
@ -150,6 +179,13 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) {
isChildComponentType(child, GraphiQL.Toolbar),
) || (
<>
<ToolbarButton
onClick={() => props.setShowDialog(true)}
label={intl.formatMessage(messages.toolbarButonLabel)}
>
<PlayIcon className="graphiql-toolbar-icon" aria-hidden="true" />
</ToolbarButton>
<ToolbarButton
onClick={() => prettify()}
label="Prettify query (Shift-Ctrl-P)"
@ -266,6 +302,14 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) {
</section>
</div>
</div>
<div ref={editorResize.dragBarRef}>
<div className="graphiql-horizontal-drag-bar" />
</div>
<div ref={editorResize.secondRef}>
<div className="graphiql-response">
<pre className={classes.pre}>{props.result}</pre>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,9 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
toolbarButonLabel: {
id: "Q2us5X",
defaultMessage: "Dry run",
description: "Dry run icon label",
},
});

View file

@ -8,6 +8,9 @@ import { useEffect } from "react";
export const useStyles = makeStyles(
() => ({
pre: {
whiteSpace: "break-spaces",
},
scrollable: {
// Overrides inline styling which breaks scroll
// on doc explorer plugin

View file

@ -9,6 +9,7 @@ import { capitalize } from "@dashboard/misc";
import { Card, CardContent, Checkbox, Typography } from "@material-ui/core";
import {
List,
ListBody,
ListHeader,
ListItem,
ListItemCell,
@ -105,7 +106,7 @@ const WebhookEvents: React.FC<WebhookEventsProps> = ({
<ListItemCell></ListItemCell>
</ListItem>
</ListHeader>
<div className={classes.listItems}>
<ListBody className={classes.listBody}>
{Object.keys(EventTypes[tab]).map((object, idx) => (
<ListItem
key={idx}
@ -130,7 +131,7 @@ const WebhookEvents: React.FC<WebhookEventsProps> = ({
</ListItemCell>
</ListItem>
))}
</div>
</ListBody>
</List>
</PageTabPanel>
</div>
@ -143,6 +144,7 @@ const WebhookEvents: React.FC<WebhookEventsProps> = ({
</ListItemCell>
</ListItem>
</ListHeader>
<ListBody className={classes.listBody}>
{object &&
EventTypes[tab][object] &&
EventTypes[tab][object].map((event, idx) => (
@ -165,6 +167,7 @@ const WebhookEvents: React.FC<WebhookEventsProps> = ({
</ListItemCell>
</ListItem>
))}
</ListBody>
</List>
</div>
</Grid>
@ -177,7 +180,7 @@ export default WebhookEvents;
type Actions = string[];
const AsyncWebhookTypes: Record<string, Actions> = {
export const AsyncWebhookTypes: Record<string, Actions> = {
ADDRESS: ["CREATED", "UPDATED", "DELETED"],
APP: ["INSTALLED", "UPDATED", "DELETED"],
ATTRIBUTE: ["CREATED", "UPDATED", "DELETED"],

View file

@ -3,7 +3,6 @@ import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
eventsWrapper: {
overflow: "scroll",
padding: theme.spacing(4),
paddingLeft: 0,
},
@ -17,6 +16,10 @@ export const useStyles = makeStyles(
padding: theme.spacing(1),
minHeight: 0,
},
listBody: {
height: 300,
overflow: "scroll",
},
listItem: {
minHeight: 0,
gap: 0,
@ -24,10 +27,7 @@ export const useStyles = makeStyles(
},
listItemCell: {
paddingLeft: "0 !important",
},
listItems: {
height: 300,
overflow: "scroll",
wordBreak: "break-all",
},
checkbox: {
padding: 0,

View file

@ -5,6 +5,7 @@ import {
WebhookEventTypeSyncEnum,
} from "@dashboard/graphql";
import { Fetcher } from "@graphiql/toolkit";
import { ApolloMockedProvider } from "@test/ApolloMockedProvider";
import { render, screen } from "@testing-library/react";
import React from "react";
@ -51,7 +52,11 @@ describe("WebhookSubscriptionQuery", () => {
// const user = userEvent.setup();
// Act
render(<WebhookSubscriptionQuery {...props} />);
render(
<ApolloMockedProvider>
<WebhookSubscriptionQuery {...props} />
</ApolloMockedProvider>,
);
// Assert
expect(screen.queryByTestId("graphiql-container")).toBeInTheDocument();

View file

@ -59,6 +59,7 @@ const WebhookSubscriptionQuery: React.FC<WebhookSubscriptionQueryProps> = ({
onEditQuery={setQuery}
plugins={[explorerPlugin]}
isHeadersEditorEnabled={false}
asyncEvents={data.asyncEvents}
/>
</CardContent>
</Card>

View file

@ -1,88 +1,6 @@
import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
import { WebhookEventTypeAsyncEnum } from "@dashboard/graphql";
import { filterSelectedAsyncEvents, mapAsyncEventsToChoices } from "./utils";
describe("Custom Apps mapping events", () => {
it("should return enabled async events choices when not any event selected", () => {
// Arrange
const events: WebhookEventTypeAsyncEnum[] = [
WebhookEventTypeAsyncEnum.ANY_EVENTS,
WebhookEventTypeAsyncEnum.PAGE_CREATED,
WebhookEventTypeAsyncEnum.PRODUCT_CREATED,
];
const selectedEvents: WebhookEventTypeAsyncEnum[] = [
WebhookEventTypeAsyncEnum.PAGE_CREATED,
];
// Act
const asyncEvents = mapAsyncEventsToChoices(events, selectedEvents);
// Assert
const expectedAsyncEvents: MultiAutocompleteChoiceType[] = [
{
label: WebhookEventTypeAsyncEnum.ANY_EVENTS,
value: WebhookEventTypeAsyncEnum.ANY_EVENTS,
badge: undefined,
disabled: false,
},
{
label: WebhookEventTypeAsyncEnum.PAGE_CREATED,
value: WebhookEventTypeAsyncEnum.PAGE_CREATED,
badge: undefined,
disabled: false,
},
{
label: WebhookEventTypeAsyncEnum.PRODUCT_CREATED,
value: WebhookEventTypeAsyncEnum.PRODUCT_CREATED,
badge: undefined,
disabled: false,
},
];
expect(asyncEvents).toHaveLength(3);
expect(asyncEvents).toEqual(expectedAsyncEvents);
});
it("should return disabled async events choices when any event selected", () => {
// Arrange
const events: WebhookEventTypeAsyncEnum[] = [
WebhookEventTypeAsyncEnum.ANY_EVENTS,
WebhookEventTypeAsyncEnum.PAGE_CREATED,
WebhookEventTypeAsyncEnum.PRODUCT_CREATED,
];
const selectedEvents: WebhookEventTypeAsyncEnum[] = [
WebhookEventTypeAsyncEnum.ANY_EVENTS,
WebhookEventTypeAsyncEnum.PAGE_CREATED,
];
// Act
const asyncEvents = mapAsyncEventsToChoices(events, selectedEvents);
// Assert
const expectedAsyncEvents: MultiAutocompleteChoiceType[] = [
{
label: WebhookEventTypeAsyncEnum.ANY_EVENTS,
value: WebhookEventTypeAsyncEnum.ANY_EVENTS,
badge: undefined,
disabled: false,
},
{
label: WebhookEventTypeAsyncEnum.PAGE_CREATED,
value: WebhookEventTypeAsyncEnum.PAGE_CREATED,
badge: undefined,
disabled: true,
},
{
label: WebhookEventTypeAsyncEnum.PRODUCT_CREATED,
value: WebhookEventTypeAsyncEnum.PRODUCT_CREATED,
badge: undefined,
disabled: true,
},
];
expect(asyncEvents).toHaveLength(3);
expect(asyncEvents).toEqual(expectedAsyncEvents);
});
});
import { filterSelectedAsyncEvents } from "./utils";
describe("Custom Apps filtering events", () => {
it("should return selected async event types when not any event selected", () => {

View file

@ -1,71 +1,9 @@
import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
import PreviewPill from "@dashboard/components/PreviewPill";
import {
WebhookEventTypeAsyncEnum,
WebhookEventTypeSyncEnum,
WebhookFragment,
} from "@dashboard/graphql";
import React from "react";
import { WebhookEventTypeAsyncEnum, WebhookFragment } from "@dashboard/graphql";
export function isUnnamed(webhook: WebhookFragment | undefined): boolean {
return !webhook?.name;
}
type WebhookEventType = WebhookEventTypeSyncEnum | WebhookEventTypeAsyncEnum;
const isWebhookInPreview = (webhook: WebhookEventType) =>
(
[
WebhookEventTypeSyncEnum.CHECKOUT_CALCULATE_TAXES,
WebhookEventTypeSyncEnum.ORDER_CALCULATE_TAXES,
] as WebhookEventType[]
).includes(webhook);
const isAsyncWebhookInPreview = (webhook: WebhookEventType) =>
(
[
WebhookEventTypeAsyncEnum.GIFT_CARD_METADATA_UPDATED,
WebhookEventTypeAsyncEnum.ORDER_METADATA_UPDATED,
WebhookEventTypeAsyncEnum.CUSTOMER_METADATA_UPDATED,
WebhookEventTypeAsyncEnum.COLLECTION_METADATA_UPDATED,
WebhookEventTypeAsyncEnum.PRODUCT_METADATA_UPDATED,
WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_METADATA_UPDATED,
WebhookEventTypeAsyncEnum.CHECKOUT_METADATA_UPDATED,
WebhookEventTypeAsyncEnum.FULFILLMENT_METADATA_UPDATED,
WebhookEventTypeAsyncEnum.SHIPPING_ZONE_METADATA_UPDATED,
WebhookEventTypeAsyncEnum.TRANSACTION_ITEM_METADATA_UPDATED,
WebhookEventTypeAsyncEnum.WAREHOUSE_METADATA_UPDATED,
WebhookEventTypeAsyncEnum.VOUCHER_METADATA_UPDATED,
] as WebhookEventType[]
).includes(webhook);
export function mapSyncEventsToChoices(
events: WebhookEventTypeSyncEnum[],
): MultiAutocompleteChoiceType[] {
return events.map(event => ({
label: event,
value: event,
badge: isWebhookInPreview(event) ? <PreviewPill /> : undefined,
}));
}
export function mapAsyncEventsToChoices(
events: WebhookEventTypeAsyncEnum[],
selectedEvents: WebhookEventTypeAsyncEnum[],
): MultiAutocompleteChoiceType[] {
const isAnyAsyncEventSelected = selectedEvents.includes(
WebhookEventTypeAsyncEnum.ANY_EVENTS,
);
return events.map(event => ({
label: event,
value: event,
badge: isAsyncWebhookInPreview(event) ? <PreviewPill /> : undefined,
disabled:
event !== WebhookEventTypeAsyncEnum.ANY_EVENTS && isAnyAsyncEventSelected,
}));
}
export const filterSelectedAsyncEvents = (
asyncEvents: WebhookEventTypeAsyncEnum[],
) => {

View file

@ -297,9 +297,9 @@
"ProductVariant",
"Product",
"ProductType",
"Collection",
"Category",
"ProductMedia",
"Category",
"Collection",
"PageType"
]
}

View file

@ -5095,6 +5095,129 @@ export function useAddressValidationRulesLazyQuery(baseOptions?: ApolloReactHook
export type AddressValidationRulesQueryHookResult = ReturnType<typeof useAddressValidationRulesQuery>;
export type AddressValidationRulesLazyQueryHookResult = ReturnType<typeof useAddressValidationRulesLazyQuery>;
export type AddressValidationRulesQueryResult = Apollo.QueryResult<Types.AddressValidationRulesQuery, Types.AddressValidationRulesQueryVariables>;
export const TriggerWebhookDryRunDocument = gql`
mutation TriggerWebhookDryRun($objectId: ID!, $query: String!) {
webhookDryRun(objectId: $objectId, query: $query) {
payload
errors {
field
message
}
}
}
`;
export type TriggerWebhookDryRunMutationFn = Apollo.MutationFunction<Types.TriggerWebhookDryRunMutation, Types.TriggerWebhookDryRunMutationVariables>;
/**
* __useTriggerWebhookDryRunMutation__
*
* To run a mutation, you first call `useTriggerWebhookDryRunMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useTriggerWebhookDryRunMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [triggerWebhookDryRunMutation, { data, loading, error }] = useTriggerWebhookDryRunMutation({
* variables: {
* objectId: // value for 'objectId'
* query: // value for 'query'
* },
* });
*/
export function useTriggerWebhookDryRunMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<Types.TriggerWebhookDryRunMutation, Types.TriggerWebhookDryRunMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useMutation<Types.TriggerWebhookDryRunMutation, Types.TriggerWebhookDryRunMutationVariables>(TriggerWebhookDryRunDocument, options);
}
export type TriggerWebhookDryRunMutationHookResult = ReturnType<typeof useTriggerWebhookDryRunMutation>;
export type TriggerWebhookDryRunMutationResult = Apollo.MutationResult<Types.TriggerWebhookDryRunMutation>;
export type TriggerWebhookDryRunMutationOptions = Apollo.BaseMutationOptions<Types.TriggerWebhookDryRunMutation, Types.TriggerWebhookDryRunMutationVariables>;
export const CheckoutListDocument = gql`
query CheckoutList($first: Int, $after: String, $last: Int, $before: String) {
checkouts(before: $before, after: $after, first: $first, last: $last) {
edges {
cursor
node {
id
created
}
}
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
}
}
`;
/**
* __useCheckoutListQuery__
*
* To run a query within a React component, call `useCheckoutListQuery` and pass it any options that fit your needs.
* When your component renders, `useCheckoutListQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useCheckoutListQuery({
* variables: {
* first: // value for 'first'
* after: // value for 'after'
* last: // value for 'last'
* before: // value for 'before'
* },
* });
*/
export function useCheckoutListQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<Types.CheckoutListQuery, Types.CheckoutListQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useQuery<Types.CheckoutListQuery, Types.CheckoutListQueryVariables>(CheckoutListDocument, options);
}
export function useCheckoutListLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<Types.CheckoutListQuery, Types.CheckoutListQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useLazyQuery<Types.CheckoutListQuery, Types.CheckoutListQueryVariables>(CheckoutListDocument, options);
}
export type CheckoutListQueryHookResult = ReturnType<typeof useCheckoutListQuery>;
export type CheckoutListLazyQueryHookResult = ReturnType<typeof useCheckoutListLazyQuery>;
export type CheckoutListQueryResult = Apollo.QueryResult<Types.CheckoutListQuery, Types.CheckoutListQueryVariables>;
export const ChannelListDocument = gql`
query ChannelList {
channels {
id
name
}
}
`;
/**
* __useChannelListQuery__
*
* To run a query within a React component, call `useChannelListQuery` and pass it any options that fit your needs.
* When your component renders, `useChannelListQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useChannelListQuery({
* variables: {
* },
* });
*/
export function useChannelListQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<Types.ChannelListQuery, Types.ChannelListQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useQuery<Types.ChannelListQuery, Types.ChannelListQueryVariables>(ChannelListDocument, options);
}
export function useChannelListLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<Types.ChannelListQuery, Types.ChannelListQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useLazyQuery<Types.ChannelListQuery, Types.ChannelListQueryVariables>(ChannelListDocument, options);
}
export type ChannelListQueryHookResult = ReturnType<typeof useChannelListQuery>;
export type ChannelListLazyQueryHookResult = ReturnType<typeof useChannelListLazyQuery>;
export type ChannelListQueryResult = Apollo.QueryResult<Types.ChannelListQuery, Types.ChannelListQueryVariables>;
export const CheckIfOrderExistsDocument = gql`
query CheckIfOrderExists($id: ID!) {
order(id: $id) {

File diff suppressed because one or more lines are too long

View file

@ -329,6 +329,12 @@ export type AttributeCreateInput = {
storefrontSearchPosition?: InputMaybe<Scalars['Int']>;
/** Whether the attribute can be displayed in the admin product list. */
availableInGrid?: InputMaybe<Scalars['Boolean']>;
/**
* External ID of this attribute.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
};
/** An enumeration. */
@ -459,6 +465,12 @@ export type AttributeUpdateInput = {
storefrontSearchPosition?: InputMaybe<Scalars['Int']>;
/** Whether the attribute can be displayed in the admin product list. */
availableInGrid?: InputMaybe<Scalars['Boolean']>;
/**
* External ID of this product.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
};
export type AttributeValueCreateInput = {
@ -482,6 +494,12 @@ export type AttributeValueCreateInput = {
fileUrl?: InputMaybe<Scalars['String']>;
/** File content type. */
contentType?: InputMaybe<Scalars['String']>;
/**
* External ID of this attribute value.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
/** Name of a value displayed in the interface. */
name: Scalars['String'];
};
@ -494,8 +512,26 @@ export type AttributeValueFilterInput = {
export type AttributeValueInput = {
/** ID of the selected attribute. */
id?: InputMaybe<Scalars['ID']>;
/** The value or slug of an attribute to resolve. If the passed value is non-existent, it will be created. */
/** The value or slug of an attribute to resolve. If the passed value is non-existent, it will be created. This field will be removed in Saleor 4.0. */
values?: InputMaybe<Array<Scalars['String']>>;
/**
* Attribute value ID.
*
* Added in Saleor 3.9.
*/
dropdown?: InputMaybe<AttributeValueSelectableTypeInput>;
/**
* List of attribute value IDs.
*
* Added in Saleor 3.9.
*/
multiselect?: InputMaybe<Array<AttributeValueSelectableTypeInput>>;
/**
* Numeric value of an attribute.
*
* Added in Saleor 3.9.
*/
numeric?: InputMaybe<Scalars['String']>;
/** URL of the file attribute. Every time, a new value is created. */
file?: InputMaybe<Scalars['String']>;
/** File content type. */
@ -514,6 +550,18 @@ export type AttributeValueInput = {
dateTime?: InputMaybe<Scalars['DateTime']>;
};
/**
* Represents attribute value. If no ID provided, value will be resolved.
*
* Added in Saleor 3.9.
*/
export type AttributeValueSelectableTypeInput = {
/** ID of an attribute value. */
id?: InputMaybe<Scalars['ID']>;
/** The value or slug of an attribute to resolve. If the passed value is non-existent, it will be created. */
value?: InputMaybe<Scalars['String']>;
};
export type AttributeValueTranslationInput = {
name?: InputMaybe<Scalars['String']>;
/**
@ -547,6 +595,12 @@ export type AttributeValueUpdateInput = {
fileUrl?: InputMaybe<Scalars['String']>;
/** File content type. */
contentType?: InputMaybe<Scalars['String']>;
/**
* External ID of this attribute value.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
/** Name of a value displayed in the interface. */
name?: InputMaybe<Scalars['String']>;
};
@ -1388,6 +1442,12 @@ export type CustomerInput = {
note?: InputMaybe<Scalars['String']>;
/** User language code. */
languageCode?: InputMaybe<LanguageCodeEnum>;
/**
* External ID of the customer.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
};
export type DateRangeInput = {
@ -1511,6 +1571,12 @@ export type DraftOrderCreateInput = {
channelId?: InputMaybe<Scalars['ID']>;
/** URL of a view where users should be redirected to see the order details. URL in RFC 1808 format. */
redirectUrl?: InputMaybe<Scalars['String']>;
/**
* External ID of this order.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
/** Variant line input consisting of variant ID and quantity of products. */
lines?: InputMaybe<Array<OrderLineCreateInput>>;
};
@ -1536,6 +1602,12 @@ export type DraftOrderInput = {
channelId?: InputMaybe<Scalars['ID']>;
/** URL of a view where users should be redirected to see the order details. URL in RFC 1808 format. */
redirectUrl?: InputMaybe<Scalars['String']>;
/**
* External ID of this order.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
};
export enum EventDeliveryAttemptSortField {
@ -2785,6 +2857,18 @@ export enum MeasurementUnitsEnum {
TONNE = 'TONNE'
}
export enum MediaChoicesSortField {
/** Sort media by ID. */
ID = 'ID'
}
export type MediaSortingInput = {
/** Specifies the direction in which to sort products. */
direction: OrderDirection;
/** Sort media by the selected field. */
field: MediaChoicesSortField;
};
export type MenuCreateInput = {
/** Name of the menu. */
name: Scalars['String'];
@ -3351,6 +3435,12 @@ export type OrderUpdateInput = {
userEmail?: InputMaybe<Scalars['String']>;
/** Shipping address of the customer. */
shippingAddress?: InputMaybe<AddressInput>;
/**
* External ID of this order.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
};
export type OrderUpdateShippingInput = {
@ -3883,6 +3973,12 @@ export type ProductCreateInput = {
* Added in Saleor 3.8.
*/
privateMetadata?: InputMaybe<Array<MetadataInput>>;
/**
* External ID of this product.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
/** ID of the type that product belongs to. */
productType: Scalars['ID'];
};
@ -4028,6 +4124,12 @@ export type ProductInput = {
* Added in Saleor 3.8.
*/
privateMetadata?: InputMaybe<Array<MetadataInput>>;
/**
* External ID of this product.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
};
export type ProductMediaCreateInput = {
@ -4244,6 +4346,12 @@ export type ProductVariantBulkCreateInput = {
* Added in Saleor 3.8.
*/
privateMetadata?: InputMaybe<Array<MetadataInput>>;
/**
* External ID of this product variant.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
/** Stocks of a product available for sale. */
stocks?: InputMaybe<Array<StockInput>>;
/** List of prices assigned to channels. */
@ -4306,6 +4414,12 @@ export type ProductVariantCreateInput = {
* Added in Saleor 3.8.
*/
privateMetadata?: InputMaybe<Array<MetadataInput>>;
/**
* External ID of this product variant.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
/** Product ID of which type is the variant. */
product: Scalars['ID'];
/** Stocks of a product available for sale. */
@ -4359,6 +4473,12 @@ export type ProductVariantInput = {
* Added in Saleor 3.8.
*/
privateMetadata?: InputMaybe<Array<MetadataInput>>;
/**
* External ID of this product variant.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
};
export enum ProductVariantSortField {
@ -5142,6 +5262,12 @@ export type UserCreateInput = {
note?: InputMaybe<Scalars['String']>;
/** User language code. */
languageCode?: InputMaybe<LanguageCodeEnum>;
/**
* External ID of the customer.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
/** URL of a view where users should be redirected to set the password. URL in RFC 1808 format. */
redirectUrl?: InputMaybe<Scalars['String']>;
/** Slug of a channel which will be used for notify user. Optional when only one channel exists. */
@ -5320,6 +5446,12 @@ export type WarehouseCreateInput = {
slug?: InputMaybe<Scalars['String']>;
/** The email address of the warehouse. */
email?: InputMaybe<Scalars['String']>;
/**
* External ID of the warehouse.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
/** Warehouse name. */
name: Scalars['String'];
/** Address of the warehouse. */
@ -5368,6 +5500,12 @@ export type WarehouseUpdateInput = {
slug?: InputMaybe<Scalars['String']>;
/** The email address of the warehouse. */
email?: InputMaybe<Scalars['String']>;
/**
* External ID of the warehouse.
*
* Added in Saleor 3.10.
*/
externalReference?: InputMaybe<Scalars['String']>;
/** Warehouse name. */
name?: InputMaybe<Scalars['String']>;
/** Address of the warehouse. */
@ -5425,13 +5563,22 @@ export type WebhookCreateInput = {
query?: InputMaybe<Scalars['String']>;
};
/** An enumeration. */
export enum WebhookDryRunErrorCode {
GRAPHQL_ERROR = 'GRAPHQL_ERROR',
NOT_FOUND = 'NOT_FOUND',
INVALID_ID = 'INVALID_ID',
MISSING_PERMISSION = 'MISSING_PERMISSION'
}
/** An enumeration. */
export enum WebhookErrorCode {
GRAPHQL_ERROR = 'GRAPHQL_ERROR',
INVALID = 'INVALID',
NOT_FOUND = 'NOT_FOUND',
REQUIRED = 'REQUIRED',
UNIQUE = 'UNIQUE'
UNIQUE = 'UNIQUE',
DELETE_FAILED = 'DELETE_FAILED'
}
/** Enum determining type of webhook. */
@ -6200,6 +6347,15 @@ export enum WebhookSampleEventTypeEnum {
OBSERVABILITY = 'OBSERVABILITY'
}
/** An enumeration. */
export enum WebhookTriggerErrorCode {
GRAPHQL_ERROR = 'GRAPHQL_ERROR',
NOT_FOUND = 'NOT_FOUND',
INVALID_ID = 'INVALID_ID',
MISSING_PERMISSION = 'MISSING_PERMISSION',
MISSING_QUERY = 'MISSING_QUERY'
}
export type WebhookUpdateInput = {
/** The new name of the webhook. */
name?: InputMaybe<Scalars['String']>;
@ -6677,6 +6833,29 @@ export type AddressValidationRulesQueryVariables = Exact<{
export type AddressValidationRulesQuery = { __typename: 'Query', addressValidationRules: { __typename: 'AddressValidationData', allowedFields: Array<string>, countryAreaChoices: Array<{ __typename: 'ChoiceValue', raw: string | null, verbose: string | null }> } | null };
export type TriggerWebhookDryRunMutationVariables = Exact<{
objectId: Scalars['ID'];
query: Scalars['String'];
}>;
export type TriggerWebhookDryRunMutation = { __typename: 'Mutation', webhookDryRun: { __typename: 'WebhookDryRun', payload: any | null, errors: Array<{ __typename: 'WebhookDryRunError', field: string | null, message: string | null }> } | null };
export type CheckoutListQueryVariables = Exact<{
first?: InputMaybe<Scalars['Int']>;
after?: InputMaybe<Scalars['String']>;
last?: InputMaybe<Scalars['Int']>;
before?: InputMaybe<Scalars['String']>;
}>;
export type CheckoutListQuery = { __typename: 'Query', checkouts: { __typename: 'CheckoutCountableConnection', edges: Array<{ __typename: 'CheckoutCountableEdge', cursor: string, node: { __typename: 'Checkout', id: string, created: any } }>, pageInfo: { __typename: 'PageInfo', hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string | null, endCursor: string | null } } | null };
export type ChannelListQueryVariables = Exact<{ [key: string]: never; }>;
export type ChannelListQuery = { __typename: 'Query', channels: Array<{ __typename: 'Channel', id: string, name: string }> | null };
export type CheckIfOrderExistsQueryVariables = Exact<{
id: Scalars['ID'];
}>;

View file

@ -1,4 +1,5 @@
export * from "./address";
export * from "./apps";
export * from "./pageTypes";
export * from "./products";
export * from "./warehouses";

277
testUtils/mocks/products.ts Normal file
View file

@ -0,0 +1,277 @@
import { MockedResponse } from "@apollo/client/testing";
import { productListQuery } from "@dashboard/products/queries";
export const productsMocks: MockedResponse[] = [
{
request: {
query: productListQuery,
variables: {
first: 3,
hasChannel: true,
hasSelectedAttributes: true,
},
},
result: {
data: {
products: {
edges: [
{
node: {
id: "UHJvZHVjdDo3Mg==",
name: "Apple Juice",
thumbnail: {
url: "https://wb-t-3-4-6.staging.saleor.cloud/media/thumbnails/products/saleordemoproduct_fd_juice_06_XO6p2Xu_thumbnail_256.png",
__typename: "Image",
},
productType: {
id: "UHJvZHVjdFR5cGU6OQ==",
name: "Juice",
hasVariants: true,
__typename: "ProductType",
},
channelListings: [
{
isPublished: true,
publicationDate: "2020-01-01",
isAvailableForPurchase: true,
availableForPurchase: "2020-08-31",
visibleInListings: true,
channel: {
id: "Q2hhbm5lbDox",
name: "Default channel",
currencyCode: "USD",
__typename: "Channel",
},
__typename: "ProductChannelListing",
pricing: {
priceRange: {
start: {
net: {
amount: 5.0,
currency: "USD",
__typename: "Money",
},
__typename: "TaxedMoney",
},
stop: {
net: {
amount: 7.0,
currency: "USD",
__typename: "Money",
},
__typename: "TaxedMoney",
},
__typename: "TaxedMoneyRange",
},
__typename: "ProductPricingInfo",
},
},
],
__typename: "Product",
updatedAt: "2021-03-10T12:31:34.521213+00:00",
attributes: [
{
attribute: {
id: "QXR0cmlidXRlOjE2",
__typename: "Attribute",
},
values: [
{
id: "QXR0cmlidXRlVmFsdWU6Mw==",
name: "Apple",
slug: "apple",
file: null,
reference: null,
boolean: null,
date: null,
dateTime: null,
value: "",
__typename: "AttributeValue",
},
],
__typename: "SelectedAttribute",
},
],
},
__typename: "ProductCountableEdge",
},
{
node: {
id: "UHJvZHVjdDo3NA==",
name: "Banana Juice",
thumbnail: {
url: "https://wb-t-3-4-6.staging.saleor.cloud/media/thumbnails/products/saleordemoproduct_fd_juice_01_thumbnail_256.png",
__typename: "Image",
},
productType: {
id: "UHJvZHVjdFR5cGU6OQ==",
name: "Juice",
hasVariants: true,
__typename: "ProductType",
},
channelListings: [
{
isPublished: true,
publicationDate: "2020-01-01",
isAvailableForPurchase: true,
availableForPurchase: "2020-08-31",
visibleInListings: true,
channel: {
id: "Q2hhbm5lbDox",
name: "Default channel",
currencyCode: "USD",
__typename: "Channel",
},
__typename: "ProductChannelListing",
pricing: {
priceRange: {
start: {
net: {
amount: 5.0,
currency: "USD",
__typename: "Money",
},
__typename: "TaxedMoney",
},
stop: {
net: {
amount: 7.0,
currency: "USD",
__typename: "Money",
},
__typename: "TaxedMoney",
},
__typename: "TaxedMoneyRange",
},
__typename: "ProductPricingInfo",
},
},
],
__typename: "Product",
updatedAt: "2021-03-10T12:31:34.897799+00:00",
attributes: [
{
attribute: {
id: "QXR0cmlidXRlOjE2",
__typename: "Attribute",
},
values: [
{
id: "QXR0cmlidXRlVmFsdWU6NTA=",
name: "Banana",
slug: "banana",
file: null,
reference: null,
boolean: null,
date: null,
dateTime: null,
value: "",
__typename: "AttributeValue",
},
],
__typename: "SelectedAttribute",
},
],
},
__typename: "ProductCountableEdge",
},
{
node: {
id: "UHJvZHVjdDoxMjI=",
name: "Bathroom Songs",
thumbnail: {
url: "https://wb-t-3-4-6.staging.saleor.cloud/media/thumbnails/products/saleor-digital-03_4_thumbnail_256.png",
__typename: "Image",
},
productType: {
id: "UHJvZHVjdFR5cGU6MTU=",
name: "Audiobook",
hasVariants: true,
__typename: "ProductType",
},
channelListings: [
{
isPublished: true,
publicationDate: "2020-01-01",
isAvailableForPurchase: true,
availableForPurchase: "2020-08-31",
visibleInListings: false,
channel: {
id: "Q2hhbm5lbDox",
name: "Default channel",
currencyCode: "USD",
__typename: "Channel",
},
__typename: "ProductChannelListing",
pricing: {
priceRange: {
start: {
net: {
amount: 6.0,
currency: "USD",
__typename: "Money",
},
__typename: "TaxedMoney",
},
stop: {
net: {
amount: 6.0,
currency: "USD",
__typename: "Money",
},
__typename: "TaxedMoney",
},
__typename: "TaxedMoneyRange",
},
__typename: "ProductPricingInfo",
},
},
],
__typename: "Product",
updatedAt: "2021-03-10T12:31:40.785454+00:00",
attributes: [
{
attribute: {
id: "QXR0cmlidXRlOjI2",
__typename: "Attribute",
},
values: [
{
id: "QXR0cmlidXRlVmFsdWU6ODY=",
name: "Digital Audio",
slug: "digital-audio",
file: null,
reference: null,
boolean: null,
date: null,
dateTime: null,
value: "",
__typename: "AttributeValue",
},
],
__typename: "SelectedAttribute",
},
],
},
__typename: "ProductCountableEdge",
},
],
pageInfo: {
hasPreviousPage: false,
hasNextPage: true,
startCursor: "WyJhcHBsZS1qdWljZSJd",
endCursor: "WyJiYXRocm9vbS1zb25ncyJd",
__typename: "PageInfo",
},
totalCount: 41,
__typename: "ProductCountableConnection",
},
},
extensions: {
cost: { requestedQueryCost: 13, maximumAvailable: 50000 },
},
},
},
];
export default productsMocks;