Add redirect to GraphiQL from product & order (#2940)

This commit is contained in:
Jakub Neander 2023-01-26 12:04:39 +01:00 committed by GitHub
parent ffb16ff1ed
commit b510d68fa9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 160 additions and 12 deletions

View file

@ -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

View file

@ -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
View file

@ -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",

View file

@ -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",

View file

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

View file

@ -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",
},
}); });

View file

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

View file

@ -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();
}); });
}); });

View file

@ -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>

View 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",
},
});

View file

@ -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
View 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();
};