diff --git a/CHANGELOG.md b/CHANGELOG.md index 84ba62605..be556b60b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - 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 redirect to GraphiQL from product & order details pages - #2940 by @zaiste ## 3.4 diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index f66314ac8..109b956d4 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1015,6 +1015,10 @@ "context": "translations section name", "string": "Translations" }, + "5i/InH": { + "context": "open new window button", + "string": "Open this order in GraphiQL" + }, "5kvaFR": { "context": "product field", "string": "Export Variant SKU" diff --git a/package-lock.json b/package-lock.json index 7a180696b..ed69e1eac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "jwt-decode": "^3.1.2", "keycode": "^2.2.0", "lodash": "^4.17.20", + "lz-string": "^1.4.4", "marked": "^4.0.17", "moment-timezone": "^0.5.32", "qs": "^6.9.0", @@ -26945,7 +26946,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", - "optional": true, "bin": { "lz-string": "bin/bin.js" } @@ -60149,8 +60149,7 @@ "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", - "optional": true + "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==" }, "macos-release": { "version": "2.4.1", diff --git a/package.json b/package.json index fccd17a58..70a02e634 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "jwt-decode": "^3.1.2", "keycode": "^2.2.0", "lodash": "^4.17.20", + "lz-string": "^1.4.4", "marked": "^4.0.17", "moment-timezone": "^0.5.32", "qs": "^6.9.0", diff --git a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx index 15296d363..164ff1902 100644 --- a/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx +++ b/src/orders/components/OrderDetailsPage/OrderDetailsPage.tsx @@ -24,7 +24,9 @@ import { SubmitPromise } from "@dashboard/hooks/useForm"; import useNavigator from "@dashboard/hooks/useNavigator"; import { sectionNames } from "@dashboard/intl"; import OrderChannelSectionCard from "@dashboard/orders/components/OrderChannelSectionCard"; +import { defaultGraphiQLQuery } from "@dashboard/orders/queries"; import { orderListUrl } from "@dashboard/orders/urls"; +import { playgroundOpenHandler } from "@dashboard/utils/graphql"; import { mapMetadataItemToInput } from "@dashboard/utils/maps"; import useMetadataChangeTrigger from "@dashboard/utils/metadata/useMetadataChangeTrigger"; import { Typography } from "@material-ui/core"; @@ -196,6 +198,13 @@ const OrderDetailsPage: React.FC = props => { order?.id, ); + const openPlaygroundURL = playgroundOpenHandler({ + query: defaultGraphiQLQuery, + headers: "", + operationName: "", + variables: `{ "id": "${order?.id}" }`, + }); + return (
{({ change, data, submit }) => { @@ -211,9 +220,19 @@ const OrderDetailsPage: React.FC = props => { inline title={} cardMenu={ - <CardMenu - menuItems={[...selectCardMenuItems, ...extensionMenuItems]} - /> + <> + <CardMenu + menuItems={[ + ...selectCardMenuItems, + ...extensionMenuItems, + { + label: intl.formatMessage(messages.openGraphiQL), + onSelect: openPlaygroundURL, + testId: "graphiql-redirect", + }, + ]} + /> + </> } /> <div className={classes.date}> diff --git a/src/orders/components/OrderDetailsPage/messages.ts b/src/orders/components/OrderDetailsPage/messages.ts index 6c4ca55ed..420fb7555 100644 --- a/src/orders/components/OrderDetailsPage/messages.ts +++ b/src/orders/components/OrderDetailsPage/messages.ts @@ -16,4 +16,9 @@ export const messages = defineMessages({ defaultMessage: "Return / Replace order", description: "return button", }, + openGraphiQL: { + id: "5i/InH", + defaultMessage: "Open this order in GraphiQL", + description: "open new window button", + }, }); diff --git a/src/orders/queries.ts b/src/orders/queries.ts index 842fff3bd..23a9745b2 100644 --- a/src/orders/queries.ts +++ b/src/orders/queries.ts @@ -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 + } +}`; diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx index 258e6aafe..0abf21ac6 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx @@ -117,5 +117,13 @@ describe("Product details page", () => { ); // Assert 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(); }); }); diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index e9c3b1c18..1a6c61eeb 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -44,10 +44,12 @@ import useStateFromProps from "@dashboard/hooks/useStateFromProps"; import { sectionNames } from "@dashboard/intl"; import { maybe } from "@dashboard/misc"; import ProductExternalMediaDialog from "@dashboard/products/components/ProductExternalMediaDialog"; +import { defaultGraphiQLQuery } from "@dashboard/products/queries"; import { productImageUrl, productListUrl } from "@dashboard/products/urls"; import { ProductVariantListError } from "@dashboard/products/views/ProductUpdate/handlers/errors"; import { UseProductUpdateHandlerError } from "@dashboard/products/views/ProductUpdate/handlers/useProductUpdateHandler"; import { FetchMoreProps, RelayToFlat } from "@dashboard/types"; +import { playgroundOpenHandler } from "@dashboard/utils/graphql"; import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; import React from "react"; import { useIntl } from "react-intl"; @@ -59,6 +61,7 @@ import ProductOrganization from "../ProductOrganization"; import ProductTaxes from "../ProductTaxes"; import ProductVariants from "../ProductVariants"; import ProductUpdateForm from "./form"; +import { messages } from "./messages"; import ProductChannelsListingsDialog from "./ProductChannelsListingsDialog"; import { ProductUpdateData, @@ -242,6 +245,13 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({ productId, ); + const openPlaygroundURL = playgroundOpenHandler({ + query: defaultGraphiQLQuery, + headers: "", + operationName: "", + variables: `{ "id": "${product?.id}" }`, + }); + return ( <ProductUpdateForm isSimpleProduct={isSimpleProduct} @@ -321,12 +331,17 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({ {intl.formatMessage(sectionNames.products)} </Backlink> <PageHeader title={header}> - {extensionMenuItems.length > 0 && ( - <CardMenu - menuItems={extensionMenuItems} - data-test-id="menu" - /> - )} + <CardMenu + menuItems={[ + ...extensionMenuItems, + { + label: intl.formatMessage(messages.openGraphiQL), + onSelect: openPlaygroundURL, + testId: "graphiql-redirect", + }, + ]} + data-test-id="menu" + /> </PageHeader> <Grid richText> <div> diff --git a/src/products/components/ProductUpdatePage/messages.ts b/src/products/components/ProductUpdatePage/messages.ts new file mode 100644 index 000000000..e54740799 --- /dev/null +++ b/src/products/components/ProductUpdatePage/messages.ts @@ -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", + }, +}); diff --git a/src/products/queries.ts b/src/products/queries.ts index 6ae86fa4d..450fc8bd0 100644 --- a/src/products/queries.ts +++ b/src/products/queries.ts @@ -260,3 +260,12 @@ export const gridAttributes = gql` } } `; + +export const defaultGraphiQLQuery = `query ProductDetails($id: ID!) { + product(id: $id) { + id + name + slug + description + } +}`; diff --git a/src/utils/graphql.ts b/src/utils/graphql.ts new file mode 100644 index 000000000..5934d6e42 --- /dev/null +++ b/src/utils/graphql.ts @@ -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(); +};