Add DevMode panel (trigger: CMD+'
) (#3333)
This commit is contained in:
parent
b31e4522a1
commit
c0b69e7c31
16 changed files with 982 additions and 29 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
52
src/components/DevModePanel/DevModePanel.tsx
Normal file
52
src/components/DevModePanel/DevModePanel.tsx
Normal 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>
|
||||
);
|
||||
};
|
27
src/components/DevModePanel/DevModeProvider.tsx
Normal file
27
src/components/DevModePanel/DevModeProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
22
src/components/DevModePanel/hooks.ts
Normal file
22
src/components/DevModePanel/hooks.ts
Normal 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;
|
||||
};
|
0
src/components/DevModePanel/index.ts
Normal file
0
src/components/DevModePanel/index.ts
Normal file
9
src/components/DevModePanel/messages.ts
Normal file
9
src/components/DevModePanel/messages.ts
Normal 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",
|
||||
},
|
||||
});
|
14
src/components/DevModePanel/useDevModeKeyTrigger.ts
Normal file
14
src/components/DevModePanel/useDevModeKeyTrigger.ts
Normal 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]);
|
||||
};
|
|
@ -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}
|
||||
|
|
|
@ -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 };
|
||||
|
|
808
src/components/GraphiQLPlain/GraphiQL.tsx
Normal file
808
src/components/GraphiQLPlain/GraphiQL.tsx
Normal 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;
|
2
src/components/GraphiQLPlain/index.ts
Normal file
2
src/components/GraphiQLPlain/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./GraphiQL";
|
||||
export * from "./GraphiQL";
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue