Add DevMode panel (trigger: CMD+') (#3333)

This commit is contained in:
Jakub Neander 2023-03-17 15:10:58 +01:00 committed by GitHub
parent b31e4522a1
commit c0b69e7c31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 982 additions and 29 deletions

View file

@ -23,6 +23,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Add custom request headers to webhook form - #3107 by @2can
- Allow subscription query for sync events - #3099 by @2can
- Simplify codegen configuration to generate the graphql schema - #2538 by @abumalick
- Add DevMode panel (trigger: CMD+') - #3333 by @zaiste
## 3.4

View file

@ -7345,6 +7345,10 @@
"context": "stock exceeded dialog title",
"string": "Not enough stock"
},
"qa/dDe": {
"context": "GraphiQL Playground in the dev mode panel",
"string": "Playground"
},
"qbFKVI": {
"context": "notification, form submitted",
"string": "Refund for order #{orderNumber} was granted"

View file

@ -2,8 +2,11 @@ import useAppState from "@dashboard/hooks/useAppState";
import { LinearProgress } from "@material-ui/core";
import { useActionBar } from "@saleor/macaw-ui";
import { Box } from "@saleor/macaw-ui/next";
import React from "react";
import React, { useState } from "react";
import { DevModePanel } from "../DevModePanel/DevModePanel";
import { useDevModeContext } from "../DevModePanel/hooks";
import { useDevModeKeyTrigger } from "../DevModePanel/useDevModeKeyTrigger";
import Navigator from "../Navigator";
import { Sidebar } from "../Sidebar";
import { contentMaxWidth } from "./consts";
@ -18,10 +21,18 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
const classes = useStyles();
const { anchor: appActionAnchor } = useActionBar();
const [appState] = useAppState();
const [isNavigatorVisible, setNavigatorVisibility] = React.useState(false);
const [isNavigatorVisible, setNavigatorVisibility] = useState(false);
const { isDevModeVisible, setDevModeVisibility } = useDevModeContext();
useDevModeKeyTrigger(() => setDevModeVisibility(!isDevModeVisible));
return (
<>
<DevModePanel
isDevModeVisible={isDevModeVisible}
setDevModeVisibility={setDevModeVisibility}
/>
<Navigator
visible={isNavigatorVisible}
setVisibility={setNavigatorVisibility}

View file

@ -0,0 +1,52 @@
import { createGraphiQLFetcher } from "@graphiql/toolkit";
import { Dialog, DialogContent } from "@material-ui/core";
import { DialogHeader } from "@saleor/macaw-ui";
import { createFetch } from "@saleor/sdk";
import React from "react";
import { useIntl } from "react-intl";
import GraphiQL from "../GraphiQLPlain";
import { useDevModeContext } from "./hooks";
import { messages } from "./messages";
const authorizedFetch = createFetch();
interface DevModePanelProps {
isDevModeVisible: boolean;
setDevModeVisibility: (value: boolean) => void;
}
export const DevModePanel: React.FC<DevModePanelProps> = ({
isDevModeVisible,
setDevModeVisibility,
}) => {
const fetcher = createGraphiQLFetcher({
url: process.env.API_URI,
fetch: authorizedFetch,
});
const intl = useIntl();
const { devModeContent, variables } = useDevModeContext();
return (
<Dialog
maxWidth="xl"
fullWidth
open={isDevModeVisible}
style={{ zIndex: 5 }}
PaperProps={{ style: { height: "100%" } }}
>
<DialogHeader onClose={() => setDevModeVisibility(false)}>
{intl.formatMessage(messages.title)}
</DialogHeader>
<DialogContent style={{ padding: 0, margin: 1, overflowY: "auto" }}>
<GraphiQL
query={devModeContent}
variables={variables}
fetcher={fetcher}
/>
</DialogContent>
</Dialog>
);
};

View file

@ -0,0 +1,27 @@
import React, { useState } from "react";
import { DevModeContext } from "./hooks";
export function DevModeProvider({ children }) {
// stringified variables (as key/value) passed along with the query
const [variables, setVariables] = useState("");
// stringified GraphQL query; queries can be constructed anywhere in the
// dashboard to be passed to the dev mode panel
const [devModeContent, setDevModeContent] = useState("");
const [isDevModeVisible, setDevModeVisibility] = useState(false);
return (
<DevModeContext.Provider
value={{
variables,
setVariables,
devModeContent,
setDevModeContent,
isDevModeVisible,
setDevModeVisibility,
}}
>
{children}
</DevModeContext.Provider>
);
}

View file

@ -0,0 +1,22 @@
import { createContext, Dispatch, SetStateAction, useContext } from "react";
interface DevModeContextShape {
variables: string;
setVariables: Dispatch<SetStateAction<string>>;
isDevModeVisible: boolean;
setDevModeVisibility: Dispatch<SetStateAction<boolean>>;
devModeContent: string;
setDevModeContent: Dispatch<SetStateAction<string>>;
}
export const DevModeContext = createContext<null | DevModeContextShape>(null);
export const useDevModeContext = () => {
const context = useContext(DevModeContext);
if (context === null) {
throw new Error("you are outside of context");
}
return context;
};

View file

View file

@ -0,0 +1,9 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
title: {
id: "qa/dDe",
defaultMessage: "Playground",
description: "GraphiQL Playground in the dev mode panel",
},
});

View file

@ -0,0 +1,14 @@
import { useEffect } from "react";
export const useDevModeKeyTrigger = (callback?: () => void) => {
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.metaKey && event.code === "Quote") {
callback();
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [callback]);
};

View file

@ -279,10 +279,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) {
aria-label="Query Editor"
style={{ borderBottom: 0 }}
>
<div
className="graphiql-query-editor-wrapper"
style={{ fontSize: "1.6rem" }}
>
<div className="graphiql-query-editor-wrapper">
<QueryEditor
editorTheme={props.editorTheme}
keyMap={props.keyMap}

View file

@ -4,6 +4,7 @@ import {
useTheme as useGraphiQLTheme,
} from "@graphiql/react";
import { makeStyles, useTheme } from "@saleor/macaw-ui";
import { vars } from "@saleor/macaw-ui/next";
import { useEffect } from "react";
export const useStyles = makeStyles(
@ -54,15 +55,15 @@ export const useEditorStyles = () => {
};
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,
"--font-size-body": vars.fontSize.bodyMedium,
"--font-size-h2": vars.fontSize.headingLarge,
"--font-size-h3": vars.fontSize.headingMedium,
"--font-size-h4": vars.fontSize.headingSmall,
"--font-weight-regular": vars.fontWeight.bodyLarge,
"--font-size-hint": vars.fontSize.bodyEmpLarge,
"--font-size-inline-code": vars.fontSize.bodySmall,
"--color-base": vars.colors.background.plain,
} as React.CSSProperties;
return { rootStyle };

View file

@ -0,0 +1,808 @@
/**
* Copyright (c) 2020 GraphQL Contributors.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
Button,
ButtonGroup,
ChevronDownIcon,
ChevronUpIcon,
CopyIcon,
Dialog,
ExecuteButton,
GraphiQLProvider,
GraphiQLProviderProps,
HeaderEditor,
MergeIcon,
PlusIcon,
PrettifyIcon,
QueryEditor,
ReloadIcon,
ResponseEditor,
Spinner,
Tab,
Tabs,
ToolbarButton,
Tooltip,
UnStyledButton,
useCopyQuery,
useDragResize,
useEditorContext,
useExecutionContext,
UseHeaderEditorArgs,
useMergeQuery,
usePluginContext,
usePrettifyEditors,
UseQueryEditorArgs,
UseResponseEditorArgs,
useSchemaContext,
useStorageContext,
useTheme,
UseVariableEditorArgs,
VariableEditor,
WriteableEditorProps,
} from "@graphiql/react";
import React, {
ComponentType,
PropsWithChildren,
ReactNode,
useState,
} from "react";
import {
useDashboardTheme,
useGraphiQLThemeSwitcher,
} from "../GraphiQL/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;
}
/**
* API docs for this live here:
*
* https://graphiql-test.netlify.app/typedoc/modules/graphiql.html#graphiqlprops
*/
export type GraphiQLProps = Omit<GraphiQLProviderProps, "children"> &
GraphiQLInterfaceProps;
/**
* The top-level React component for GraphiQL, intended to encompass the entire
* browser viewport.
*
* @see https://github.com/graphql/graphiql#usage
*/
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
showPersistHeadersSettings={shouldPersistHeaders !== false}
{...props}
/>
</GraphiQLProvider>
);
}
GraphiQL.Toolbar = GraphiQLToolbar;
GraphiQL.Footer = GraphiQLFooter;
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;
/**
* Set the default state for the editor tools.
* - `false` hides the editor tools
* - `true` shows the editor tools
* - `'variables'` specifically shows the variables editor
* - `'headers'` specifically shows the headers editor
* By default the editor tools are initially shown when at least one of the
* editors has contents.
*/
defaultEditorToolsVisibility?: boolean | "variables" | "headers";
/**
* Toggle if the headers editor should be shown inside the editor tools.
* @default true
*/
isHeadersEditorEnabled?: boolean;
/**
* An object that allows configuration of the toolbar next to the query
* editor.
*/
toolbar?: GraphiQLToolbarConfig;
/**
* Indicates if settings for persisting headers should appear in the
* settings modal.
*/
showPersistHeadersSettings?: boolean;
};
export function GraphiQLInterface(props: GraphiQLInterfaceProps) {
const isHeadersEditorEnabled = props.isHeadersEditorEnabled ?? true;
const editorContext = useEditorContext({ nonNull: true });
const executionContext = useExecutionContext({ nonNull: true });
const schemaContext = useSchemaContext({ nonNull: true });
const storageContext = useStorageContext();
const pluginContext = usePluginContext();
const copy = useCopyQuery({ onCopyQuery: props.onCopyQuery });
const merge = useMergeQuery();
const prettify = usePrettifyEditors();
const { rootStyle } = useDashboardTheme();
useGraphiQLThemeSwitcher();
const { theme, setTheme } = useTheme();
const PluginContent = pluginContext?.visiblePlugin?.content;
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",
});
const [activeSecondaryEditor, setActiveSecondaryEditor] = useState<
"variables" | "headers"
>(() => {
if (
props.defaultEditorToolsVisibility === "variables" ||
props.defaultEditorToolsVisibility === "headers"
) {
return props.defaultEditorToolsVisibility;
}
return !editorContext.initialVariables &&
editorContext.initialHeaders &&
isHeadersEditorEnabled
? "headers"
: "variables";
});
const [showDialog, setShowDialog] = useState<
"settings" | "short-keys" | null
>(null);
const [clearStorageStatus, setClearStorageStatus] = useState<
"success" | "error" | null
>(null);
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={() => merge()}
label="Merge fragments into query (Shift-Ctrl-M)"
>
<MergeIcon 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 footer = children.find(child =>
isChildComponentType(child, GraphiQL.Footer),
);
const onClickReference = () => {
if (pluginResize.hiddenElement === "first") {
pluginResize.setHiddenElement(null);
}
};
const modifier =
window.navigator.platform.toLowerCase().indexOf("mac") === 0 ? (
<code className="graphiql-key">Cmd</code>
) : (
<code className="graphiql-key">Ctrl</code>
);
return (
<div
data-testid="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">
<Tooltip label="Re-fetch GraphQL schema">
<UnStyledButton
type="button"
disabled={schemaContext.isFetching}
onClick={() => schemaContext.introspect()}
aria-label="Re-fetch GraphQL schema"
>
<ReloadIcon
className={schemaContext.isFetching ? "graphiql-spin" : ""}
aria-hidden="true"
/>
</UnStyledButton>
</Tooltip>
</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="graphiql-plugin">
{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 className="graphiql-session-header">
<Tabs aria-label="Select active operation">
{editorContext.tabs.length > 1 ? (
<>
{editorContext.tabs.map((tab, index) => (
<Tab
key={tab.id}
isActive={index === editorContext.activeTabIndex}
>
<Tab.Button
aria-controls="graphiql-session"
id={`graphiql-session-tab-${index}`}
onClick={() => {
executionContext.stop();
editorContext.changeTab(index);
}}
>
{tab.title}
</Tab.Button>
<Tab.Close
onClick={() => {
if (editorContext.activeTabIndex === index) {
executionContext.stop();
}
editorContext.closeTab(index);
}}
/>
</Tab>
))}
<div>
<Tooltip label="Add tab">
<UnStyledButton
type="button"
className="graphiql-tab-add"
onClick={() => editorContext.addTab()}
aria-label="Add tab"
>
<PlusIcon aria-hidden="true" />
</UnStyledButton>
</Tooltip>
</div>
</>
) : null}
</Tabs>
<div className="graphiql-session-header-right">
{editorContext.tabs.length === 1 ? (
<div className="graphiql-add-tab-wrapper">
<Tooltip label="Add tab">
<UnStyledButton
type="button"
className="graphiql-tab-add"
onClick={() => editorContext.addTab()}
aria-label="Add tab"
>
<PlusIcon aria-hidden="true" />
</UnStyledButton>
</Tooltip>
</div>
) : null}
</div>
</div>
<div
role="tabpanel"
id="graphiql-session"
className="graphiql-session"
aria-labelledby={`graphiql-session-tab-${editorContext.activeTabIndex}`}
>
<div ref={editorResize.firstRef}>
<div
className={`graphiql-editors${
editorContext.tabs.length === 1 ? " full-height" : ""
}`}
>
<div ref={editorToolsResize.firstRef}>
<section
className="graphiql-query-editor"
aria-label="Query Editor"
>
<div className="graphiql-query-editor-wrapper">
<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"
>
<ExecuteButton />
{toolbar}
</div>
</section>
</div>
<div ref={editorToolsResize.dragBarRef}>
<div className="graphiql-editor-tools">
<div className="graphiql-editor-tools-tabs">
<UnStyledButton
type="button"
className={
activeSecondaryEditor === "variables" &&
editorToolsResize.hiddenElement !== "second"
? "active"
: ""
}
onClick={() => {
if (editorToolsResize.hiddenElement === "second") {
editorToolsResize.setHiddenElement(null);
}
setActiveSecondaryEditor("variables");
}}
>
Variables
</UnStyledButton>
{isHeadersEditorEnabled ? (
<UnStyledButton
type="button"
className={
activeSecondaryEditor === "headers" &&
editorToolsResize.hiddenElement !== "second"
? "active"
: ""
}
onClick={() => {
if (
editorToolsResize.hiddenElement === "second"
) {
editorToolsResize.setHiddenElement(null);
}
setActiveSecondaryEditor("headers");
}}
>
Headers
</UnStyledButton>
) : null}
</div>
<Tooltip
label={
editorToolsResize.hiddenElement === "second"
? "Show editor tools"
: "Hide editor tools"
}
>
<UnStyledButton
type="button"
onClick={() => {
editorToolsResize.setHiddenElement(
editorToolsResize.hiddenElement === "second"
? null
: "second",
);
}}
aria-label={
editorToolsResize.hiddenElement === "second"
? "Show editor tools"
: "Hide editor tools"
}
>
{editorToolsResize.hiddenElement === "second" ? (
<ChevronUpIcon
className="graphiql-chevron-icon"
aria-hidden="true"
/>
) : (
<ChevronDownIcon
className="graphiql-chevron-icon"
aria-hidden="true"
/>
)}
</UnStyledButton>
</Tooltip>
</div>
</div>
<div ref={editorToolsResize.secondRef}>
<section
className="graphiql-editor-tool"
aria-label={
activeSecondaryEditor === "variables"
? "Variables"
: "Headers"
}
>
<VariableEditor
editorTheme={props.editorTheme}
isHidden={activeSecondaryEditor !== "variables"}
keyMap={props.keyMap}
onEdit={props.onEditVariables}
onClickReference={onClickReference}
readOnly={props.readOnly}
/>
{isHeadersEditorEnabled && (
<HeaderEditor
editorTheme={props.editorTheme}
isHidden={activeSecondaryEditor !== "headers"}
keyMap={props.keyMap}
onEdit={props.onEditHeaders}
readOnly={props.readOnly}
/>
)}
</section>
</div>
</div>
</div>
<div ref={editorResize.dragBarRef}>
<div className="graphiql-horizontal-drag-bar" />
</div>
<div ref={editorResize.secondRef}>
<div className="graphiql-response">
{executionContext.isFetching ? <Spinner /> : null}
<ResponseEditor
editorTheme={props.editorTheme}
responseTooltip={props.responseTooltip}
keyMap={props.keyMap}
/>
{footer}
</div>
</div>
</div>
</div>
</div>
</div>
<Dialog
isOpen={showDialog === "short-keys"}
onDismiss={() => setShowDialog(null)}
>
<div className="graphiql-dialog-header">
<div className="graphiql-dialog-title">Short Keys</div>
<Dialog.Close onClick={() => setShowDialog(null)} />
</div>
<div className="graphiql-dialog-section">
<div>
<table className="graphiql-table">
<thead>
<tr>
<th>Short key</th>
<th>Function</th>
</tr>
</thead>
<tbody>
<tr>
<td>
{modifier}
{" + "}
<code className="graphiql-key">F</code>
</td>
<td>Search in editor</td>
</tr>
<tr>
<td>
{modifier}
{" + "}
<code className="graphiql-key">K</code>
</td>
<td>Search in documentation</td>
</tr>
<tr>
<td>
{modifier}
{" + "}
<code className="graphiql-key">Enter</code>
</td>
<td>Execute query</td>
</tr>
<tr>
<td>
<code className="graphiql-key">Ctrl</code>
{" + "}
<code className="graphiql-key">Shift</code>
{" + "}
<code className="graphiql-key">P</code>
</td>
<td>Prettify editors</td>
</tr>
<tr>
<td>
<code className="graphiql-key">Ctrl</code>
{" + "}
<code className="graphiql-key">Shift</code>
{" + "}
<code className="graphiql-key">M</code>
</td>
<td>Merge fragments definitions into operation definition</td>
</tr>
<tr>
<td>
<code className="graphiql-key">Ctrl</code>
{" + "}
<code className="graphiql-key">Shift</code>
{" + "}
<code className="graphiql-key">C</code>
</td>
<td>Copy query</td>
</tr>
<tr>
<td>
<code className="graphiql-key">Ctrl</code>
{" + "}
<code className="graphiql-key">Shift</code>
{" + "}
<code className="graphiql-key">R</code>
</td>
<td>Re-fetch schema using introspection</td>
</tr>
</tbody>
</table>
<p>
The editors use{" "}
<a
href="https://codemirror.net/5/doc/manual.html#keymaps"
target="_blank"
rel="noopener noreferrer"
>
CodeMirror Key Maps
</a>{" "}
that add more short keys. This instance of Graph<em>i</em>QL uses{" "}
<code>{props.keyMap || "sublime"}</code>.
</p>
</div>
</div>
</Dialog>
<Dialog
isOpen={showDialog === "settings"}
onDismiss={() => {
setShowDialog(null);
setClearStorageStatus(null);
}}
>
<div className="graphiql-dialog-header">
<div className="graphiql-dialog-title">Settings</div>
<Dialog.Close
onClick={() => {
setShowDialog(null);
setClearStorageStatus(null);
}}
/>
</div>
<div className="graphiql-dialog-section">
<div>
<div className="graphiql-dialog-section-title">Theme</div>
<div className="graphiql-dialog-section-caption">
Adjust how the interface looks like.
</div>
</div>
<div>
<ButtonGroup>
<Button
type="button"
className={theme === null ? "active" : ""}
onClick={() => setTheme(null)}
>
System
</Button>
<Button
type="button"
className={theme === "light" ? "active" : ""}
onClick={() => setTheme("light")}
>
Light
</Button>
<Button
type="button"
className={theme === "dark" ? "active" : ""}
onClick={() => setTheme("dark")}
>
Dark
</Button>
</ButtonGroup>
</div>
</div>
{storageContext ? (
<div className="graphiql-dialog-section">
<div>
<div className="graphiql-dialog-section-title">Clear storage</div>
<div className="graphiql-dialog-section-caption">
Remove all locally stored data and start fresh.
</div>
</div>
<div>
<Button
type="button"
state={clearStorageStatus || undefined}
disabled={clearStorageStatus === "success"}
onClick={() => {
try {
setClearStorageStatus("success");
} catch {
setClearStorageStatus("error");
}
}}
>
{clearStorageStatus === "success"
? "Cleared data"
: clearStorageStatus === "error"
? "Failed"
: "Clear data"}
</Button>
</div>
</div>
) : null}
</Dialog>
</div>
);
}
// Configure the UI by providing this Component as a child of GraphiQL.
function GraphiQLToolbar<TProps>(props: PropsWithChildren<TProps>) {
return <>{props.children}</>;
}
GraphiQLToolbar.displayName = "GraphiQLToolbar";
// Configure the UI by providing this Component as a child of GraphiQL.
function GraphiQLFooter<TProps>(props: PropsWithChildren<TProps>) {
return <div className="graphiql-footer">{props.children}</div>;
}
GraphiQLFooter.displayName = "GraphiQLFooter";
// Determines if the React child is of the same type of the provided React component
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;

View file

@ -0,0 +1,2 @@
export { default } from "./GraphiQL";
export * from "./GraphiQL";

View file

@ -33,6 +33,7 @@ import useAppChannel, {
AppChannelProvider,
} from "./components/AppLayout/AppChannelContext";
import { DateProvider } from "./components/Date";
import { DevModeProvider } from "./components/DevModePanel/DevModeProvider";
import ErrorPage from "./components/ErrorPage";
import ExitFormDialogProvider from "./components/Form/ExitFormDialogProvider";
import { LocaleProvider } from "./components/Locale";
@ -120,7 +121,9 @@ const App: React.FC = () => (
<AppChannelProvider>
<ExternalAppProvider>
<ExitFormDialogProvider>
<Routes />
<DevModeProvider>
<Routes />
</DevModeProvider>
</ExitFormDialogProvider>
</ExternalAppProvider>
</AppChannelProvider>

View file

@ -6,6 +6,7 @@ import {
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import CardMenu from "@dashboard/components/CardMenu";
import { CardSpacer } from "@dashboard/components/CardSpacer";
import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
@ -22,7 +23,6 @@ import useNavigator from "@dashboard/hooks/useNavigator";
import { defaultGraphiQLQuery } from "@dashboard/orders/queries";
import { OrderErrorFragment, OrderSharedType } from "@dashboard/orders/types";
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 { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
@ -196,12 +196,13 @@ const OrderDetailsPage: React.FC<OrderDetailsPageProps> = props => {
order?.id,
);
const openPlaygroundURL = playgroundOpenHandler({
query: defaultGraphiQLQuery,
headers: "",
operationName: "",
variables: `{ "id": "${order?.id}" }`,
});
const context = useDevModeContext();
const openPlaygroundURL = () => {
context.setDevModeContent(defaultGraphiQLQuery);
context.setVariables(`{ "id": "${order?.id}" }`);
context.setDevModeVisibility(true);
};
return (
<Form confirmLeave initial={initial} onSubmit={handleSubmit}>

View file

@ -14,6 +14,7 @@ import Attributes, { AttributeInput } from "@dashboard/components/Attributes";
import CardMenu from "@dashboard/components/CardMenu";
import CardSpacer from "@dashboard/components/CardSpacer";
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata/Metadata";
import Savebar from "@dashboard/components/Savebar";
@ -46,7 +47,6 @@ 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";
@ -242,12 +242,13 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
productId,
);
const openPlaygroundURL = playgroundOpenHandler({
query: defaultGraphiQLQuery,
headers: "",
operationName: "",
variables: `{ "id": "${product?.id}" }`,
});
const context = useDevModeContext();
const openPlaygroundURL = () => {
context.setDevModeContent(defaultGraphiQLQuery);
context.setVariables(`{ "id": "${product?.id}" }`);
context.setDevModeVisibility(true);
};
return (
<ProductUpdateForm