From 2e06209f4e0a708119fef177ad69268029b07965 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sat, 17 Jun 2023 00:34:07 +0200 Subject: [PATCH] commit @graphiql/react commit graphiql remove unused and add editor theme add monaco-editor-webpack-plugin remove --- packages/graphiql-react/package.json | 4 +- packages/graphiql-react/src/constants.ts | 449 ++++++++++++++++++ packages/graphiql-react/src/create-editor.ts | 57 +++ .../graphiql-react/src/editor/completion.ts | 10 +- .../src/editor/components/header-editor.tsx | 1 + .../src/editor/components/headers-editor.tsx | 48 ++ .../src/editor/components/index.ts | 4 + .../editor/components/operations-editor.tsx | 8 + .../src/editor/components/results-editor.tsx | 29 ++ .../src/editor/components/variable-editor.tsx | 5 +- .../editor/components/variables-editor.tsx | 47 ++ .../graphiql-react/src/editor/context.tsx | 40 +- .../src/editor/header-editor.ts | 1 + packages/graphiql-react/src/editor/hooks.ts | 71 +-- packages/graphiql-react/src/editor/index.ts | 4 + .../src/editor/operations-editor.ts | 150 ++++++ .../graphiql-react/src/editor/query-editor.ts | 17 +- .../src/editor/response-editor.tsx | 1 + .../src/editor/style/editor.css | 5 + packages/graphiql-react/src/editor/tabs.ts | 20 +- .../src/editor/variable-editor.ts | 1 + packages/graphiql-react/src/execution.tsx | 4 + .../src/explorer/components/search.tsx | 11 +- .../graphiql-react/src/explorer/context.tsx | 143 +++--- packages/graphiql-react/src/index.ts | 12 +- packages/graphiql-react/src/plugin.tsx | 5 +- .../graphiql-react/src/toolbar/execute.tsx | 3 + packages/graphiql-react/src/utility/resize.ts | 19 +- packages/graphiql-react/tsconfig.json | 12 +- packages/graphiql-react/tsconfig.node.json | 3 +- packages/graphiql-react/vite.config.ts | 8 + packages/graphiql/package.json | 1 + packages/graphiql/resources/webpack.config.js | 14 + packages/graphiql/src/components/GraphiQL.tsx | 17 +- yarn.lock | 5 + 35 files changed, 1042 insertions(+), 187 deletions(-) create mode 100644 packages/graphiql-react/src/constants.ts create mode 100644 packages/graphiql-react/src/create-editor.ts create mode 100644 packages/graphiql-react/src/editor/components/headers-editor.tsx create mode 100644 packages/graphiql-react/src/editor/components/operations-editor.tsx create mode 100644 packages/graphiql-react/src/editor/components/results-editor.tsx create mode 100644 packages/graphiql-react/src/editor/components/variables-editor.tsx create mode 100644 packages/graphiql-react/src/editor/operations-editor.ts diff --git a/packages/graphiql-react/package.json b/packages/graphiql-react/package.json index c02c6821696..7266d981483 100644 --- a/packages/graphiql-react/package.json +++ b/packages/graphiql-react/package.json @@ -38,7 +38,7 @@ "types" ], "scripts": { - "dev": "concurrently 'tsc --emitDeclarationOnly --watch' 'vite build --watch'", + "dev": "vite build --watch", "build": "tsc --emitDeclarationOnly && vite build" }, "peerDependencies": { @@ -47,6 +47,8 @@ "react-dom": "^16.8.0 || ^17 || ^18" }, "dependencies": { + "monaco-editor": "^0.39.0", + "monaco-graphql": "^1.2.3", "@headlessui/react": "^1.7.15", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-visually-hidden": "^1.0.3", diff --git a/packages/graphiql-react/src/constants.ts b/packages/graphiql-react/src/constants.ts new file mode 100644 index 00000000000..7461d2353e0 --- /dev/null +++ b/packages/graphiql-react/src/constants.ts @@ -0,0 +1,449 @@ +import { initializeMode } from 'monaco-graphql/esm/initializeMode'; +import { editor, Uri } from 'monaco-editor'; +import { DEFAULT_QUERY } from './editor/context'; + +const OPERATIONS_URI = 'operations.graphql'; +const VARIABLES_URI = 'variables.json'; +const HEADERS_URI = 'headers.json'; +const RESULTS_URI = 'results.json'; + +export const MONACO_GRAPHQL_API = initializeMode({ + diagnosticSettings: { + validateVariablesJSON: { + [Uri.file(OPERATIONS_URI).toString()]: [ + Uri.file(VARIABLES_URI).toString(), + ], + }, + jsonDiagnosticSettings: { + validate: true, + schemaValidation: 'error', + // set these again, because we are entirely re-setting them here + allowComments: true, + trailingCommas: 'ignore', + }, + }, +}); + +export function getOrCreateModel({ + uri, + value, +}: { + uri: string; + value: string; +}) { + const language = uri.split('.').pop(); + + return ( + editor.getModel(Uri.file(uri)) ?? + editor.createModel(value, language, Uri.file(uri)) + ); +} + +export const OPERATIONS_MODEL = getOrCreateModel({ + uri: OPERATIONS_URI, + value: DEFAULT_QUERY, +}); + +export const VARIABLES_MODEL = getOrCreateModel({ + uri: VARIABLES_URI, + value: '', +}); + +export const HEADERS_MODEL = getOrCreateModel({ + uri: HEADERS_URI, + value: '', +}); + +export const RESULTS_MODEL = getOrCreateModel({ + uri: RESULTS_URI, + value: '', +}); + +const editorColors = { + dark: { + indentGuides: '#363739', + delimiters: '#55565c', + delimitersActive: '#a4a4a4', + selections: '#363739', + keywords: '#a4a4a4', + operators: '#a4a4a4', + text1: '#fff', + text2: '#bfbfbf', + text3: '#a4a4a4', + fields: '#948ae3', + arguments: '#fc618d', + types: '#5ad4e6', + values: '#7bd88f', + orange_default: '#ee8e57', + yellow_default: '#fce566', + }, + light: { + indentGuides: '#d7d7d7', // surface3 + delimiters: '#bcbcbc', // text4 + delimitersActive: '#757575', // text3 + selections: '#d7d7d7', // surface3 + keywords: '#757575', // text3 + operators: '#757575', // text3 + text1: '#1b1b1b', // text1 + text2: '#4a4a4a', // text2 + text3: '#757575', // text3 + fields: '#5c4cdd', // violet_default + arguments: '#d60690', // pink_default + types: '#0b6af9', // blue_default + values: '#128934', // green_default + orange_default: '#af5f15', // orange_default + yellow_default: '#767800', // yellow_default + }, +}; + +export const editorThemeDark: editor.IStandaloneThemeData = { + base: 'vs-dark', + inherit: true, + colors: { + 'editor.foreground': editorColors.dark.delimiters, // Editor default foreground color. + 'editorCursor.foreground': editorColors.dark.yellow_default, // Color of the editor cursor. + 'editor.selectionBackground': editorColors.dark.selections, // Color of the editor selection. + 'editor.background': '#ffffff00', // white with a 00 alpha value + 'editorLineNumber.foreground': editorColors.dark.delimiters, // Color of editor line numbers. + 'editorLineNumber.activeForeground': editorColors.dark.delimitersActive, // Color of editor active line number. + 'editorError.foreground': editorColors.dark.orange_default, // Foreground color of error squigglies in the editor. + 'editorWarning.foreground': editorColors.dark.orange_default, // Foreground color of warning squigglies in the editor. + 'editor.lineHighlightBorder': '#ffffff00', // Background color for the border around the line at the cursor position. + 'editorBracketMatch.background': '#ffffff00', // Background color behind matching brackets + 'editorBracketMatch.border': editorColors.dark.selections, // Color for matching brackets boxes + 'editorIndentGuide.background': editorColors.dark.indentGuides, // Color of the editor indentation guides. + 'scrollbar.shadow': '#ffffff00', // Scrollbar shadow to indicate that the view is scrolled. + 'editorOverviewRuler.border': '#ffffff00', // Color of the overview ruler border. + // 'editorMarkerNavigationError.background': '#FFFFFF00', // Editor marker navigation widget error color. + 'editorMarkerNavigationWarning.background': '#ffffff00', // Editor marker navigation widget warning color. + }, + rules: [ + // operations editor (graphql) + { + foreground: editorColors.dark.keywords, + token: 'string.quote.gql', + }, + { + foreground: editorColors.dark.orange_default, + token: 'string.invalid.gql', + }, + { + foreground: editorColors.dark.yellow_default, + token: 'string.gql', + }, + { + foreground: editorColors.dark.yellow_default, + token: 'number.gql', + }, + { + foreground: editorColors.dark.yellow_default, + token: 'number.float.gql', + }, + { + foreground: editorColors.dark.keywords, + token: 'keyword.gql', + }, + { + foreground: editorColors.dark.operators, + token: 'operator.gql', + }, + { + foreground: editorColors.dark.types, + token: 'type.identifier.gql', + }, + { + foreground: editorColors.dark.fields, + token: 'key.identifier.gql', + }, + { + foreground: editorColors.dark.arguments, + token: 'argument.identifier.gql', + }, + { + foreground: editorColors.dark.delimiters, + token: 'delimiter.gql', + }, + { + foreground: editorColors.dark.delimiters, + token: 'delimiter.parenthesis.gql', + }, + { + foreground: editorColors.dark.delimiters, + token: 'delimiter.curly.gql', + }, + { + foreground: editorColors.dark.delimiters, + token: 'delimiter.square.gql', + }, + { + foreground: editorColors.dark.text2, + token: 'comment.gql', + }, + // variables editor & results viewer (json) + { + foreground: editorColors.dark.text2, + token: 'delimiter.bracket.json', + }, + { + foreground: editorColors.dark.text2, + token: 'delimiter.array.json', + }, + { + foreground: editorColors.dark.text2, + token: 'delimiter.comma.json', + }, + { + foreground: editorColors.dark.text2, + token: 'delimiter.colon.json', + }, + { + foreground: editorColors.dark.keywords, + token: 'string.key.json', + }, + { + foreground: editorColors.dark.values, + token: 'string.value.json', + }, + { + foreground: editorColors.dark.values, + token: 'number.json', + }, + { + foreground: editorColors.dark.values, + token: 'keyword.json', + }, + ], +}; + +export const editorThemeLight: editor.IStandaloneThemeData = { + base: 'vs', + inherit: true, + colors: { + 'editor.foreground': editorColors.light.delimiters, // Editor default foreground color. + 'editorCursor.foreground': editorColors.light.yellow_default, // Color of the editor cursor. + 'editor.selectionBackground': editorColors.light.selections, // Color of the editor selection. + 'editor.background': '#ffffff00', // white with a 00 alpha value + 'editorLineNumber.foreground': editorColors.light.delimiters, // Color of editor line numbers. + 'editorLineNumber.activeForeground': editorColors.light.delimitersActive, // Color of editor active line number. + 'editorError.foreground': editorColors.light.orange_default, // Foreground color of error squigglies in the editor. + 'editorWarning.foreground': editorColors.light.orange_default, // Foreground color of warning squigglies in the editor. + 'editor.lineHighlightBorder': '#ffffff00', // Background color for the border around the line at the cursor position. + 'editorBracketMatch.background': '#ffffff00', // Background color behind matching brackets + 'editorBracketMatch.border': editorColors.light.selections, // Color for matching brackets boxes + 'editorIndentGuide.background': editorColors.light.indentGuides, // Color of the editor indentation guides. + 'scrollbar.shadow': '#ffffff00', // Scrollbar shadow to indicate that the view is scrolled. + 'editorOverviewRuler.border': '#ffffff00', // Color of the overview ruler border. + // 'editorMarkerNavigationError.background': '#FFFFFF00', // Editor marker navigation widget error color. + 'editorMarkerNavigationWarning.background': '#ffffff00', // Editor marker navigation widget warning color. + }, + rules: [ + // operations editor (graphql) + { + foreground: editorColors.light.keywords, + token: 'string.quote.gql', + }, + { + foreground: editorColors.light.orange_default, + token: 'string.invalid.gql', + }, + { + foreground: editorColors.light.yellow_default, + token: 'string.gql', + }, + { + foreground: editorColors.light.yellow_default, + token: 'number.gql', + }, + { + foreground: editorColors.light.yellow_default, + token: 'number.float.gql', + }, + { + foreground: editorColors.light.operators, + token: 'operator.gql', + }, + { + foreground: editorColors.light.types, + token: 'type.identifier.gql', + }, + { + foreground: editorColors.light.fields, + token: 'key.identifier.gql', + }, + { + foreground: editorColors.light.arguments, + token: 'argument.identifier.gql', + }, + { + foreground: editorColors.light.delimiters, + token: 'delimiter.gql', + }, + { + foreground: editorColors.light.delimiters, + token: 'delimiter.parenthesis.gql', + }, + { + foreground: editorColors.light.delimiters, + token: 'delimiter.curly.gql', + }, + { + foreground: editorColors.light.delimiters, + token: 'delimiter.square.gql', + }, + { + foreground: editorColors.light.text2, + token: 'comment.gql', + }, + // variables editor & results viewer (json) + { + foreground: editorColors.light.text2, + token: 'delimiter.bracket.json', + }, + { + foreground: editorColors.light.text2, + token: 'delimiter.array.json', + }, + { + foreground: editorColors.light.text2, + token: 'delimiter.comma.json', + }, + { + foreground: editorColors.light.text2, + token: 'delimiter.colon.json', + }, + { + foreground: editorColors.light.keywords, + token: 'string.key.json', + }, + { + foreground: editorColors.light.values, + token: 'string.value.json', + }, + { + foreground: editorColors.light.values, + token: 'number.json', + }, + { + foreground: editorColors.light.values, + token: 'keyword.json', + }, + ], +}; + +// A list of color names: +// 'foreground': "#FFFFFF00", // Overall foreground color. This color is only used if not overridden by a component. +// 'errorForeground': "#FFFFFF00", // Overall foreground color for error messages. This color is only used if not overridden by a component. +// 'descriptionForeground': "#FFFFFF00", // Foreground color for description text providing additional information, for example for a label. +// 'focusBorder': "#FFFFFF00", // Overall border color for focused elements. This color is only used if not overridden by a component. +// 'contrastBorder': "#FFFFFF00", // An extra border around elements to separate them from others for greater contrast. +// 'contrastActiveBorder': "#FFFFFF00", // An extra border around active elements to separate them from others for greater contrast. +// 'selection.background': "#FFFFFF00", // The background color of text selections in the workbench (e.g. for input fields or text areas). Note that this does not apply to selections within the editor. +// 'textSeparator.foreground': "#FFFFFF00", // Color for text separators. +// 'textLink.foreground': "#FFFFFF00", // Foreground color for links in text. +// 'textLink.activeForeground': "#FFFFFF00", // Foreground color for active links in text. +// 'textPreformat.foreground': "#FFFFFF00", // Foreground color for preformatted text segments. +// 'textBlockQuote.background': "#FFFFFF00", // Background color for block quotes in text. +// 'textBlockQuote.border': "#FFFFFF00", // Border color for block quotes in text. +// 'textCodeBlock.background': "#FFFFFF00", // Background color for code blocks in text. +// 'widget.shadow': "#FFFFFF00", // Shadow color of widgets such as find/replace inside the editor. +// 'input.background': "#FFFFFF00", // Input box background. +// 'input.foreground': "#FFFFFF00", // Input box foreground. +// 'input.border': "#FFFFFF00", // Input box border. +// 'inputOption.activeBorder': "#FFFFFF00", // Border color of activated options in input fields. +// 'input.placeholderForeground': "#FFFFFF00", // Input box foreground color for placeholder text. +// 'inputValidation.infoBackground': "#FFFFFF00", // Input validation background color for information severity. +// 'inputValidation.infoBorder': "#FFFFFF00", // Input validation border color for information severity. +// 'inputValidation.warningBackground': "#FFFFFF00", // Input validation background color for information warning. +// 'inputValidation.warningBorder': "#FFFFFF00", // Input validation border color for warning severity. +// 'inputValidation.errorBackground': "#FFFFFF00", // Input validation background color for error severity. +// 'inputValidation.errorBorder': "#FFFFFF00", // Input validation border color for error severity. +// 'dropdown.background': "#FFFFFF00", // Dropdown background. +// 'dropdown.foreground': "#FFFFFF00", // Dropdown foreground. +// 'dropdown.border': "#FFFFFF00", // Dropdown border. +// 'list.focusBackground': "#FFFFFF00", // List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not. +// 'list.focusForeground': "#FFFFFF00", // List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not. +// 'list.activeSelectionBackground': "#FFFFFF00", // List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not. +// 'list.activeSelectionForeground': "#FFFFFF00", // List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not. +// 'list.inactiveSelectionBackground': "#FFFFFF00", // List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not. +// 'list.inactiveSelectionForeground': "#FFFFFF00", // List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not. +// 'list.hoverBackground': "#FFFFFF00", // List/Tree background when hovering over items using the mouse. +// 'list.hoverForeground': "#FFFFFF00", // List/Tree foreground when hovering over items using the mouse. +// 'list.dropBackground': "#FFFFFF00", // List/Tree drag and drop background when moving items around using the mouse. +// 'list.highlightForeground': "#FFFFFF00", // List/Tree foreground color of the match highlights when searching inside the list/tree. +// 'pickerGroup.foreground': "#FFFFFF00", // Quick picker color for grouping labels. +// 'pickerGroup.border': "#FFFFFF00", // Quick picker color for grouping borders. +// 'button.foreground': "#FFFFFF00", // Button foreground color. +// 'button.background': "#FFFFFF00", // Button background color. +// 'button.hoverBackground': "#FFFFFF00", // Button background color when hovering. +// 'badge.background': "#FFFFFF00", // Badge background color. Badges are small information labels, e.g. for search results count. +// 'badge.foreground': "#FFFFFF00", // Badge foreground color. Badges are small information labels, e.g. for search results count. +// 'scrollbar.shadow': "#FFFFFF00", // Scrollbar shadow to indicate that the view is scrolled. +// 'scrollbarSlider.background': "#FFFFFF00", // Slider background color. +// 'scrollbarSlider.hoverBackground': "#FFFFFF00", // Slider background color when hovering. +// 'scrollbarSlider.activeBackground': "#FFFFFF00", // Slider background color when active. +// 'progressBar.background': "#FFFFFF00", // Background color of the progress bar that can show for long running operations. +// 'editor.background': "#FFFFFF00", // Editor background color. +// 'editor.foreground': "#FFFFFF00", // Editor default foreground color. +// 'editorWidget.background': "#FFFFFF00", // Background color of editor widgets, such as find/replace. +// 'editorWidget.border': "#FFFFFF00", // Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget. +// 'editor.selectionBackground': "#FFFFFF00", // Color of the editor selection. +// 'editor.selectionForeground': "#FFFFFF00", // Color of the selected text for high contrast. +// 'editor.inactiveSelectionBackground': "#FFFFFF00", // Color of the selection in an inactive editor. +// 'editor.selectionHighlightBackground': "#FFFFFF00", // Color for regions with the same content as the selection. +// 'editor.findMatchBackground': "#FFFFFF00", // Color of the current search match. +// 'editor.findMatchHighlightBackground': "#FFFFFF00", // Color of the other search matches. +// 'editor.findRangeHighlightBackground': "#FFFFFF00", // Color the range limiting the search. +// 'editor.hoverHighlightBackground': "#FFFFFF00", // Highlight below the word for which a hover is shown. +// 'editorHoverWidget.background': "#FFFFFF00", // Background color of the editor hover. +// 'editorHoverWidget.border': "#FFFFFF00", // Border color of the editor hover. +// 'editorLink.activeForeground': "#FFFFFF00", // Color of active links. +// 'diffEditor.insertedTextBackground': "#FFFFFF00", // Background color for text that got inserted. +// 'diffEditor.removedTextBackground': "#FFFFFF00", // Background color for text that got removed. +// 'diffEditor.insertedTextBorder': "#FFFFFF00", // Outline color for the text that got inserted. +// 'diffEditor.removedTextBorder': "#FFFFFF00", // Outline color for text that got removed. +// 'editorOverviewRuler.currentContentForeground': "#FFFFFF00", // Current overview ruler foreground for inline merge-conflicts. +// 'editorOverviewRuler.incomingContentForeground': "#FFFFFF00", // Incoming overview ruler foreground for inline merge-conflicts. +// 'editorOverviewRuler.commonContentForeground': "#FFFFFF00", // Common ancestor overview ruler foreground for inline merge-conflicts. +// 'editor.lineHighlightBackground': "#FFFFFF00", // Background color for the highlight of line at the cursor position. +// 'editor.lineHighlightBorder': "#FFFFFF00", // Background color for the border around the line at the cursor position. +// 'editor.rangeHighlightBackground': "#FFFFFF00", // Background color of highlighted ranges, like by quick open and find features. +// 'editorCursor.foreground': "#FFFFFF00", // Color of the editor cursor. +// 'editorWhitespace.foreground': "#FFFFFF00", // Color of whitespace characters in the editor. +// 'editorIndentGuide.background': "#FFFFFF00", // Color of the editor indentation guides. +// 'editorLineNumber.foreground': "#FFFFFF00", // Color of editor line numbers. +// 'editorLineNumber.activeForeground': "#FFFFFF00", // Color of editor active line number. +// 'editorRuler.foreground': "#FFFFFF00", // Color of the editor rulers. +// 'editorCodeLens.foreground': "#FFFFFF00", // Foreground color of editor code lenses +// 'editorInlayHint.foreground': "#FFFFFF00", // Foreground color of editor inlay hints +// 'editorInlayHint.background': "#FFFFFF00", // Background color of editor inlay hints +// 'editorBracketMatch.background': "#FFFFFF00", // Background color behind matching brackets +// 'editorBracketMatch.border': "#FFFFFF00", // Color for matching brackets boxes +// 'editorOverviewRuler.border': "#FFFFFF00", // Color of the overview ruler border. +// 'editorGutter.background': "#FFFFFF00", // Background color of the editor gutter. The gutter contains the glyph margins and the line numbers. +// 'editorError.foreground': "#FFFFFF00", // Foreground color of error squigglies in the editor. +// 'editorError.border': "#FFFFFF00", // Border color of error squigglies in the editor. +// 'editorWarning.foreground': "#FFFFFF00", // Foreground color of warning squigglies in the editor. +// 'editorWarning.border': "#FFFFFF00", // Border color of warning squigglies in the editor. +// 'editorMarkerNavigationError.background': "#FFFFFF00", // Editor marker navigation widget error color. +// 'editorMarkerNavigationWarning.background': "#FFFFFF00", // Editor marker navigation widget warning color. +// 'editorMarkerNavigation.background': "#FFFFFF00", // Editor marker navigation widget background. +// 'editorSuggestWidget.background': "#FFFFFF00", // Background color of the suggest widget. +// 'editorSuggestWidget.border': "#FFFFFF00", // Border color of the suggest widget. +// 'editorSuggestWidget.foreground': "#FFFFFF00", // Foreground color of the suggest widget. +// 'editorSuggestWidget.selectedBackground': "#FFFFFF00", // Background color of the selected entry in the suggest widget. +// 'editorSuggestWidget.highlightForeground': "#FFFFFF00", // Color of the match highlights in the suggest widget. +// 'editor.wordHighlightBackground': "#FFFFFF00", // Background color of a symbol during read-access, like reading a variable. +// 'editor.wordHighlightStrongBackground': "#FFFFFF00", // Background color of a symbol during write-access, like writing to a variable. +// 'peekViewTitle.background': "#FFFFFF00", // Background color of the peek view title area. +// 'peekViewTitleLabel.foreground': "#FFFFFF00", // Color of the peek view title. +// 'peekViewTitleDescription.foreground': "#FFFFFF00", // Color of the peek view title info. +// 'peekView.border': "#FFFFFF00", // Color of the peek view borders and arrow. +// 'peekViewResult.background': "#FFFFFF00", // Background color of the peek view result list. +// 'peekViewResult.lineForeground': "#FFFFFF00", // Foreground color for line nodes in the peek view result list. +// 'peekViewResult.fileForeground': "#FFFFFF00", // Foreground color for file nodes in the peek view result list. +// 'peekViewResult.selectionBackground': "#FFFFFF00", // Background color of the selected entry in the peek view result list. +// 'peekViewResult.selectionForeground': "#FFFFFF00", // Foreground color of the selected entry in the peek view result list. +// 'peekViewEditor.background': "#FFFFFF00", // Background color of the peek view editor. +// 'peekViewEditorGutter.background': "#FFFFFF00", // Background color of the gutter in the peek view editor. +// 'peekViewResult.matchHighlightBackground': "#FFFFFF00", // Match highlight color in the peek view result list. +// 'peekViewEditor.matchHighlightBackground': "#FFFFFF00", // Match highlight color in the peek view editor. diff --git a/packages/graphiql-react/src/create-editor.ts b/packages/graphiql-react/src/create-editor.ts new file mode 100644 index 00000000000..c951e296e57 --- /dev/null +++ b/packages/graphiql-react/src/create-editor.ts @@ -0,0 +1,57 @@ +import { editor as MONACO_EDITOR } from 'monaco-editor'; +import { + OPERATIONS_MODEL, + VARIABLES_MODEL, + HEADERS_MODEL, + RESULTS_MODEL, + editorThemeDark, + editorThemeLight, +} from './constants'; + +// this should be called somewhere else, but fine here for now +MONACO_EDITOR.defineTheme('graphiql-DARK', editorThemeDark); +MONACO_EDITOR.defineTheme('graphiql-LIGHT', editorThemeLight); + +export function createEditor( + type: 'operations' | 'variables' | 'headers' | 'results', + domElement: HTMLDivElement, +) { + return MONACO_EDITOR.create(domElement, { + language: type === 'operations' ? 'graphql' : 'json', + // the default theme + theme: 'graphiql-DARK', + automaticLayout: true, + // folding: false, // disable folding + fontFamily: "'Fira Code', monospace", // TODO: set the font (this is problematic because the font has to be installed locally) + fontSize: 13, // default is 12 + // lineDecorationsWidth: 100, + lineNumbersMinChars: 2, + minimap: { + enabled: false, // disable the minimap + }, + overviewRulerLanes: 0, // remove unnecessary cruft on right side of editors + scrollbar: { + // hide the scrollbars + horizontal: 'hidden', + // vertical: 'hidden', + verticalScrollbarSize: 4, + }, + // scrollPredominantAxis: false, + scrollBeyondLastLine: false, // cleans up unnecessary "padding" on the bottom of each editor + tabSize: 2, + wordWrap: 'on', + // wrappingIndent: 'none', + wrappingStrategy: 'advanced', + fixedOverflowWidgets: true, + ...(type === 'results' && { + readOnly: true, + lineNumbers: 'off', + }), + model: { + operations: OPERATIONS_MODEL, + variables: VARIABLES_MODEL, + headers: HEADERS_MODEL, + results: RESULTS_MODEL, + }[type], + }); +} diff --git a/packages/graphiql-react/src/editor/completion.ts b/packages/graphiql-react/src/editor/completion.ts index b60e4b6d050..b2e526cbf28 100644 --- a/packages/graphiql-react/src/editor/completion.ts +++ b/packages/graphiql-react/src/editor/completion.ts @@ -164,12 +164,10 @@ export function onHasCompletion( if (event.target === hintsUl) { hintsUl.removeEventListener('scroll', handleScroll); hintsUl.removeEventListener('DOMNodeRemoved', onRemoveFn); - if (information) { - information.removeEventListener( - 'click', - onClickHintInformation, - ); - } + information?.removeEventListener( + 'click', + onClickHintInformation, + ); information = null; fieldName = null; typeNamePill = null; diff --git a/packages/graphiql-react/src/editor/components/header-editor.tsx b/packages/graphiql-react/src/editor/components/header-editor.tsx index 672a2b759a2..70e826a359e 100644 --- a/packages/graphiql-react/src/editor/components/header-editor.tsx +++ b/packages/graphiql-react/src/editor/components/header-editor.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck -- codemirror editor complain about type errors import { useEffect } from 'react'; import { clsx } from 'clsx'; diff --git a/packages/graphiql-react/src/editor/components/headers-editor.tsx b/packages/graphiql-react/src/editor/components/headers-editor.tsx new file mode 100644 index 00000000000..9084259ce0f --- /dev/null +++ b/packages/graphiql-react/src/editor/components/headers-editor.tsx @@ -0,0 +1,48 @@ +import { useEffect, useRef } from 'react'; +import { clsx } from 'clsx'; +import { useEditorContext } from '../context'; +import { UseHeaderEditorArgs } from '../header-editor'; +import '../style/editor.css'; +import { createEditor } from '@/create-editor'; +import { HEADERS_MODEL } from '@/constants'; +import debounce from '@/utility/debounce'; +import { useStorageContext } from '@/storage'; + +type HeaderEditorProps = UseHeaderEditorArgs & { + /** + * Visually hide the header editor. + * @default false + */ + isHidden?: boolean; +}; + +export function HeadersEditor({ isHidden, ...hookArgs }: HeaderEditorProps) { + const { setHeaderEditor, updateActiveTabValues, shouldPersistHeaders } = + useEditorContext({ + nonNull: true, + caller: HeadersEditor, + }); + const ref = useRef(null); + const storage = useStorageContext(); + + useEffect(() => { + setHeaderEditor(createEditor('headers', ref.current!)); + if (!shouldPersistHeaders) { + return; + } + HEADERS_MODEL.onDidChangeContent( + debounce(500, () => { + const value = HEADERS_MODEL.getValue(); + storage?.set(STORAGE_KEY, value); + updateActiveTabValues({ headers: value }); + }), + ); + // eslint-disable-next-line react-hooks/exhaustive-deps -- only on mount + }, []); + + return ( +
+ ); +} + +export const STORAGE_KEY = 'headers'; diff --git a/packages/graphiql-react/src/editor/components/index.ts b/packages/graphiql-react/src/editor/components/index.ts index 9fbe6db2a47..9fa93354609 100644 --- a/packages/graphiql-react/src/editor/components/index.ts +++ b/packages/graphiql-react/src/editor/components/index.ts @@ -3,3 +3,7 @@ export { ImagePreview } from './image-preview'; export { QueryEditor } from './query-editor'; export { ResponseEditor } from './response-editor'; export { VariableEditor } from './variable-editor'; +export { OperationsEditor } from './operations-editor'; +export { VariablesEditor } from './variables-editor'; +export { HeadersEditor } from './headers-editor'; +export { ResultsEditor } from './results-editor'; diff --git a/packages/graphiql-react/src/editor/components/operations-editor.tsx b/packages/graphiql-react/src/editor/components/operations-editor.tsx new file mode 100644 index 00000000000..a15d0aa7703 --- /dev/null +++ b/packages/graphiql-react/src/editor/components/operations-editor.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { useQueryEditor, UseQueryEditorArgs } from '../operations-editor'; + +export function OperationsEditor(props: UseQueryEditorArgs) { + const ref = useQueryEditor(props, OperationsEditor); + + return
; +} diff --git a/packages/graphiql-react/src/editor/components/results-editor.tsx b/packages/graphiql-react/src/editor/components/results-editor.tsx new file mode 100644 index 00000000000..0e5567af839 --- /dev/null +++ b/packages/graphiql-react/src/editor/components/results-editor.tsx @@ -0,0 +1,29 @@ +import { UseResponseEditorArgs } from '../response-editor'; +import { useEffect, useRef } from 'react'; +import { createEditor } from '@/create-editor'; +import { useEditorContext } from '@/editor'; +import '../style/editor.css'; + +export function ResultsEditor(props: UseResponseEditorArgs) { + const ref = useRef(null); + + const { setResponseEditor } = useEditorContext({ + nonNull: true, + caller: ResultsEditor, + }); + + useEffect(() => { + setResponseEditor(createEditor('results', ref.current!)); + // eslint-disable-next-line react-hooks/exhaustive-deps -- only on mount + }, []); + + return ( +
+ ); +} diff --git a/packages/graphiql-react/src/editor/components/variable-editor.tsx b/packages/graphiql-react/src/editor/components/variable-editor.tsx index 3d354157d7e..8621eb2490b 100644 --- a/packages/graphiql-react/src/editor/components/variable-editor.tsx +++ b/packages/graphiql-react/src/editor/components/variable-editor.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck -- codemirror editor complain about type errors import { useEffect } from 'react'; import { clsx } from 'clsx'; @@ -26,8 +27,8 @@ export function VariableEditor({ isHidden, ...hookArgs }: VariableEditorProps) { const ref = useVariableEditor(hookArgs, VariableEditor); useEffect(() => { - if (variableEditor && !isHidden) { - variableEditor.refresh(); + if (!isHidden) { + variableEditor?.refresh(); } }, [variableEditor, isHidden]); diff --git a/packages/graphiql-react/src/editor/components/variables-editor.tsx b/packages/graphiql-react/src/editor/components/variables-editor.tsx new file mode 100644 index 00000000000..e69149eab06 --- /dev/null +++ b/packages/graphiql-react/src/editor/components/variables-editor.tsx @@ -0,0 +1,47 @@ +import { useEffect, useRef } from 'react'; +import { clsx } from 'clsx'; +import { UseVariableEditorArgs } from '../variable-editor'; +import { createEditor } from '@/create-editor'; +import { VARIABLES_MODEL } from '@/constants'; +import { useStorageContext } from '@/storage'; +import { useEditorContext } from '@/editor'; +import debounce from '@/utility/debounce'; + +type VariableEditorProps = UseVariableEditorArgs & { + /** + * Visually hide the header editor. + * @default false + */ + isHidden?: boolean; +}; + +export function VariablesEditor({ + isHidden, + ...hookArgs +}: VariableEditorProps) { + const ref = useRef(null); + const storage = useStorageContext(); + + const { updateActiveTabValues, setVariableEditor } = useEditorContext({ + nonNull: true, + caller: VariablesEditor, + }); + + useEffect(() => { + setVariableEditor(createEditor('variables', ref.current!)); + VARIABLES_MODEL.onDidChangeContent( + debounce(500, () => { + const value = VARIABLES_MODEL.getValue(); + storage?.set(STORAGE_KEY, value); + updateActiveTabValues({ variables: value }); + }), + ); + // eslint-disable-next-line react-hooks/exhaustive-deps -- only on mount + }, []); + + return ( +
+ ); +} + +const STORAGE_KEY = 'variables'; diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index 0168c3285bf..c30a62b4921 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -37,6 +37,7 @@ import { } from './tabs'; import { CodeMirrorEditor } from './types'; import { STORAGE_KEY as STORAGE_KEY_VARIABLES } from './variable-editor'; +import { editor } from 'monaco-editor'; export type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & { documentAST: DocumentNode | null; @@ -79,39 +80,39 @@ export type EditorContextType = TabsState & { ): void; /** - * The CodeMirror editor instance for the headers editor. + * The CodeMirror editor instance for the headers' editor. */ - headerEditor: CodeMirrorEditor | null; + headerEditor: editor.IStandaloneCodeEditor | null; /** * The CodeMirror editor instance for the query editor. This editor also * stores the operation facts that are derived from the current editor * contents. */ - queryEditor: CodeMirrorEditorWithOperationFacts | null; + queryEditor: editor.IStandaloneCodeEditor | null; /** * The CodeMirror editor instance for the response editor. */ - responseEditor: CodeMirrorEditor | null; + responseEditor: editor.IStandaloneCodeEditor | null; /** - * The CodeMirror editor instance for the variables editor. + * The CodeMirror editor instance for the variables' editor. */ - variableEditor: CodeMirrorEditor | null; + variableEditor: editor.IStandaloneCodeEditor | null; /** - * Set the CodeMirror editor instance for the headers editor. + * Set the CodeMirror editor instance for the headers' editor. */ - setHeaderEditor(newEditor: CodeMirrorEditor): void; + setHeaderEditor(newEditor: editor.IStandaloneCodeEditor): void; /** * Set the CodeMirror editor instance for the query editor. */ - setQueryEditor(newEditor: CodeMirrorEditorWithOperationFacts): void; + setQueryEditor(newEditor: editor.IStandaloneCodeEditor): void; /** * Set the CodeMirror editor instance for the response editor. */ - setResponseEditor(newEditor: CodeMirrorEditor): void; + setResponseEditor(newEditor: editor.IStandaloneCodeEditor): void; /** - * Set the CodeMirror editor instance for the variables editor. + * Set the CodeMirror editor instance for the variables' editor. */ - setVariableEditor(newEditor: CodeMirrorEditor): void; + setVariableEditor(newEditor: editor.IStandaloneCodeEditor): void; /** * Changes the operation name and invokes the `onEditOperationName` callback. @@ -207,7 +208,7 @@ export type EditorContextProviderProps = { /** * Invoked when the operation name changes. Possible triggers are: * - Editing the contents of the query editor - * - Selecting a operation for execution in a document that contains multiple + * - Selecting an operation for execution in a document that contains multiple * operation definitions * @param operationName The operation name after it has been changed. */ @@ -248,7 +249,7 @@ export type EditorContextProviderProps = { */ validationRules?: ValidationRule[]; /** - * This prop can be used to set the contents of the variables editor. Every + * This prop can be used to set the contents of the variables' editor. Every * time this prop changes, the contents of the variables editor are replaced. * Note that the editor contents can be changed in between these updates by * typing in the editor. @@ -263,15 +264,15 @@ export type EditorContextProviderProps = { export function EditorContextProvider(props: EditorContextProviderProps) { const storage = useStorageContext(); - const [headerEditor, setHeaderEditor] = useState( + const [headerEditor, setHeaderEditor] = useState( null, ); const [queryEditor, setQueryEditor] = - useState(null); - const [responseEditor, setResponseEditor] = useState( + useState(null); + const [responseEditor, setResponseEditor] = useState( null, ); - const [variableEditor, setVariableEditor] = useState( + const [variableEditor, setVariableEditor] = useState( null, ); @@ -458,6 +459,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { return; } + // @ts-expect-error FIXME: MONACO queryEditor.operationName = operationName; updateActiveTabValues({ operationName }); onEditOperationName?.(operationName); @@ -555,7 +557,7 @@ export const useEditorContext = createContextHook(EditorContext); const PERSIST_HEADERS_STORAGE_KEY = 'shouldPersistHeaders'; -const DEFAULT_QUERY = `# Welcome to GraphiQL +export const DEFAULT_QUERY = `# Welcome to GraphiQL # # GraphiQL is an in-browser tool for writing, validating, and # testing GraphQL queries. diff --git a/packages/graphiql-react/src/editor/header-editor.ts b/packages/graphiql-react/src/editor/header-editor.ts index db700f56fea..bf03301e99f 100644 --- a/packages/graphiql-react/src/editor/header-editor.ts +++ b/packages/graphiql-react/src/editor/header-editor.ts @@ -1,3 +1,4 @@ +// @ts-nocheck -- codemirror editor complain about type errors import { useEffect, useRef } from 'react'; import { useExecutionContext } from '../execution'; diff --git a/packages/graphiql-react/src/editor/hooks.ts b/packages/graphiql-react/src/editor/hooks.ts index d86d2837e91..b392114743b 100644 --- a/packages/graphiql-react/src/editor/hooks.ts +++ b/packages/graphiql-react/src/editor/hooks.ts @@ -13,10 +13,11 @@ import debounce from '../utility/debounce'; import { onHasCompletion } from './completion'; import { useEditorContext } from './context'; import { CodeMirrorEditor } from './types'; +import { editor as MONACO_EDITOR } from 'monaco-editor'; export function useSynchronizeValue( - editor: CodeMirrorEditor | null, - value: string | undefined, + editor: MONACO_EDITOR.IStandaloneCodeEditor | null, + value?: string, ) { useEffect(() => { if (editor && typeof value === 'string' && value !== editor.getValue()) { @@ -31,9 +32,7 @@ export function useSynchronizeOption( value: EditorConfiguration[K], ) { useEffect(() => { - if (editor) { - editor.setOption(option, value); - } + editor?.setOption(option, value); }, [editor, option, value]); } @@ -130,7 +129,7 @@ type EmptyCallback = () => void; export function useKeyMap( editor: CodeMirrorEditor | null, keys: string[], - callback: EmptyCallback | undefined, + callback?: EmptyCallback, ) { useEffect(() => { if (!editor) { @@ -194,6 +193,7 @@ export function useMergeQuery({ caller }: UseMergeQueryArgs = {}) { }); const { schema } = useSchemaContext({ nonNull: true, caller: useMergeQuery }); return useCallback(() => { + // @ts-expect-error FIXME: MONACO const documentAST = queryEditor?.documentAST; const query = queryEditor?.getValue(); if (!documentAST || !query) { @@ -298,35 +298,36 @@ export function useAutoCompleteLeafs({ getDefaultFieldNames, ); if (insertions && insertions.length > 0) { - queryEditor.operation(() => { - const cursor = queryEditor.getCursor(); - const cursorIndex = queryEditor.indexFromPos(cursor); - queryEditor.setValue(result || ''); - let added = 0; - const markers = insertions.map(({ index, string }) => - queryEditor.markText( - queryEditor.posFromIndex(index + added), - queryEditor.posFromIndex(index + (added += string.length)), - { - className: 'auto-inserted-leaf', - clearOnEnter: true, - title: 'Automatically added leaf fields', - }, - ), - ); - setTimeout(() => { - for (const marker of markers) { - marker.clear(); - } - }, 7000); - let newCursorIndex = cursorIndex; - for (const { index, string } of insertions) { - if (index < cursorIndex) { - newCursorIndex += string.length; - } - } - queryEditor.setCursor(queryEditor.posFromIndex(newCursorIndex)); - }); + // FIXME: MONACO + // queryEditor.operation(() => { + // const cursor = queryEditor.getCursor(); + // const cursorIndex = queryEditor.indexFromPos(cursor); + // queryEditor.setValue(result || ''); + // let added = 0; + // const markers = insertions.map(({ index, string }) => + // queryEditor.markText( + // queryEditor.posFromIndex(index + added), + // queryEditor.posFromIndex(index + (added += string.length)), + // { + // className: 'auto-inserted-leaf', + // clearOnEnter: true, + // title: 'Automatically added leaf fields', + // }, + // ), + // ); + // setTimeout(() => { + // for (const marker of markers) { + // marker.clear(); + // } + // }, 7000); + // let newCursorIndex = cursorIndex; + // for (const { index, string } of insertions) { + // if (index < cursorIndex) { + // newCursorIndex += string.length; + // } + // } + // queryEditor.setCursor(queryEditor.posFromIndex(newCursorIndex)); + // }); } return result; diff --git a/packages/graphiql-react/src/editor/index.ts b/packages/graphiql-react/src/editor/index.ts index a62201b24a5..b01d2217e71 100644 --- a/packages/graphiql-react/src/editor/index.ts +++ b/packages/graphiql-react/src/editor/index.ts @@ -4,6 +4,10 @@ export { QueryEditor, ResponseEditor, VariableEditor, + OperationsEditor, + VariablesEditor, + HeadersEditor, + ResultsEditor } from './components'; export { EditorContext, diff --git a/packages/graphiql-react/src/editor/operations-editor.ts b/packages/graphiql-react/src/editor/operations-editor.ts new file mode 100644 index 00000000000..f571becb12a --- /dev/null +++ b/packages/graphiql-react/src/editor/operations-editor.ts @@ -0,0 +1,150 @@ +import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; +import type { DocumentNode } from 'graphql'; +import { useEffect, useRef } from 'react'; + +import { useExecutionContext } from '../execution'; +import { useExplorerContext } from '../explorer'; +import { DOC_EXPLORER_PLUGIN, usePluginContext } from '../plugin'; +import { useSchemaContext } from '../schema'; +import { useStorageContext } from '../storage'; +import debounce from '../utility/debounce'; +import { DEFAULT_EDITOR_THEME, DEFAULT_KEY_MAP } from './common'; +import { useEditorContext } from './context'; +import { + useCopyQuery, + UseCopyQueryArgs, + useMergeQuery, + usePrettifyEditors, +} from './hooks'; +import { WriteableEditorProps } from './types'; +import { KeyCode, KeyMod } from 'monaco-editor'; +import { MONACO_GRAPHQL_API, OPERATIONS_MODEL } from '@/constants'; +import { createEditor } from '@/create-editor'; + +export type UseQueryEditorArgs = WriteableEditorProps & + Pick & { + /** + * Invoked when a reference to the GraphQL schema (type or field) is clicked + * as part of the editor or one of its tooltips. + * @param reference The reference that has been clicked. + */ + onClickReference?(reference: SchemaReference): void; + /** + * Invoked when the contents of the query editor change. + * @param value The new contents of the editor. + * @param documentAST The editor contents parsed into a GraphQL document. + */ + onEdit?(value: string, documentAST?: DocumentNode): void; + }; + +export function useQueryEditor( + { + editorTheme = DEFAULT_EDITOR_THEME, + keyMap = DEFAULT_KEY_MAP, + onClickReference, + onCopyQuery, + onEdit, + readOnly = false, + }: UseQueryEditorArgs = {}, + caller?: Function, +) { + const { schema } = useSchemaContext({ + nonNull: true, + caller: caller || useQueryEditor, + }); + const { + externalFragments, + initialQuery, + queryEditor, + setOperationName, + setQueryEditor, + validationRules, + variableEditor, + updateActiveTabValues, + } = useEditorContext({ + nonNull: true, + caller: caller || useQueryEditor, + }); + const executionContext = useExecutionContext(); + const storage = useStorageContext(); + const explorer = useExplorerContext(); + const plugin = usePluginContext(); + const copy = useCopyQuery({ caller: caller || useQueryEditor, onCopyQuery }); + const merge = useMergeQuery({ caller: caller || useQueryEditor }); + const prettify = usePrettifyEditors({ caller: caller || useQueryEditor }); + const ref = useRef(null); + const { run } = executionContext!; + + useEffect(() => { + setQueryEditor(createEditor('operations', ref.current!)); + OPERATIONS_MODEL.onDidChangeContent( + debounce(100, () => { + const value = OPERATIONS_MODEL.getValue(); + storage?.set(STORAGE_KEY_QUERY, value); + updateActiveTabValues({ + query: value, + operationName: /* operationFacts?.operationName ?? */ null, + }); + }), + ); + // eslint-disable-next-line react-hooks/exhaustive-deps -- only on mount + }, []); + + useEffect(() => { + // add the runOperationAction to the operation and variables editors + queryEditor?.addAction({ + id: 'graphql-run-operation', + label: 'Run Operation', + contextMenuOrder: 0, + contextMenuGroupId: 'graphql', + // eslint-disable-next-line no-bitwise + keybindings: [KeyMod.CtrlCmd | KeyCode.Enter], + run, + }); + }, [queryEditor, run]); + + useEffect(() => { + if (schema) { + MONACO_GRAPHQL_API.setSchemaConfig([{ uri: 'schema.graphql', schema }]); + } + }, [schema]); + + const onClickReferenceRef = useRef< + NonNullable + >(() => {}); + useEffect(() => { + onClickReferenceRef.current = reference => { + if (!explorer || !plugin) { + return; + } + plugin.setVisiblePlugin(DOC_EXPLORER_PLUGIN); + switch (reference.kind) { + case 'Type': { + explorer.push({ name: reference.type.name, def: reference.type }); + break; + } + case 'Field': { + explorer.push({ name: reference.field.name, def: reference.field }); + break; + } + case 'Argument': { + if (reference.field) { + explorer.push({ name: reference.field.name, def: reference.field }); + } + break; + } + case 'EnumValue': { + if (reference.type) { + explorer.push({ name: reference.type.name, def: reference.type }); + } + break; + } + } + onClickReference?.(reference); + }; + }, [explorer, onClickReference, plugin]); + + return ref; +} + +export const STORAGE_KEY_QUERY = 'query'; diff --git a/packages/graphiql-react/src/editor/query-editor.ts b/packages/graphiql-react/src/editor/query-editor.ts index 9d3909aa914..c5fca98d696 100644 --- a/packages/graphiql-react/src/editor/query-editor.ts +++ b/packages/graphiql-react/src/editor/query-editor.ts @@ -1,3 +1,4 @@ +// @ts-nocheck -- codemirror editor complain about type errors import { getSelectedOperationName } from '@graphiql/toolkit'; import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; import type { @@ -190,13 +191,13 @@ export function useQueryEditor( info: { schema: undefined, renderDescription: (text: string) => markdown.render(text), - onClick: (reference: SchemaReference) => { + onClick(reference: SchemaReference) { onClickReferenceRef.current(reference); }, }, jump: { schema: undefined, - onClick: (reference: SchemaReference) => { + onClick(reference: SchemaReference) { onClickReferenceRef.current(reference); }, }, @@ -449,8 +450,8 @@ function useSynchronizeSchema( editor.options.info.schema = schema; editor.options.jump.schema = schema; - if (didChange && codeMirrorRef.current) { - codeMirrorRef.current.signal(editor, 'change', editor); + if (didChange) { + codeMirrorRef.current?.signal(editor, 'change', editor); } }, [editor, schema, codeMirrorRef]); } @@ -470,8 +471,8 @@ function useSynchronizeValidationRules( editor.state.lint.linterOptions.validationRules = validationRules; editor.options.lint.validationRules = validationRules; - if (didChange && codeMirrorRef.current) { - codeMirrorRef.current.signal(editor, 'change', editor); + if (didChange) { + codeMirrorRef.current?.signal(editor, 'change', editor); } }, [editor, validationRules, codeMirrorRef]); } @@ -498,8 +499,8 @@ function useSynchronizeExternalFragments( editor.options.lint.externalFragments = externalFragmentList; editor.options.hintOptions.externalFragments = externalFragmentList; - if (didChange && codeMirrorRef.current) { - codeMirrorRef.current.signal(editor, 'change', editor); + if (didChange) { + codeMirrorRef.current?.signal(editor, 'change', editor); } }, [editor, externalFragmentList, codeMirrorRef]); } diff --git a/packages/graphiql-react/src/editor/response-editor.tsx b/packages/graphiql-react/src/editor/response-editor.tsx index c7b7cb31b75..f4731827a81 100644 --- a/packages/graphiql-react/src/editor/response-editor.tsx +++ b/packages/graphiql-react/src/editor/response-editor.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck -- codemirror editor complain about type errors import { formatError } from '@graphiql/toolkit'; import type { Position, Token } from 'codemirror'; import { ComponentType, useEffect, useRef } from 'react'; diff --git a/packages/graphiql-react/src/editor/style/editor.css b/packages/graphiql-react/src/editor/style/editor.css index c9d0767d20c..bc4d2ce5c7e 100644 --- a/packages/graphiql-react/src/editor/style/editor.css +++ b/packages/graphiql-react/src/editor/style/editor.css @@ -11,3 +11,8 @@ visibility: hidden; } } + +.monaco-editor { + /* otherwise editor could grow the width and not shrink it */ + position: absolute; +} diff --git a/packages/graphiql-react/src/editor/tabs.ts b/packages/graphiql-react/src/editor/tabs.ts index 7622cb0cfc1..342bdbba9f8 100644 --- a/packages/graphiql-react/src/editor/tabs.ts +++ b/packages/graphiql-react/src/editor/tabs.ts @@ -1,9 +1,8 @@ import { StorageAPI } from '@graphiql/toolkit'; import { useCallback, useMemo } from 'react'; +import { editor } from 'monaco-editor'; import debounce from '../utility/debounce'; -import { CodeMirrorEditorWithOperationFacts } from './context'; -import { CodeMirrorEditor } from './types'; export type TabDefinition = { /** @@ -185,16 +184,17 @@ export function useSynchronizeActiveTabValues({ headerEditor, responseEditor, }: { - queryEditor: CodeMirrorEditorWithOperationFacts | null; - variableEditor: CodeMirrorEditor | null; - headerEditor: CodeMirrorEditor | null; - responseEditor: CodeMirrorEditor | null; + queryEditor: editor.IStandaloneCodeEditor | null; + variableEditor: editor.IStandaloneCodeEditor | null; + headerEditor: editor.IStandaloneCodeEditor | null; + responseEditor: editor.IStandaloneCodeEditor | null; }) { return useCallback<(state: TabsState) => TabsState>( state => { const query = queryEditor?.getValue() ?? null; const variables = variableEditor?.getValue() ?? null; const headers = headerEditor?.getValue() ?? null; + // @ts-expect-error FIXME: MONACO const operationName = queryEditor?.operationName ?? null; const response = responseEditor?.getValue() ?? null; return setPropertiesInActiveTab(state, { @@ -250,10 +250,10 @@ export function useSetEditorValues({ headerEditor, responseEditor, }: { - queryEditor: CodeMirrorEditorWithOperationFacts | null; - variableEditor: CodeMirrorEditor | null; - headerEditor: CodeMirrorEditor | null; - responseEditor: CodeMirrorEditor | null; + queryEditor: editor.IStandaloneCodeEditor | null; + variableEditor: editor.IStandaloneCodeEditor | null; + headerEditor: editor.IStandaloneCodeEditor | null; + responseEditor: editor.IStandaloneCodeEditor | null; }) { return useCallback( ({ diff --git a/packages/graphiql-react/src/editor/variable-editor.ts b/packages/graphiql-react/src/editor/variable-editor.ts index 2213c383e27..2be523c9cbe 100644 --- a/packages/graphiql-react/src/editor/variable-editor.ts +++ b/packages/graphiql-react/src/editor/variable-editor.ts @@ -1,3 +1,4 @@ +// @ts-nocheck -- codemirror editor complain about type errors import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; import { useEffect, useRef } from 'react'; diff --git a/packages/graphiql-react/src/execution.tsx b/packages/graphiql-react/src/execution.tsx index d66b4eea78b..ee9a8bfc9a6 100644 --- a/packages/graphiql-react/src/execution.tsx +++ b/packages/graphiql-react/src/execution.tsx @@ -155,8 +155,10 @@ export function ExecutionContextProvider({ } if (externalFragments) { + // @ts-expect-error FIXME: MONACO const fragmentDependencies = queryEditor.documentAST ? getFragmentDependenciesForAST( + // @ts-expect-error FIXME: MONACO queryEditor.documentAST, externalFragments, ) @@ -173,6 +175,7 @@ export function ExecutionContextProvider({ setResponse(''); setIsFetching(true); + // @ts-expect-error FIXME: MONACO const opName = operationName ?? queryEditor.operationName ?? undefined; history?.addToHistory({ @@ -255,6 +258,7 @@ export function ExecutionContextProvider({ }, { headers: headers ?? undefined, + // @ts-expect-error FIXME: MONACO documentAST: queryEditor.documentAST ?? undefined, }, ); diff --git a/packages/graphiql-react/src/explorer/components/search.tsx b/packages/graphiql-react/src/explorer/components/search.tsx index 3c4e01ff8ea..7affdeca08a 100644 --- a/packages/graphiql-react/src/explorer/components/search.tsx +++ b/packages/graphiql-react/src/explorer/components/search.tsx @@ -7,7 +7,14 @@ import { isInterfaceType, isObjectType, } from 'graphql'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + FocusEventHandler, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { Combobox } from '@headlessui/react'; import { MagnifyingGlassIcon } from '../../icons'; import { useSchemaContext } from '../../schema'; @@ -63,7 +70,7 @@ export function Search() { [push], ); const [isFocused, setIsFocused] = useState(false); - const handleFocus = useCallback(e => { + const handleFocus: FocusEventHandler = useCallback(e => { setIsFocused(e.type === 'focus'); }, []); diff --git a/packages/graphiql-react/src/explorer/context.tsx b/packages/graphiql-react/src/explorer/context.tsx index cf545334306..2d2fd7bffe8 100644 --- a/packages/graphiql-react/src/explorer/context.tsx +++ b/packages/graphiql-react/src/explorer/context.tsx @@ -109,85 +109,84 @@ export function ExplorerContextProvider(props: ExplorerContextProviderProps) { // Whenever the schema changes, we must revalidate/replace the nav stack. if (schema == null || validationErrors.length > 0) { reset(); - } else { - // Replace the nav stack with an updated version using the new schema - setNavStack(oldNavStack => { - if (oldNavStack.length === 1) { - return oldNavStack; + return; + } + // Replace the nav stack with an updated version using the new schema + setNavStack(oldNavStack => { + if (oldNavStack.length === 1) { + return oldNavStack; + } + const newNavStack: ExplorerNavStack = [initialNavStackItem]; + let lastEntity: GraphQLNamedType | GraphQLField | null = null; + for (const item of oldNavStack) { + if (item === initialNavStackItem) { + // No need to copy the initial item + continue; } - const newNavStack: ExplorerNavStack = [initialNavStackItem]; - let lastEntity: GraphQLNamedType | GraphQLField | null = - null; - for (const item of oldNavStack) { - if (item === initialNavStackItem) { - // No need to copy the initial item - continue; - } - if (item.def) { - // If item.def isn't a named type, it must be a field, inputField, or argument - if (isNamedType(item.def)) { - // The type needs to be replaced with the new schema type of the same name - const newType = schema.getType(item.def.name); - if (newType) { - newNavStack.push({ - name: item.name, - def: newType, - }); - lastEntity = newType; - } else { - // This type no longer exists; the stack cannot be built beyond here - break; - } - } else if (lastEntity === null) { - // We can't have a sub-entity if we have no entity; stop rebuilding the nav stack - break; - } else if ( - isObjectType(lastEntity) || - isInputObjectType(lastEntity) - ) { - // item.def must be a Field / input field; replace with the new field of the same name - const field = lastEntity.getFields()[item.name]; - if (field) { - newNavStack.push({ - name: item.name, - def: field, - }); - } else { - // This field no longer exists; the stack cannot be built beyond here - break; - } - } else if ( - isScalarType(lastEntity) || - isEnumType(lastEntity) || - isInterfaceType(lastEntity) || - isUnionType(lastEntity) - ) { - // These don't (currently) have non-type sub-entries; something has gone wrong. - // Handle gracefully by discontinuing rebuilding the stack. + if (item.def) { + // If item.def isn't a named type, it must be a field, inputField, or argument + if (isNamedType(item.def)) { + // The type needs to be replaced with the new schema type of the same name + const newType = schema.getType(item.def.name); + if (newType) { + newNavStack.push({ + name: item.name, + def: newType, + }); + lastEntity = newType; + } else { + // This type no longer exists; the stack cannot be built beyond here break; + } + } else if (lastEntity === null) { + // We can't have a sub-entity if we have no entity; stop rebuilding the nav stack + break; + } else if ( + isObjectType(lastEntity) || + isInputObjectType(lastEntity) + ) { + // item.def must be a Field / input field; replace with the new field of the same name + const field = lastEntity.getFields()[item.name]; + if (field) { + newNavStack.push({ + name: item.name, + def: field, + }); } else { - // lastEntity must be a field (because it's not a named type) - const field: GraphQLField = lastEntity; - // Thus item.def must be an argument, so find the same named argument in the new schema - const arg = field.args.find(a => a.name === item.name); - if (arg) { - newNavStack.push({ - name: item.name, - def: field, - }); - } else { - // This argument no longer exists; the stack cannot be built beyond here - break; - } + // This field no longer exists; the stack cannot be built beyond here + break; } + } else if ( + isScalarType(lastEntity) || + isEnumType(lastEntity) || + isInterfaceType(lastEntity) || + isUnionType(lastEntity) + ) { + // These don't (currently) have non-type sub-entries; something has gone wrong. + // Handle gracefully by discontinuing rebuilding the stack. + break; } else { - lastEntity = null; - newNavStack.push(item); + // lastEntity must be a field (because it's not a named type) + const field: GraphQLField = lastEntity; + // Thus item.def must be an argument, so find the same named argument in the new schema + const arg = field.args.find(a => a.name === item.name); + if (arg) { + newNavStack.push({ + name: item.name, + def: field, + }); + } else { + // This argument no longer exists; the stack cannot be built beyond here + break; + } } + } else { + lastEntity = null; + newNavStack.push(item); } - return newNavStack; - }); - } + } + return newNavStack; + }); }, [reset, schema, validationErrors]); const value = useMemo( diff --git a/packages/graphiql-react/src/index.ts b/packages/graphiql-react/src/index.ts index 4e9b87fad90..db7fcc4907b 100644 --- a/packages/graphiql-react/src/index.ts +++ b/packages/graphiql-react/src/index.ts @@ -3,20 +3,16 @@ import './style/root.css'; export { EditorContext, EditorContextProvider, - HeaderEditor, ImagePreview, - QueryEditor, - ResponseEditor, useAutoCompleteLeafs, useCopyQuery, useEditorContext, - useHeaderEditor, useMergeQuery, usePrettifyEditors, - useQueryEditor, - useResponseEditor, - useVariableEditor, - VariableEditor, + OperationsEditor, + VariablesEditor, + HeadersEditor, + ResultsEditor, } from './editor'; export { ExecutionContext, diff --git a/packages/graphiql-react/src/plugin.tsx b/packages/graphiql-react/src/plugin.tsx index 27194b13f97..e0c19ff2117 100644 --- a/packages/graphiql-react/src/plugin.tsx +++ b/packages/graphiql-react/src/plugin.tsx @@ -120,10 +120,9 @@ export function PluginContextProvider(props: PluginContextProviderProps) { throw new Error( `All GraphiQL plugins must have a unique title, found two plugins with the title '${plugin.title}'`, ); - } else { - pluginList.push(plugin); - pluginTitles[plugin.title] = true; } + pluginList.push(plugin); + pluginTitles[plugin.title] = true; } return pluginList; diff --git a/packages/graphiql-react/src/toolbar/execute.tsx b/packages/graphiql-react/src/toolbar/execute.tsx index ff7eb1e70f9..74ac1c3c5de 100644 --- a/packages/graphiql-react/src/toolbar/execute.tsx +++ b/packages/graphiql-react/src/toolbar/execute.tsx @@ -16,6 +16,7 @@ export function ExecuteButton() { caller: ExecuteButton, }); + // @ts-expect-error FIXME: MONACO const operations = queryEditor?.operations || []; const hasOptions = operations.length > 1 && typeof operationName !== 'string'; const isRunning = isFetching || isSubscribed; @@ -35,6 +36,7 @@ export function ExecuteButton() { + {/* @ts-expect-error FIXME: MONACO */} {operations.map((operation, i) => { const opName = operation.name ? operation.name.value @@ -47,6 +49,7 @@ export function ExecuteButton() { if ( queryEditor && selectedOperationName && + // @ts-expect-error FIXME: MONACO selectedOperationName !== queryEditor.operationName ) { setOperationName(selectedOperationName); diff --git a/packages/graphiql-react/src/utility/resize.ts b/packages/graphiql-react/src/utility/resize.ts index d8cd0383793..ac1cb18b16f 100644 --- a/packages/graphiql-react/src/utility/resize.ts +++ b/packages/graphiql-react/src/utility/resize.ts @@ -63,8 +63,8 @@ export function useDragResize({ const store = useMemo( () => debounce(500, (value: string) => { - if (storage && storageKey) { - storage.set(storageKey, value); + if (storageKey) { + storage?.set(storageKey, value); } }), [storage, storageKey], @@ -72,8 +72,7 @@ export function useDragResize({ const [hiddenElement, setHiddenElement] = useState( () => { - const storedValue = - storage && storageKey ? storage.get(storageKey) : null; + const storedValue = storageKey && storage?.get(storageKey); if (storedValue === HIDE_FIRST || initiallyHidden === 'first') { return 'first'; } @@ -105,9 +104,7 @@ export function useDragResize({ */ useLayoutEffect(() => { const storedValue = - storage && storageKey - ? storage.get(storageKey) || defaultFlexRef.current - : defaultFlexRef.current; + (storageKey && storage?.get(storageKey)) || defaultFlexRef.current; const flexDirection = direction === 'horizontal' ? 'row' : 'column'; if (firstRef.current) { @@ -172,9 +169,13 @@ export function useDragResize({ element.style.position = ''; element.style.left = ''; - if (firstRef.current && storage && storageKey) { + if (storageKey) { const storedValue = storage?.get(storageKey); - if (storedValue !== HIDE_FIRST && storedValue !== HIDE_SECOND) { + if ( + firstRef.current && + storedValue !== HIDE_FIRST && + storedValue !== HIDE_SECOND + ) { firstRef.current.style.flex = storedValue || defaultFlexRef.current; } } diff --git a/packages/graphiql-react/tsconfig.json b/packages/graphiql-react/tsconfig.json index 95480738c1d..1b35e647332 100644 --- a/packages/graphiql-react/tsconfig.json +++ b/packages/graphiql-react/tsconfig.json @@ -16,8 +16,16 @@ "jsx": "react-jsx", "declaration": true, "declarationDir": "types", - "outDir": "tsc" + "outDir": "tsc", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } }, "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] + "references": [ + { + "path": "./tsconfig.node.json" + } + ] } diff --git a/packages/graphiql-react/tsconfig.node.json b/packages/graphiql-react/tsconfig.node.json index e993792cb12..a336f895aab 100644 --- a/packages/graphiql-react/tsconfig.node.json +++ b/packages/graphiql-react/tsconfig.node.json @@ -2,7 +2,8 @@ "compilerOptions": { "composite": true, "module": "esnext", - "moduleResolution": "node" + "moduleResolution": "node", + "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } diff --git a/packages/graphiql-react/vite.config.ts b/packages/graphiql-react/vite.config.ts index 08df2c79573..ced4aa24020 100644 --- a/packages/graphiql-react/vite.config.ts +++ b/packages/graphiql-react/vite.config.ts @@ -1,3 +1,4 @@ +import path from 'node:path'; import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import svgr from 'vite-plugin-svgr'; @@ -34,6 +35,7 @@ export default defineConfig({ rollupOptions: { external: [ 'react/jsx-runtime', + 'monaco-graphql/esm/initializeMode', // Exclude peer dependencies and dependencies from bundle ...Object.keys(packageJSON.peerDependencies), ...Object.keys(packageJSON.dependencies).filter( @@ -44,5 +46,11 @@ export default defineConfig({ chunkFileNames: '[name].[format].js', }, }, + emptyOutDir: false, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, }, }); diff --git a/packages/graphiql/package.json b/packages/graphiql/package.json index e94a496e7e9..a736f12d723 100644 --- a/packages/graphiql/package.json +++ b/packages/graphiql/package.json @@ -61,6 +61,7 @@ "react-dom": "^16.8.0 || ^17 || ^18" }, "devDependencies": { + "monaco-editor-webpack-plugin": "^7.0.1", "@cypress/webpack-preprocessor": "^5.5.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "12.1.5", diff --git a/packages/graphiql/resources/webpack.config.js b/packages/graphiql/resources/webpack.config.js index c9e23e20a64..3ac5a816040 100644 --- a/packages/graphiql/resources/webpack.config.js +++ b/packages/graphiql/resources/webpack.config.js @@ -7,6 +7,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const graphql = require('graphql'); const rimraf = require('rimraf'); +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); const isDev = process.env.NODE_ENV === 'development'; const isHMR = Boolean(isDev && process.env.WEBPACK_DEV_SERVER); @@ -106,6 +107,19 @@ const resultConfig = { }); } })(), + new MonacoWebpackPlugin({ + languages: ['json', 'graphql'], + publicPath: '/', + customLanguages: [ + { + label: 'graphql', + worker: { + id: 'graphql', + entry: 'monaco-graphql/esm/graphql.worker.js', + }, + }, + ], + }), ], resolve: { extensions: ['.mjs', '.js', '.json', '.jsx', '.css', '.ts', '.tsx'], diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 78c04e86588..f5e4577b26c 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -26,14 +26,11 @@ import { ExecuteButton, GraphiQLProvider, GraphiQLProviderProps, - HeaderEditor, KeyboardShortcutIcon, MergeIcon, PlusIcon, PrettifyIcon, - QueryEditor, ReloadIcon, - ResponseEditor, SettingsIcon, Spinner, Tab, @@ -55,8 +52,11 @@ import { useStorageContext, useTheme, UseVariableEditorArgs, - VariableEditor, WriteableEditorProps, + OperationsEditor, + VariablesEditor, + HeadersEditor, + ResultsEditor, } from '@graphiql/react'; const majorVersion = parseInt(React.version.slice(0, 2), 10); @@ -213,7 +213,6 @@ export type GraphiQLInterfaceProps = WriteableEditorProps & 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 }); @@ -577,7 +576,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { aria-label="Query Editor" >
- - {isHeadersEditorEnabled && ( -
{executionContext.isFetching ? : null} -