Add WebhookHeaders component (#3107)
* Add custom request headers to webhook form
This commit is contained in:
parent
0093f8dad4
commit
ab2ce01c8a
20 changed files with 713 additions and 16 deletions
|
@ -20,6 +20,7 @@ All notable, unreleased changes to this project will be documented in this file.
|
|||
- Add GraphiQL editor to webhook form for defining the subscription query #2885 by @2can @zaiste
|
||||
- Add redirect to GraphiQL from product & order details pages - #2940 by @zaiste
|
||||
- Extract permissions for subscription query - #3155 by @zaiste
|
||||
- Add custom request headers to webhook form - #3107 by @2can
|
||||
|
||||
## 3.4
|
||||
|
||||
|
|
|
@ -3811,6 +3811,12 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "INVALID_CUSTOM_HEADERS",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "MANIFEST_URL_CANT_CONNECT",
|
||||
"description": null,
|
||||
|
@ -56083,13 +56089,9 @@
|
|||
"name": "oldPassword",
|
||||
"description": "Current user password.",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
|
@ -112428,6 +112430,18 @@
|
|||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "customHeaders",
|
||||
"description": "Custom headers, which will be added to HTTP request.\n\nAdded in Saleor 3.12.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "JSONString",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
@ -112649,6 +112663,18 @@
|
|||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "customHeaders",
|
||||
"description": "Custom headers, which will be added to HTTP request. There is a limitation of 5 headers per webhook and 998 characters per header.Only \"X-*\" and \"Authorization*\" keys are allowed.\n\nAdded in Saleor 3.12.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "JSONString",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
|
@ -113007,6 +113033,12 @@
|
|||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "INVALID_CUSTOM_HEADERS",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
|
@ -115715,6 +115747,18 @@
|
|||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "customHeaders",
|
||||
"description": "Custom headers, which will be added to HTTP request. There is a limitation of 5 headers per webhook and 998 characters per header.Only \"X-*\" and \"Authorization*\" keys are allowed.\n\nAdded in Saleor 3.12.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "JSONString",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
|
|
|
@ -156,6 +156,10 @@
|
|||
"context": "product type shipping settings, section header",
|
||||
"string": "Shipping"
|
||||
},
|
||||
"/4bJkA": {
|
||||
"context": "header field value, header",
|
||||
"string": "Value"
|
||||
},
|
||||
"/5r4he": {
|
||||
"context": "label for button",
|
||||
"string": "Add country"
|
||||
|
@ -550,6 +554,10 @@
|
|||
"context": "gift card not found message",
|
||||
"string": "Couldn't find gift card"
|
||||
},
|
||||
"2BHjVL": {
|
||||
"context": "header",
|
||||
"string": "Custom request headers"
|
||||
},
|
||||
"2CBcub": {
|
||||
"context": "CreateVariantTitle manage",
|
||||
"string": "Manage"
|
||||
|
@ -1443,6 +1451,10 @@
|
|||
"9UHfux": {
|
||||
"string": "Voucher Specific Information"
|
||||
},
|
||||
"9Y5i/8": {
|
||||
"context": "number of webhook headers in model",
|
||||
"string": "{number,plural,one{{number} header} other{{number} custom request headers}}"
|
||||
},
|
||||
"9Y6vg+": {
|
||||
"context": "dialog header",
|
||||
"string": "Add product"
|
||||
|
@ -3375,6 +3387,10 @@
|
|||
"context": "channels section name",
|
||||
"string": "Channels"
|
||||
},
|
||||
"No4lyL": {
|
||||
"context": "header field name, header",
|
||||
"string": "Name"
|
||||
},
|
||||
"Np7J92": {
|
||||
"context": "deactivate local named app",
|
||||
"string": "Are you sure you want to disable {name}? Your data will be kept until you reactivate the app."
|
||||
|
@ -5185,6 +5201,10 @@
|
|||
"b+jcaN": {
|
||||
"string": "There are still fulfillments created for this order. Cancel the fulfillments first before you cancel the order."
|
||||
},
|
||||
"b1t9bM": {
|
||||
"context": "empty headers text",
|
||||
"string": "No custom request headers created for this webhook. Use the button below to add new custom request header."
|
||||
},
|
||||
"b1zuN9": {
|
||||
"string": "Price"
|
||||
},
|
||||
|
@ -5980,6 +6000,10 @@
|
|||
"context": "sale start date",
|
||||
"string": "Starts"
|
||||
},
|
||||
"iERn5G": {
|
||||
"context": "header name input",
|
||||
"string": "Should start with `x-` or `authorization`"
|
||||
},
|
||||
"iEeIhY": {
|
||||
"context": "draft order",
|
||||
"string": "Customer"
|
||||
|
@ -7495,6 +7519,10 @@
|
|||
"uMpv1v": {
|
||||
"string": "Fulfillment successfully cancelled"
|
||||
},
|
||||
"uQNm59": {
|
||||
"context": "add header,button",
|
||||
"string": "Add custom request header"
|
||||
},
|
||||
"uQk8gB": {
|
||||
"context": "export all items to csv file",
|
||||
"string": "All gift cards ({number})"
|
||||
|
@ -7776,6 +7804,10 @@
|
|||
"context": "see error log label in notification",
|
||||
"string": "See error log"
|
||||
},
|
||||
"wChjN/": {
|
||||
"context": "accepted header names",
|
||||
"string": "Headers with in following format are accepted: `authorization*`, `x-*`"
|
||||
},
|
||||
"wDUBLR": {
|
||||
"context": "order refund amount",
|
||||
"string": "Proposed refund amount"
|
||||
|
|
|
@ -727,6 +727,7 @@ enum AppErrorCode {
|
|||
INVALID_PERMISSION
|
||||
INVALID_URL_FORMAT
|
||||
INVALID_MANIFEST_FORMAT
|
||||
INVALID_CUSTOM_HEADERS
|
||||
MANIFEST_URL_CANT_CONNECT
|
||||
NOT_FOUND
|
||||
REQUIRED
|
||||
|
@ -12947,7 +12948,7 @@ type Mutation {
|
|||
newPassword: String!
|
||||
|
||||
"""Current user password."""
|
||||
oldPassword: String!
|
||||
oldPassword: String
|
||||
): PasswordChange
|
||||
|
||||
"""
|
||||
|
@ -26045,6 +26046,15 @@ type Webhook implements Node {
|
|||
|
||||
"""Used to define payloads for specific events."""
|
||||
subscriptionQuery: String
|
||||
|
||||
"""
|
||||
Custom headers, which will be added to HTTP request.
|
||||
|
||||
Added in Saleor 3.12.
|
||||
|
||||
Note: this API is currently in Feature Preview and can be subject to changes at later point.
|
||||
"""
|
||||
customHeaders: JSONString
|
||||
}
|
||||
|
||||
"""
|
||||
|
@ -26099,6 +26109,15 @@ input WebhookCreateInput {
|
|||
Note: this API is currently in Feature Preview and can be subject to changes at later point.
|
||||
"""
|
||||
query: String
|
||||
|
||||
"""
|
||||
Custom headers, which will be added to HTTP request. There is a limitation of 5 headers per webhook and 998 characters per header.Only "X-*" and "Authorization*" keys are allowed.
|
||||
|
||||
Added in Saleor 3.12.
|
||||
|
||||
Note: this API is currently in Feature Preview and can be subject to changes at later point.
|
||||
"""
|
||||
customHeaders: JSONString
|
||||
}
|
||||
|
||||
"""
|
||||
|
@ -26178,6 +26197,7 @@ enum WebhookErrorCode {
|
|||
MISSING_SUBSCRIPTION
|
||||
UNABLE_TO_PARSE
|
||||
MISSING_EVENT
|
||||
INVALID_CUSTOM_HEADERS
|
||||
}
|
||||
|
||||
"""Webhook event."""
|
||||
|
@ -27380,6 +27400,15 @@ input WebhookUpdateInput {
|
|||
Note: this API is currently in Feature Preview and can be subject to changes at later point.
|
||||
"""
|
||||
query: String
|
||||
|
||||
"""
|
||||
Custom headers, which will be added to HTTP request. There is a limitation of 5 headers per webhook and 998 characters per header.Only "X-*" and "Authorization*" keys are allowed.
|
||||
|
||||
Added in Saleor 3.12.
|
||||
|
||||
Note: this API is currently in Feature Preview and can be subject to changes at later point.
|
||||
"""
|
||||
customHeaders: JSONString
|
||||
}
|
||||
|
||||
"""Represents weight value in a specific weight unit."""
|
||||
|
|
|
@ -27,6 +27,7 @@ import React, { useEffect, useState } from "react";
|
|||
import { useIntl } from "react-intl";
|
||||
|
||||
import PermissionAlert from "../PermissionAlert";
|
||||
import WebhookHeaders from "../WebhookHeaders";
|
||||
import WebhookSubscriptionQuery from "../WebhookSubscriptionQuery";
|
||||
import { getHeaderTitle } from "./messages";
|
||||
|
||||
|
@ -38,6 +39,7 @@ export interface WebhookFormData {
|
|||
secretKey?: string;
|
||||
targetUrl: string;
|
||||
subscriptionQuery: string;
|
||||
customHeaders: string;
|
||||
}
|
||||
|
||||
export interface WebhookDetailsPageProps {
|
||||
|
@ -63,7 +65,7 @@ const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
|
|||
|
||||
let prettified: string;
|
||||
try {
|
||||
prettified = print(parse(webhook?.subscriptionQuery));
|
||||
prettified = print(parse(webhook?.subscriptionQuery || ""));
|
||||
} catch {
|
||||
prettified = webhook?.subscriptionQuery || "";
|
||||
}
|
||||
|
@ -76,6 +78,7 @@ const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
|
|||
secretKey: webhook?.secretKey || "",
|
||||
targetUrl: webhook?.targetUrl || "",
|
||||
subscriptionQuery: prettified || "",
|
||||
customHeaders: webhook?.customHeaders || "{}",
|
||||
};
|
||||
|
||||
const backUrl = CustomAppUrls.resolveAppUrl(appId);
|
||||
|
@ -134,6 +137,8 @@ const WebhookDetailsPage: React.FC<WebhookDetailsPageProps> = ({
|
|||
/>
|
||||
<FormSpacer />
|
||||
<PermissionAlert query={query} />
|
||||
<FormSpacer />
|
||||
<WebhookHeaders data={data} onChange={change} />
|
||||
</Box>
|
||||
</Content>
|
||||
<Savebar
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import useForm from "@dashboard/hooks/useForm";
|
||||
import Wrapper from "@test/wrapper";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
|
||||
import { customHeaders } from "./utils.test";
|
||||
import WebhookHeaders, { WebhookHeadersProps } from "./WebhookHeaders";
|
||||
|
||||
export const props: WebhookHeadersProps = {
|
||||
data: {
|
||||
syncEvents: [],
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "Test webhook",
|
||||
targetUrl: "http://localhost:3000",
|
||||
subscriptionQuery: "",
|
||||
customHeaders,
|
||||
},
|
||||
onChange: () => undefined,
|
||||
};
|
||||
|
||||
const Component = () => {
|
||||
const { change, data } = useForm(props.data, jest.fn());
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<WebhookHeaders data={data} onChange={change} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const getExpandIcon = () => screen.getByTestId("expand");
|
||||
|
||||
describe("WebhookHeaders", () => {
|
||||
it("is available on the webhook page", async () => {
|
||||
// Arrange
|
||||
render(<Component />);
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId("webhook-headers-editor")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("can expand field", async () => {
|
||||
// Arrange
|
||||
render(<Component />);
|
||||
const user = userEvent.setup();
|
||||
const isExpandedAttribute = "data-test-expanded";
|
||||
const editor = screen.getByTestId("webhook-headers-editor");
|
||||
// Assert
|
||||
expect(editor).toHaveAttribute(isExpandedAttribute, "true");
|
||||
// Act
|
||||
await act(async () => {
|
||||
await user.click(getExpandIcon());
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(editor).toHaveAttribute(isExpandedAttribute, "false");
|
||||
});
|
||||
});
|
159
src/custom-apps/components/WebhookHeaders/WebhookHeaders.tsx
Normal file
159
src/custom-apps/components/WebhookHeaders/WebhookHeaders.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import Skeleton from "@dashboard/components/Skeleton";
|
||||
import TableRowLink from "@dashboard/components/TableRowLink";
|
||||
import { FormChange } from "@dashboard/hooks/useForm";
|
||||
import {
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { ExpandIcon, IconButton } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { WebhookFormData } from "../WebhookDetailsPage";
|
||||
import { messages } from "./messages";
|
||||
import useStyles from "./styles";
|
||||
import { mapHeaders, stringifyHeaders } from "./utils";
|
||||
import WebhookHeadersTableBody from "./WebhookHeadersTableBody";
|
||||
|
||||
export interface WebhookHeadersProps {
|
||||
data: WebhookFormData;
|
||||
onChange: FormChange;
|
||||
}
|
||||
|
||||
const WebhookHeaders: React.FC<WebhookHeadersProps> = ({
|
||||
data: { customHeaders },
|
||||
onChange,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const classes = useStyles();
|
||||
const headers = useMemo(() => mapHeaders(customHeaders), [customHeaders]);
|
||||
|
||||
useEffect(() => {
|
||||
if (headers.length > 0) {
|
||||
setExpanded(true);
|
||||
}
|
||||
}, [headers.length]);
|
||||
|
||||
const add = () => {
|
||||
const items = [...headers];
|
||||
items.push({ name: "", value: "", error: false });
|
||||
|
||||
onChange({
|
||||
target: {
|
||||
name: "customHeaders",
|
||||
value: stringifyHeaders(items),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card data-test-id="webhook-headers-editor" data-test-expanded={expanded}>
|
||||
<CardTitle
|
||||
className={classes.header}
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage(messages.header)}
|
||||
<IconButton
|
||||
className={clsx(classes.expandBtn, {
|
||||
[classes.rotate]: expanded,
|
||||
})}
|
||||
hoverOutline={false}
|
||||
variant="secondary"
|
||||
data-test-id="expand"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<ExpandIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{headers === undefined ? (
|
||||
<CardContent>
|
||||
<Skeleton />
|
||||
</CardContent>
|
||||
) : (
|
||||
<>
|
||||
<CardContent className={classes.content}>
|
||||
{headers.length > 0 && (
|
||||
<Typography color="textSecondary" variant="body2">
|
||||
<FormattedMessage
|
||||
{...messages.headersCount}
|
||||
values={{
|
||||
number: headers.length,
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
{expanded && (
|
||||
<>
|
||||
{headers.length === 0 ? (
|
||||
<CardContent className={classes.emptyContainer}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<FormattedMessage {...messages.noHeaders} />
|
||||
</Typography>
|
||||
</CardContent>
|
||||
) : (
|
||||
<>
|
||||
<CardContent>
|
||||
<Typography variant="body2">
|
||||
<FormattedMessage {...messages.acceptedFormat} />
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
<Table className={classes.table}>
|
||||
<TableHead>
|
||||
<TableRowLink>
|
||||
<TableCell
|
||||
className={clsx(
|
||||
classes.colNameHeader,
|
||||
classes.tableCell,
|
||||
)}
|
||||
>
|
||||
<FormattedMessage {...messages.headerName} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={clsx(classes.colValue, classes.tableCell)}
|
||||
>
|
||||
<FormattedMessage {...messages.headerValue} />
|
||||
</TableCell>
|
||||
<TableCell className={classes.colActionHeader}>
|
||||
<FormattedMessage {...messages.actions} />
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
</TableHead>
|
||||
<WebhookHeadersTableBody
|
||||
onChange={onChange}
|
||||
headers={headers}
|
||||
/>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
<CardActions className={classes.actions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
data-test-id="add-header"
|
||||
onClick={add}
|
||||
>
|
||||
<FormattedMessage {...messages.add} />
|
||||
</Button>
|
||||
</CardActions>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
WebhookHeaders.displayName = "WebhookHeaders";
|
||||
export default WebhookHeaders;
|
|
@ -0,0 +1,125 @@
|
|||
import TableRowLink from "@dashboard/components/TableRowLink";
|
||||
import { FormChange } from "@dashboard/hooks/useForm";
|
||||
import { removeAtIndex, updateAtIndex } from "@dashboard/utils/lists";
|
||||
import { TableBody, TableCell, TextField } from "@material-ui/core";
|
||||
import { DeleteIcon, IconButton } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import React, { ChangeEvent } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { messages } from "./messages";
|
||||
import useStyles from "./styles";
|
||||
import { Header, stringifyHeaders } from "./utils";
|
||||
|
||||
const nameSeparator = ":";
|
||||
const nameInputPrefix = "name";
|
||||
const valueInputPrefix = "value";
|
||||
|
||||
export interface WebhookHeadersTableBodyProps {
|
||||
onChange: FormChange;
|
||||
headers: Header[];
|
||||
}
|
||||
|
||||
const WebhookHeadersTableBody: React.FC<WebhookHeadersTableBodyProps> = ({
|
||||
onChange,
|
||||
headers,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const intl = useIntl();
|
||||
|
||||
const updateWebhookItem = (target: EventTarget & HTMLTextAreaElement) => {
|
||||
const { name, value } = target;
|
||||
const [field, index] = name.split(nameSeparator);
|
||||
|
||||
const item: Header = headers[index];
|
||||
|
||||
// lowercase header name
|
||||
if (field === nameInputPrefix) {
|
||||
item[field] = value.toLowerCase();
|
||||
} else {
|
||||
item[field] = value;
|
||||
}
|
||||
|
||||
return {
|
||||
item,
|
||||
index: parseInt(index, 10),
|
||||
};
|
||||
};
|
||||
|
||||
const change = ({ target }: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const { item, index } = updateWebhookItem(target);
|
||||
|
||||
onChange({
|
||||
target: {
|
||||
name: "customHeaders",
|
||||
value: stringifyHeaders(updateAtIndex(item, headers, index)),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TableBody>
|
||||
{headers.map((field, fieldIndex) => (
|
||||
<TableRowLink data-test-id="field" key={fieldIndex}>
|
||||
<TableCell className={clsx(classes.colName, classes.tableCell)}>
|
||||
<TextField
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input,
|
||||
},
|
||||
}}
|
||||
inputProps={{
|
||||
"aria-label": `${nameInputPrefix}${nameSeparator}${fieldIndex}`,
|
||||
}}
|
||||
name={`${nameInputPrefix}${nameSeparator}${fieldIndex}`}
|
||||
fullWidth
|
||||
onChange={change}
|
||||
value={field.name}
|
||||
error={field.error}
|
||||
helperText={
|
||||
(field.error && intl.formatMessage(messages.headerNameError)) ||
|
||||
" "
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={clsx(classes.colValue, classes.tableCell)}>
|
||||
<TextField
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input,
|
||||
},
|
||||
}}
|
||||
inputProps={{
|
||||
"aria-label": `${valueInputPrefix}${nameSeparator}${fieldIndex}`,
|
||||
}}
|
||||
name={`${valueInputPrefix}${nameSeparator}${fieldIndex}`}
|
||||
fullWidth
|
||||
onChange={change}
|
||||
value={field.value}
|
||||
helperText={" "}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colAction}>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
data-test-id={"delete-field-" + fieldIndex}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
target: {
|
||||
name: "customHeaders",
|
||||
value: stringifyHeaders(removeAtIndex(headers, fieldIndex)),
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
))}
|
||||
</TableBody>
|
||||
);
|
||||
};
|
||||
|
||||
WebhookHeadersTableBody.displayName = "WebhookHeadersTableRow";
|
||||
export default WebhookHeadersTableBody;
|
2
src/custom-apps/components/WebhookHeaders/index.ts
Normal file
2
src/custom-apps/components/WebhookHeaders/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./WebhookHeaders";
|
||||
export * from "./WebhookHeaders";
|
52
src/custom-apps/components/WebhookHeaders/messages.ts
Normal file
52
src/custom-apps/components/WebhookHeaders/messages.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const messages = defineMessages({
|
||||
header: {
|
||||
id: "2BHjVL",
|
||||
defaultMessage: "Custom request headers",
|
||||
description: "header",
|
||||
},
|
||||
noHeaders: {
|
||||
id: "b1t9bM",
|
||||
defaultMessage:
|
||||
"No custom request headers created for this webhook. Use the button below to add new custom request header.",
|
||||
description: "empty headers text",
|
||||
},
|
||||
acceptedFormat: {
|
||||
id: "wChjN/",
|
||||
defaultMessage:
|
||||
"Headers with in following format are accepted: `authorization*`, `x-*`",
|
||||
description: "accepted header names",
|
||||
},
|
||||
headerName: {
|
||||
defaultMessage: "Name",
|
||||
id: "No4lyL",
|
||||
description: "header field name, header",
|
||||
},
|
||||
headerNameError: {
|
||||
id: "iERn5G",
|
||||
defaultMessage: "Should start with `x-` or `authorization`",
|
||||
description: "header name input",
|
||||
},
|
||||
headerValue: {
|
||||
id: "/4bJkA",
|
||||
defaultMessage: "Value",
|
||||
description: "header field value, header",
|
||||
},
|
||||
actions: {
|
||||
id: "nEixpu",
|
||||
defaultMessage: "Actions",
|
||||
description: "table action",
|
||||
},
|
||||
add: {
|
||||
id: "uQNm59",
|
||||
defaultMessage: "Add custom request header",
|
||||
description: "add header,button",
|
||||
},
|
||||
headersCount: {
|
||||
id: "9Y5i/8",
|
||||
defaultMessage:
|
||||
"{number,plural,one{{number} header} other{{number} custom request headers}}",
|
||||
description: "number of webhook headers in model",
|
||||
},
|
||||
});
|
94
src/custom-apps/components/WebhookHeaders/styles.ts
Normal file
94
src/custom-apps/components/WebhookHeaders/styles.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => {
|
||||
const colAction: React.CSSProperties = {
|
||||
textAlign: "right",
|
||||
width: 130,
|
||||
};
|
||||
const colName: React.CSSProperties = {
|
||||
width: 250,
|
||||
textAlign: "right",
|
||||
};
|
||||
|
||||
return {
|
||||
tableCell: {
|
||||
paddingTop: theme.spacing(3),
|
||||
paddingLeft: theme.spacing(1),
|
||||
paddingRight: theme.spacing(1),
|
||||
|
||||
"& .MuiFormHelperText-root": {
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
"&.MuiTableCell-root:first-child:not(.MuiTableCell-paddingCheckbox)": {
|
||||
paddingRight: theme.spacing(0),
|
||||
paddingTop: theme.spacing(3),
|
||||
},
|
||||
},
|
||||
colAction: {
|
||||
"&:last-child": {
|
||||
...colAction,
|
||||
paddingRight: theme.spacing(3),
|
||||
},
|
||||
},
|
||||
colActionHeader: {
|
||||
...colAction,
|
||||
},
|
||||
colName: {
|
||||
...colName,
|
||||
},
|
||||
colNameHeader: {
|
||||
...colName,
|
||||
},
|
||||
colValue: {},
|
||||
actions: {
|
||||
"&&": {
|
||||
paddingBottom: theme.spacing(2),
|
||||
paddingTop: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
content: {
|
||||
paddingBottom: 0,
|
||||
paddingTop: theme.spacing(),
|
||||
},
|
||||
emptyContainer: {
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0,
|
||||
},
|
||||
expandBtn: {
|
||||
position: "relative",
|
||||
left: theme.spacing(1),
|
||||
top: -2,
|
||||
transition: theme.transitions.create("transform", {
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}),
|
||||
border: 0,
|
||||
},
|
||||
header: {
|
||||
"&&": {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
input: {
|
||||
padding: theme.spacing(1.5, 2),
|
||||
},
|
||||
table: {
|
||||
marginTop: theme.spacing(2),
|
||||
tableLayout: "fixed",
|
||||
|
||||
head: {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
rotate: {
|
||||
transform: "rotate(-180deg)",
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
name: "WebhookHeaders",
|
||||
},
|
||||
);
|
||||
|
||||
export default useStyles;
|
35
src/custom-apps/components/WebhookHeaders/utils.test.ts
Normal file
35
src/custom-apps/components/WebhookHeaders/utils.test.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {
|
||||
mapHeaders,
|
||||
stringifyHeaders,
|
||||
} from "@dashboard/custom-apps/components/WebhookHeaders/utils";
|
||||
|
||||
export const customHeaders = '{"x-auth-token":"ABC","authorization":"XYZ"}';
|
||||
|
||||
const parsedHeaders = [
|
||||
{
|
||||
name: "x-auth-token",
|
||||
value: "ABC",
|
||||
error: false,
|
||||
},
|
||||
{
|
||||
name: "authorization",
|
||||
value: "XYZ",
|
||||
error: false,
|
||||
},
|
||||
];
|
||||
|
||||
describe("mapHeaders", () => {
|
||||
it("should map customHeaders string to Object", () => {
|
||||
const headers = mapHeaders(customHeaders);
|
||||
|
||||
expect(headers).toEqual(parsedHeaders);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stringifyHeaders", () => {
|
||||
it("should return stringified headers", () => {
|
||||
const stringified = stringifyHeaders(parsedHeaders);
|
||||
|
||||
expect(stringified).toEqual(customHeaders);
|
||||
});
|
||||
});
|
35
src/custom-apps/components/WebhookHeaders/utils.ts
Normal file
35
src/custom-apps/components/WebhookHeaders/utils.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import keyBy from "lodash/keyBy";
|
||||
import mapValues from "lodash/mapValues";
|
||||
|
||||
export interface Header {
|
||||
name: string;
|
||||
value: string;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
export const stringifyHeaders = (headers: Header[]): string =>
|
||||
JSON.stringify(mapValues(keyBy(headers, "name"), "value"));
|
||||
|
||||
const validateName = (name: string) => {
|
||||
if (
|
||||
name.toLowerCase().match("(^x$)|(^x-)|(^authorization$)|(^authorization-)")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const mapHeaders = (customHeaders: string): Header[] => {
|
||||
const parsedHeaders = JSON.parse(customHeaders);
|
||||
|
||||
return Object.keys(parsedHeaders).map(key => ({
|
||||
name: key,
|
||||
value: parsedHeaders[key],
|
||||
error: validateName(key),
|
||||
}));
|
||||
};
|
|
@ -47,6 +47,7 @@ describe("WebhookSubscriptionQuery", () => {
|
|||
name: "",
|
||||
targetUrl: "",
|
||||
subscriptionQuery: "",
|
||||
customHeaders: "",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -16,4 +16,5 @@ export const webhook: WebhookDetailsFragment = {
|
|||
subscriptionQuery:
|
||||
"subscription { event { ... on ProductUpdated { product { name } } } }",
|
||||
targetUrl: "http://www.getsaleor.com",
|
||||
customHeaders: "{}",
|
||||
};
|
||||
|
|
|
@ -63,6 +63,7 @@ export const CustomAppWebhookDetails: React.FC<
|
|||
secretKey: data.secretKey,
|
||||
targetUrl: data.targetUrl,
|
||||
query: data.subscriptionQuery,
|
||||
customHeaders: data.customHeaders,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -24,5 +24,6 @@ export const webhookDetailsFragment = gql`
|
|||
secretKey
|
||||
targetUrl
|
||||
subscriptionQuery
|
||||
customHeaders
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -2841,6 +2841,7 @@ export const WebhookDetailsFragmentDoc = gql`
|
|||
secretKey
|
||||
targetUrl
|
||||
subscriptionQuery
|
||||
customHeaders
|
||||
}
|
||||
${WebhookFragmentDoc}`;
|
||||
export const AppCreateDocument = gql`
|
||||
|
|
|
@ -5545,7 +5545,7 @@ export type WarehouseUpdatedFieldPolicy = {
|
|||
recipient?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
warehouse?: FieldPolicy<any> | FieldReadFunction<any>
|
||||
};
|
||||
export type WebhookKeySpecifier = ('id' | 'name' | 'events' | 'syncEvents' | 'asyncEvents' | 'app' | 'eventDeliveries' | 'targetUrl' | 'isActive' | 'secretKey' | 'subscriptionQuery' | WebhookKeySpecifier)[];
|
||||
export type WebhookKeySpecifier = ('id' | 'name' | 'events' | 'syncEvents' | 'asyncEvents' | 'app' | 'eventDeliveries' | 'targetUrl' | 'isActive' | 'secretKey' | 'subscriptionQuery' | 'customHeaders' | WebhookKeySpecifier)[];
|
||||
export type WebhookFieldPolicy = {
|
||||
id?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
name?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
|
@ -5557,7 +5557,8 @@ export type WebhookFieldPolicy = {
|
|||
targetUrl?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
isActive?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
secretKey?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
subscriptionQuery?: FieldPolicy<any> | FieldReadFunction<any>
|
||||
subscriptionQuery?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
customHeaders?: FieldPolicy<any> | FieldReadFunction<any>
|
||||
};
|
||||
export type WebhookCreateKeySpecifier = ('webhookErrors' | 'errors' | 'webhook' | WebhookCreateKeySpecifier)[];
|
||||
export type WebhookCreateFieldPolicy = {
|
||||
|
|
|
@ -179,6 +179,7 @@ export enum AppErrorCode {
|
|||
INVALID_PERMISSION = 'INVALID_PERMISSION',
|
||||
INVALID_URL_FORMAT = 'INVALID_URL_FORMAT',
|
||||
INVALID_MANIFEST_FORMAT = 'INVALID_MANIFEST_FORMAT',
|
||||
INVALID_CUSTOM_HEADERS = 'INVALID_CUSTOM_HEADERS',
|
||||
MANIFEST_URL_CANT_CONNECT = 'MANIFEST_URL_CANT_CONNECT',
|
||||
NOT_FOUND = 'NOT_FOUND',
|
||||
REQUIRED = 'REQUIRED',
|
||||
|
@ -5813,6 +5814,14 @@ export type WebhookCreateInput = {
|
|||
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
|
||||
*/
|
||||
query?: InputMaybe<Scalars['String']>;
|
||||
/**
|
||||
* Custom headers, which will be added to HTTP request. There is a limitation of 5 headers per webhook and 998 characters per header.Only "X-*" and "Authorization*" keys are allowed.
|
||||
*
|
||||
* Added in Saleor 3.12.
|
||||
*
|
||||
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
|
||||
*/
|
||||
customHeaders?: InputMaybe<Scalars['JSONString']>;
|
||||
};
|
||||
|
||||
/** An enumeration. */
|
||||
|
@ -5839,7 +5848,8 @@ export enum WebhookErrorCode {
|
|||
SYNTAX = 'SYNTAX',
|
||||
MISSING_SUBSCRIPTION = 'MISSING_SUBSCRIPTION',
|
||||
UNABLE_TO_PARSE = 'UNABLE_TO_PARSE',
|
||||
MISSING_EVENT = 'MISSING_EVENT'
|
||||
MISSING_EVENT = 'MISSING_EVENT',
|
||||
INVALID_CUSTOM_HEADERS = 'INVALID_CUSTOM_HEADERS'
|
||||
}
|
||||
|
||||
/** Enum determining type of webhook. */
|
||||
|
@ -6712,6 +6722,14 @@ export type WebhookUpdateInput = {
|
|||
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
|
||||
*/
|
||||
query?: InputMaybe<Scalars['String']>;
|
||||
/**
|
||||
* Custom headers, which will be added to HTTP request. There is a limitation of 5 headers per webhook and 998 characters per header.Only "X-*" and "Authorization*" keys are allowed.
|
||||
*
|
||||
* Added in Saleor 3.12.
|
||||
*
|
||||
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
|
||||
*/
|
||||
customHeaders?: InputMaybe<Scalars['JSONString']>;
|
||||
};
|
||||
|
||||
/** An enumeration. */
|
||||
|
@ -7236,7 +7254,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, 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 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, customHeaders: any | 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<{
|
||||
id: Scalars['ID'];
|
||||
|
@ -7244,7 +7262,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, 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 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, customHeaders: any | 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<{
|
||||
id: Scalars['ID'];
|
||||
|
@ -7258,7 +7276,7 @@ export type WebhookDetailsQueryVariables = Exact<{
|
|||
}>;
|
||||
|
||||
|
||||
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 WebhookDetailsQuery = { __typename: 'Query', webhook: { __typename: 'Webhook', secretKey: string | null, targetUrl: string, subscriptionQuery: string | null, customHeaders: any | 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<{
|
||||
id: Scalars['ID'];
|
||||
|
@ -7999,7 +8017,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 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 WebhookDetailsFragment = { __typename: 'Webhook', secretKey: string | null, targetUrl: string, subscriptionQuery: string | null, customHeaders: any | 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 };
|
||||
|
||||
|
|
Loading…
Reference in a new issue