Add redirect to GraphiQL from product & order (#2940)
This commit is contained in:
parent
ffb16ff1ed
commit
b510d68fa9
12 changed files with 160 additions and 12 deletions
|
@ -18,6 +18,7 @@ All notable, unreleased changes to this project will be documented in this file.
|
||||||
- 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
|
- 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
|
||||||
|
|
||||||
## 3.4
|
## 3.4
|
||||||
|
|
||||||
|
|
|
@ -1015,6 +1015,10 @@
|
||||||
"context": "translations section name",
|
"context": "translations section name",
|
||||||
"string": "Translations"
|
"string": "Translations"
|
||||||
},
|
},
|
||||||
|
"5i/InH": {
|
||||||
|
"context": "open new window button",
|
||||||
|
"string": "Open this order in GraphiQL"
|
||||||
|
},
|
||||||
"5kvaFR": {
|
"5kvaFR": {
|
||||||
"context": "product field",
|
"context": "product field",
|
||||||
"string": "Export Variant SKU"
|
"string": "Export Variant SKU"
|
||||||
|
|
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -54,6 +54,7 @@
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"keycode": "^2.2.0",
|
"keycode": "^2.2.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
"lz-string": "^1.4.4",
|
||||||
"marked": "^4.0.17",
|
"marked": "^4.0.17",
|
||||||
"moment-timezone": "^0.5.32",
|
"moment-timezone": "^0.5.32",
|
||||||
"qs": "^6.9.0",
|
"qs": "^6.9.0",
|
||||||
|
@ -26945,7 +26946,6 @@
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
||||||
"integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==",
|
"integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==",
|
||||||
"optional": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"lz-string": "bin/bin.js"
|
"lz-string": "bin/bin.js"
|
||||||
}
|
}
|
||||||
|
@ -60149,8 +60149,7 @@
|
||||||
"lz-string": {
|
"lz-string": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
||||||
"integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==",
|
"integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ=="
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"macos-release": {
|
"macos-release": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"keycode": "^2.2.0",
|
"keycode": "^2.2.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
"lz-string": "^1.4.4",
|
||||||
"marked": "^4.0.17",
|
"marked": "^4.0.17",
|
||||||
"moment-timezone": "^0.5.32",
|
"moment-timezone": "^0.5.32",
|
||||||
"qs": "^6.9.0",
|
"qs": "^6.9.0",
|
||||||
|
|
|
@ -24,7 +24,9 @@ import { SubmitPromise } from "@dashboard/hooks/useForm";
|
||||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||||
import { sectionNames } from "@dashboard/intl";
|
import { sectionNames } from "@dashboard/intl";
|
||||||
import OrderChannelSectionCard from "@dashboard/orders/components/OrderChannelSectionCard";
|
import OrderChannelSectionCard from "@dashboard/orders/components/OrderChannelSectionCard";
|
||||||
|
import { defaultGraphiQLQuery } from "@dashboard/orders/queries";
|
||||||
import { orderListUrl } from "@dashboard/orders/urls";
|
import { orderListUrl } from "@dashboard/orders/urls";
|
||||||
|
import { playgroundOpenHandler } from "@dashboard/utils/graphql";
|
||||||
import { mapMetadataItemToInput } from "@dashboard/utils/maps";
|
import { mapMetadataItemToInput } from "@dashboard/utils/maps";
|
||||||
import useMetadataChangeTrigger from "@dashboard/utils/metadata/useMetadataChangeTrigger";
|
import useMetadataChangeTrigger from "@dashboard/utils/metadata/useMetadataChangeTrigger";
|
||||||
import { Typography } from "@material-ui/core";
|
import { Typography } from "@material-ui/core";
|
||||||
|
@ -196,6 +198,13 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
|
||||||
order?.id,
|
order?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openPlaygroundURL = playgroundOpenHandler({
|
||||||
|
query: defaultGraphiQLQuery,
|
||||||
|
headers: "",
|
||||||
|
operationName: "",
|
||||||
|
variables: `{ "id": "${order?.id}" }`,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form confirmLeave initial={initial} onSubmit={handleSubmit}>
|
<Form confirmLeave initial={initial} onSubmit={handleSubmit}>
|
||||||
{({ change, data, submit }) => {
|
{({ change, data, submit }) => {
|
||||||
|
@ -211,9 +220,19 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
|
||||||
inline
|
inline
|
||||||
title={<Title order={order} />}
|
title={<Title order={order} />}
|
||||||
cardMenu={
|
cardMenu={
|
||||||
|
<>
|
||||||
<CardMenu
|
<CardMenu
|
||||||
menuItems={[...selectCardMenuItems, ...extensionMenuItems]}
|
menuItems={[
|
||||||
|
...selectCardMenuItems,
|
||||||
|
...extensionMenuItems,
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.openGraphiQL),
|
||||||
|
onSelect: openPlaygroundURL,
|
||||||
|
testId: "graphiql-redirect",
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className={classes.date}>
|
<div className={classes.date}>
|
||||||
|
|
|
@ -16,4 +16,9 @@ export const messages = defineMessages({
|
||||||
defaultMessage: "Return / Replace order",
|
defaultMessage: "Return / Replace order",
|
||||||
description: "return button",
|
description: "return button",
|
||||||
},
|
},
|
||||||
|
openGraphiQL: {
|
||||||
|
id: "5i/InH",
|
||||||
|
defaultMessage: "Open this order in GraphiQL",
|
||||||
|
description: "open new window button",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -200,3 +200,18 @@ export const channelUsabilityData = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const defaultGraphiQLQuery = `query OrderDetails($id: ID!) {
|
||||||
|
order(id: $id) {
|
||||||
|
id
|
||||||
|
number
|
||||||
|
status
|
||||||
|
isShippingRequired
|
||||||
|
canFinalize
|
||||||
|
created
|
||||||
|
customerNote
|
||||||
|
paymentStatus
|
||||||
|
userEmail
|
||||||
|
isPaid
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
|
@ -117,5 +117,13 @@ describe("Product details page", () => {
|
||||||
);
|
);
|
||||||
// Assert
|
// Assert
|
||||||
expect(onSubmit.mock.calls[0][0].attributes[0].value.length).toEqual(0);
|
expect(onSubmit.mock.calls[0][0].attributes[0].value.length).toEqual(0);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const moreButton = screen.queryAllByTestId("show-more-button")[0];
|
||||||
|
await user.click(moreButton);
|
||||||
|
const graphiQLLink = screen.queryAllByTestId("graphiql-redirect")[0];
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(graphiQLLink).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,10 +44,12 @@ import useStateFromProps from "@dashboard/hooks/useStateFromProps";
|
||||||
import { sectionNames } from "@dashboard/intl";
|
import { sectionNames } from "@dashboard/intl";
|
||||||
import { maybe } from "@dashboard/misc";
|
import { maybe } from "@dashboard/misc";
|
||||||
import ProductExternalMediaDialog from "@dashboard/products/components/ProductExternalMediaDialog";
|
import ProductExternalMediaDialog from "@dashboard/products/components/ProductExternalMediaDialog";
|
||||||
|
import { defaultGraphiQLQuery } from "@dashboard/products/queries";
|
||||||
import { productImageUrl, productListUrl } from "@dashboard/products/urls";
|
import { productImageUrl, productListUrl } from "@dashboard/products/urls";
|
||||||
import { ProductVariantListError } from "@dashboard/products/views/ProductUpdate/handlers/errors";
|
import { ProductVariantListError } from "@dashboard/products/views/ProductUpdate/handlers/errors";
|
||||||
import { UseProductUpdateHandlerError } from "@dashboard/products/views/ProductUpdate/handlers/useProductUpdateHandler";
|
import { UseProductUpdateHandlerError } from "@dashboard/products/views/ProductUpdate/handlers/useProductUpdateHandler";
|
||||||
import { FetchMoreProps, RelayToFlat } from "@dashboard/types";
|
import { FetchMoreProps, RelayToFlat } from "@dashboard/types";
|
||||||
|
import { playgroundOpenHandler } from "@dashboard/utils/graphql";
|
||||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
@ -59,6 +61,7 @@ import ProductOrganization from "../ProductOrganization";
|
||||||
import ProductTaxes from "../ProductTaxes";
|
import ProductTaxes from "../ProductTaxes";
|
||||||
import ProductVariants from "../ProductVariants";
|
import ProductVariants from "../ProductVariants";
|
||||||
import ProductUpdateForm from "./form";
|
import ProductUpdateForm from "./form";
|
||||||
|
import { messages } from "./messages";
|
||||||
import ProductChannelsListingsDialog from "./ProductChannelsListingsDialog";
|
import ProductChannelsListingsDialog from "./ProductChannelsListingsDialog";
|
||||||
import {
|
import {
|
||||||
ProductUpdateData,
|
ProductUpdateData,
|
||||||
|
@ -242,6 +245,13 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
||||||
productId,
|
productId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openPlaygroundURL = playgroundOpenHandler({
|
||||||
|
query: defaultGraphiQLQuery,
|
||||||
|
headers: "",
|
||||||
|
operationName: "",
|
||||||
|
variables: `{ "id": "${product?.id}" }`,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProductUpdateForm
|
<ProductUpdateForm
|
||||||
isSimpleProduct={isSimpleProduct}
|
isSimpleProduct={isSimpleProduct}
|
||||||
|
@ -321,12 +331,17 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
||||||
{intl.formatMessage(sectionNames.products)}
|
{intl.formatMessage(sectionNames.products)}
|
||||||
</Backlink>
|
</Backlink>
|
||||||
<PageHeader title={header}>
|
<PageHeader title={header}>
|
||||||
{extensionMenuItems.length > 0 && (
|
|
||||||
<CardMenu
|
<CardMenu
|
||||||
menuItems={extensionMenuItems}
|
menuItems={[
|
||||||
|
...extensionMenuItems,
|
||||||
|
{
|
||||||
|
label: intl.formatMessage(messages.openGraphiQL),
|
||||||
|
onSelect: openPlaygroundURL,
|
||||||
|
testId: "graphiql-redirect",
|
||||||
|
},
|
||||||
|
]}
|
||||||
data-test-id="menu"
|
data-test-id="menu"
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Grid richText>
|
<Grid richText>
|
||||||
<div>
|
<div>
|
||||||
|
|
9
src/products/components/ProductUpdatePage/messages.ts
Normal file
9
src/products/components/ProductUpdatePage/messages.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
|
export const messages = defineMessages({
|
||||||
|
openGraphiQL: {
|
||||||
|
id: "5i/InH",
|
||||||
|
defaultMessage: "Open this order in GraphiQL",
|
||||||
|
description: "open new window button",
|
||||||
|
},
|
||||||
|
});
|
|
@ -260,3 +260,12 @@ export const gridAttributes = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const defaultGraphiQLQuery = `query ProductDetails($id: ID!) {
|
||||||
|
product(id: $id) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
63
src/utils/graphql.ts
Normal file
63
src/utils/graphql.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import LzString from "lz-string";
|
||||||
|
|
||||||
|
export type EditorContent = Record<keyof typeof longKeysToShortKeys, string>;
|
||||||
|
|
||||||
|
type ShorterEditorContent = Record<
|
||||||
|
typeof longKeysToShortKeys[keyof typeof longKeysToShortKeys],
|
||||||
|
string
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function removeEmptyValues<T extends object>(
|
||||||
|
editorContent: T,
|
||||||
|
): Partial<T> {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(editorContent).filter(([, val]) => !!val),
|
||||||
|
) as Partial<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const longKeysToShortKeys = {
|
||||||
|
query: "q",
|
||||||
|
headers: "h",
|
||||||
|
operationName: "o",
|
||||||
|
variables: "v",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const encodeGraphQLStatement = (editorContent: EditorContent) => {
|
||||||
|
const shorterContent: ShorterEditorContent = {
|
||||||
|
q: editorContent.query,
|
||||||
|
h: editorContent.headers,
|
||||||
|
o: editorContent.operationName,
|
||||||
|
v: editorContent.variables,
|
||||||
|
};
|
||||||
|
const stringifiedContent = JSON.stringify(removeEmptyValues(shorterContent));
|
||||||
|
|
||||||
|
const editorContentToSaveInUrl =
|
||||||
|
stringifiedContent === "{}"
|
||||||
|
? ""
|
||||||
|
: LzString.compressToEncodedURIComponent(stringifiedContent);
|
||||||
|
|
||||||
|
return `saleor/${editorContentToSaveInUrl}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PlaygroundOpenHandlerInput {
|
||||||
|
query: string;
|
||||||
|
headers: string;
|
||||||
|
operationName: string;
|
||||||
|
variables: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const playgroundOpenHandler = ({
|
||||||
|
query,
|
||||||
|
headers,
|
||||||
|
operationName,
|
||||||
|
variables,
|
||||||
|
}: PlaygroundOpenHandlerInput) => () => {
|
||||||
|
const playgroundURL = new URL(process.env.API_URI);
|
||||||
|
playgroundURL.hash = encodeGraphQLStatement({
|
||||||
|
query,
|
||||||
|
headers,
|
||||||
|
operationName,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
window.open(playgroundURL, "_blank").focus();
|
||||||
|
};
|
Loading…
Reference in a new issue