Add GraphiQL for defining subscription queries for webhooks (#2885)
Co-authored-by: Bartłomiej Wiaduch <tukan2can@gmail.com> Co-authored-by: timur <timuric@gmail.com> Co-authored-by: Michał Droń <dron.official@yahoo.com>
This commit is contained in:
parent
93df011c7b
commit
112747a51c
28 changed files with 2603 additions and 338 deletions
|
@ -1,4 +1,3 @@
|
||||||
types/
|
types/
|
||||||
schema.graphql
|
schema.graphql
|
||||||
*.generated.*
|
*.generated.*
|
||||||
src/graphql/extendedTypes.ts
|
|
|
@ -17,6 +17,7 @@ All notable, unreleased changes to this project will be documented in this file.
|
||||||
- Enable save button on page pages - #2325 by @orzechdev
|
- Enable save button on page pages - #2325 by @orzechdev
|
||||||
- Fix pagination errors on voucher and sale pages - #2317 by @orzechdev
|
- Fix pagination errors on voucher and sale pages - #2317 by @orzechdev
|
||||||
- Add format tip for text attribute rows - #2340 by @orzechdev
|
- Add format tip for text attribute rows - #2340 by @orzechdev
|
||||||
|
- Add GraphiQL editor to webhook form for defining the subscription query #2885 by @2can @zaiste
|
||||||
|
|
||||||
## 3.4
|
## 3.4
|
||||||
|
|
||||||
|
|
|
@ -394,10 +394,6 @@
|
||||||
"context": "delete custom app",
|
"context": "delete custom app",
|
||||||
"string": "Deleting this app, you will delete all the data and webhooks regarding this app."
|
"string": "Deleting this app, you will delete all the data and webhooks regarding this app."
|
||||||
},
|
},
|
||||||
"1+M/52": {
|
|
||||||
"context": "section header",
|
|
||||||
"string": "Webhook Status"
|
|
||||||
},
|
|
||||||
"1/oG76": {
|
"1/oG76": {
|
||||||
"context": "dialog header",
|
"context": "dialog header",
|
||||||
"string": "Delete Sale"
|
"string": "Delete Sale"
|
||||||
|
@ -422,6 +418,10 @@
|
||||||
"context": "weeks after label",
|
"context": "weeks after label",
|
||||||
"string": "weeks after issue"
|
"string": "weeks after issue"
|
||||||
},
|
},
|
||||||
|
"16Dpgb": {
|
||||||
|
"context": "Synchronous events description",
|
||||||
|
"string": "Synchronous webhook sends payload and waits for a response from the target URL to continue processing."
|
||||||
|
},
|
||||||
"16PGt9": {
|
"16PGt9": {
|
||||||
"context": "WarehouseSettings all warehouses label",
|
"context": "WarehouseSettings all warehouses label",
|
||||||
"string": "All warehouses"
|
"string": "All warehouses"
|
||||||
|
@ -581,6 +581,10 @@
|
||||||
"context": "search modal shipping title",
|
"context": "search modal shipping title",
|
||||||
"string": "Shipping address"
|
"string": "Shipping address"
|
||||||
},
|
},
|
||||||
|
"2RYRdr": {
|
||||||
|
"context": "webhooks active label",
|
||||||
|
"string": "Active"
|
||||||
|
},
|
||||||
"2Sx05f": {
|
"2Sx05f": {
|
||||||
"context": "Previous discount label",
|
"context": "Previous discount label",
|
||||||
"string": "Previous discount value"
|
"string": "Previous discount value"
|
||||||
|
@ -1427,10 +1431,6 @@
|
||||||
"9YazHG": {
|
"9YazHG": {
|
||||||
"string": "Company"
|
"string": "Company"
|
||||||
},
|
},
|
||||||
"9Yhddc": {
|
|
||||||
"context": "input label",
|
|
||||||
"string": "Registered events"
|
|
||||||
},
|
|
||||||
"9Zlogd": {
|
"9Zlogd": {
|
||||||
"context": "staff member status",
|
"context": "staff member status",
|
||||||
"string": "Active"
|
"string": "Active"
|
||||||
|
@ -2160,6 +2160,10 @@
|
||||||
"context": "settings menu item",
|
"context": "settings menu item",
|
||||||
"string": "Settings"
|
"string": "Settings"
|
||||||
},
|
},
|
||||||
|
"F6LHyk": {
|
||||||
|
"context": "Webhook details objects",
|
||||||
|
"string": "Objects"
|
||||||
|
},
|
||||||
"F8gsds": {
|
"F8gsds": {
|
||||||
"context": "unpublish page, button",
|
"context": "unpublish page, button",
|
||||||
"string": "Unpublish"
|
"string": "Unpublish"
|
||||||
|
@ -2355,14 +2359,14 @@
|
||||||
"context": "billing address",
|
"context": "billing address",
|
||||||
"string": "Same as shipping address"
|
"string": "Same as shipping address"
|
||||||
},
|
},
|
||||||
"GLewww": {
|
|
||||||
"context": "section header",
|
|
||||||
"string": "Events"
|
|
||||||
},
|
|
||||||
"GLy2UR": {
|
"GLy2UR": {
|
||||||
"context": "order history message",
|
"context": "order history message",
|
||||||
"string": "Fulfillment was cancelled"
|
"string": "Fulfillment was cancelled"
|
||||||
},
|
},
|
||||||
|
"GNKG74": {
|
||||||
|
"context": "webhooks inactive label",
|
||||||
|
"string": "Inactive"
|
||||||
|
},
|
||||||
"GOdq5V": {
|
"GOdq5V": {
|
||||||
"string": "Catalog"
|
"string": "Catalog"
|
||||||
},
|
},
|
||||||
|
@ -2615,10 +2619,6 @@
|
||||||
"context": "PageTypeDeleteWarningDialog single assigned items button label",
|
"context": "PageTypeDeleteWarningDialog single assigned items button label",
|
||||||
"string": "View pages"
|
"string": "View pages"
|
||||||
},
|
},
|
||||||
"IBCBi1": {
|
|
||||||
"context": "webhook active description",
|
|
||||||
"string": "If you want to disable this webhook please uncheck the box below."
|
|
||||||
},
|
|
||||||
"IBw72y": {
|
"IBw72y": {
|
||||||
"context": "switch button",
|
"context": "switch button",
|
||||||
"string": "Is this product shippable?"
|
"string": "Is this product shippable?"
|
||||||
|
@ -3687,10 +3687,6 @@
|
||||||
"context": "button",
|
"context": "button",
|
||||||
"string": "Delete Search"
|
"string": "Delete Search"
|
||||||
},
|
},
|
||||||
"QEvH8Q": {
|
|
||||||
"context": "section description",
|
|
||||||
"string": "Assign permissions to register asynchronous events for this webhook."
|
|
||||||
},
|
|
||||||
"QFBjlV": {
|
"QFBjlV": {
|
||||||
"context": "error message",
|
"context": "error message",
|
||||||
"string": "Stock for this warehouse already exists for this product variant"
|
"string": "Stock for this warehouse already exists for this product variant"
|
||||||
|
@ -3796,6 +3792,10 @@
|
||||||
"context": "label for radio button",
|
"context": "label for radio button",
|
||||||
"string": "Product prices are entered without tax"
|
"string": "Product prices are entered without tax"
|
||||||
},
|
},
|
||||||
|
"QpSQ5w": {
|
||||||
|
"context": "Webhook subscription query card title",
|
||||||
|
"string": "Payload Query"
|
||||||
|
},
|
||||||
"Qph0GE": {
|
"Qph0GE": {
|
||||||
"context": "dialog content",
|
"context": "dialog content",
|
||||||
"string": "Add a new address:"
|
"string": "Add a new address:"
|
||||||
|
@ -4135,10 +4135,6 @@
|
||||||
"context": "product inventory, checkbox",
|
"context": "product inventory, checkbox",
|
||||||
"string": "Track Inventory"
|
"string": "Track Inventory"
|
||||||
},
|
},
|
||||||
"TjbB4Y": {
|
|
||||||
"context": "section subheader",
|
|
||||||
"string": "Asynchronous events"
|
|
||||||
},
|
|
||||||
"Tl+7X4": {
|
"Tl+7X4": {
|
||||||
"context": "tabel column header",
|
"context": "tabel column header",
|
||||||
"string": "Refunded Qty"
|
"string": "Refunded Qty"
|
||||||
|
@ -4460,10 +4456,6 @@
|
||||||
"context": "button label",
|
"context": "button label",
|
||||||
"string": "Deactivate"
|
"string": "Deactivate"
|
||||||
},
|
},
|
||||||
"W+x5ZI": {
|
|
||||||
"context": "webhooks active label",
|
|
||||||
"string": "Webhook is active"
|
|
||||||
},
|
|
||||||
"W/Es0H": {
|
"W/Es0H": {
|
||||||
"string": "Order successfully cancelled"
|
"string": "Order successfully cancelled"
|
||||||
},
|
},
|
||||||
|
@ -5367,6 +5359,10 @@
|
||||||
"context": "empty metadata text",
|
"context": "empty metadata text",
|
||||||
"string": "No metadata created for this element. Use the button below to add new metadata field."
|
"string": "No metadata created for this element. Use the button below to add new metadata field."
|
||||||
},
|
},
|
||||||
|
"cZN5Jd": {
|
||||||
|
"context": "Webhook details events",
|
||||||
|
"string": "Events"
|
||||||
|
},
|
||||||
"caMMWN": {
|
"caMMWN": {
|
||||||
"string": "Search Warehouse"
|
"string": "Search Warehouse"
|
||||||
},
|
},
|
||||||
|
@ -5470,10 +5466,6 @@
|
||||||
"dPYqy0": {
|
"dPYqy0": {
|
||||||
"string": "We are currently exporting your requested CSV. As soon as it is available it will be sent to your email address"
|
"string": "We are currently exporting your requested CSV. As soon as it is available it will be sent to your email address"
|
||||||
},
|
},
|
||||||
"dQdxLT": {
|
|
||||||
"context": "section subheader",
|
|
||||||
"string": "Synchronous events"
|
|
||||||
},
|
|
||||||
"dS+SOT": {
|
"dS+SOT": {
|
||||||
"context": "header",
|
"context": "header",
|
||||||
"string": "Column settings"
|
"string": "Column settings"
|
||||||
|
@ -5617,10 +5609,6 @@
|
||||||
"context": "gift card history message",
|
"context": "gift card history message",
|
||||||
"string": "Gift card was activated by {activatedBy}"
|
"string": "Gift card was activated by {activatedBy}"
|
||||||
},
|
},
|
||||||
"fHopox": {
|
|
||||||
"context": "section description",
|
|
||||||
"string": "Assign permissions to register synchronous events for this webhook."
|
|
||||||
},
|
|
||||||
"fLhj3a": {
|
"fLhj3a": {
|
||||||
"context": "gift card history message",
|
"context": "gift card history message",
|
||||||
"string": "Gift card expiry date was updated"
|
"string": "Gift card expiry date was updated"
|
||||||
|
@ -6462,6 +6450,10 @@
|
||||||
"context": "taxes title",
|
"context": "taxes title",
|
||||||
"string": "Taxes (VAT included)"
|
"string": "Taxes (VAT included)"
|
||||||
},
|
},
|
||||||
|
"mSCZd4": {
|
||||||
|
"context": "Webhook details asynchronous events",
|
||||||
|
"string": "Asynchronous"
|
||||||
|
},
|
||||||
"mSLr9d": {
|
"mSLr9d": {
|
||||||
"context": "voucher code, button",
|
"context": "voucher code, button",
|
||||||
"string": "Generate Code"
|
"string": "Generate Code"
|
||||||
|
@ -7922,6 +7914,10 @@
|
||||||
"context": "button",
|
"context": "button",
|
||||||
"string": "Create Voucher"
|
"string": "Create Voucher"
|
||||||
},
|
},
|
||||||
|
"yAFaVK": {
|
||||||
|
"context": "Webhook details synchronous events",
|
||||||
|
"string": "Synchronous"
|
||||||
|
},
|
||||||
"yDkmX7": {
|
"yDkmX7": {
|
||||||
"context": "dialog content",
|
"context": "dialog content",
|
||||||
"string": "{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}"
|
"string": "{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}"
|
||||||
|
@ -7944,6 +7940,10 @@
|
||||||
"context": "voucher uses",
|
"context": "voucher uses",
|
||||||
"string": "Uses"
|
"string": "Uses"
|
||||||
},
|
},
|
||||||
|
"yJqbYv": {
|
||||||
|
"context": "Asynchronous events description",
|
||||||
|
"string": "Asynchronous webhook sends payload and continues processing."
|
||||||
|
},
|
||||||
"yJynYK": {
|
"yJynYK": {
|
||||||
"context": "discount value",
|
"context": "discount value",
|
||||||
"string": "discount"
|
"string": "discount"
|
||||||
|
|
1573
package-lock.json
generated
1573
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -25,6 +25,9 @@
|
||||||
"@editorjs/paragraph": "^2.8.0",
|
"@editorjs/paragraph": "^2.8.0",
|
||||||
"@editorjs/quote": "^2.4.0",
|
"@editorjs/quote": "^2.4.0",
|
||||||
"@glideapps/glide-data-grid": "^5.0.0",
|
"@glideapps/glide-data-grid": "^5.0.0",
|
||||||
|
"@graphiql/plugin-explorer": "^0.1.12",
|
||||||
|
"@graphiql/react": "^0.15.0",
|
||||||
|
"@graphiql/toolkit": "^0.8.0",
|
||||||
"@material-ui/core": "^4.12.4",
|
"@material-ui/core": "^4.12.4",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||||
|
@ -50,6 +53,7 @@
|
||||||
"find-test-names": "^1.17.1",
|
"find-test-names": "^1.17.1",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"fuzzaldrin": "^2.1.0",
|
"fuzzaldrin": "^2.1.0",
|
||||||
|
"graphiql": "^2.2.0",
|
||||||
"graphql": "^15.4.0",
|
"graphql": "^15.4.0",
|
||||||
"hotkeys-js": "^3.8.1",
|
"hotkeys-js": "^3.8.1",
|
||||||
"is-url": "^1.2.4",
|
"is-url": "^1.2.4",
|
||||||
|
@ -207,7 +211,7 @@
|
||||||
"mochawesome": "^7.0.1",
|
"mochawesome": "^7.0.1",
|
||||||
"mochawesome-merge": "^4.2.1",
|
"mochawesome-merge": "^4.2.1",
|
||||||
"mochawesome-report-generator": "^6.0.1",
|
"mochawesome-report-generator": "^6.0.1",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^2.8.3",
|
||||||
"setup-polly-jest": "^0.9.1",
|
"setup-polly-jest": "^0.9.1",
|
||||||
"ts-jest": "^27.1.5",
|
"ts-jest": "^27.1.5",
|
||||||
"jest-environment-jsdom": "^27.5.1"
|
"jest-environment-jsdom": "^27.5.1"
|
||||||
|
|
298
src/components/GraphiQL/GraphiQL.tsx
Normal file
298
src/components/GraphiQL/GraphiQL.tsx
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
import {
|
||||||
|
CopyIcon,
|
||||||
|
GraphiQLProvider,
|
||||||
|
GraphiQLProviderProps,
|
||||||
|
PrettifyIcon,
|
||||||
|
QueryEditor,
|
||||||
|
ToolbarButton,
|
||||||
|
Tooltip,
|
||||||
|
UnStyledButton,
|
||||||
|
useCopyQuery,
|
||||||
|
useEditorContext,
|
||||||
|
UseHeaderEditorArgs,
|
||||||
|
usePluginContext,
|
||||||
|
usePrettifyEditors,
|
||||||
|
UseQueryEditorArgs,
|
||||||
|
UseResponseEditorArgs,
|
||||||
|
UseVariableEditorArgs,
|
||||||
|
WriteableEditorProps,
|
||||||
|
} from "@graphiql/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React, { ComponentType, PropsWithChildren, ReactNode } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useDashboardTheme,
|
||||||
|
useEditorStyles,
|
||||||
|
useGraphiQLThemeSwitcher,
|
||||||
|
useStyles,
|
||||||
|
} from "./styles";
|
||||||
|
|
||||||
|
export interface GraphiQLToolbarConfig {
|
||||||
|
/**
|
||||||
|
* This content will be rendered after the built-in buttons of the toolbar.
|
||||||
|
* Note that this will not apply if you provide a completely custom toolbar
|
||||||
|
* (by passing `GraphiQL.Toolbar` as child to the `GraphiQL` component).
|
||||||
|
*/
|
||||||
|
additionalContent?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GraphiQLProps = Omit<GraphiQLProviderProps, "children"> &
|
||||||
|
GraphiQLInterfaceProps;
|
||||||
|
|
||||||
|
export function GraphiQL({
|
||||||
|
dangerouslyAssumeSchemaIsValid,
|
||||||
|
defaultQuery,
|
||||||
|
defaultTabs,
|
||||||
|
externalFragments,
|
||||||
|
fetcher,
|
||||||
|
getDefaultFieldNames,
|
||||||
|
headers,
|
||||||
|
initialTabs,
|
||||||
|
inputValueDeprecation,
|
||||||
|
introspectionQueryName,
|
||||||
|
maxHistoryLength,
|
||||||
|
onEditOperationName,
|
||||||
|
onSchemaChange,
|
||||||
|
onTabChange,
|
||||||
|
onTogglePluginVisibility,
|
||||||
|
operationName,
|
||||||
|
plugins,
|
||||||
|
query,
|
||||||
|
response,
|
||||||
|
schema,
|
||||||
|
schemaDescription,
|
||||||
|
shouldPersistHeaders,
|
||||||
|
storage,
|
||||||
|
validationRules,
|
||||||
|
variables,
|
||||||
|
visiblePlugin,
|
||||||
|
defaultHeaders,
|
||||||
|
...props
|
||||||
|
}: GraphiQLProps) {
|
||||||
|
// Ensure props are correct
|
||||||
|
if (typeof fetcher !== "function") {
|
||||||
|
throw new TypeError(
|
||||||
|
"The `GraphiQL` component requires a `fetcher` function to be passed as prop.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GraphiQLProvider
|
||||||
|
getDefaultFieldNames={getDefaultFieldNames}
|
||||||
|
dangerouslyAssumeSchemaIsValid={dangerouslyAssumeSchemaIsValid}
|
||||||
|
defaultQuery={defaultQuery}
|
||||||
|
defaultHeaders={defaultHeaders}
|
||||||
|
defaultTabs={defaultTabs}
|
||||||
|
externalFragments={externalFragments}
|
||||||
|
fetcher={fetcher}
|
||||||
|
headers={headers}
|
||||||
|
initialTabs={initialTabs}
|
||||||
|
inputValueDeprecation={inputValueDeprecation}
|
||||||
|
introspectionQueryName={introspectionQueryName}
|
||||||
|
maxHistoryLength={maxHistoryLength}
|
||||||
|
onEditOperationName={onEditOperationName}
|
||||||
|
onSchemaChange={onSchemaChange}
|
||||||
|
onTabChange={onTabChange}
|
||||||
|
onTogglePluginVisibility={onTogglePluginVisibility}
|
||||||
|
plugins={plugins}
|
||||||
|
visiblePlugin={visiblePlugin}
|
||||||
|
operationName={operationName}
|
||||||
|
query={query}
|
||||||
|
response={response}
|
||||||
|
schema={schema}
|
||||||
|
schemaDescription={schemaDescription}
|
||||||
|
shouldPersistHeaders={shouldPersistHeaders}
|
||||||
|
storage={storage}
|
||||||
|
validationRules={validationRules}
|
||||||
|
variables={variables}
|
||||||
|
>
|
||||||
|
<GraphiQLInterface {...props} />
|
||||||
|
</GraphiQLProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Export main windows/panes to be used separately if desired.
|
||||||
|
GraphiQL.Toolbar = GraphiQLToolbar;
|
||||||
|
|
||||||
|
type AddSuffix<Obj extends Record<string, any>, Suffix extends string> = {
|
||||||
|
[Key in keyof Obj as `${string & Key}${Suffix}`]: Obj[Key];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GraphiQLInterfaceProps = WriteableEditorProps &
|
||||||
|
AddSuffix<Pick<UseQueryEditorArgs, "onEdit">, "Query"> &
|
||||||
|
Pick<UseQueryEditorArgs, "onCopyQuery"> &
|
||||||
|
AddSuffix<Pick<UseVariableEditorArgs, "onEdit">, "Variables"> &
|
||||||
|
AddSuffix<Pick<UseHeaderEditorArgs, "onEdit">, "Headers"> &
|
||||||
|
Pick<UseResponseEditorArgs, "responseTooltip"> & {
|
||||||
|
children?: ReactNode;
|
||||||
|
defaultEditorToolsVisibility?: boolean | "variables" | "headers";
|
||||||
|
isHeadersEditorEnabled?: boolean;
|
||||||
|
toolbar?: GraphiQLToolbarConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GraphiQLInterface(props: GraphiQLInterfaceProps) {
|
||||||
|
const editorContext = useEditorContext({ nonNull: true });
|
||||||
|
const pluginContext = usePluginContext();
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
const { pluginResize, editorResize, editorToolsResize } = useEditorStyles();
|
||||||
|
|
||||||
|
const copy = useCopyQuery({ onCopyQuery: props.onCopyQuery });
|
||||||
|
const prettify = usePrettifyEditors();
|
||||||
|
const { rootStyle } = useDashboardTheme();
|
||||||
|
|
||||||
|
useGraphiQLThemeSwitcher();
|
||||||
|
|
||||||
|
const PluginContent = pluginContext?.visiblePlugin?.content;
|
||||||
|
|
||||||
|
const children = React.Children.toArray(props.children);
|
||||||
|
|
||||||
|
const toolbar = children.find(child =>
|
||||||
|
isChildComponentType(child, GraphiQL.Toolbar),
|
||||||
|
) || (
|
||||||
|
<>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => prettify()}
|
||||||
|
label="Prettify query (Shift-Ctrl-P)"
|
||||||
|
>
|
||||||
|
<PrettifyIcon className="graphiql-toolbar-icon" aria-hidden="true" />
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton onClick={() => copy()} label="Copy query (Shift-Ctrl-C)">
|
||||||
|
<CopyIcon className="graphiql-toolbar-icon" aria-hidden="true" />
|
||||||
|
</ToolbarButton>
|
||||||
|
{props.toolbar?.additionalContent || null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClickReference = () => {
|
||||||
|
if (pluginResize.hiddenElement === "first") {
|
||||||
|
pluginResize.setHiddenElement(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-test-id="graphiql-container"
|
||||||
|
className="graphiql-container"
|
||||||
|
style={rootStyle}
|
||||||
|
>
|
||||||
|
<div className="graphiql-sidebar">
|
||||||
|
<div className="graphiql-sidebar-section">
|
||||||
|
{pluginContext?.plugins.map(plugin => {
|
||||||
|
const isVisible = plugin === pluginContext.visiblePlugin;
|
||||||
|
const label = `${isVisible ? "Hide" : "Show"} ${plugin.title}`;
|
||||||
|
const Icon = plugin.icon;
|
||||||
|
return (
|
||||||
|
<Tooltip key={plugin.title} label={label}>
|
||||||
|
<UnStyledButton
|
||||||
|
type="button"
|
||||||
|
className={isVisible ? "active" : ""}
|
||||||
|
onClick={() => {
|
||||||
|
if (isVisible) {
|
||||||
|
pluginContext.setVisiblePlugin(null);
|
||||||
|
pluginResize.setHiddenElement("first");
|
||||||
|
} else {
|
||||||
|
pluginContext.setVisiblePlugin(plugin);
|
||||||
|
pluginResize.setHiddenElement(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
<Icon aria-hidden="true" />
|
||||||
|
</UnStyledButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="graphiql-sidebar-section"></div>
|
||||||
|
</div>
|
||||||
|
<div className="graphiql-main">
|
||||||
|
<div
|
||||||
|
ref={pluginResize.firstRef}
|
||||||
|
style={{
|
||||||
|
// Make sure the container shrinks when containing long
|
||||||
|
// non-breaking texts
|
||||||
|
minWidth: "200px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={clsx("graphiql-plugin", classes.scrollable)}>
|
||||||
|
{PluginContent ? <PluginContent /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref={pluginResize.dragBarRef}>
|
||||||
|
{pluginContext?.visiblePlugin ? (
|
||||||
|
<div className="graphiql-horizontal-drag-bar" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div ref={pluginResize.secondRef} style={{ minWidth: 0 }}>
|
||||||
|
<div className="graphiql-sessions">
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
id="graphiql-session"
|
||||||
|
className="graphiql-session"
|
||||||
|
style={{ padding: "2rem 0 0 0" }}
|
||||||
|
aria-labelledby={`graphiql-session-tab-${editorContext.activeTabIndex}`}
|
||||||
|
>
|
||||||
|
<div ref={editorResize.firstRef}>
|
||||||
|
<div
|
||||||
|
className="graphiql-editors full-height"
|
||||||
|
style={{ boxShadow: "none" }}
|
||||||
|
>
|
||||||
|
<div ref={editorToolsResize.firstRef}>
|
||||||
|
<section
|
||||||
|
className="graphiql-query-editor"
|
||||||
|
aria-label="Query Editor"
|
||||||
|
style={{ borderBottom: 0 }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="graphiql-query-editor-wrapper"
|
||||||
|
style={{ fontSize: "1.6rem" }}
|
||||||
|
>
|
||||||
|
<QueryEditor
|
||||||
|
editorTheme={props.editorTheme}
|
||||||
|
keyMap={props.keyMap}
|
||||||
|
onClickReference={onClickReference}
|
||||||
|
onCopyQuery={props.onCopyQuery}
|
||||||
|
onEdit={props.onEditQuery}
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="graphiql-toolbar"
|
||||||
|
role="toolbar"
|
||||||
|
aria-label="Editor Commands"
|
||||||
|
>
|
||||||
|
{toolbar}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GraphiQLToolbar<TProps>(props: PropsWithChildren<TProps>) {
|
||||||
|
return <>{props.children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
GraphiQLToolbar.displayName = "GraphiQLToolbar";
|
||||||
|
|
||||||
|
function isChildComponentType<T extends ComponentType>(
|
||||||
|
child: any,
|
||||||
|
component: T,
|
||||||
|
): child is T {
|
||||||
|
if (
|
||||||
|
child?.type?.displayName &&
|
||||||
|
child.type.displayName === component.displayName
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return child.type === component;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GraphiQL;
|
2
src/components/GraphiQL/index.ts
Normal file
2
src/components/GraphiQL/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default } from "./GraphiQL";
|
||||||
|
export * from "./GraphiQL";
|
75
src/components/GraphiQL/styles.ts
Normal file
75
src/components/GraphiQL/styles.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import {
|
||||||
|
useDragResize,
|
||||||
|
usePluginContext,
|
||||||
|
useTheme as useGraphiQLTheme,
|
||||||
|
} from "@graphiql/react";
|
||||||
|
import { makeStyles, useTheme } from "@saleor/macaw-ui";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(
|
||||||
|
() => ({
|
||||||
|
scrollable: {
|
||||||
|
// Overrides inline styling which breaks scroll
|
||||||
|
// on doc explorer plugin
|
||||||
|
"& > :first-child": {
|
||||||
|
overflowY: "scroll !important",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ name: "GraphiQL" },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useEditorStyles = () => {
|
||||||
|
const pluginContext = usePluginContext();
|
||||||
|
|
||||||
|
const pluginResize = useDragResize({
|
||||||
|
defaultSizeRelation: 1 / 3,
|
||||||
|
direction: "horizontal",
|
||||||
|
initiallyHidden: pluginContext?.visiblePlugin ? undefined : "first",
|
||||||
|
sizeThresholdSecond: 200,
|
||||||
|
storageKey: "docExplorerFlex",
|
||||||
|
});
|
||||||
|
const editorResize = useDragResize({
|
||||||
|
direction: "horizontal",
|
||||||
|
storageKey: "editorFlex",
|
||||||
|
});
|
||||||
|
const editorToolsResize = useDragResize({
|
||||||
|
defaultSizeRelation: 3,
|
||||||
|
direction: "vertical",
|
||||||
|
sizeThresholdSecond: 60,
|
||||||
|
storageKey: "secondaryEditorFlex",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
pluginResize,
|
||||||
|
editorResize,
|
||||||
|
editorToolsResize,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDashboardTheme = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const rootStyle = {
|
||||||
|
"--font-size-body": theme.typography?.body2.fontSize,
|
||||||
|
"--font-size-h2": theme.typography?.h3.fontSize,
|
||||||
|
"--font-size-h3": theme.typography?.h3.fontSize,
|
||||||
|
"--font-size-h4": theme.typography?.h4.fontSize,
|
||||||
|
"--font-size-hint": theme.typography?.caption.fontSize,
|
||||||
|
"--font-size-inline-code": theme.typography?.caption.fontSize,
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
|
return { rootStyle };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGraphiQLThemeSwitcher = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { theme: graphiqlTheme, setTheme: setGraphiqlTheme } =
|
||||||
|
useGraphiQLTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (theme.themeType !== graphiqlTheme) {
|
||||||
|
setGraphiqlTheme(theme.themeType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -2,7 +2,6 @@ import { Backlink } from "@dashboard/components/Backlink";
|
||||||
import Container from "@dashboard/components/Container";
|
import Container from "@dashboard/components/Container";
|
||||||
import Form from "@dashboard/components/Form";
|
import Form from "@dashboard/components/Form";
|
||||||
import FormSpacer from "@dashboard/components/FormSpacer";
|
import FormSpacer from "@dashboard/components/FormSpacer";
|
||||||
import Grid from "@dashboard/components/Grid";
|
|
||||||
import PageHeader from "@dashboard/components/PageHeader";
|
import PageHeader from "@dashboard/components/PageHeader";
|
||||||
import Savebar from "@dashboard/components/Savebar";
|
import Savebar from "@dashboard/components/Savebar";
|
||||||
import WebhookEvents from "@dashboard/custom-apps/components/WebhookEvents";
|
import WebhookEvents from "@dashboard/custom-apps/components/WebhookEvents";
|
||||||
|
@ -13,10 +12,6 @@ import {
|
||||||
createSyncEventsSelectHandler,
|
createSyncEventsSelectHandler,
|
||||||
} from "@dashboard/custom-apps/handlers";
|
} from "@dashboard/custom-apps/handlers";
|
||||||
import { CustomAppUrls } from "@dashboard/custom-apps/urls";
|
import { CustomAppUrls } from "@dashboard/custom-apps/urls";
|
||||||
import {
|
|
||||||
mapAsyncEventsToChoices,
|
|
||||||
mapSyncEventsToChoices,
|
|
||||||
} from "@dashboard/custom-apps/utils";
|
|
||||||
import {
|
import {
|
||||||
WebhookDetailsFragment,
|
WebhookDetailsFragment,
|
||||||
WebhookErrorFragment,
|
WebhookErrorFragment,
|
||||||
|
@ -26,9 +21,11 @@ import {
|
||||||
import { SubmitPromise } from "@dashboard/hooks/useForm";
|
import { SubmitPromise } from "@dashboard/hooks/useForm";
|
||||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import { parse, print } from "graphql";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import WebhookSubscriptionQuery from "../WebhookSubscriptionQuery/WebhookSubscriptionQuery";
|
||||||
import { getHeaderTitle } from "./messages";
|
import { getHeaderTitle } from "./messages";
|
||||||
|
|
||||||
export interface WebhookFormData {
|
export interface WebhookFormData {
|
||||||
|
@ -36,8 +33,9 @@ export interface WebhookFormData {
|
||||||
asyncEvents: WebhookEventTypeAsyncEnum[];
|
asyncEvents: WebhookEventTypeAsyncEnum[];
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
secretKey: string | null;
|
secretKey?: string;
|
||||||
targetUrl: string;
|
targetUrl: string;
|
||||||
|
subscriptionQuery: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebhookDetailsPageProps {
|
export interface WebhookDetailsPageProps {
|
||||||
|
@ -62,6 +60,13 @@ const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const navigate = useNavigator();
|
const navigate = useNavigator();
|
||||||
|
|
||||||
|
let prettified: string;
|
||||||
|
try {
|
||||||
|
prettified = print(parse(webhook?.subscriptionQuery));
|
||||||
|
} catch {
|
||||||
|
prettified = webhook?.subscriptionQuery || "";
|
||||||
|
}
|
||||||
|
|
||||||
const initialForm: WebhookFormData = {
|
const initialForm: WebhookFormData = {
|
||||||
syncEvents: webhook?.syncEvents?.map(event => event.eventType) || [],
|
syncEvents: webhook?.syncEvents?.map(event => event.eventType) || [],
|
||||||
asyncEvents: webhook?.asyncEvents?.map(event => event.eventType) || [],
|
asyncEvents: webhook?.asyncEvents?.map(event => event.eventType) || [],
|
||||||
|
@ -69,61 +74,64 @@ const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
|
||||||
name: webhook?.name || "",
|
name: webhook?.name || "",
|
||||||
secretKey: webhook?.secretKey || "",
|
secretKey: webhook?.secretKey || "",
|
||||||
targetUrl: webhook?.targetUrl || "",
|
targetUrl: webhook?.targetUrl || "",
|
||||||
|
subscriptionQuery: prettified || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const backUrl = CustomAppUrls.resolveAppUrl(appId);
|
const backUrl = CustomAppUrls.resolveAppUrl(appId);
|
||||||
|
|
||||||
return (
|
const [query, setQuery] = useState(prettified);
|
||||||
<Form confirmLeave initial={initialForm} onSubmit={onSubmit}>
|
|
||||||
{({ data, submit, change }) => {
|
|
||||||
const syncEventsChoices = disabled
|
|
||||||
? []
|
|
||||||
: mapSyncEventsToChoices(Object.values(WebhookEventTypeSyncEnum));
|
|
||||||
const asyncEventsChoices = disabled
|
|
||||||
? []
|
|
||||||
: mapAsyncEventsToChoices(
|
|
||||||
Object.values(WebhookEventTypeAsyncEnum),
|
|
||||||
data.asyncEvents,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setQuery(prettified);
|
||||||
|
}, [prettified]);
|
||||||
|
|
||||||
|
const handleSubmit = (data: WebhookFormData) => {
|
||||||
|
onSubmit({ ...data, ...{ subscriptionQuery: query } });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form confirmLeave initial={initialForm} onSubmit={handleSubmit}>
|
||||||
|
{({ data, submit, change }) => {
|
||||||
const handleSyncEventsSelect = createSyncEventsSelectHandler(
|
const handleSyncEventsSelect = createSyncEventsSelectHandler(
|
||||||
change,
|
change,
|
||||||
data.syncEvents,
|
data.syncEvents,
|
||||||
|
setQuery,
|
||||||
);
|
);
|
||||||
const handleAsyncEventsSelect = createAsyncEventsSelectHandler(
|
const handleAsyncEventsSelect = createAsyncEventsSelectHandler(
|
||||||
change,
|
change,
|
||||||
data.asyncEvents,
|
data.asyncEvents,
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Backlink href={backUrl}>{appName}</Backlink>
|
<Backlink href={backUrl}>{appName}</Backlink>
|
||||||
<PageHeader title={getHeaderTitle(intl, webhook)} />
|
<PageHeader title={getHeaderTitle(intl, webhook)}>
|
||||||
<Grid variant="uniform">
|
<WebhookStatus
|
||||||
<div>
|
data={data.isActive}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={change}
|
||||||
|
/>
|
||||||
|
</PageHeader>
|
||||||
<WebhookInfo
|
<WebhookInfo
|
||||||
data={data}
|
data={data}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
onChange={change}
|
onChange={change}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<WebhookStatus
|
|
||||||
data={data.isActive}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={change}
|
|
||||||
/>
|
|
||||||
<FormSpacer />
|
<FormSpacer />
|
||||||
<WebhookEvents
|
<WebhookEvents
|
||||||
data={data}
|
data={data}
|
||||||
syncEventsChoices={syncEventsChoices}
|
|
||||||
asyncEventsChoices={asyncEventsChoices}
|
|
||||||
onSyncEventChange={handleSyncEventsSelect}
|
onSyncEventChange={handleSyncEventsSelect}
|
||||||
onAsyncEventChange={handleAsyncEventsSelect}
|
onAsyncEventChange={handleAsyncEventsSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
<FormSpacer />
|
||||||
</Grid>
|
<WebhookSubscriptionQuery
|
||||||
|
query={query}
|
||||||
|
setQuery={setQuery}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
<Savebar
|
<Savebar
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
state={saveButtonBarState}
|
state={saveButtonBarState}
|
||||||
|
|
|
@ -1,97 +1,235 @@
|
||||||
import VerticalSpacer from "@dashboard/apps/components/VerticalSpacer";
|
import Grid from "@dashboard/components/Grid";
|
||||||
import CardTitle from "@dashboard/components/CardTitle";
|
|
||||||
import Hr from "@dashboard/components/Hr";
|
import Hr from "@dashboard/components/Hr";
|
||||||
import MultiAutocompleteSelectField, {
|
|
||||||
MultiAutocompleteChoiceType,
|
|
||||||
} from "@dashboard/components/MultiAutocompleteSelectField";
|
|
||||||
import {
|
|
||||||
mapAsyncEventsToChoices,
|
|
||||||
mapSyncEventsToChoices,
|
|
||||||
} from "@dashboard/custom-apps/utils";
|
|
||||||
import {
|
import {
|
||||||
WebhookEventTypeAsyncEnum,
|
WebhookEventTypeAsyncEnum,
|
||||||
WebhookEventTypeSyncEnum,
|
WebhookEventTypeSyncEnum,
|
||||||
} from "@dashboard/graphql";
|
} from "@dashboard/graphql";
|
||||||
import { ChangeEvent } from "@dashboard/hooks/useForm";
|
import { ChangeEvent } from "@dashboard/hooks/useForm";
|
||||||
import { Card, CardContent, Typography } from "@material-ui/core";
|
import { capitalize } from "@dashboard/misc";
|
||||||
import React from "react";
|
import { Card, CardContent, Checkbox, Typography } from "@material-ui/core";
|
||||||
|
import {
|
||||||
|
List,
|
||||||
|
ListHeader,
|
||||||
|
ListItem,
|
||||||
|
ListItemCell,
|
||||||
|
PageTab,
|
||||||
|
PageTabPanel,
|
||||||
|
PageTabs,
|
||||||
|
Pill,
|
||||||
|
useListWidths,
|
||||||
|
} from "@saleor/macaw-ui";
|
||||||
|
import React, { useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import { messages } from "./messages";
|
import { messages } from "./messages";
|
||||||
|
import { useStyles } from "./styles";
|
||||||
|
|
||||||
interface WebhookEventsProps {
|
interface WebhookEventsProps {
|
||||||
data: {
|
data: {
|
||||||
syncEvents: WebhookEventTypeSyncEnum[];
|
syncEvents: WebhookEventTypeSyncEnum[];
|
||||||
asyncEvents: WebhookEventTypeAsyncEnum[];
|
asyncEvents: WebhookEventTypeAsyncEnum[];
|
||||||
};
|
};
|
||||||
syncEventsChoices: MultiAutocompleteChoiceType[];
|
|
||||||
asyncEventsChoices: MultiAutocompleteChoiceType[];
|
|
||||||
onSyncEventChange: (event: ChangeEvent) => void;
|
onSyncEventChange: (event: ChangeEvent) => void;
|
||||||
onAsyncEventChange: (event: ChangeEvent) => void;
|
onAsyncEventChange: (event: ChangeEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WebhookEvents: React.FC<WebhookEventsProps> = ({
|
const WebhookEvents: React.FC<WebhookEventsProps> = ({
|
||||||
data,
|
data,
|
||||||
syncEventsChoices,
|
|
||||||
asyncEventsChoices,
|
|
||||||
onSyncEventChange,
|
onSyncEventChange,
|
||||||
onAsyncEventChange,
|
onAsyncEventChange,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const classes = useStyles();
|
||||||
|
const { checkbox } = useListWidths();
|
||||||
|
|
||||||
|
const [tab, setTab] = useState("async");
|
||||||
|
const [object, setObject] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleEventChange = event => {
|
||||||
|
if (tab === "sync") {
|
||||||
|
return onSyncEventChange(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return onAsyncEventChange(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabChange = value => {
|
||||||
|
setObject(null);
|
||||||
|
setTab(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const countEvents = object => {
|
||||||
|
const selected = tab === "sync" ? data.syncEvents : data.asyncEvents;
|
||||||
|
const objectEvents = EventTypes[tab][object].map(
|
||||||
|
event => `${object}_${event}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return objectEvents.filter((event: never) => selected.includes(event))
|
||||||
|
.length;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<CardTitle title={intl.formatMessage(messages.events)} />
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="caption">
|
<PageTabs value={tab} onChange={handleTabChange}>
|
||||||
<FormattedMessage {...messages.synchronousEvents} />
|
<PageTab
|
||||||
</Typography>
|
label={intl.formatMessage(messages.asynchronous)}
|
||||||
<VerticalSpacer />
|
value="async"
|
||||||
<Typography variant="body1">
|
|
||||||
<FormattedMessage
|
|
||||||
{...messages.assignPermissionsToSynchronousEvents}
|
|
||||||
/>
|
/>
|
||||||
</Typography>
|
<PageTab
|
||||||
<VerticalSpacer />
|
label={intl.formatMessage(messages.synchronous)}
|
||||||
<MultiAutocompleteSelectField
|
value="sync"
|
||||||
displayValues={mapSyncEventsToChoices(data.syncEvents)}
|
|
||||||
label={intl.formatMessage(messages.registeredEvents)}
|
|
||||||
choices={syncEventsChoices}
|
|
||||||
name="syncEvents"
|
|
||||||
value={data.syncEvents}
|
|
||||||
onChange={onSyncEventChange}
|
|
||||||
data-test-id="syncEvents"
|
|
||||||
testId="syncEvent"
|
|
||||||
/>
|
/>
|
||||||
<VerticalSpacer spacing={2} />
|
</PageTabs>
|
||||||
<Hr />
|
|
||||||
<VerticalSpacer spacing={2} />
|
<Typography variant="subtitle2" style={{ padding: "1rem 0" }}>
|
||||||
<Typography variant="caption">
|
<PageTabPanel show={tab === "sync"}>
|
||||||
<FormattedMessage {...messages.asynchronousEvents} />
|
<FormattedMessage {...messages.synchronousDescription} />
|
||||||
|
</PageTabPanel>
|
||||||
|
<PageTabPanel show={tab === "async"}>
|
||||||
|
<FormattedMessage {...messages.asynchronousDescription} />
|
||||||
|
</PageTabPanel>
|
||||||
</Typography>
|
</Typography>
|
||||||
<VerticalSpacer />
|
|
||||||
<Typography variant="body1">
|
|
||||||
<FormattedMessage
|
|
||||||
{...messages.assignPermissionsToAsynchronousEvents}
|
|
||||||
/>
|
|
||||||
</Typography>
|
|
||||||
<VerticalSpacer />
|
|
||||||
<MultiAutocompleteSelectField
|
|
||||||
displayValues={mapAsyncEventsToChoices(
|
|
||||||
data.asyncEvents,
|
|
||||||
data.asyncEvents,
|
|
||||||
)}
|
|
||||||
label={intl.formatMessage(messages.registeredEvents)}
|
|
||||||
choices={asyncEventsChoices}
|
|
||||||
name="asyncEvents"
|
|
||||||
value={data.asyncEvents}
|
|
||||||
onChange={onAsyncEventChange}
|
|
||||||
data-test-id="asyncEvents"
|
|
||||||
testId="asyncEvent"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
<Hr />
|
||||||
|
<Grid variant="uniform">
|
||||||
|
<div className={classes.objectsWrapper}>
|
||||||
|
<PageTabPanel show={true}>
|
||||||
|
<List gridTemplate={["1fr 50px"]}>
|
||||||
|
<ListHeader>
|
||||||
|
<ListItem className={classes.listHeader}>
|
||||||
|
<ListItemCell className={classes.listItemCell}>
|
||||||
|
<FormattedMessage {...messages.objects} />
|
||||||
|
</ListItemCell>
|
||||||
|
<ListItemCell></ListItemCell>
|
||||||
|
</ListItem>
|
||||||
|
</ListHeader>
|
||||||
|
<div className={classes.listItems}>
|
||||||
|
{Object.keys(EventTypes[tab]).map((object, idx) => (
|
||||||
|
<ListItem
|
||||||
|
key={idx}
|
||||||
|
className={classes.listItem}
|
||||||
|
onClick={() => setObject(object)}
|
||||||
|
>
|
||||||
|
<ListItemCell className={classes.listItemCell}>
|
||||||
|
<strong>
|
||||||
|
{capitalize(
|
||||||
|
object.replaceAll("_", " ").toLowerCase(),
|
||||||
|
)}
|
||||||
|
</strong>
|
||||||
|
</ListItemCell>
|
||||||
|
<ListItemCell>
|
||||||
|
{countEvents(object) > 0 && (
|
||||||
|
<Pill
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
label={countEvents(object)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ListItemCell>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</List>
|
||||||
|
</PageTabPanel>
|
||||||
|
</div>
|
||||||
|
<div className={classes.eventsWrapper}>
|
||||||
|
<List gridTemplate={["1fr", checkbox]}>
|
||||||
|
<ListHeader>
|
||||||
|
<ListItem className={classes.listHeader}>
|
||||||
|
<ListItemCell className={classes.listItemCell}>
|
||||||
|
<FormattedMessage {...messages.events} />
|
||||||
|
</ListItemCell>
|
||||||
|
</ListItem>
|
||||||
|
</ListHeader>
|
||||||
|
{object &&
|
||||||
|
EventTypes[tab][object] &&
|
||||||
|
EventTypes[tab][object].map((event, idx) => (
|
||||||
|
<ListItem className={classes.listItem} key={idx}>
|
||||||
|
<ListItemCell className={classes.listItemCell}>
|
||||||
|
<strong>
|
||||||
|
{capitalize(event.toLowerCase().replaceAll("_", " "))}
|
||||||
|
</strong>
|
||||||
|
</ListItemCell>
|
||||||
|
<ListItemCell>
|
||||||
|
<Checkbox
|
||||||
|
name={`${tab}Events`}
|
||||||
|
checked={data[`${tab}Events`].includes(
|
||||||
|
getEventName(object, event),
|
||||||
|
)}
|
||||||
|
value={getEventName(object, event)}
|
||||||
|
onChange={handleEventChange}
|
||||||
|
className={classes.checkbox}
|
||||||
|
/>
|
||||||
|
</ListItemCell>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
</Card>
|
</Card>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
WebhookEvents.displayName = "WebhookEvents";
|
WebhookEvents.displayName = "WebhookEvents";
|
||||||
export default WebhookEvents;
|
export default WebhookEvents;
|
||||||
|
|
||||||
|
type Actions = string[];
|
||||||
|
|
||||||
|
const AsyncWebhookTypes: Record<string, Actions> = {
|
||||||
|
ADDRESS: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
APP: ["INSTALLED", "UPDATED", "DELETED"],
|
||||||
|
ATTRIBUTE: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
CATEGORY: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
CHANNEL: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
GIFT_CARD: ["CREATED", "UPDATED", "DELETED", "STATUS_CHANGED"],
|
||||||
|
|
||||||
|
CHECKOUT: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
COLLECTION: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
CUSTOMER: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
FULFILLMENT: ["CREATED"],
|
||||||
|
INVOICE: ["DELETED", "REQUESTED", "SENT"],
|
||||||
|
MENU: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
ORDER: [
|
||||||
|
"CANCELLED",
|
||||||
|
"CONFIRMED",
|
||||||
|
"CREATED",
|
||||||
|
"FULFILLED",
|
||||||
|
"FULLY_PAID",
|
||||||
|
"UPDATED",
|
||||||
|
],
|
||||||
|
PAGE: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
PRODUCT: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
PRODUCT_VARIANT: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
SALE: ["CREATED", "UPDATED", "DELETED", "TOGGLE"],
|
||||||
|
SHIPPING_PRICE: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
SHIPPING_ZONE: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
STAFF: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
TRANSLATION: ["ACTION_REQUEST", "CREATED", "UPDATED"],
|
||||||
|
VOUCHER: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
WAREHOUSE: ["CREATED", "UPDATED", "DELETED"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const SyncWebhookTypes: Record<string, Actions> = {
|
||||||
|
PAYMENT: [
|
||||||
|
"AUTHORIZE",
|
||||||
|
"CAPTURE",
|
||||||
|
"CONFIRM",
|
||||||
|
"LIST_GATEWAYS",
|
||||||
|
"PROCESS",
|
||||||
|
"REFUND",
|
||||||
|
"VOID",
|
||||||
|
],
|
||||||
|
CHECKOUT: ["CALCULATE_TAXES", "FILTER_SHIPPING_METHODS"],
|
||||||
|
ORDER: ["CALCULATE_TAXES", "FILTER_SHIPPING_METHODS"],
|
||||||
|
SHIPPING: ["LIST_METHODS_FOR_CHECKOUT"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventTypes = {
|
||||||
|
async: AsyncWebhookTypes,
|
||||||
|
sync: SyncWebhookTypes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEventName = (object: string, event: string) =>
|
||||||
|
[object, event].join("_").toUpperCase() as WebhookEventTypeSyncEnum;
|
||||||
|
|
|
@ -2,35 +2,35 @@ import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
events: {
|
events: {
|
||||||
id: "GLewww",
|
id: "cZN5Jd",
|
||||||
defaultMessage: "Events",
|
defaultMessage: "Events",
|
||||||
description: "section header",
|
description: "Webhook details events",
|
||||||
},
|
},
|
||||||
synchronousEvents: {
|
synchronous: {
|
||||||
id: "dQdxLT",
|
id: "yAFaVK",
|
||||||
defaultMessage: "Synchronous events",
|
defaultMessage: "Synchronous",
|
||||||
description: "section subheader",
|
description: "Webhook details synchronous events",
|
||||||
},
|
},
|
||||||
asynchronousEvents: {
|
asynchronous: {
|
||||||
id: "TjbB4Y",
|
id: "mSCZd4",
|
||||||
defaultMessage: "Asynchronous events",
|
defaultMessage: "Asynchronous",
|
||||||
description: "section subheader",
|
description: "Webhook details asynchronous events",
|
||||||
},
|
},
|
||||||
assignPermissionsToSynchronousEvents: {
|
synchronousDescription: {
|
||||||
id: "fHopox",
|
id: "16Dpgb",
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
"Assign permissions to register synchronous events for this webhook.",
|
"Synchronous webhook sends payload and waits for a response from the target URL to continue processing.",
|
||||||
description: "section description",
|
description: "Synchronous events description",
|
||||||
},
|
},
|
||||||
assignPermissionsToAsynchronousEvents: {
|
asynchronousDescription: {
|
||||||
id: "QEvH8Q",
|
id: "yJqbYv",
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
"Assign permissions to register asynchronous events for this webhook.",
|
"Asynchronous webhook sends payload and continues processing.",
|
||||||
description: "section description",
|
description: "Asynchronous events description",
|
||||||
},
|
},
|
||||||
registeredEvents: {
|
objects: {
|
||||||
id: "9Yhddc",
|
defaultMessage: "Objects",
|
||||||
defaultMessage: "Registered events",
|
id: "F6LHyk",
|
||||||
description: "input label",
|
description: "Webhook details objects",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
37
src/custom-apps/components/WebhookEvents/styles.ts
Normal file
37
src/custom-apps/components/WebhookEvents/styles.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
eventsWrapper: {
|
||||||
|
overflow: "scroll",
|
||||||
|
padding: theme.spacing(4),
|
||||||
|
paddingLeft: 0,
|
||||||
|
},
|
||||||
|
objectsWrapper: {
|
||||||
|
borderRight: "1px solid",
|
||||||
|
borderRightColor: theme.palette.divider,
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
},
|
||||||
|
listHeader: {
|
||||||
|
textTransform: "uppercase",
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
minHeight: 0,
|
||||||
|
},
|
||||||
|
listItem: {
|
||||||
|
minHeight: 0,
|
||||||
|
gap: 0,
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
},
|
||||||
|
listItemCell: {
|
||||||
|
paddingLeft: "0 !important",
|
||||||
|
},
|
||||||
|
listItems: {
|
||||||
|
height: 300,
|
||||||
|
overflow: "scroll",
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ name: "WebhookEvents" },
|
||||||
|
);
|
|
@ -1,7 +1,5 @@
|
||||||
import CardTitle from "@dashboard/components/CardTitle";
|
import ControlledSwitch from "@dashboard/components/ControlledSwitch";
|
||||||
import ControlledCheckbox from "@dashboard/components/ControlledCheckbox";
|
|
||||||
import { ChangeEvent } from "@dashboard/hooks/useForm";
|
import { ChangeEvent } from "@dashboard/hooks/useForm";
|
||||||
import { Card, CardContent, Typography } from "@material-ui/core";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
@ -21,22 +19,19 @@ const WebhookStatus: React.FC<WebhookStatusProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<Card>
|
<ControlledSwitch
|
||||||
<CardTitle title={intl.formatMessage(messages.webhookStatus)} />
|
|
||||||
<CardContent>
|
|
||||||
<Typography variant="body1">
|
|
||||||
{intl.formatMessage(messages.webhookActiveDescription)}
|
|
||||||
</Typography>
|
|
||||||
<ControlledCheckbox
|
|
||||||
name={"isActive" as keyof WebhookFormData}
|
name={"isActive" as keyof WebhookFormData}
|
||||||
label={intl.formatMessage(messages.webhookActive)}
|
label={
|
||||||
|
data
|
||||||
|
? intl.formatMessage(messages.webhookActive)
|
||||||
|
: intl.formatMessage(messages.webhookInactive)
|
||||||
|
}
|
||||||
checked={data}
|
checked={data}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
WebhookStatus.displayName = "WebhookStatus";
|
WebhookStatus.displayName = "WebhookStatus";
|
||||||
export default WebhookStatus;
|
export default WebhookStatus;
|
||||||
|
|
|
@ -1,20 +1,14 @@
|
||||||
import { defineMessages } from "react-intl";
|
import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
webhookStatus: {
|
|
||||||
id: "1+M/52",
|
|
||||||
defaultMessage: "Webhook Status",
|
|
||||||
description: "section header",
|
|
||||||
},
|
|
||||||
webhookActive: {
|
webhookActive: {
|
||||||
id: "W+x5ZI",
|
id: "2RYRdr",
|
||||||
defaultMessage: "Webhook is active",
|
defaultMessage: "Active",
|
||||||
description: "webhooks active label",
|
description: "webhooks active label",
|
||||||
},
|
},
|
||||||
webhookActiveDescription: {
|
webhookInactive: {
|
||||||
id: "IBCBi1",
|
id: "GNKG74",
|
||||||
defaultMessage:
|
defaultMessage: "Inactive",
|
||||||
"If you want to disable this webhook please uncheck the box below.",
|
description: "webhooks inactive label",
|
||||||
description: "webhook active description",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
|
||||||
|
import {
|
||||||
|
WebhookEventTypeAsyncEnum,
|
||||||
|
WebhookEventTypeSyncEnum,
|
||||||
|
} from "@dashboard/graphql";
|
||||||
|
import { Fetcher } from "@graphiql/toolkit";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import WebhookSubscriptionQuery from "./WebhookSubscriptionQuery";
|
||||||
|
|
||||||
|
jest.mock("@graphiql/toolkit", () => ({
|
||||||
|
clear: jest.fn(),
|
||||||
|
createGraphiQLFetcher: jest.fn(_x => jest.fn() as Fetcher),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("react-intl", () => ({
|
||||||
|
useIntl: jest.fn(() => ({
|
||||||
|
formatMessage: jest.fn(x => x.defaultMessage),
|
||||||
|
})),
|
||||||
|
defineMessages: jest.fn(x => x),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@saleor/macaw-ui", () => ({
|
||||||
|
useTheme: jest.fn(() => () => ({})),
|
||||||
|
useStyles: jest.fn(() => () => ({})),
|
||||||
|
makeStyles: jest.fn(() => () => ({})),
|
||||||
|
DialogHeader: jest.fn(() => () => <></>),
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
window.localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("WebhookSubscriptionQuery", () => {
|
||||||
|
it("is available on the webhook page", async () => {
|
||||||
|
// Arrange
|
||||||
|
const props = {
|
||||||
|
query: "",
|
||||||
|
setQuery: jest.fn(),
|
||||||
|
data: {
|
||||||
|
syncEvents: [] as WebhookEventTypeSyncEnum[],
|
||||||
|
asyncEvents: [] as WebhookEventTypeAsyncEnum[],
|
||||||
|
isActive: false,
|
||||||
|
name: "",
|
||||||
|
targetUrl: "",
|
||||||
|
subscriptionQuery: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// const user = userEvent.setup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<WebhookSubscriptionQuery {...props} />);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.queryByTestId("graphiql-container")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("graphiql-container2")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
it("triggers setQuery when user enters text", async () => {
|
||||||
|
// Arrange
|
||||||
|
const props = {
|
||||||
|
query: '',
|
||||||
|
setQuery: jest.fn(),
|
||||||
|
data: {
|
||||||
|
syncEvents: [] as WebhookEventTypeSyncEnum[],
|
||||||
|
asyncEvents: [] as WebhookEventTypeAsyncEnum[],
|
||||||
|
isActive: false,
|
||||||
|
name: '',
|
||||||
|
targetUrl: '',
|
||||||
|
subscriptionQuery: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { getByTestId } = render(<WebhookSubscriptionQuery {...props} />);
|
||||||
|
const graphiQLContainer = getByTestId("graphiql-container");
|
||||||
|
user.type(graphiQLContainer, "{}");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(props.setQuery).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
});
|
|
@ -0,0 +1,69 @@
|
||||||
|
import "graphiql/graphiql.min.css";
|
||||||
|
|
||||||
|
import CardTitle from "@dashboard/components/CardTitle";
|
||||||
|
import { useExplorerPlugin } from "@graphiql/plugin-explorer";
|
||||||
|
import { createGraphiQLFetcher } from "@graphiql/toolkit";
|
||||||
|
import { Card, CardContent } from "@material-ui/core";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
import { defineMessages, useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import GraphiQL from "../../../components/GraphiQL";
|
||||||
|
import { WebhookFormData } from "../WebhookDetailsPage";
|
||||||
|
import { useStyles } from "./styles";
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: {
|
||||||
|
id: "QpSQ5w",
|
||||||
|
defaultMessage: "Payload Query",
|
||||||
|
description: "Webhook subscription query card title",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface WebhookSubscriptionQueryProps {
|
||||||
|
query: any;
|
||||||
|
setQuery: React.Dispatch<any>;
|
||||||
|
data: WebhookFormData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = createGraphiQLFetcher({
|
||||||
|
url: process.env.API_URI,
|
||||||
|
});
|
||||||
|
|
||||||
|
const WebhookSubscriptionQuery: React.FC<WebhookSubscriptionQueryProps> = ({
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
|
data,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const explorerPlugin = useExplorerPlugin({
|
||||||
|
query,
|
||||||
|
onEdit: setQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={clsx(classes.card, data.syncEvents.length && classes.disabled)}
|
||||||
|
>
|
||||||
|
<CardTitle title={intl.formatMessage(messages.title)} />
|
||||||
|
<CardContent className={classes.cardContent}>
|
||||||
|
<GraphiQL
|
||||||
|
data-test-id="graphiql-webhook"
|
||||||
|
defaultEditorToolsVisibility={"headers"}
|
||||||
|
fetcher={fetcher}
|
||||||
|
query={query}
|
||||||
|
storage={null}
|
||||||
|
onEditQuery={setQuery}
|
||||||
|
plugins={[explorerPlugin]}
|
||||||
|
isHeadersEditorEnabled={false}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
WebhookSubscriptionQuery.displayName = "WebhookSubscriptionQuery";
|
||||||
|
export default WebhookSubscriptionQuery;
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default } from "./WebhookSubscriptionQuery";
|
||||||
|
export * from "./WebhookSubscriptionQuery";
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
|
export const messages = defineMessages({
|
||||||
|
createWebhook: {
|
||||||
|
id: "wlr0Si",
|
||||||
|
defaultMessage: "Create Webhook",
|
||||||
|
description: "button",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
id: "a/QJBx",
|
||||||
|
defaultMessage: "Action",
|
||||||
|
description: "user action bar",
|
||||||
|
},
|
||||||
|
unnamedWebhook: {
|
||||||
|
id: "Lm2Zw7",
|
||||||
|
defaultMessage: "Unnamed webhook",
|
||||||
|
description: "name placeholder",
|
||||||
|
},
|
||||||
|
noWebhooks: {
|
||||||
|
id: "Bkxrhw",
|
||||||
|
defaultMessage: "No webhooks found",
|
||||||
|
description: "no webhooks message",
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
disabled: {
|
||||||
|
pointerEvents: "none",
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
cardContent: {
|
||||||
|
height: 500,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ name: "WebhookSubscriptionQuery" },
|
||||||
|
);
|
|
@ -13,5 +13,7 @@ export const webhook: WebhookDetailsFragment = {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
name: "Webhook Test 2",
|
name: "Webhook Test 2",
|
||||||
secretKey: "zxczx_asdas",
|
secretKey: "zxczx_asdas",
|
||||||
|
subscriptionQuery:
|
||||||
|
"subscription { event { ... on ProductUpdated { product { name } } } }",
|
||||||
targetUrl: "http://www.getsaleor.com",
|
targetUrl: "http://www.getsaleor.com",
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,35 +3,127 @@ import {
|
||||||
WebhookEventTypeSyncEnum,
|
WebhookEventTypeSyncEnum,
|
||||||
} from "@dashboard/graphql";
|
} from "@dashboard/graphql";
|
||||||
import { ChangeEvent } from "@dashboard/hooks/useForm";
|
import { ChangeEvent } from "@dashboard/hooks/useForm";
|
||||||
|
import { capitalize } from "@dashboard/misc";
|
||||||
import { toggle } from "@dashboard/utils/lists";
|
import { toggle } from "@dashboard/utils/lists";
|
||||||
|
import {
|
||||||
|
InlineFragmentNode,
|
||||||
|
ObjectFieldNode,
|
||||||
|
parse,
|
||||||
|
print,
|
||||||
|
visit,
|
||||||
|
} from "graphql";
|
||||||
|
|
||||||
import { filterSelectedAsyncEvents } from "./utils";
|
import { filterSelectedAsyncEvents } from "./utils";
|
||||||
|
|
||||||
export const createSyncEventsSelectHandler = (
|
export const createSyncEventsSelectHandler =
|
||||||
|
(
|
||||||
change: (event: ChangeEvent, cb?: () => void) => void,
|
change: (event: ChangeEvent, cb?: () => void) => void,
|
||||||
syncEvents: WebhookEventTypeSyncEnum[],
|
syncEvents: WebhookEventTypeSyncEnum[],
|
||||||
) => (event: ChangeEvent) => {
|
setQuery: React.Dispatch<React.SetStateAction<string>>,
|
||||||
|
) =>
|
||||||
|
(event: ChangeEvent) => {
|
||||||
const events = toggle(event.target.value, syncEvents, (a, b) => a === b);
|
const events = toggle(event.target.value, syncEvents, (a, b) => a === b);
|
||||||
|
|
||||||
|
// Clear query
|
||||||
|
setQuery("");
|
||||||
|
|
||||||
|
// Clear asyncEvents
|
||||||
|
change({
|
||||||
|
target: {
|
||||||
|
name: "asyncEvents",
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
change({
|
change({
|
||||||
target: {
|
target: {
|
||||||
name: "syncEvents",
|
name: "syncEvents",
|
||||||
value: events,
|
value: events,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createAsyncEventsSelectHandler = (
|
export const createAsyncEventsSelectHandler =
|
||||||
|
(
|
||||||
change: (event: ChangeEvent, cb?: () => void) => void,
|
change: (event: ChangeEvent, cb?: () => void) => void,
|
||||||
asyncEvents: WebhookEventTypeAsyncEnum[],
|
asyncEvents: WebhookEventTypeAsyncEnum[],
|
||||||
) => (event: ChangeEvent) => {
|
query: string,
|
||||||
|
setQuery: React.Dispatch<React.SetStateAction<string>>,
|
||||||
|
) =>
|
||||||
|
(event: ChangeEvent) => {
|
||||||
const events = toggle(event.target.value, asyncEvents, (a, b) => a === b);
|
const events = toggle(event.target.value, asyncEvents, (a, b) => a === b);
|
||||||
const filteredEvents = filterSelectedAsyncEvents(events);
|
const filteredEvents = filterSelectedAsyncEvents(events);
|
||||||
|
|
||||||
|
// Clear syncEvents
|
||||||
|
change({
|
||||||
|
target: {
|
||||||
|
name: "syncEvents",
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
change({
|
change({
|
||||||
target: {
|
target: {
|
||||||
name: "asyncEvents",
|
name: "asyncEvents",
|
||||||
value: filteredEvents,
|
value: filteredEvents,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
handleQuery(filteredEvents, query, setQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuery = (
|
||||||
|
events: WebhookEventTypeAsyncEnum[],
|
||||||
|
query: string,
|
||||||
|
setQuery: React.Dispatch<React.SetStateAction<string>>,
|
||||||
|
) => {
|
||||||
|
if (events.length > 0 && query.length === 0) {
|
||||||
|
const event = events[0]
|
||||||
|
.toLowerCase()
|
||||||
|
.split("_")
|
||||||
|
.map(chunk => capitalize(chunk))
|
||||||
|
.join("");
|
||||||
|
setQuery(
|
||||||
|
print(parse(`subscription { event { ... on ${event} { __typename } } }`)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.length > 0) {
|
||||||
|
const ast = parse(query);
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
const newEvents = events
|
||||||
|
.map(event =>
|
||||||
|
event
|
||||||
|
.toLowerCase()
|
||||||
|
.split("_")
|
||||||
|
.map(chunk => capitalize(chunk))
|
||||||
|
.join(""),
|
||||||
|
)
|
||||||
|
.filter(event => !queryEvents.includes(event));
|
||||||
|
|
||||||
|
if (newEvents.length > 0) {
|
||||||
|
// TODO modify AST
|
||||||
|
|
||||||
|
const inserted = query.replace(/\n/g, " ").replace(
|
||||||
|
" } } ",
|
||||||
|
newEvents
|
||||||
|
.map(event => ` ... on ${event} { __typename }`)
|
||||||
|
.join("")
|
||||||
|
.concat(" } } "),
|
||||||
|
);
|
||||||
|
setQuery(print(parse(inserted)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,13 +14,16 @@ export function isUnnamed(webhook: WebhookFragment | undefined): boolean {
|
||||||
type WebhookEventType = WebhookEventTypeSyncEnum | WebhookEventTypeAsyncEnum;
|
type WebhookEventType = WebhookEventTypeSyncEnum | WebhookEventTypeAsyncEnum;
|
||||||
|
|
||||||
const isWebhookInPreview = (webhook: WebhookEventType) =>
|
const isWebhookInPreview = (webhook: WebhookEventType) =>
|
||||||
([
|
(
|
||||||
|
[
|
||||||
WebhookEventTypeSyncEnum.CHECKOUT_CALCULATE_TAXES,
|
WebhookEventTypeSyncEnum.CHECKOUT_CALCULATE_TAXES,
|
||||||
WebhookEventTypeSyncEnum.ORDER_CALCULATE_TAXES,
|
WebhookEventTypeSyncEnum.ORDER_CALCULATE_TAXES,
|
||||||
] as WebhookEventType[]).includes(webhook);
|
] as WebhookEventType[]
|
||||||
|
).includes(webhook);
|
||||||
|
|
||||||
const isAsyncWebhookInPreview = (webhook: WebhookEventType) =>
|
const isAsyncWebhookInPreview = (webhook: WebhookEventType) =>
|
||||||
([
|
(
|
||||||
|
[
|
||||||
WebhookEventTypeAsyncEnum.GIFT_CARD_METADATA_UPDATED,
|
WebhookEventTypeAsyncEnum.GIFT_CARD_METADATA_UPDATED,
|
||||||
WebhookEventTypeAsyncEnum.ORDER_METADATA_UPDATED,
|
WebhookEventTypeAsyncEnum.ORDER_METADATA_UPDATED,
|
||||||
WebhookEventTypeAsyncEnum.CUSTOMER_METADATA_UPDATED,
|
WebhookEventTypeAsyncEnum.CUSTOMER_METADATA_UPDATED,
|
||||||
|
@ -33,7 +36,8 @@ const isAsyncWebhookInPreview = (webhook: WebhookEventType) =>
|
||||||
WebhookEventTypeAsyncEnum.TRANSACTION_ITEM_METADATA_UPDATED,
|
WebhookEventTypeAsyncEnum.TRANSACTION_ITEM_METADATA_UPDATED,
|
||||||
WebhookEventTypeAsyncEnum.WAREHOUSE_METADATA_UPDATED,
|
WebhookEventTypeAsyncEnum.WAREHOUSE_METADATA_UPDATED,
|
||||||
WebhookEventTypeAsyncEnum.VOUCHER_METADATA_UPDATED,
|
WebhookEventTypeAsyncEnum.VOUCHER_METADATA_UPDATED,
|
||||||
] as WebhookEventType[]).includes(webhook);
|
] as WebhookEventType[]
|
||||||
|
).includes(webhook);
|
||||||
|
|
||||||
export function mapSyncEventsToChoices(
|
export function mapSyncEventsToChoices(
|
||||||
events: WebhookEventTypeSyncEnum[],
|
events: WebhookEventTypeSyncEnum[],
|
||||||
|
|
|
@ -58,6 +58,7 @@ export const CustomAppWebhookCreate: React.FC<CustomAppWebhookCreateProps> = ({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
secretKey: data.secretKey,
|
secretKey: data.secretKey,
|
||||||
targetUrl: data.targetUrl,
|
targetUrl: data.targetUrl,
|
||||||
|
query: data.subscriptionQuery,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -62,10 +62,12 @@ export const CustomAppWebhookDetails: React.FC<CustomAppWebhookDetailsProps> = (
|
||||||
name: data.name,
|
name: data.name,
|
||||||
secretKey: data.secretKey,
|
secretKey: data.secretKey,
|
||||||
targetUrl: data.targetUrl,
|
targetUrl: data.targetUrl,
|
||||||
|
query: data.subscriptionQuery,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!webhook && !loading) {
|
if (!webhook && !loading) {
|
||||||
return <NotFoundPage backHref={CustomAppUrls.resolveAppListUrl()} />;
|
return <NotFoundPage backHref={CustomAppUrls.resolveAppListUrl()} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,5 +23,6 @@ export const webhookDetailsFragment = gql`
|
||||||
}
|
}
|
||||||
secretKey
|
secretKey
|
||||||
targetUrl
|
targetUrl
|
||||||
|
subscriptionQuery
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2828,6 +2828,7 @@ export const WebhookDetailsFragmentDoc = gql`
|
||||||
}
|
}
|
||||||
secretKey
|
secretKey
|
||||||
targetUrl
|
targetUrl
|
||||||
|
subscriptionQuery
|
||||||
}
|
}
|
||||||
${WebhookFragmentDoc}`;
|
${WebhookFragmentDoc}`;
|
||||||
export const AppCreateDocument = gql`
|
export const AppCreateDocument = gql`
|
||||||
|
|
|
@ -6734,7 +6734,7 @@ export type WebhookCreateMutationVariables = Exact<{
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type WebhookCreateMutation = { __typename: 'Mutation', webhookCreate: { __typename: 'WebhookCreate', errors: Array<{ __typename: 'WebhookError', code: WebhookErrorCode, field: string | null, message: string | null }>, webhook: { __typename: 'Webhook', secretKey: string | null, targetUrl: string, id: string, name: string, isActive: boolean, syncEvents: Array<{ __typename: 'WebhookEventSync', eventType: WebhookEventTypeSyncEnum }>, asyncEvents: Array<{ __typename: 'WebhookEventAsync', eventType: WebhookEventTypeAsyncEnum }>, app: { __typename: 'App', id: string, name: string | null } } | null } | null };
|
export type WebhookCreateMutation = { __typename: 'Mutation', webhookCreate: { __typename: 'WebhookCreate', errors: Array<{ __typename: 'WebhookError', code: WebhookErrorCode, field: string | null, message: string | null }>, webhook: { __typename: 'Webhook', secretKey: string | null, targetUrl: string, subscriptionQuery: string | null, id: string, name: string, isActive: boolean, syncEvents: Array<{ __typename: 'WebhookEventSync', eventType: WebhookEventTypeSyncEnum }>, asyncEvents: Array<{ __typename: 'WebhookEventAsync', eventType: WebhookEventTypeAsyncEnum }>, app: { __typename: 'App', id: string, name: string | null } } | null } | null };
|
||||||
|
|
||||||
export type WebhookUpdateMutationVariables = Exact<{
|
export type WebhookUpdateMutationVariables = Exact<{
|
||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
|
@ -6742,7 +6742,7 @@ export type WebhookUpdateMutationVariables = Exact<{
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type WebhookUpdateMutation = { __typename: 'Mutation', webhookUpdate: { __typename: 'WebhookUpdate', errors: Array<{ __typename: 'WebhookError', code: WebhookErrorCode, field: string | null, message: string | null }>, webhook: { __typename: 'Webhook', secretKey: string | null, targetUrl: string, id: string, name: string, isActive: boolean, syncEvents: Array<{ __typename: 'WebhookEventSync', eventType: WebhookEventTypeSyncEnum }>, asyncEvents: Array<{ __typename: 'WebhookEventAsync', eventType: WebhookEventTypeAsyncEnum }>, app: { __typename: 'App', id: string, name: string | null } } | null } | null };
|
export type WebhookUpdateMutation = { __typename: 'Mutation', webhookUpdate: { __typename: 'WebhookUpdate', errors: Array<{ __typename: 'WebhookError', code: WebhookErrorCode, field: string | null, message: string | null }>, webhook: { __typename: 'Webhook', secretKey: string | null, targetUrl: string, subscriptionQuery: string | null, id: string, name: string, isActive: boolean, syncEvents: Array<{ __typename: 'WebhookEventSync', eventType: WebhookEventTypeSyncEnum }>, asyncEvents: Array<{ __typename: 'WebhookEventAsync', eventType: WebhookEventTypeAsyncEnum }>, app: { __typename: 'App', id: string, name: string | null } } | null } | null };
|
||||||
|
|
||||||
export type WebhookDeleteMutationVariables = Exact<{
|
export type WebhookDeleteMutationVariables = Exact<{
|
||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
|
@ -6756,7 +6756,7 @@ export type WebhookDetailsQueryVariables = Exact<{
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type WebhookDetailsQuery = { __typename: 'Query', webhook: { __typename: 'Webhook', secretKey: string | null, targetUrl: string, id: string, name: string, isActive: boolean, syncEvents: Array<{ __typename: 'WebhookEventSync', eventType: WebhookEventTypeSyncEnum }>, asyncEvents: Array<{ __typename: 'WebhookEventAsync', eventType: WebhookEventTypeAsyncEnum }>, app: { __typename: 'App', id: string, name: string | null } } | null };
|
export type WebhookDetailsQuery = { __typename: 'Query', webhook: { __typename: 'Webhook', secretKey: string | null, targetUrl: string, subscriptionQuery: string | null, id: string, name: string, isActive: boolean, syncEvents: Array<{ __typename: 'WebhookEventSync', eventType: WebhookEventTypeSyncEnum }>, asyncEvents: Array<{ __typename: 'WebhookEventAsync', eventType: WebhookEventTypeAsyncEnum }>, app: { __typename: 'App', id: string, name: string | null } } | null };
|
||||||
|
|
||||||
export type UpdateCustomerMutationVariables = Exact<{
|
export type UpdateCustomerMutationVariables = Exact<{
|
||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
|
@ -7491,7 +7491,7 @@ export type WarehouseDetailsFragment = { __typename: 'Warehouse', isPrivate: boo
|
||||||
|
|
||||||
export type WebhookFragment = { __typename: 'Webhook', id: string, name: string, isActive: boolean, app: { __typename: 'App', id: string, name: string | null } };
|
export type WebhookFragment = { __typename: 'Webhook', id: string, name: string, isActive: boolean, app: { __typename: 'App', id: string, name: string | null } };
|
||||||
|
|
||||||
export type WebhookDetailsFragment = { __typename: 'Webhook', secretKey: string | null, targetUrl: string, id: string, name: string, isActive: boolean, syncEvents: Array<{ __typename: 'WebhookEventSync', eventType: WebhookEventTypeSyncEnum }>, asyncEvents: Array<{ __typename: 'WebhookEventAsync', eventType: WebhookEventTypeAsyncEnum }>, app: { __typename: 'App', id: string, name: string | null } };
|
export type WebhookDetailsFragment = { __typename: 'Webhook', secretKey: string | null, targetUrl: string, subscriptionQuery: string | null, id: string, name: string, isActive: boolean, syncEvents: Array<{ __typename: 'WebhookEventSync', eventType: WebhookEventTypeSyncEnum }>, asyncEvents: Array<{ __typename: 'WebhookEventAsync', eventType: WebhookEventTypeAsyncEnum }>, app: { __typename: 'App', id: string, name: string | null } };
|
||||||
|
|
||||||
export type WeightFragment = { __typename: 'Weight', unit: WeightUnitsEnum, value: number };
|
export type WeightFragment = { __typename: 'Weight', unit: WeightUnitsEnum, value: number };
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,22 @@ import { configure } from "@testing-library/react";
|
||||||
|
|
||||||
document.getElementById = () => document.createElement("div");
|
document.getElementById = () => document.createElement("div");
|
||||||
|
|
||||||
|
// workaround for `jsdom`
|
||||||
|
// https://github.com/jsdom/jsdom/issues/3002
|
||||||
|
document.createRange = () => {
|
||||||
|
const range = new Range();
|
||||||
|
|
||||||
|
range.getBoundingClientRect = jest.fn();
|
||||||
|
|
||||||
|
range.getClientRects = () => ({
|
||||||
|
item: () => null,
|
||||||
|
length: 0,
|
||||||
|
[Symbol.iterator]: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return range;
|
||||||
|
};
|
||||||
|
|
||||||
window.__SALEOR_CONFIG__ = {
|
window.__SALEOR_CONFIG__ = {
|
||||||
API_URL: "http://localhost:8000/graphql/",
|
API_URL: "http://localhost:8000/graphql/",
|
||||||
APP_MOUNT_URI: "/",
|
APP_MOUNT_URI: "/",
|
||||||
|
|
Loading…
Reference in a new issue