From edf3b8089f26f0b99c6cc2765cd2a0f07bd5969d Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 00:54:22 +0200 Subject: [PATCH 01/73] upd --- .changeset/ten-peas-carry.md | 5 +++ .../src/components/header-editor.tsx | 6 ++-- .../src/components/provider.tsx | 31 ++++++++++--------- .../src/components/query-editor.tsx | 15 ++++----- .../src/components/variable-editor.tsx | 6 ++-- packages/graphiql-react/src/constants.ts | 10 ++++++ packages/graphiql-react/src/stores/editor.ts | 18 +++++------ packages/graphiql-react/src/stores/index.ts | 4 +++ packages/graphiql-react/src/stores/plugin.ts | 5 ++- .../graphiql-react/src/utility/tabs.spec.ts | 6 ++-- packages/graphiql-react/src/utility/tabs.ts | 9 +++--- 11 files changed, 63 insertions(+), 52 deletions(-) create mode 100644 .changeset/ten-peas-carry.md diff --git a/.changeset/ten-peas-carry.md b/.changeset/ten-peas-carry.md new file mode 100644 index 0000000000..9ef67dcc24 --- /dev/null +++ b/.changeset/ten-peas-carry.md @@ -0,0 +1,5 @@ +--- +'@graphiql/react': minor +--- + +extract storage key constants diff --git a/packages/graphiql-react/src/components/header-editor.tsx b/packages/graphiql-react/src/components/header-editor.tsx index 71f8fdfa27..d0bbabf006 100644 --- a/packages/graphiql-react/src/components/header-editor.tsx +++ b/packages/graphiql-react/src/components/header-editor.tsx @@ -1,7 +1,7 @@ import { FC, useEffect, useRef } from 'react'; import { useGraphiQL, useGraphiQLActions } from './provider'; import { EditorProps } from '../types'; -import { HEADER_URI, KEY_BINDINGS } from '../constants'; +import { HEADER_URI, KEY_BINDINGS, STORAGE_KEY } from '../constants'; import { getOrCreateModel, createEditor, @@ -28,7 +28,7 @@ export const HeaderEditor: FC = ({ onEdit, ...props }) => { const ref = useRef(null!); useChangeHandler( onEdit, - shouldPersistHeaders ? STORAGE_KEY : null, + shouldPersistHeaders ? STORAGE_KEY.headers : null, 'headers', ); useEffect(() => { @@ -56,5 +56,3 @@ export const HeaderEditor: FC = ({ onEdit, ...props }) => { /> ); }; - -export const STORAGE_KEY = 'headers'; diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 1e32f905db..e3a88f1f3e 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -14,11 +14,11 @@ import { createExecutionSlice, createPluginSlice, createSchemaSlice, + EditorProps, + ExecutionProps, + PluginProps, + SchemaProps, } from '../stores'; -import { EditorProps, PERSIST_HEADERS_STORAGE_KEY } from '../stores/editor'; -import { ExecutionProps } from '../stores/execution'; -import { PluginProps, STORAGE_KEY_VISIBLE_PLUGIN } from '../stores/plugin'; -import { SchemaProps } from '../stores/schema'; import { StorageStore, useStorage } from '../stores/storage'; import { ThemeStore } from '../stores/theme'; import { SlicesWithActions } from '../types'; @@ -30,10 +30,11 @@ import { isSchema, validateSchema, } from 'graphql'; -import { DEFAULT_PRETTIFY_QUERY, DEFAULT_QUERY } from '../constants'; -import { STORAGE_KEY_QUERY } from './query-editor'; -import { STORAGE_KEY as STORAGE_KEY_VARIABLES } from './variable-editor'; -import { STORAGE_KEY as STORAGE_KEY_HEADERS } from './header-editor'; +import { + DEFAULT_PRETTIFY_QUERY, + DEFAULT_QUERY, + STORAGE_KEY, +} from '../constants'; import { getDefaultTabState } from '../utility/tabs'; interface InnerGraphiQLProviderProps @@ -121,7 +122,7 @@ const InnerGraphiQLProvider: FC = ({ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- false positive if (storeRef.current === null) { function getInitialVisiblePlugin() { - const storedValue = storage.get(STORAGE_KEY_VISIBLE_PLUGIN); + const storedValue = storage.get(STORAGE_KEY.visiblePlugin); const pluginForStoredValue = plugins.find( plugin => plugin.title === storedValue, ); @@ -129,17 +130,17 @@ const InnerGraphiQLProvider: FC = ({ return pluginForStoredValue; } if (storedValue) { - storage.set(STORAGE_KEY_VISIBLE_PLUGIN, ''); + storage.set(STORAGE_KEY.visiblePlugin, ''); } return visiblePlugin; } function getInitialState() { // We only need to compute it lazily during the initial render. - const query = props.query ?? storage.get(STORAGE_KEY_QUERY) ?? null; + const query = props.query ?? storage.get(STORAGE_KEY.query) ?? null; const variables = - props.variables ?? storage.get(STORAGE_KEY_VARIABLES) ?? null; - const headers = props.headers ?? storage.get(STORAGE_KEY_HEADERS) ?? null; + props.variables ?? storage.get(STORAGE_KEY.variables) ?? null; + const headers = props.headers ?? storage.get(STORAGE_KEY.headers) ?? null; const response = props.response ?? ''; const { tabs, activeTabIndex } = getDefaultTabState({ @@ -152,11 +153,11 @@ const InnerGraphiQLProvider: FC = ({ variables, }); - const isStored = storage.get(PERSIST_HEADERS_STORAGE_KEY) !== null; + const isStored = storage.get(STORAGE_KEY.persistHeaders) !== null; const $shouldPersistHeaders = shouldPersistHeaders !== false && isStored - ? storage.get(PERSIST_HEADERS_STORAGE_KEY) === 'true' + ? storage.get(STORAGE_KEY.persistHeaders) === 'true' : shouldPersistHeaders; const store = create((...args) => { diff --git a/packages/graphiql-react/src/components/query-editor.tsx b/packages/graphiql-react/src/components/query-editor.tsx index 513e6e6298..9ab79f2f65 100644 --- a/packages/graphiql-react/src/components/query-editor.tsx +++ b/packages/graphiql-react/src/components/query-editor.tsx @@ -14,7 +14,12 @@ import { cn, } from '../utility'; import { MonacoEditor, EditorProps, SchemaReference } from '../types'; -import { KEY_BINDINGS, MONACO_GRAPHQL_API, QUERY_URI } from '../constants'; +import { + KEY_BINDINGS, + MONACO_GRAPHQL_API, + QUERY_URI, + STORAGE_KEY, +} from '../constants'; import { type editor as monacoEditor, languages, @@ -210,11 +215,11 @@ export const QueryEditor: FC = ({ // have additional logic that updates the operation facts that we save in `editorStore` const handleChange = debounce(100, () => { const query = editor.getValue(); - storage.set(STORAGE_KEY_QUERY, query); + storage.set(STORAGE_KEY.query, query); const operationFacts = getAndUpdateOperationFacts(editor); if (operationFacts?.operationName !== undefined) { - storage.set(STORAGE_KEY_OPERATION_NAME, operationFacts.operationName); + storage.set(STORAGE_KEY.operationName, operationFacts.operationName); } // Invoke callback props only after the operation facts have been updated @@ -333,7 +338,3 @@ export const QueryEditor: FC = ({ /> ); }; - -export const STORAGE_KEY_QUERY = 'query'; - -const STORAGE_KEY_OPERATION_NAME = 'operationName'; diff --git a/packages/graphiql-react/src/components/variable-editor.tsx b/packages/graphiql-react/src/components/variable-editor.tsx index 4214a65301..b8fc3e49bc 100644 --- a/packages/graphiql-react/src/components/variable-editor.tsx +++ b/packages/graphiql-react/src/components/variable-editor.tsx @@ -1,7 +1,7 @@ import { FC, useEffect, useRef } from 'react'; import { useGraphiQL, useGraphiQLActions } from './provider'; import { EditorProps } from '../types'; -import { KEY_BINDINGS, VARIABLE_URI } from '../constants'; +import { KEY_BINDINGS, STORAGE_KEY, VARIABLE_URI } from '../constants'; import { getOrCreateModel, createEditor, @@ -26,7 +26,7 @@ export const VariableEditor: FC = ({ const { setEditor, run, prettifyEditors, mergeQuery } = useGraphiQLActions(); const initialVariables = useGraphiQL(state => state.initialVariables); const ref = useRef(null!); - useChangeHandler(onEdit, STORAGE_KEY, 'variables'); + useChangeHandler(onEdit, STORAGE_KEY.variables, 'variables'); useEffect(() => { const model = getOrCreateModel({ uri: VARIABLE_URI, @@ -54,5 +54,3 @@ export const VariableEditor: FC = ({ /> ); }; - -export const STORAGE_KEY = 'variables'; diff --git a/packages/graphiql-react/src/constants.ts b/packages/graphiql-react/src/constants.ts index e7ed34f2a7..70b083ed3e 100644 --- a/packages/graphiql-react/src/constants.ts +++ b/packages/graphiql-react/src/constants.ts @@ -42,6 +42,16 @@ export const KEY_MAP = Object.freeze({ }, }); +export const STORAGE_KEY = { + headers: 'headers', + visiblePlugin: 'visiblePlugin', + query: 'query', + variables: 'variables', + tabs: 'tabState', + operationName: 'operationName', + persistHeaders: 'shouldPersistHeaders', +} as const; + export const DEFAULT_QUERY = `# Welcome to GraphiQL # # GraphiQL is an in-browser tool for writing, validating, and testing diff --git a/packages/graphiql-react/src/stores/editor.ts b/packages/graphiql-react/src/stores/editor.ts index 38cfc4cbf6..59b1550e60 100644 --- a/packages/graphiql-react/src/stores/editor.ts +++ b/packages/graphiql-react/src/stores/editor.ts @@ -6,9 +6,8 @@ import type { } from 'graphql'; import { OperationFacts } from 'graphql-language-service'; import { MaybePromise, mergeAst } from '@graphiql/toolkit'; +import { print } from 'graphql'; import { storageStore } from './storage'; -import { STORAGE_KEY as STORAGE_KEY_HEADERS } from '../components/header-editor'; - import { createTab, setPropertiesInActiveTab, @@ -17,11 +16,10 @@ import { TabState, clearHeadersFromTabs, serializeTabState, - STORAGE_KEY as STORAGE_KEY_TABS, } from '../utility/tabs'; import { SlicesWithActions, MonacoEditor } from '../types'; import { debounce, formatJSONC } from '../utility'; -import { print } from 'graphql'; +import { STORAGE_KEY } from '../constants'; export interface EditorSlice extends TabsState { /** @@ -478,24 +476,24 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { const { headerEditor, tabs, activeTabIndex } = get(); const { storage } = storageStore.getState(); if (persist) { - storage.set(STORAGE_KEY_HEADERS, headerEditor?.getValue() ?? ''); + storage.set(STORAGE_KEY.headers, headerEditor?.getValue() ?? ''); const serializedTabs = serializeTabState( { tabs, activeTabIndex }, true, ); - storage.set(STORAGE_KEY_TABS, serializedTabs); + storage.set(STORAGE_KEY.tabs, serializedTabs); } else { - storage.set(STORAGE_KEY_HEADERS, ''); + storage.set(STORAGE_KEY.headers, ''); clearHeadersFromTabs(); } set({ shouldPersistHeaders: persist }); - storage.set(PERSIST_HEADERS_STORAGE_KEY, persist.toString()); + storage.set(STORAGE_KEY.persistHeaders, persist.toString()); }, storeTabs({ tabs, activeTabIndex }) { const { storage } = storageStore.getState(); const { shouldPersistHeaders } = get(); const store = debounce(500, (value: string) => { - storage.set(STORAGE_KEY_TABS, value); + storage.set(STORAGE_KEY.tabs, value); }); store(serializeTabState({ tabs, activeTabIndex }, shouldPersistHeaders)); }, @@ -587,5 +585,3 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { actions: $actions, }; }; - -export const PERSIST_HEADERS_STORAGE_KEY = 'shouldPersistHeaders'; diff --git a/packages/graphiql-react/src/stores/index.ts b/packages/graphiql-react/src/stores/index.ts index bb8201c98e..2f4a7c1c96 100644 --- a/packages/graphiql-react/src/stores/index.ts +++ b/packages/graphiql-react/src/stores/index.ts @@ -2,21 +2,25 @@ export { createEditorSlice, type EditorSlice, type EditorActions, + type EditorProps, } from './editor'; export { createExecutionSlice, type ExecutionSlice, type ExecutionActions, + type ExecutionProps, } from './execution'; export { createPluginSlice, type PluginSlice, type PluginActions, + type PluginProps, } from './plugin'; export { createSchemaSlice, type SchemaSlice, type SchemaActions, + type SchemaProps, } from './schema'; export { storageStore, useStorage } from './storage'; export { themeStore, useThemeStore, type Theme } from './theme'; diff --git a/packages/graphiql-react/src/stores/plugin.ts b/packages/graphiql-react/src/stores/plugin.ts index 892974cee4..6344d69e7b 100644 --- a/packages/graphiql-react/src/stores/plugin.ts +++ b/packages/graphiql-react/src/stores/plugin.ts @@ -2,6 +2,7 @@ import { ComponentType } from 'react'; import type { StateCreator } from 'zustand'; import type { SlicesWithActions } from '../types'; import { storageStore } from './storage'; +import { STORAGE_KEY } from '../constants'; export interface GraphiQLPlugin { /** @@ -107,7 +108,7 @@ export const createPluginSlice: CreatePluginSlice = initial => set => ({ } onTogglePluginVisibility?.(newVisiblePlugin); const { storage } = storageStore.getState(); - storage.set(STORAGE_KEY_VISIBLE_PLUGIN, newVisiblePlugin?.title ?? ''); + storage.set(STORAGE_KEY.visiblePlugin, newVisiblePlugin?.title ?? ''); return { visiblePlugin: newVisiblePlugin }; }); }, @@ -129,5 +130,3 @@ export const createPluginSlice: CreatePluginSlice = initial => set => ({ }, }, }); - -export const STORAGE_KEY_VISIBLE_PLUGIN = 'visiblePlugin'; diff --git a/packages/graphiql-react/src/utility/tabs.spec.ts b/packages/graphiql-react/src/utility/tabs.spec.ts index 1ea5931e85..5ccdb96c0e 100644 --- a/packages/graphiql-react/src/utility/tabs.spec.ts +++ b/packages/graphiql-react/src/utility/tabs.spec.ts @@ -5,9 +5,9 @@ import { fuzzyExtractOperationName, getDefaultTabState, clearHeadersFromTabs, - STORAGE_KEY, } from './tabs'; import { storageStore } from '../stores'; +import { STORAGE_KEY } from '../constants'; describe('createTab', () => { it('creates with default title', () => { @@ -179,10 +179,10 @@ describe('clearHeadersFromTabs', () => { }, headers: '{ "authorization": "secret" }', }; - storage.set(STORAGE_KEY, JSON.stringify(stateWithHeaders)); + storage.set(STORAGE_KEY.tabs, JSON.stringify(stateWithHeaders)); clearHeadersFromTabs(); - expect(JSON.parse(storage.get(STORAGE_KEY)!)).toEqual({ + expect(JSON.parse(storage.get(STORAGE_KEY.tabs)!)).toEqual({ ...stateWithHeaders, headers: null, }); diff --git a/packages/graphiql-react/src/utility/tabs.ts b/packages/graphiql-react/src/utility/tabs.ts index 4b8d3f5812..a75e908688 100644 --- a/packages/graphiql-react/src/utility/tabs.ts +++ b/packages/graphiql-react/src/utility/tabs.ts @@ -1,5 +1,6 @@ 'use no memo'; // can't figure why it isn't optimized +import { STORAGE_KEY } from '../constants'; import { storageStore } from '../stores'; export interface TabDefinition { @@ -85,7 +86,7 @@ export function getDefaultTabState({ shouldPersistHeaders?: boolean; }) { const { storage } = storageStore.getState(); - const storedState = storage.get(STORAGE_KEY); + const storedState = storage.get(STORAGE_KEY.tabs); try { if (!storedState) { throw new Error('Storage for tabs is empty'); @@ -266,11 +267,11 @@ export function fuzzyExtractOperationName(str: string): string | null { export function clearHeadersFromTabs() { const { storage } = storageStore.getState(); - const persistedTabs = storage.get(STORAGE_KEY); + const persistedTabs = storage.get(STORAGE_KEY.tabs); if (persistedTabs) { const parsedTabs = JSON.parse(persistedTabs); storage.set( - STORAGE_KEY, + STORAGE_KEY.tabs, JSON.stringify(parsedTabs, (key, value) => key === 'headers' ? null : value, ), @@ -279,5 +280,3 @@ export function clearHeadersFromTabs() { } const DEFAULT_TITLE = ''; - -export const STORAGE_KEY = 'tabState'; From 85e37ae486823672cfef9c6ee7d2af871184c602 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 01:05:31 +0200 Subject: [PATCH 02/73] upd --- packages/graphiql-react/src/constants.ts | 1 + packages/graphiql-react/src/stores/theme.ts | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/graphiql-react/src/constants.ts b/packages/graphiql-react/src/constants.ts index 70b083ed3e..5a3a9a0ffd 100644 --- a/packages/graphiql-react/src/constants.ts +++ b/packages/graphiql-react/src/constants.ts @@ -50,6 +50,7 @@ export const STORAGE_KEY = { tabs: 'tabState', operationName: 'operationName', persistHeaders: 'shouldPersistHeaders', + theme: 'theme', } as const; export const DEFAULT_QUERY = `# Welcome to GraphiQL diff --git a/packages/graphiql-react/src/stores/theme.ts b/packages/graphiql-react/src/stores/theme.ts index a87dad6c47..893b22cf2e 100644 --- a/packages/graphiql-react/src/stores/theme.ts +++ b/packages/graphiql-react/src/stores/theme.ts @@ -1,9 +1,10 @@ import { FC, ReactElement, ReactNode, useEffect } from 'react'; -import { storageStore } from './index'; +import { storageStore } from './storage'; import { createStore } from 'zustand'; import { createBoundedUseStore } from '../utility'; import { EDITOR_THEME } from '../utility/create-editor'; import { editor as monacoEditor } from '../monaco-editor'; +import { STORAGE_KEY } from '../constants'; /** * The value `null` semantically means that the user does not explicitly choose @@ -47,7 +48,7 @@ export const themeStore = createStore(set => ({ theme: null, setTheme(theme) { const { storage } = storageStore.getState(); - storage.set(STORAGE_KEY, theme ?? ''); + storage.set(STORAGE_KEY.theme, theme ?? ''); set({ theme }); }, })); @@ -62,7 +63,7 @@ export const ThemeStore: FC = ({ const { storage } = storageStore.getState(); function getInitialTheme() { - const stored = storage.get(STORAGE_KEY); + const stored = storage.get(STORAGE_KEY.theme); switch (stored) { case 'light': return 'light'; @@ -71,7 +72,7 @@ export const ThemeStore: FC = ({ default: if (typeof stored === 'string') { // Remove the invalid stored value - storage.set(STORAGE_KEY, ''); + storage.set(STORAGE_KEY.theme, ''); } return defaultTheme; } @@ -103,6 +104,4 @@ function getSystemTheme() { return systemTheme; } -const STORAGE_KEY = 'theme'; - export const useThemeStore = createBoundedUseStore(themeStore); From f9fa3ef05673dd4c0626287e7f750613673b876a Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 12:46:52 +0200 Subject: [PATCH 03/73] upd --- packages/graphiql-react/src/index.ts | 2 +- packages/graphiql-react/src/stores/index.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/graphiql-react/src/index.ts b/packages/graphiql-react/src/index.ts index dd3a7f039b..24eaf5f50e 100644 --- a/packages/graphiql-react/src/index.ts +++ b/packages/graphiql-react/src/index.ts @@ -1,6 +1,6 @@ import './style/root.css'; -export { useStorage, useThemeStore, type Theme } from './stores'; +export { useStorage, type Theme } from './stores'; export * from './utility'; export type { TabsState } from './utility/tabs'; diff --git a/packages/graphiql-react/src/stores/index.ts b/packages/graphiql-react/src/stores/index.ts index 2f4a7c1c96..73e2cdee21 100644 --- a/packages/graphiql-react/src/stores/index.ts +++ b/packages/graphiql-react/src/stores/index.ts @@ -23,4 +23,10 @@ export { type SchemaProps, } from './schema'; export { storageStore, useStorage } from './storage'; -export { themeStore, useThemeStore, type Theme } from './theme'; +export { + createThemeSlice, + type ThemeSlice, + type ThemeActions, + type ThemeProps, + type Theme, +} from './theme'; From fdc800ccbbf32fc37ef4023851fc9d62d11abfab Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 12:47:04 +0200 Subject: [PATCH 04/73] upd --- packages/graphiql-react/src/style/root.css | 8 -------- packages/graphiql-react/src/types.test-d.ts | 12 ++++++++++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/graphiql-react/src/style/root.css b/packages/graphiql-react/src/style/root.css index e1f4463256..915e656ca5 100644 --- a/packages/graphiql-react/src/style/root.css +++ b/packages/graphiql-react/src/style/root.css @@ -14,8 +14,6 @@ } .graphiql-container, -.CodeMirror-info, -.CodeMirror-lint-tooltip, .graphiql-dialog, .graphiql-dialog-overlay, .graphiql-tooltip, @@ -82,8 +80,6 @@ @media (prefers-color-scheme: dark) { body:not(.graphiql-light) .graphiql-container, - body:not(.graphiql-light) .CodeMirror-info, - body:not(.graphiql-light) .CodeMirror-lint-tooltip, body:not(.graphiql-light) .graphiql-dialog, body:not(.graphiql-light) .graphiql-dialog-overlay, body:not(.graphiql-light) .graphiql-tooltip, @@ -104,8 +100,6 @@ } body.graphiql-dark .graphiql-container, -body.graphiql-dark .CodeMirror-info, -body.graphiql-dark .CodeMirror-lint-tooltip, body.graphiql-dark .graphiql-dialog, body.graphiql-dark .graphiql-dialog-overlay, body.graphiql-dark .graphiql-tooltip, @@ -125,8 +119,6 @@ body.graphiql-dark [data-radix-popper-content-wrapper] { } .graphiql-container, -.CodeMirror-info, -.CodeMirror-lint-tooltip, .graphiql-dialog { &, &:is(button) { diff --git a/packages/graphiql-react/src/types.test-d.ts b/packages/graphiql-react/src/types.test-d.ts index baa2efd134..006ad8a362 100644 --- a/packages/graphiql-react/src/types.test-d.ts +++ b/packages/graphiql-react/src/types.test-d.ts @@ -3,11 +3,13 @@ import type { ExecutionSlice, PluginSlice, SchemaSlice, + ThemeSlice, // EditorActions, ExecutionActions, PluginActions, SchemaActions, + ThemeActions, } from './stores'; import { AllSlices, AllActions } from './types'; @@ -34,7 +36,7 @@ describe('Should not have conflicting types', () => { it('AllSlices', () => { type Actual = MergeMany< - [EditorSlice, ExecutionSlice, PluginSlice, SchemaSlice] + [EditorSlice, ExecutionSlice, PluginSlice, SchemaSlice, ThemeSlice] >; // eslint-disable-next-line @typescript-eslint/no-unused-expressions expectTypeOf().toEqualTypeOf; @@ -42,7 +44,13 @@ describe('Should not have conflicting types', () => { it('AllActions', () => { type Actual = MergeMany< - [EditorActions, ExecutionActions, PluginActions, SchemaActions] + [ + EditorActions, + ExecutionActions, + PluginActions, + SchemaActions, + ThemeActions, + ] >; // eslint-disable-next-line @typescript-eslint/no-unused-expressions expectTypeOf().toEqualTypeOf; From 4fdf8df484808564b8cb5c48b9bebbeac3e27887 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 12:47:10 +0200 Subject: [PATCH 05/73] upd --- packages/graphiql-react/src/types.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/graphiql-react/src/types.ts b/packages/graphiql-react/src/types.ts index 5183d12aef..69b7d991a4 100644 --- a/packages/graphiql-react/src/types.ts +++ b/packages/graphiql-react/src/types.ts @@ -11,6 +11,8 @@ import { ExecutionActions, PluginActions, SchemaActions, + ThemeSlice, + ThemeActions, } from './stores'; import { RuleKind } from 'graphql-language-service'; @@ -26,12 +28,14 @@ export type MonacoEditor = monacoEditor.IStandaloneCodeEditor; export type AllSlices = EditorSlice & ExecutionSlice & PluginSlice & - SchemaSlice; + SchemaSlice & + ThemeSlice; export type AllActions = EditorActions & ExecutionActions & PluginActions & - SchemaActions; + SchemaActions & + ThemeActions; export interface SlicesWithActions extends AllSlices { actions: AllActions; From 65eea18dd1305a18cc25a81d3b0bd5e8ae34f62a Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 12:48:55 +0200 Subject: [PATCH 06/73] upd --- packages/graphiql/src/GraphiQL.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index 8e3f3f1c69..34905db669 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -35,7 +35,6 @@ import { useGraphiQL, pick, useStorage, - useThemeStore, VariableEditor, EditorProps, cn, @@ -233,6 +232,7 @@ const GraphiQLInterface: FC = ({ changeTab, introspect, setVisiblePlugin, + setTheme, } = useGraphiQLActions(); const { initialVariables, @@ -244,6 +244,7 @@ const GraphiQLInterface: FC = ({ isIntrospecting, visiblePlugin, plugins, + theme, } = useGraphiQL( pick( 'initialVariables', @@ -255,10 +256,10 @@ const GraphiQLInterface: FC = ({ 'isIntrospecting', 'visiblePlugin', 'plugins', + 'theme' ), ); const storageContext = useStorage(); - const { theme, setTheme } = useThemeStore(); useEffect(() => { if (forcedTheme === 'system') { From 50c16f35f5a1129650505e9750617ee1f2881694 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 13:24:06 +0200 Subject: [PATCH 07/73] upd --- packages/graphiql/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/graphiql/package.json b/packages/graphiql/package.json index 9ca6bea979..09b426e1e4 100644 --- a/packages/graphiql/package.json +++ b/packages/graphiql/package.json @@ -26,7 +26,9 @@ "files": [ "dist", "!dist/e2e.*", - "!dist/workers/*" + "!dist/workers/*", + "!dist/index.umd.*", + "!dist/cdn.*" ], "exports": { "./package.json": "./package.json", From 3fc873b709482b432ba2ad08134904d9f754fa4e Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 13:34:02 +0200 Subject: [PATCH 08/73] upd --- .../src/components/provider.tsx | 140 +++++++++++------- packages/graphiql-react/src/stores/theme.ts | 102 +++++-------- 2 files changed, 124 insertions(+), 118 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index e3a88f1f3e..2b8b2791ed 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -9,18 +9,20 @@ import type { import { createContext, useContext, useRef, useEffect } from 'react'; import { create, useStore, UseBoundStore, StoreApi } from 'zustand'; import { useShallow } from 'zustand/shallow'; +import { persist } from 'zustand/middleware'; import { createEditorSlice, createExecutionSlice, createPluginSlice, createSchemaSlice, + createThemeSlice, EditorProps, ExecutionProps, PluginProps, SchemaProps, + ThemeProps, } from '../stores'; import { StorageStore, useStorage } from '../stores/storage'; -import { ThemeStore } from '../stores/theme'; import { SlicesWithActions } from '../types'; import { pick, useDidUpdate, useSynchronizeValue } from '../utility'; import { @@ -36,20 +38,20 @@ import { STORAGE_KEY, } from '../constants'; import { getDefaultTabState } from '../utility/tabs'; +import { EDITOR_THEME } from '../utility/create-editor'; interface InnerGraphiQLProviderProps extends EditorProps, ExecutionProps, PluginProps, - SchemaProps { + SchemaProps, + ThemeProps { children: ReactNode; } type GraphiQLProviderProps = // - InnerGraphiQLProviderProps & - ComponentPropsWithoutRef & - ComponentPropsWithoutRef; + InnerGraphiQLProviderProps & ComponentPropsWithoutRef; type GraphiQLStore = UseBoundStore>; @@ -57,8 +59,6 @@ const GraphiQLContext = createContext | null>(null); export const GraphiQLProvider: FC = ({ storage, - defaultTheme, - editorTheme, ...props }) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime check @@ -75,9 +75,7 @@ export const GraphiQLProvider: FC = ({ } return ( - - - + ); }; @@ -113,7 +111,12 @@ const InnerGraphiQLProvider: FC = ({ plugins = [], referencePlugin, visiblePlugin, + children, + + defaultTheme = null, + editorTheme = EDITOR_THEME, + ...props }) => { const storage = useStorage(); @@ -160,52 +163,79 @@ const InnerGraphiQLProvider: FC = ({ ? storage.get(STORAGE_KEY.persistHeaders) === 'true' : shouldPersistHeaders; - const store = create((...args) => { - const editorSlice = createEditorSlice({ - activeTabIndex, - defaultHeaders, - defaultQuery, - externalFragments: getExternalFragments(externalFragments), - initialHeaders: headers ?? defaultHeaders ?? '', - initialQuery: - query ?? (activeTabIndex === 0 ? tabs[0]!.query : null) ?? '', - initialResponse: response, - initialVariables: variables ?? '', - onCopyQuery, - onEditOperationName, - onPrettifyQuery, - onTabChange, - shouldPersistHeaders: $shouldPersistHeaders, - tabs, - })(...args); - const executionSlice = createExecutionSlice({ - fetcher, - getDefaultFieldNames, - overrideOperationName: operationName, - })(...args); - const pluginSlice = createPluginSlice({ - onTogglePluginVisibility, - referencePlugin, - })(...args); - const schemaSlice = createSchemaSlice({ - inputValueDeprecation, - introspectionQueryName, - onSchemaChange, - schemaDescription, - })(...args); - return { - ...editorSlice, - ...executionSlice, - ...pluginSlice, - ...schemaSlice, - actions: { - ...editorSlice.actions, - ...executionSlice.actions, - ...pluginSlice.actions, - ...schemaSlice.actions, + const store = create()( + persist( + (...args) => { + const editorSlice = createEditorSlice({ + activeTabIndex, + defaultHeaders, + defaultQuery, + externalFragments: getExternalFragments(externalFragments), + initialHeaders: headers ?? defaultHeaders ?? '', + initialQuery: + query ?? (activeTabIndex === 0 ? tabs[0]!.query : null) ?? '', + initialResponse: response, + initialVariables: variables ?? '', + onCopyQuery, + onEditOperationName, + onPrettifyQuery, + onTabChange, + shouldPersistHeaders: $shouldPersistHeaders, + tabs, + })(...args); + const executionSlice = createExecutionSlice({ + fetcher, + getDefaultFieldNames, + overrideOperationName: operationName, + })(...args); + const pluginSlice = createPluginSlice({ + onTogglePluginVisibility, + referencePlugin, + })(...args); + const schemaSlice = createSchemaSlice({ + inputValueDeprecation, + introspectionQueryName, + onSchemaChange, + schemaDescription, + })(...args); + const themeSlice = createThemeSlice({ editorTheme })(...args); + return { + ...editorSlice, + ...executionSlice, + ...pluginSlice, + ...schemaSlice, + ...themeSlice, + actions: { + ...editorSlice.actions, + ...executionSlice.actions, + ...pluginSlice.actions, + ...schemaSlice.actions, + ...themeSlice.actions, + }, + }; }, - }; - }); + { + name: 'graphiql:theme', + onRehydrateStorage(_state) { + return (state, error) => { + if (state) { + const theme = + state.theme === undefined ? defaultTheme : state.theme; + state.actions.setTheme(theme); + } + if (error) { + // eslint-disable-next-line no-console + console.error('An error happened during hydration', error); + return; + } + // eslint-disable-next-line no-console + console.info('Hydration with storage finished'); + }; + }, + partialize: state => ({ theme: state.theme }), + }, + ), + ); const { actions } = store.getState(); actions.storeTabs({ activeTabIndex, tabs }); actions.setPlugins(plugins); diff --git a/packages/graphiql-react/src/stores/theme.ts b/packages/graphiql-react/src/stores/theme.ts index 893b22cf2e..659343e34b 100644 --- a/packages/graphiql-react/src/stores/theme.ts +++ b/packages/graphiql-react/src/stores/theme.ts @@ -1,10 +1,7 @@ -import { FC, ReactElement, ReactNode, useEffect } from 'react'; -import { storageStore } from './storage'; -import { createStore } from 'zustand'; -import { createBoundedUseStore } from '../utility'; +import { StateCreator } from 'zustand'; import { EDITOR_THEME } from '../utility/create-editor'; import { editor as monacoEditor } from '../monaco-editor'; -import { STORAGE_KEY } from '../constants'; +import { SlicesWithActions } from '../types'; /** * The value `null` semantically means that the user does not explicitly choose @@ -17,18 +14,23 @@ type MonacoTheme = | (typeof EDITOR_THEME)[keyof typeof EDITOR_THEME] | ({} & string); -interface ThemeStoreType { - theme: Theme; +export interface ThemeSlice { + theme?: Theme; + editorTheme: { + dark: MonacoTheme; + light: MonacoTheme; + }; +} + +export interface ThemeActions { /** * Set a new theme */ setTheme: (newTheme: Theme) => void; } -interface ThemeStoreProps { - children: ReactNode; - +export interface ThemeProps { /** * @default null */ @@ -38,60 +40,36 @@ interface ThemeStoreProps { * Sets the color theme for the monaco editors. * @default { dark: 'graphiql-DARK', light: 'graphiql-LIGHT' } */ - editorTheme?: { - dark: MonacoTheme; - light: MonacoTheme; - }; + editorTheme?: ThemeSlice['editorTheme']; } -export const themeStore = createStore(set => ({ - theme: null, - setTheme(theme) { - const { storage } = storageStore.getState(); - storage.set(STORAGE_KEY.theme, theme ?? ''); - set({ theme }); - }, -})); - -export const ThemeStore: FC = ({ - children, - defaultTheme = null, - editorTheme = EDITOR_THEME, -}) => { - const theme = useThemeStore(store => store.theme); - useEffect(() => { - const { storage } = storageStore.getState(); - - function getInitialTheme() { - const stored = storage.get(STORAGE_KEY.theme); - switch (stored) { - case 'light': - return 'light'; - case 'dark': - return 'dark'; - default: - if (typeof stored === 'string') { - // Remove the invalid stored value - storage.set(STORAGE_KEY.theme, ''); - } - return defaultTheme; - } - } +type CreateThemeSlice = ( + initial: Pick, +) => StateCreator< + SlicesWithActions, + [], + [], + ThemeSlice & { + actions: ThemeActions; + } +>; - themeStore.setState({ theme: getInitialTheme() }); - }, []); // eslint-disable-line react-hooks/exhaustive-deps -- only on mount - - useEffect(() => { - document.body.classList.remove('graphiql-light', 'graphiql-dark'); - if (theme) { - document.body.classList.add(`graphiql-${theme}`); - } - const resolvedTheme = theme ?? getSystemTheme(); - monacoEditor.setTheme(editorTheme[resolvedTheme]); - }, [theme, editorTheme]); - - return children as ReactElement; -}; +export const createThemeSlice: CreateThemeSlice = initial => set => ({ + ...initial, + actions: { + setTheme(theme) { + set(({ editorTheme }) => { + document.body.classList.remove('graphiql-light', 'graphiql-dark'); + if (theme) { + document.body.classList.add(`graphiql-${theme}`); + } + const resolvedTheme = theme ?? getSystemTheme(); + monacoEditor.setTheme(editorTheme[resolvedTheme]); + return { theme }; + }); + }, + }, +}); /** * Get the resolved theme - dark or light @@ -103,5 +81,3 @@ function getSystemTheme() { const systemTheme = isDark ? 'dark' : 'light'; return systemTheme; } - -export const useThemeStore = createBoundedUseStore(themeStore); From 80f0210fb72cad4e32f7ba23943eec9e84a4a74d Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 13:51:07 +0200 Subject: [PATCH 09/73] visible plugin --- .../src/context.tsx | 2 +- .../src/components/provider.tsx | 21 ++++--------------- packages/graphiql-react/src/constants.ts | 2 -- packages/graphiql/src/GraphiQL.tsx | 6 +++--- 4 files changed, 8 insertions(+), 23 deletions(-) diff --git a/packages/graphiql-plugin-doc-explorer/src/context.tsx b/packages/graphiql-plugin-doc-explorer/src/context.tsx index e0bb2c97a8..0541411f07 100644 --- a/packages/graphiql-plugin-doc-explorer/src/context.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/context.tsx @@ -33,7 +33,7 @@ export const DOC_EXPLORER_PLUGIN: GraphiQLPlugin = { title: 'Documentation Explorer', icon: function Icon() { const visiblePlugin = useGraphiQL(state => state.visiblePlugin); - return visiblePlugin === DOC_EXPLORER_PLUGIN ? ( + return visiblePlugin === DOC_EXPLORER_PLUGIN.title ? ( ) : ( diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 2b8b2791ed..4f217c5895 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -124,20 +124,6 @@ const InnerGraphiQLProvider: FC = ({ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- false positive if (storeRef.current === null) { - function getInitialVisiblePlugin() { - const storedValue = storage.get(STORAGE_KEY.visiblePlugin); - const pluginForStoredValue = plugins.find( - plugin => plugin.title === storedValue, - ); - if (pluginForStoredValue) { - return pluginForStoredValue; - } - if (storedValue) { - storage.set(STORAGE_KEY.visiblePlugin, ''); - } - return visiblePlugin; - } - function getInitialState() { // We only need to compute it lazily during the initial render. const query = props.query ?? storage.get(STORAGE_KEY.query) ?? null; @@ -232,15 +218,16 @@ const InnerGraphiQLProvider: FC = ({ console.info('Hydration with storage finished'); }; }, - partialize: state => ({ theme: state.theme }), + partialize: state => ({ + theme: state.theme, + visiblePlugin: state.visiblePlugin, + }), }, ), ); const { actions } = store.getState(); actions.storeTabs({ activeTabIndex, tabs }); actions.setPlugins(plugins); - const initialVisiblePlugin = getInitialVisiblePlugin(); - actions.setVisiblePlugin(initialVisiblePlugin); return store; } diff --git a/packages/graphiql-react/src/constants.ts b/packages/graphiql-react/src/constants.ts index 5a3a9a0ffd..cb0363c334 100644 --- a/packages/graphiql-react/src/constants.ts +++ b/packages/graphiql-react/src/constants.ts @@ -44,13 +44,11 @@ export const KEY_MAP = Object.freeze({ export const STORAGE_KEY = { headers: 'headers', - visiblePlugin: 'visiblePlugin', query: 'query', variables: 'variables', tabs: 'tabState', operationName: 'operationName', persistHeaders: 'shouldPersistHeaders', - theme: 'theme', } as const; export const DEFAULT_QUERY = `# Welcome to GraphiQL diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index 34905db669..ec62546342 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -269,7 +269,7 @@ const GraphiQLInterface: FC = ({ } }, [forcedTheme, setTheme]); - const PluginContent = visiblePlugin?.content; + const PluginContent = plugins.find(plugin => plugin.title === visiblePlugin)?.content; const pluginResize = useDragResize({ defaultSizeRelation: 1 / 3, @@ -390,7 +390,7 @@ const GraphiQLInterface: FC = ({ const handlePluginClick: ButtonHandler = event => { const pluginIndex = Number(event.currentTarget.dataset.index!); const plugin = plugins.find((_, index) => pluginIndex === index)!; - const isVisible = plugin === visiblePlugin; + const isVisible = plugin.title === visiblePlugin; if (isVisible) { setVisiblePlugin(null); pluginResize.setHiddenElement('first'); @@ -448,7 +448,7 @@ const GraphiQLInterface: FC = ({ const sidebar = (
{plugins.map((plugin, index) => { - const isVisible = plugin === visiblePlugin; + const isVisible = plugin.title === visiblePlugin; const label = `${isVisible ? 'Hide' : 'Show'} ${plugin.title}`; return ( From 433f1d10b5bdaabc64845266bde36a41fb99ff45 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 13:57:05 +0200 Subject: [PATCH 10/73] visible plugin --- packages/graphiql-react/src/icons/history.svg | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/graphiql-react/src/icons/history.svg b/packages/graphiql-react/src/icons/history.svg index ef69e5c9b7..1c0c9fa10e 100644 --- a/packages/graphiql-react/src/icons/history.svg +++ b/packages/graphiql-react/src/icons/history.svg @@ -2,23 +2,14 @@ height="1em" viewBox="0 0 24 20" fill="none" + stroke-width="1.5" + stroke-linecap="square" + stroke="currentColor" xmlns="http://www.w3.org/2000/svg" > - - + + From 1005c598243486e192a9a24fcd0b498020e751f1 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 14:06:38 +0200 Subject: [PATCH 11/73] visible plugin --- packages/graphiql/src/ui/short-keys.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphiql/src/ui/short-keys.tsx b/packages/graphiql/src/ui/short-keys.tsx index 5a4a666991..5b890f795d 100644 --- a/packages/graphiql/src/ui/short-keys.tsx +++ b/packages/graphiql/src/ui/short-keys.tsx @@ -2,8 +2,8 @@ import { FC, Fragment } from 'react'; import { formatShortcutForOS, KEY_MAP } from '@graphiql/react'; const SHORT_KEYS = Object.entries({ - 'Open the Command Palette (you must have focus in the editor)': 'F1', 'Execute query': formatShortcutForOS(KEY_MAP.runQuery.key), + 'Open the Command Palette (you must have focus in the editor)': 'F1', 'Prettify editors': KEY_MAP.prettify.key, 'Copy query': KEY_MAP.copyQuery.key, 'Re-fetch schema using introspection': KEY_MAP.refetchSchema.key, From 9db8f1ebaa2b9e17af5366d53e859e1a78a7cf04 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 14:15:07 +0200 Subject: [PATCH 12/73] visible plugin --- packages/graphiql-react/src/stores/plugin.ts | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/graphiql-react/src/stores/plugin.ts b/packages/graphiql-react/src/stores/plugin.ts index 6344d69e7b..6be5c81c52 100644 --- a/packages/graphiql-react/src/stores/plugin.ts +++ b/packages/graphiql-react/src/stores/plugin.ts @@ -1,8 +1,6 @@ import { ComponentType } from 'react'; import type { StateCreator } from 'zustand'; import type { SlicesWithActions } from '../types'; -import { storageStore } from './storage'; -import { STORAGE_KEY } from '../constants'; export interface GraphiQLPlugin { /** @@ -34,7 +32,7 @@ export interface PluginSlice { /** * The plugin which is currently visible. */ - visiblePlugin: GraphiQLPlugin | null; + visiblePlugin?: string; /** * The plugin which is used to display the reference documentation when selecting a type. @@ -94,22 +92,24 @@ type CreatePluginSlice = ( export const createPluginSlice: CreatePluginSlice = initial => set => ({ plugins: [], - visiblePlugin: null, ...initial, actions: { - setVisiblePlugin(plugin = null) { - set(({ visiblePlugin, plugins, onTogglePluginVisibility }) => { + setVisiblePlugin(plugin) { + set(current => { + const { + visiblePlugin: currentVisiblePlugin, + plugins, + onTogglePluginVisibility, + } = current; const byTitle = typeof plugin === 'string'; - const newVisiblePlugin: PluginSlice['visiblePlugin'] = - (plugin && plugins.find(p => (byTitle ? p.title : p) === plugin)) || - null; - if (newVisiblePlugin === visiblePlugin) { - return { visiblePlugin }; + const newVisiblePlugin = plugins.find( + p => (byTitle ? p.title : p) === plugin, + ); + if (newVisiblePlugin?.title === currentVisiblePlugin) { + return current; } - onTogglePluginVisibility?.(newVisiblePlugin); - const { storage } = storageStore.getState(); - storage.set(STORAGE_KEY.visiblePlugin, newVisiblePlugin?.title ?? ''); - return { visiblePlugin: newVisiblePlugin }; + onTogglePluginVisibility?.(newVisiblePlugin ?? null); + return { visiblePlugin: newVisiblePlugin?.title }; }); }, setPlugins(plugins) { From b6faf057c5b31959f554c79bc779372e15ffaaa8 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 12 Jun 2025 14:20:08 +0200 Subject: [PATCH 13/73] visible plugin --- .../src/{context.tsx => context.ts} | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) rename packages/graphiql-plugin-history/src/{context.tsx => context.ts} (91%) diff --git a/packages/graphiql-plugin-history/src/context.tsx b/packages/graphiql-plugin-history/src/context.ts similarity index 91% rename from packages/graphiql-plugin-history/src/context.tsx rename to packages/graphiql-plugin-history/src/context.ts index 666fb81430..2b141667c6 100644 --- a/packages/graphiql-plugin-history/src/context.tsx +++ b/packages/graphiql-plugin-history/src/context.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line react/jsx-filename-extension -- TODO import { FC, ReactElement, ReactNode, useEffect } from 'react'; import { createStore } from 'zustand'; import { @@ -45,7 +44,7 @@ type HistoryStoreType = { actions: { /** * Add an operation to the history. - * @param operation The operation that was executed, consisting of the query, + * @param operation - The operation that was executed, consisting of the query, * variables, headers, and operation name. */ addToHistory(operation: { @@ -56,11 +55,11 @@ type HistoryStoreType = { }): void; /** * Change the custom label of an item from the history. - * @param args An object containing the label (`undefined` if it should be + * @param args - An object containing the label (`undefined` if it should be * unset) and properties that identify the history item that the label should * be applied to. (This can result in the label being applied to multiple * history items.) - * @param index Index to edit. Without it, will look for the first index matching the + * @param index - Index to edit. Without it, will look for the first index matching the * operation, which may lead to misleading results if multiple items have the same label */ editLabel( @@ -91,9 +90,9 @@ type HistoryStoreType = { }): void; /** * Delete an operation from the history. - * @param args The operation that was executed, consisting of the query, + * @param args - The operation that was executed, consisting of the query, * variables, headers, and operation name. - * @param clearFavorites This is only if you press the 'clear' button + * @param clearFavorites - This is only if you press the 'clear' button */ deleteFromHistory(args: QueryStoreItem, clearFavorites?: boolean): void; /** From fa56f25f746047138418b219f6ec6f55a25c8924 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 17:22:15 +0200 Subject: [PATCH 14/73] upd --- .../src/components/provider.tsx | 1 - packages/graphiql-react/src/stores/theme.ts | 6 ++-- packages/graphiql/src/GraphiQL.tsx | 6 +++- packages/graphiql/src/ui/sidebar.tsx | 32 +++++++++++-------- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 6f864c2652..bf13b86c4e 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -17,7 +17,6 @@ import { ThemeProps, } from '../stores'; import { StorageStore, useStorage } from '../stores/storage'; -import { ThemeStore } from '../stores/theme'; import type { SlicesWithActions } from '../types'; import { useDidUpdate } from '../utility'; import { diff --git a/packages/graphiql-react/src/stores/theme.ts b/packages/graphiql-react/src/stores/theme.ts index 659343e34b..f20c86e409 100644 --- a/packages/graphiql-react/src/stores/theme.ts +++ b/packages/graphiql-react/src/stores/theme.ts @@ -1,7 +1,7 @@ -import { StateCreator } from 'zustand'; -import { EDITOR_THEME } from '../utility/create-editor'; +import type { StateCreator } from 'zustand'; +import type { EDITOR_THEME } from '../utility/create-editor'; import { editor as monacoEditor } from '../monaco-editor'; -import { SlicesWithActions } from '../types'; +import type { SlicesWithActions } from '../types'; /** * The value `null` semantically means that the user does not explicitly choose diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index 97408022ba..e2195b203f 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -212,6 +212,7 @@ const GraphiQLInterface: FC = ({ activeTabIndex, isFetching, visiblePlugin, + plugins } = useGraphiQL( pick( 'initialVariables', @@ -220,10 +221,13 @@ const GraphiQLInterface: FC = ({ 'activeTabIndex', 'isFetching', 'visiblePlugin', + 'plugins' ), ); - const PluginContent = plugins.find(plugin => plugin.title === visiblePlugin)?.content; + const PluginContent = plugins.find( + plugin => plugin.title === visiblePlugin, + )?.content; const pluginResize = useDragResize({ defaultSizeRelation: 1 / 3, diff --git a/packages/graphiql/src/ui/sidebar.tsx b/packages/graphiql/src/ui/sidebar.tsx index 17ff1bfa8a..b9c1e713f5 100644 --- a/packages/graphiql/src/ui/sidebar.tsx +++ b/packages/graphiql/src/ui/sidebar.tsx @@ -15,7 +15,6 @@ import { useGraphiQL, useGraphiQLActions, useStorage, - useTheme, VisuallyHidden, } from '@graphiql/react'; import { ShortKeys } from './short-keys'; @@ -56,18 +55,23 @@ export const Sidebar: FC = ({ $forcedTheme && THEMES.includes($forcedTheme) ? $forcedTheme : undefined; const storage = useStorage(); - const { theme, setTheme } = useTheme(); - const { setShouldPersistHeaders, introspect, setVisiblePlugin } = + const { setShouldPersistHeaders, introspect, setVisiblePlugin, setTheme } = useGraphiQLActions(); - const { shouldPersistHeaders, isIntrospecting, visiblePlugin, plugins } = - useGraphiQL( - pick( - 'shouldPersistHeaders', - 'isIntrospecting', - 'visiblePlugin', - 'plugins', - ), - ); + const { + shouldPersistHeaders, + isIntrospecting, + visiblePlugin, + plugins, + theme, + } = useGraphiQL( + pick( + 'shouldPersistHeaders', + 'isIntrospecting', + 'visiblePlugin', + 'plugins', + 'theme', + ), + ); useEffect(() => { if (forcedTheme === 'system') { @@ -127,7 +131,7 @@ export const Sidebar: FC = ({ const handlePluginClick: ButtonHandler = event => { const pluginIndex = Number(event.currentTarget.dataset.index!); const plugin = plugins.find((_, index) => pluginIndex === index)!; - const isVisible = plugin === visiblePlugin; + const isVisible = plugin.title === visiblePlugin; if (isVisible) { setVisiblePlugin(null); setHiddenElement('first'); @@ -140,7 +144,7 @@ export const Sidebar: FC = ({ return (
{plugins.map((plugin, index) => { - const isVisible = plugin === visiblePlugin; + const isVisible = plugin.title === visiblePlugin; const label = `${isVisible ? 'Hide' : 'Show'} ${plugin.title}`; return ( From dcb58f575af07f7147e10ffcb0b3ebc6824a6e62 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 17:22:52 +0200 Subject: [PATCH 15/73] upd --- packages/graphiql-react/src/stores/plugin.ts | 2 -- packages/graphiql/src/GraphiQL.tsx | 1 - 2 files changed, 3 deletions(-) diff --git a/packages/graphiql-react/src/stores/plugin.ts b/packages/graphiql-react/src/stores/plugin.ts index c59a865ebf..a1d77c3c5d 100644 --- a/packages/graphiql-react/src/stores/plugin.ts +++ b/packages/graphiql-react/src/stores/plugin.ts @@ -1,8 +1,6 @@ import type { ComponentType } from 'react'; import type { StateCreator } from 'zustand'; import type { SlicesWithActions } from '../types'; -import { storageStore } from './storage'; -import { STORAGE_KEY } from '../constants'; export interface GraphiQLPlugin { /** diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index e2195b203f..c13ba8d6f8 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -28,7 +28,6 @@ import { useDragResize, useGraphiQL, pick, - useStorage, VariableEditor, EditorProps, cn, From 4a8e5f22ee935dd7be6a21b19c0b611cea9c8fa8 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 17:29:39 +0200 Subject: [PATCH 16/73] lint --- packages/graphiql/src/GraphiQL.tsx | 4 ++-- resources/custom-words.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index c13ba8d6f8..d8638e087f 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -211,7 +211,7 @@ const GraphiQLInterface: FC = ({ activeTabIndex, isFetching, visiblePlugin, - plugins + plugins, } = useGraphiQL( pick( 'initialVariables', @@ -220,7 +220,7 @@ const GraphiQLInterface: FC = ({ 'activeTabIndex', 'isFetching', 'visiblePlugin', - 'plugins' + 'plugins', ), ); diff --git a/resources/custom-words.txt b/resources/custom-words.txt index 947d2a3752..d524199cb2 100644 --- a/resources/custom-words.txt +++ b/resources/custom-words.txt @@ -154,6 +154,7 @@ outlineable ovsx paas pabbati +partialize picomatch pieas pnpm From f7378c82be63c0939f59a3dfee863ba65d6ec1dd Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 17:56:15 +0200 Subject: [PATCH 17/73] add storage slice --- .../src/components/provider.tsx | 109 ++++++++---------- packages/graphiql-react/src/stores/index.ts | 6 +- packages/graphiql-react/src/stores/storage.ts | 50 +++----- 3 files changed, 70 insertions(+), 95 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index bf13b86c4e..0839ddefc5 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -1,22 +1,23 @@ /* eslint sort-keys: "error" */ -import type { ComponentPropsWithoutRef, FC, ReactNode, RefObject } from 'react'; +import type { FC, ReactNode, RefObject } from 'react'; import { createContext, useContext, useRef, useEffect } from 'react'; import { create, useStore, UseBoundStore, StoreApi } from 'zustand'; import { useShallow } from 'zustand/shallow'; -import { persist } from 'zustand/middleware'; +import { persist, createJSONStorage } from 'zustand/middleware'; import { createEditorSlice, createExecutionSlice, createPluginSlice, createSchemaSlice, createThemeSlice, + createStorageSlice, EditorProps, ExecutionProps, PluginProps, SchemaProps, ThemeProps, + StorageProps, } from '../stores'; -import { StorageStore, useStorage } from '../stores/storage'; import type { SlicesWithActions } from '../types'; import { useDidUpdate } from '../utility'; import { @@ -34,29 +35,58 @@ import { import { getDefaultTabState } from '../utility/tabs'; import { EDITOR_THEME } from '../utility/create-editor'; -interface InnerGraphiQLProviderProps +interface GraphiQLProviderProps extends EditorProps, ExecutionProps, PluginProps, SchemaProps, - ThemeProps { + ThemeProps, + StorageProps { children: ReactNode; } -type GraphiQLProviderProps = - // - InnerGraphiQLProviderProps & ComponentPropsWithoutRef; - type GraphiQLStore = UseBoundStore>; const GraphiQLContext = createContext | null>(null); export const GraphiQLProvider: FC = ({ - storage, + defaultHeaders, + defaultQuery = DEFAULT_QUERY, + defaultTabs, + externalFragments, + onEditOperationName, + onTabChange, + shouldPersistHeaders = false, + onCopyQuery, + onPrettifyQuery = DEFAULT_PRETTIFY_QUERY, + + dangerouslyAssumeSchemaIsValid = false, + fetcher, + inputValueDeprecation = false, + introspectionQueryName = 'IntrospectionQuery', + onSchemaChange, + schema, + schemaDescription = false, + + getDefaultFieldNames, + operationName = null, + + onTogglePluginVisibility, + plugins = [], + referencePlugin, + visiblePlugin, + + children, + + defaultTheme = null, + editorTheme = EDITOR_THEME, + + storage = createJSONStorage(() => localStorage), + ...props }) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime check - if (!props.fetcher) { + if (!fetcher) { throw new TypeError( 'The `GraphiQLProvider` component requires a `fetcher` function to be passed as prop.', ); @@ -115,58 +145,17 @@ useEffect(() => { }, [response])`, ); } - return ( - - - - ); -}; - -const InnerGraphiQLProvider: FC = ({ - defaultHeaders, - defaultQuery = DEFAULT_QUERY, - defaultTabs, - externalFragments, - onEditOperationName, - onTabChange, - shouldPersistHeaders = false, - onCopyQuery, - onPrettifyQuery = DEFAULT_PRETTIFY_QUERY, - - dangerouslyAssumeSchemaIsValid = false, - fetcher, - inputValueDeprecation = false, - introspectionQueryName = 'IntrospectionQuery', - onSchemaChange, - schema, - schemaDescription = false, - - getDefaultFieldNames, - operationName = null, - - onTogglePluginVisibility, - plugins = [], - referencePlugin, - visiblePlugin, - - children, - - defaultTheme = null, - editorTheme = EDITOR_THEME, - - ...props -}) => { - const storage = useStorage(); const storeRef = useRef(null!); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- false positive if (storeRef.current === null) { function getInitialState() { // We only need to compute it lazily during the initial render. - const query = props.initialQuery ?? storage.get(STORAGE_KEY.query); + const query = props.initialQuery ?? storage!.getItem(STORAGE_KEY.query); const variables = - props.initialVariables ?? storage.get(STORAGE_KEY.variables); - const headers = props.initialHeaders ?? storage.get(STORAGE_KEY.headers); + props.initialVariables ?? storage!.getItem(STORAGE_KEY.variables); + const headers = + props.initialHeaders ?? storage!.getItem(STORAGE_KEY.headers); const { tabs, activeTabIndex } = getDefaultTabState({ defaultHeaders, @@ -178,11 +167,11 @@ const InnerGraphiQLProvider: FC = ({ variables, }); - const isStored = storage.get(STORAGE_KEY.persistHeaders) !== null; + const isStored = storage!.getItem(STORAGE_KEY.persistHeaders) !== null; const $shouldPersistHeaders = shouldPersistHeaders !== false && isStored - ? storage.get(STORAGE_KEY.persistHeaders) === 'true' + ? storage!.getItem(STORAGE_KEY.persistHeaders) === 'true' : shouldPersistHeaders; const store = create()( @@ -220,12 +209,14 @@ const InnerGraphiQLProvider: FC = ({ schemaDescription, })(...args); const themeSlice = createThemeSlice({ editorTheme })(...args); + const storageSlice = createStorageSlice({ storage })(...args); return { ...editorSlice, ...executionSlice, ...pluginSlice, ...schemaSlice, ...themeSlice, + ...storageSlice, actions: { ...editorSlice.actions, ...executionSlice.actions, @@ -361,7 +352,7 @@ export function useGraphiQL(selector: (state: SlicesWithActions) => T): T { export const useGraphiQLActions = () => useGraphiQL(state => state.actions); function getExternalFragments( - externalFragments: InnerGraphiQLProviderProps['externalFragments'], + externalFragments: GraphiQLProviderProps['externalFragments'], ) { const map = new Map(); if (externalFragments) { diff --git a/packages/graphiql-react/src/stores/index.ts b/packages/graphiql-react/src/stores/index.ts index 73e2cdee21..9830e40fba 100644 --- a/packages/graphiql-react/src/stores/index.ts +++ b/packages/graphiql-react/src/stores/index.ts @@ -22,7 +22,11 @@ export { type SchemaActions, type SchemaProps, } from './schema'; -export { storageStore, useStorage } from './storage'; +export { + createStorageSlice, + type StorageSlice, + type StorageProps, +} from './storage'; export { createThemeSlice, type ThemeSlice, diff --git a/packages/graphiql-react/src/stores/storage.ts b/packages/graphiql-react/src/stores/storage.ts index f4eafebe24..573bc180e4 100644 --- a/packages/graphiql-react/src/stores/storage.ts +++ b/packages/graphiql-react/src/stores/storage.ts @@ -1,43 +1,23 @@ -import { Storage, StorageAPI } from '@graphiql/toolkit'; -import { FC, ReactElement, ReactNode, useEffect } from 'react'; -import { createStore } from 'zustand'; -import { createBoundedUseStore } from '../utility'; - -interface StorageStoreType { - storage: StorageAPI; -} - -interface StorageStoreProps { - children: ReactNode; +import type { PersistStorage } from 'zustand/middleware'; +import type { StateCreator } from 'zustand'; +import type { SlicesWithActions } from '../types'; +export interface StorageSlice { /** * Provide a custom storage API. - * @default localStorage - * @see {@link https://graphiql-test.netlify.app/typedoc/modules/graphiql_toolkit.html#storage-2|API docs} - * for details on the required interface. + * @default createJSONStorage(() => localStorage) + * @see https://zustand.docs.pmnd.rs/integrations/persisting-store-data#createjsonstorage */ - storage?: Storage; + storage: PersistStorage; } -export const storageStore = createStore(() => ({ - storage: null!, -})); - -export const StorageStore: FC = ({ storage, children }) => { - const isMounted = useStorageStore(state => Boolean(state.storage)); - - useEffect(() => { - storageStore.setState({ storage: new StorageAPI(storage) }); - }, [storage]); - - if (!isMounted) { - // Ensure storage was initialized - return null; - } - - return children as ReactElement; -}; +export interface StorageProps { + storage?: StorageSlice['storage']; +} -const useStorageStore = createBoundedUseStore(storageStore); +type CreateStorageSlice = ( + initial: StorageSlice, +) => StateCreator; -export const useStorage = () => useStorageStore(state => state.storage); +export const createStorageSlice: CreateStorageSlice = initial => _set => + initial; From 429a643344a5cca70c576bb893786a7a7f88877f Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 17:58:58 +0200 Subject: [PATCH 18/73] add storage slice --- packages/graphiql-react/src/index.ts | 9 ++++++--- packages/graphiql-react/src/stores/index.ts | 1 - packages/graphiql-react/src/stores/theme.ts | 8 +------- packages/graphiql-react/src/types.test-d.ts | 10 +++++++++- packages/graphiql-react/src/types.ts | 12 ++++++++++-- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/graphiql-react/src/index.ts b/packages/graphiql-react/src/index.ts index f49ec3de95..ddc7d5a064 100644 --- a/packages/graphiql-react/src/index.ts +++ b/packages/graphiql-react/src/index.ts @@ -1,13 +1,16 @@ import './style/root.css'; -export { useStorage, type Theme } from './stores'; - export * from './utility'; export type { TabsState } from './utility/tabs'; export * from './icons'; export * from './components'; -export type { EditorProps, SchemaReference, SlicesWithActions } from './types'; +export type { + EditorProps, + SchemaReference, + SlicesWithActions, + Theme, +} from './types'; export type { GraphiQLPlugin } from './stores/plugin'; export { KEY_MAP, formatShortcutForOS, isMacOs } from './constants'; export * from './deprecated'; diff --git a/packages/graphiql-react/src/stores/index.ts b/packages/graphiql-react/src/stores/index.ts index 9830e40fba..5a3705b212 100644 --- a/packages/graphiql-react/src/stores/index.ts +++ b/packages/graphiql-react/src/stores/index.ts @@ -32,5 +32,4 @@ export { type ThemeSlice, type ThemeActions, type ThemeProps, - type Theme, } from './theme'; diff --git a/packages/graphiql-react/src/stores/theme.ts b/packages/graphiql-react/src/stores/theme.ts index f20c86e409..31884d3805 100644 --- a/packages/graphiql-react/src/stores/theme.ts +++ b/packages/graphiql-react/src/stores/theme.ts @@ -1,13 +1,7 @@ import type { StateCreator } from 'zustand'; import type { EDITOR_THEME } from '../utility/create-editor'; import { editor as monacoEditor } from '../monaco-editor'; -import type { SlicesWithActions } from '../types'; - -/** - * The value `null` semantically means that the user does not explicitly choose - * any theme, so we use the system default. - */ -export type Theme = 'light' | 'dark' | null; +import type { SlicesWithActions, Theme } from '../types'; type MonacoTheme = | monacoEditor.BuiltinTheme diff --git a/packages/graphiql-react/src/types.test-d.ts b/packages/graphiql-react/src/types.test-d.ts index 13f6df2bb4..ccab5f5f83 100644 --- a/packages/graphiql-react/src/types.test-d.ts +++ b/packages/graphiql-react/src/types.test-d.ts @@ -4,6 +4,7 @@ import type { PluginSlice, SchemaSlice, ThemeSlice, + StorageSlice, // EditorActions, ExecutionActions, @@ -36,7 +37,14 @@ describe('Should not have conflicting types', () => { it('AllSlices', () => { type Actual = MergeMany< - [EditorSlice, ExecutionSlice, PluginSlice, SchemaSlice, ThemeSlice] + [ + EditorSlice, + ExecutionSlice, + PluginSlice, + SchemaSlice, + ThemeSlice, + StorageSlice, + ] >; // eslint-disable-next-line @typescript-eslint/no-unused-expressions expectTypeOf().toEqualTypeOf; diff --git a/packages/graphiql-react/src/types.ts b/packages/graphiql-react/src/types.ts index 979d11588c..2095600c54 100644 --- a/packages/graphiql-react/src/types.ts +++ b/packages/graphiql-react/src/types.ts @@ -6,16 +6,23 @@ import type { ExecutionSlice, PluginSlice, SchemaSlice, + ThemeSlice, + StorageSlice, // EditorActions, ExecutionActions, PluginActions, SchemaActions, - ThemeSlice, ThemeActions, } from './stores'; import type { RuleKind } from 'graphql-language-service'; +/** + * The value `null` semantically means that the user does not explicitly choose + * any theme, so we use the system default. + */ +export type Theme = 'light' | 'dark' | null; + export type EditorProps = ComponentPropsWithoutRef<'div'>; export interface SchemaReference { @@ -29,7 +36,8 @@ export type AllSlices = EditorSlice & ExecutionSlice & PluginSlice & SchemaSlice & - ThemeSlice; + ThemeSlice & + StorageSlice; export type AllActions = EditorActions & ExecutionActions & From cd1b7bf3591e1bdcab218a32e0e4f654134c00db Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 18:06:46 +0200 Subject: [PATCH 19/73] upd --- packages/graphiql-react/src/components/provider.tsx | 1 + packages/graphiql-react/src/stores/storage.ts | 9 +++++++-- packages/graphiql-react/src/utility/tabs.ts | 3 +-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 0839ddefc5..9c332371a7 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -248,6 +248,7 @@ useEffect(() => { theme: state.theme, visiblePlugin: state.visiblePlugin, }), + storage, }, ), ); diff --git a/packages/graphiql-react/src/stores/storage.ts b/packages/graphiql-react/src/stores/storage.ts index 573bc180e4..d47b17b56f 100644 --- a/packages/graphiql-react/src/stores/storage.ts +++ b/packages/graphiql-react/src/stores/storage.ts @@ -1,6 +1,11 @@ import type { PersistStorage } from 'zustand/middleware'; import type { StateCreator } from 'zustand'; -import type { SlicesWithActions } from '../types'; +import type { SlicesWithActions, Theme } from '../types'; + +interface GraphiQLPersistedState { + theme?: Theme; + visiblePlugin?: string; +} export interface StorageSlice { /** @@ -8,7 +13,7 @@ export interface StorageSlice { * @default createJSONStorage(() => localStorage) * @see https://zustand.docs.pmnd.rs/integrations/persisting-store-data#createjsonstorage */ - storage: PersistStorage; + storage: PersistStorage; } export interface StorageProps { diff --git a/packages/graphiql-react/src/utility/tabs.ts b/packages/graphiql-react/src/utility/tabs.ts index 849d413b9a..6087404c61 100644 --- a/packages/graphiql-react/src/utility/tabs.ts +++ b/packages/graphiql-react/src/utility/tabs.ts @@ -1,7 +1,6 @@ 'use no memo'; // can't figure why it isn't optimized import { STORAGE_KEY } from '../constants'; -import { storageStore } from '../stores'; export interface TabDefinition { /** @@ -29,7 +28,7 @@ export interface TabState extends TabDefinition { /** * A hash that is unique for a combination of the contents of the query - * editor, the variable editor and the header editor (i.e. all the editor + * editor, the variable editor and the header editor (i.e., all the editor * where the contents are persisted in storage). */ hash: string; From d4e07db913c8a6b219321155b8d3187c6f0d19dc Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 18:13:28 +0200 Subject: [PATCH 20/73] upd --- packages/graphiql-react/src/stores/editor.ts | 16 +++++++--------- packages/graphiql-react/src/utility/hooks.ts | 5 ++++- packages/graphiql-react/src/utility/tabs.spec.ts | 7 ------- packages/graphiql-react/src/utility/tabs.ts | 4 ++++ 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/graphiql-react/src/stores/editor.ts b/packages/graphiql-react/src/stores/editor.ts index fad01441b5..43b2b3460e 100644 --- a/packages/graphiql-react/src/stores/editor.ts +++ b/packages/graphiql-react/src/stores/editor.ts @@ -7,7 +7,6 @@ import type { import type { OperationFacts } from 'graphql-language-service'; import { MaybePromise, mergeAst } from '@graphiql/toolkit'; import { print } from 'graphql'; -import { storageStore } from './storage'; import { createTab, setPropertiesInActiveTab, @@ -424,27 +423,26 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { }); }, setShouldPersistHeaders(persist) { - const { headerEditor, tabs, activeTabIndex } = get(); - const { storage } = storageStore.getState(); + const { headerEditor, tabs, activeTabIndex, storage } = get(); if (persist) { - storage.set(STORAGE_KEY.headers, headerEditor?.getValue() ?? ''); + storage.setItem(STORAGE_KEY.headers, headerEditor?.getValue() ?? ''); const serializedTabs = serializeTabState( { tabs, activeTabIndex }, true, ); - storage.set(STORAGE_KEY.tabs, serializedTabs); + storage.setItem(STORAGE_KEY.tabs, serializedTabs); } else { - storage.set(STORAGE_KEY.headers, ''); + storage.setItem(STORAGE_KEY.headers, ''); clearHeadersFromTabs(); } - storage.set(STORAGE_KEY.persistHeaders, persist.toString()); + storage.setItem(STORAGE_KEY.persistHeaders, persist.toString()); set({ shouldPersistHeaders: persist }); }, storeTabs({ tabs, activeTabIndex }) { - const { storage } = storageStore.getState(); + const { storage } = get(); const { shouldPersistHeaders } = get(); const store = debounce(500, (value: string) => { - storage.set(STORAGE_KEY.tabs, value); + storage.setItem(STORAGE_KEY.tabs, value); }); store(serializeTabState({ tabs, activeTabIndex }, shouldPersistHeaders)); }, diff --git a/packages/graphiql-react/src/utility/hooks.ts b/packages/graphiql-react/src/utility/hooks.ts index f7b8343588..b2c3aae456 100644 --- a/packages/graphiql-react/src/utility/hooks.ts +++ b/packages/graphiql-react/src/utility/hooks.ts @@ -1,10 +1,13 @@ // eslint-disable-next-line @typescript-eslint/no-restricted-imports -- TODO: check why query builder update only 1st field https://github.com/graphql/graphiql/issues/3836 import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { storageStore } from '../stores'; import { debounce } from './debounce'; import type { editor as monacoEditor } from '../monaco-editor'; import { useGraphiQL, useGraphiQLActions } from '../components'; +const storageStore = { + getState: () => ({ storage: null }), +}; + export function useChangeHandler( callback: ((value: string) => void) | undefined, storageKey: string | null, diff --git a/packages/graphiql-react/src/utility/tabs.spec.ts b/packages/graphiql-react/src/utility/tabs.spec.ts index 5ccdb96c0e..cf537b7f07 100644 --- a/packages/graphiql-react/src/utility/tabs.spec.ts +++ b/packages/graphiql-react/src/utility/tabs.spec.ts @@ -6,7 +6,6 @@ import { getDefaultTabState, clearHeadersFromTabs, } from './tabs'; -import { storageStore } from '../stores'; import { STORAGE_KEY } from '../constants'; describe('createTab', () => { @@ -105,12 +104,6 @@ describe('fuzzyExtractionOperationTitle', () => { }); describe('getDefaultTabState', () => { - beforeEach(() => { - act(() => { - storageStore.setState({ storage: new StorageAPI() }); - }); - }); - it('returns default tab', () => { expect( getDefaultTabState({ diff --git a/packages/graphiql-react/src/utility/tabs.ts b/packages/graphiql-react/src/utility/tabs.ts index 6087404c61..afaf985a0f 100644 --- a/packages/graphiql-react/src/utility/tabs.ts +++ b/packages/graphiql-react/src/utility/tabs.ts @@ -2,6 +2,10 @@ import { STORAGE_KEY } from '../constants'; +const storageStore = { + getState: () => ({ storage: null }), +}; + export interface TabDefinition { /** * The contents of the query editor of this tab. From fd6d42b5ee1d8c291e515638e3ed2a1c001f261a Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 18:19:51 +0200 Subject: [PATCH 21/73] upd --- examples/graphiql-webpack/src/index.jsx | 6 +++--- .../graphiql-webpack/src/select-server-plugin.jsx | 6 +++--- packages/graphiql-plugin-history/src/context.ts | 9 ++------- .../graphiql-react/src/components/query-editor.tsx | 5 +++-- packages/graphiql-react/src/utility/resize.ts | 12 ++++++------ packages/graphiql/src/ui/sidebar.tsx | 4 ++-- 6 files changed, 19 insertions(+), 23 deletions(-) diff --git a/examples/graphiql-webpack/src/index.jsx b/examples/graphiql-webpack/src/index.jsx index d7c8c00a03..2510a84831 100644 --- a/examples/graphiql-webpack/src/index.jsx +++ b/examples/graphiql-webpack/src/index.jsx @@ -6,7 +6,7 @@ import { explorerPlugin } from '@graphiql/plugin-explorer'; import { getSnippets } from './snippets'; import { codeExporterPlugin } from '@graphiql/plugin-code-exporter'; import { createGraphiQLFetcher } from '@graphiql/toolkit'; -import { useStorage } from '@graphiql/react'; +import { useGraphiQL } from '@graphiql/react'; import { serverSelectPlugin, LAST_URL_KEY } from './select-server-plugin'; import 'graphiql/setup-workers/webpack'; import './index.css'; @@ -91,8 +91,8 @@ function App() { * provider tree. `` must be rendered as a child of ``. */ function GraphiQLStorageBound({ setUrl }) { - const storage = useStorage(); - const lastUrl = storage.get(LAST_URL_KEY) ?? STARTING_URL; + const storage = useGraphiQL(state => state.storage); + const lastUrl = storage.getItem(LAST_URL_KEY) ?? STARTING_URL; useEffect(() => { setUrl(lastUrl); diff --git a/examples/graphiql-webpack/src/select-server-plugin.jsx b/examples/graphiql-webpack/src/select-server-plugin.jsx index 6c4715ab1f..f61975f0b8 100644 --- a/examples/graphiql-webpack/src/select-server-plugin.jsx +++ b/examples/graphiql-webpack/src/select-server-plugin.jsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useStorage, useSchemaStore } from '@graphiql/react'; +import { useGraphiQL, useSchemaStore } from '@graphiql/react'; export const LAST_URL_KEY = 'lastURL'; @@ -7,8 +7,8 @@ export const PREV_URLS_KEY = 'previousURLs'; const SelectServer = ({ url, setUrl }) => { const inputRef = React.useRef(null); - const storage = useStorage(); - const lastUrl = storage.get(LAST_URL_KEY); + const storage = useGraphiQL(state => state.storage); + const lastUrl = storage.getItem(LAST_URL_KEY); const currentUrl = lastUrl ?? url; const [inputValue, setInputValue] = React.useState(currentUrl); const [previousUrls, setPreviousUrls] = React.useState( diff --git a/packages/graphiql-plugin-history/src/context.ts b/packages/graphiql-plugin-history/src/context.ts index 2b141667c6..b21856cda4 100644 --- a/packages/graphiql-plugin-history/src/context.ts +++ b/packages/graphiql-plugin-history/src/context.ts @@ -4,12 +4,7 @@ import { HistoryStore as ToolkitHistoryStore, QueryStoreItem, } from '@graphiql/toolkit'; -import { - useGraphiQL, - pick, - useStorage, - createBoundedUseStore, -} from '@graphiql/react'; +import { useGraphiQL, pick, createBoundedUseStore } from '@graphiql/react'; const historyStore = createStore((set, get) => ({ historyStorage: null, @@ -126,7 +121,7 @@ export const HistoryStore: FC = ({ pick('isFetching', 'tabs', 'activeTabIndex'), ); const activeTab = tabs[activeTabIndex]!; - const storage = useStorage(); + const storage = useGraphiQL(state => state.storage); const historyStorage = // eslint-disable-line react-hooks/exhaustive-deps -- false positive, code is optimized by React Compiler new ToolkitHistoryStore(storage, maxHistoryLength); diff --git a/packages/graphiql-react/src/components/query-editor.tsx b/packages/graphiql-react/src/components/query-editor.tsx index c32724715d..dabe276f78 100644 --- a/packages/graphiql-react/src/components/query-editor.tsx +++ b/packages/graphiql-react/src/components/query-editor.tsx @@ -69,6 +69,7 @@ export const QueryEditor: FC = ({ operations, operationName, externalFragments, + storage, } = useGraphiQL( pick( 'initialQuery', @@ -77,9 +78,9 @@ export const QueryEditor: FC = ({ 'operations', 'operationName', 'externalFragments', + 'storage', ), ); - const storage = useStorage(); const ref = useRef(null!); const onClickReferenceRef = useRef( null!, @@ -215,7 +216,7 @@ export const QueryEditor: FC = ({ // have additional logic that updates the operation facts that we save in `editorStore` const handleChange = debounce(100, () => { const query = editor.getValue(); - storage.set(STORAGE_KEY.query, query); + storage.setItem(STORAGE_KEY.query, query); const operationFacts = getAndUpdateOperationFacts(editor); // Invoke callback props only after the operation facts have been updated diff --git a/packages/graphiql-react/src/utility/resize.ts b/packages/graphiql-react/src/utility/resize.ts index ba4372e815..77c830a286 100644 --- a/packages/graphiql-react/src/utility/resize.ts +++ b/packages/graphiql-react/src/utility/resize.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { useStorage } from '../stores'; +import { useGraphiQL } from '../components'; import { debounce } from './debounce'; type ResizableElement = 'first' | 'second'; @@ -55,11 +55,11 @@ export function useDragResize({ sizeThresholdSecond = 100, storageKey, }: UseDragResizeArgs) { - const storage = useStorage(); + const storage = useGraphiQL(state => state.storage); const [hiddenElement, setHiddenElement] = useState( () => { - const storedValue = storageKey && storage.get(storageKey); + const storedValue = storageKey && storage.getItem(storageKey); if (storedValue === HIDE_FIRST || initiallyHidden === 'first') { return 'first'; } @@ -81,7 +81,7 @@ export function useDragResize({ */ useEffect(() => { const storedValue = - (storageKey && storage.get(storageKey)) || defaultFlexRef.current; + (storageKey && storage.getItem(storageKey)) || defaultFlexRef.current; if (firstRef.current) { firstRef.current.style.flex = @@ -123,7 +123,7 @@ export function useDragResize({ if (!storageKey) { return; } - const storedValue = storage.get(storageKey); + const storedValue = storage.getItem(storageKey); if ( firstRef.current && storedValue !== HIDE_FIRST && @@ -151,7 +151,7 @@ export function useDragResize({ } const store = debounce(500, (value: string) => { if (storageKey) { - storage.set(storageKey, value); + storage.setItem(storageKey, value); } }); diff --git a/packages/graphiql/src/ui/sidebar.tsx b/packages/graphiql/src/ui/sidebar.tsx index b9c1e713f5..85ff1a6935 100644 --- a/packages/graphiql/src/ui/sidebar.tsx +++ b/packages/graphiql/src/ui/sidebar.tsx @@ -14,7 +14,6 @@ import { useDragResize, useGraphiQL, useGraphiQLActions, - useStorage, VisuallyHidden, } from '@graphiql/react'; import { ShortKeys } from './short-keys'; @@ -54,7 +53,6 @@ export const Sidebar: FC = ({ const forcedTheme = $forcedTheme && THEMES.includes($forcedTheme) ? $forcedTheme : undefined; - const storage = useStorage(); const { setShouldPersistHeaders, introspect, setVisiblePlugin, setTheme } = useGraphiQLActions(); const { @@ -63,6 +61,7 @@ export const Sidebar: FC = ({ visiblePlugin, plugins, theme, + storage, } = useGraphiQL( pick( 'shouldPersistHeaders', @@ -70,6 +69,7 @@ export const Sidebar: FC = ({ 'visiblePlugin', 'plugins', 'theme', + 'storage', ), ); From 31ffc69e0f21f5890d1b3ba5b1dee254fa01ebac Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 18:25:26 +0200 Subject: [PATCH 22/73] upd --- packages/graphiql-toolkit/package.json | 3 +- .../src/storage/__tests__/base.spec.ts | 99 ------------ packages/graphiql-toolkit/src/storage/base.ts | 141 ------------------ .../graphiql-toolkit/src/storage/query.ts | 12 +- 4 files changed, 8 insertions(+), 247 deletions(-) delete mode 100644 packages/graphiql-toolkit/src/storage/__tests__/base.spec.ts delete mode 100644 packages/graphiql-toolkit/src/storage/base.ts diff --git a/packages/graphiql-toolkit/package.json b/packages/graphiql-toolkit/package.json index 8b456840bc..b32770a8bd 100644 --- a/packages/graphiql-toolkit/package.json +++ b/packages/graphiql-toolkit/package.json @@ -33,7 +33,8 @@ "graphql-ws": "^5.5.5", "isomorphic-fetch": "^3.0.0", "subscriptions-transport-ws": "0.11.0", - "tsup": "^8.2.4" + "tsup": "^8.2.4", + "zustand": "^5" }, "peerDependencies": { "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", diff --git a/packages/graphiql-toolkit/src/storage/__tests__/base.spec.ts b/packages/graphiql-toolkit/src/storage/__tests__/base.spec.ts deleted file mode 100644 index 376aaf34ff..0000000000 --- a/packages/graphiql-toolkit/src/storage/__tests__/base.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { StorageAPI } from '../base'; - -describe('StorageAPI', () => { - let storage = new StorageAPI(); - - beforeEach(() => { - storage = new StorageAPI(); - }); - - it('returns nothing if no value set', () => { - const result = storage.get('key1'); - expect(result).toBeNull(); - }); - - it('sets and gets a value correctly', () => { - const result = storage.set('key2', 'value'); - expect(result).toEqual({ - error: null, - isQuotaError: false, - }); - - const newResult = storage.get('key2'); - expect(newResult).toEqual('value'); - }); - - it('sets and removes a value correctly', () => { - let result = storage.set('key3', 'value'); - expect(result).toEqual({ - error: null, - isQuotaError: false, - }); - - result = storage.set('key3', ''); - expect(result).toEqual({ - error: null, - isQuotaError: false, - }); - - const getResult = storage.get('key3'); - expect(getResult).toBeNull(); - }); - - it('sets and overrides a value correctly', () => { - let result = storage.set('key4', 'value'); - expect(result).toEqual({ - error: null, - isQuotaError: false, - }); - - result = storage.set('key4', 'value2'); - expect(result).toEqual({ - error: null, - isQuotaError: false, - }); - - const getResult = storage.get('key4'); - expect(getResult).toEqual('value2'); - }); - - it('cleans up `null` value', () => { - storage.set('key5', 'null'); - const result = storage.get('key5'); - expect(result).toBeNull(); - }); - - it('cleans up `undefined` value', () => { - storage.set('key6', 'undefined'); - const result = storage.get('key6'); - expect(result).toBeNull(); - }); - - it('returns any error while setting a value', () => { - // @ts-expect-error - const throwingStorage = new StorageAPI({ - setItem() { - throw new DOMException('Terrible Error'); - }, - length: 1, - }); - const result = throwingStorage.set('key', 'value'); - - expect(result.error!.message).toEqual('Error: Terrible Error'); - expect(result.isQuotaError).toBe(false); - }); - - it('returns isQuotaError to true if isQuotaError is thrown', () => { - // @ts-expect-error - const throwingStorage = new StorageAPI({ - setItem() { - throw new DOMException('Terrible Error', 'QuotaExceededError'); - }, - length: 1, - }); - const result = throwingStorage.set('key', 'value'); - - expect(result.error!.message).toEqual('QuotaExceededError: Terrible Error'); - expect(result.isQuotaError).toBe(true); - }); -}); diff --git a/packages/graphiql-toolkit/src/storage/base.ts b/packages/graphiql-toolkit/src/storage/base.ts deleted file mode 100644 index f059b11df6..0000000000 --- a/packages/graphiql-toolkit/src/storage/base.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * This describes the attributes and methods that a store has to support in - * order to be used with GraphiQL. It closely resembles the `localStorage` - * API as it is the default storage used in GraphiQL. - */ -export type Storage = { - /** - * Retrieve an item from the store by its key. - * @param key The key of the item to retrieve. - * @returns {?string} The stored value for the given key if it exists, `null` - * otherwise. - */ - getItem(key: string): string | null; - /** - * Add a value to the store for a given key. If there already exists a value - * for the given key, this method will override the value. - * @param key The key to store the value for. - * @param value The value to store. - */ - setItem(key: string, value: string): void; - /** - * Remove the value for a given key from the store. If there is no value for - * the given key this method does nothing. - * @param key The key to remove the value from the store. - */ - removeItem(key: string): void; - /** - * Remove all items from the store. - */ - clear(): void; - /** - * The number of items that are currently stored. - */ - length: number; -}; - -function isQuotaError(storage: Storage, e: unknown) { - return ( - e instanceof DOMException && - // everything except Firefox - (e.code === 22 || - // Firefox - e.code === 1014 || - // test name field too, because code might not be present - // everything except Firefox - e.name === 'QuotaExceededError' || - // Firefox - e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && - // acknowledge QuotaExceededError only if there's something already stored - storage.length !== 0 - ); -} - -export class StorageAPI { - storage: Storage | null; - - constructor(storage?: Storage | null) { - if (storage) { - this.storage = storage; - } else if (storage === null) { - // Passing `null` creates a noop storage - this.storage = null; - } else if (typeof window === 'undefined') { - this.storage = null; - } else { - this.storage = { - getItem: localStorage.getItem.bind(localStorage), - setItem: localStorage.setItem.bind(localStorage), - removeItem: localStorage.removeItem.bind(localStorage), - - get length() { - let keys = 0; - for (const key in localStorage) { - if (key.indexOf(`${STORAGE_NAMESPACE}:`) === 0) { - keys += 1; - } - } - return keys; - }, - - clear() { - // We only want to clear the namespaced items - for (const key in localStorage) { - if (key.indexOf(`${STORAGE_NAMESPACE}:`) === 0) { - localStorage.removeItem(key); - } - } - }, - }; - } - } - - get(name: string): string | null { - if (!this.storage) { - return null; - } - - const key = `${STORAGE_NAMESPACE}:${name}`; - const value = this.storage.getItem(key); - // Clean up any inadvertently saved null/undefined values. - if (value === 'null' || value === 'undefined') { - this.storage.removeItem(key); - return null; - } - - return value || null; - } - - set( - name: string, - value: string, - ): { isQuotaError: boolean; error: Error | null } { - let quotaError = false; - let error: Error | null = null; - - if (this.storage) { - const key = `${STORAGE_NAMESPACE}:${name}`; - if (value) { - try { - this.storage.setItem(key, value); - } catch (e) { - error = e instanceof Error ? e : new Error(`${e}`); - quotaError = isQuotaError(this.storage, e); - } - } else { - // Clean up by removing the item if there's no value to set - this.storage.removeItem(key); - } - } - - return { isQuotaError: quotaError, error }; - } - - clear() { - if (this.storage) { - this.storage.clear(); - } - } -} - -const STORAGE_NAMESPACE = 'graphiql'; diff --git a/packages/graphiql-toolkit/src/storage/query.ts b/packages/graphiql-toolkit/src/storage/query.ts index 9fea0e93b4..2f46174ef2 100644 --- a/packages/graphiql-toolkit/src/storage/query.ts +++ b/packages/graphiql-toolkit/src/storage/query.ts @@ -1,4 +1,4 @@ -import { StorageAPI } from './base'; +import type { PersistStorage } from 'zustand/middleware'; export type QueryStoreItem = { query?: string; @@ -10,11 +10,11 @@ export type QueryStoreItem = { }; export class QueryStore { - items: Array; + items: QueryStoreItem[]; constructor( private key: string, - private storage: StorageAPI, + private storage: PersistStorage, private maxSize: number | null = null, ) { this.items = this.fetchAll(); @@ -81,7 +81,7 @@ export class QueryStore { } fetchAll() { - const raw = this.storage.get(this.key); + const raw = this.storage.getItem(this.key); if (raw) { return JSON.parse(raw)[this.key] as Array; } @@ -96,7 +96,7 @@ export class QueryStore { } for (let attempts = 0; attempts < 5; attempts++) { - const response = this.storage.set( + const response = this.storage.setItem( this.key, JSON.stringify({ [this.key]: items }), ); @@ -112,6 +112,6 @@ export class QueryStore { } save() { - this.storage.set(this.key, JSON.stringify({ [this.key]: this.items })); + this.storage.setItem(this.key, JSON.stringify({ [this.key]: this.items })); } } From 25a7af8165083ca9856df57be79f6e78ab24ef5f Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 18:28:24 +0200 Subject: [PATCH 23/73] upd --- .../graphiql-toolkit/src/storage/custom.ts | 47 ------------------- .../graphiql-toolkit/src/storage/history.ts | 5 +- .../graphiql-toolkit/src/storage/index.ts | 2 - 3 files changed, 2 insertions(+), 52 deletions(-) delete mode 100644 packages/graphiql-toolkit/src/storage/custom.ts diff --git a/packages/graphiql-toolkit/src/storage/custom.ts b/packages/graphiql-toolkit/src/storage/custom.ts deleted file mode 100644 index 7e82cdd5b6..0000000000 --- a/packages/graphiql-toolkit/src/storage/custom.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * This function enables a custom namespace for localStorage - */ - -import { Storage } from './base'; - -export type CreateLocalStorageOptions = { - /** - * specify a different storage namespace prefix from the default of 'graphiql' - */ - namespace?: string; -}; -/** - * generate a custom local storage adapter for GraphiQL `storage` prop. - */ -export function createLocalStorage({ - namespace, -}: CreateLocalStorageOptions): Storage { - const storageKeyPrefix = `${namespace}:`; - const getStorageKey = (key: string) => `${storageKeyPrefix}${key}`; - - const storage: Storage = { - setItem: (key, value) => localStorage.setItem(getStorageKey(key), value), - getItem: key => localStorage.getItem(getStorageKey(key)), - removeItem: key => localStorage.removeItem(getStorageKey(key)), - get length() { - let keys = 0; - for (const key in localStorage) { - if (key.indexOf(storageKeyPrefix) === 0) { - keys += 1; - } - } - return keys; - }, - - clear() { - // We only want to clear the namespaced items - for (const key in localStorage) { - if (key.indexOf(storageKeyPrefix) === 0) { - localStorage.removeItem(key); - } - } - }, - }; - - return storage; -} diff --git a/packages/graphiql-toolkit/src/storage/history.ts b/packages/graphiql-toolkit/src/storage/history.ts index 3f301b5e4e..c855c48f80 100644 --- a/packages/graphiql-toolkit/src/storage/history.ts +++ b/packages/graphiql-toolkit/src/storage/history.ts @@ -1,6 +1,5 @@ import { parse } from 'graphql'; - -import { StorageAPI } from './base'; +import type { PersistStorage } from 'zustand/middleware'; import { QueryStore, QueryStoreItem } from './query'; const MAX_QUERY_SIZE = 100000; @@ -11,7 +10,7 @@ export class HistoryStore { favorite: QueryStore; constructor( - private storage: StorageAPI, + private storage: PersistStorage, private maxHistoryLength: number, ) { this.history = new QueryStore( diff --git a/packages/graphiql-toolkit/src/storage/index.ts b/packages/graphiql-toolkit/src/storage/index.ts index fe60fb4e20..a715d16c79 100644 --- a/packages/graphiql-toolkit/src/storage/index.ts +++ b/packages/graphiql-toolkit/src/storage/index.ts @@ -1,4 +1,2 @@ -export * from './base'; export * from './history'; export * from './query'; -export * from './custom'; From a8824ee02aedca7467c0ddaaff02c3044c453efc Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 18:30:42 +0200 Subject: [PATCH 24/73] upd --- packages/graphiql-toolkit/src/create-fetcher/types.ts | 6 +++--- .../graphiql-toolkit/src/graphql-helpers/auto-complete.ts | 2 +- .../graphiql-toolkit/src/storage/__tests__/query.spec.ts | 7 +++++-- packages/graphiql-toolkit/src/storage/query.ts | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/graphiql-toolkit/src/create-fetcher/types.ts b/packages/graphiql-toolkit/src/create-fetcher/types.ts index f44d0b6a31..035483ed90 100644 --- a/packages/graphiql-toolkit/src/create-fetcher/types.ts +++ b/packages/graphiql-toolkit/src/create-fetcher/types.ts @@ -38,12 +38,12 @@ export type FetcherOpts = { export type ExecutionResultPayload = | { data: IntrospectionQuery; - errors?: Array; + errors?: any[]; } // normal result payloads - | { data?: any; errors?: Array } + | { data?: any; errors?: any[] } // for the initial Stream/Defer payload - | { data?: any; errors?: Array; hasNext: boolean } + | { data?: any; errors?: any[]; hasNext: boolean } // for successive Stream/Defer payloads | { data?: any; diff --git a/packages/graphiql-toolkit/src/graphql-helpers/auto-complete.ts b/packages/graphiql-toolkit/src/graphql-helpers/auto-complete.ts index 4000b4672a..cc4c5339c5 100644 --- a/packages/graphiql-toolkit/src/graphql-helpers/auto-complete.ts +++ b/packages/graphiql-toolkit/src/graphql-helpers/auto-complete.ts @@ -108,7 +108,7 @@ function defaultGetDefaultFieldNames(type: GraphQLType) { } // Include all leaf-type fields. - const leafFieldNames: Array = []; + const leafFieldNames: string[] = []; for (const fieldName of Object.keys(fields)) { if (isLeafType(fields[fieldName].type)) { leafFieldNames.push(fieldName); diff --git a/packages/graphiql-toolkit/src/storage/__tests__/query.spec.ts b/packages/graphiql-toolkit/src/storage/__tests__/query.spec.ts index ddfab64c5b..7339635cac 100644 --- a/packages/graphiql-toolkit/src/storage/__tests__/query.spec.ts +++ b/packages/graphiql-toolkit/src/storage/__tests__/query.spec.ts @@ -1,5 +1,5 @@ -import { StorageAPI } from '../base'; import { QueryStore } from '../query'; +import { createJSONStorage } from 'zustand/middleware'; class StorageMock { shouldThrow: () => boolean; @@ -40,7 +40,10 @@ class StorageMock { describe('QueryStore', () => { describe('with no max items', () => { it('can push multiple items', () => { - const store = new QueryStore('normal', new StorageAPI()); + const store = new QueryStore( + 'normal', + createJSONStorage(() => localStorage), + ); for (let i = 0; i < 100; i++) { store.push({ query: `item${i}` }); diff --git a/packages/graphiql-toolkit/src/storage/query.ts b/packages/graphiql-toolkit/src/storage/query.ts index 2f46174ef2..1338130067 100644 --- a/packages/graphiql-toolkit/src/storage/query.ts +++ b/packages/graphiql-toolkit/src/storage/query.ts @@ -83,7 +83,7 @@ export class QueryStore { fetchAll() { const raw = this.storage.getItem(this.key); if (raw) { - return JSON.parse(raw)[this.key] as Array; + return JSON.parse(raw)[this.key] as QueryStoreItem[]; } return []; } From 06c5061d41935083bf4e5c6fba3870bb44aaa720 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 18:32:12 +0200 Subject: [PATCH 25/73] lint --- packages/graphiql-react/src/components/query-editor.tsx | 1 - packages/graphiql-react/src/utility/tabs.spec.ts | 2 -- yarn.lock | 1 + 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/graphiql-react/src/components/query-editor.tsx b/packages/graphiql-react/src/components/query-editor.tsx index dabe276f78..80f24ce75d 100644 --- a/packages/graphiql-react/src/components/query-editor.tsx +++ b/packages/graphiql-react/src/components/query-editor.tsx @@ -2,7 +2,6 @@ import { getSelectedOperationName } from '@graphiql/toolkit'; import type { DocumentNode } from 'graphql'; import { getOperationFacts } from 'graphql-language-service'; import { FC, useEffect, useRef } from 'react'; -import { useStorage } from '../stores'; import { useGraphiQL, useGraphiQLActions } from './provider'; import { debounce, diff --git a/packages/graphiql-react/src/utility/tabs.spec.ts b/packages/graphiql-react/src/utility/tabs.spec.ts index cf537b7f07..5f8ed10106 100644 --- a/packages/graphiql-react/src/utility/tabs.spec.ts +++ b/packages/graphiql-react/src/utility/tabs.spec.ts @@ -1,5 +1,3 @@ -import { act } from 'react'; -import { StorageAPI } from '@graphiql/toolkit'; import { createTab, fuzzyExtractOperationName, diff --git a/yarn.lock b/yarn.lock index 0921747f23..7fdd1c58bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3352,6 +3352,7 @@ __metadata: meros: "npm:^1.1.4" subscriptions-transport-ws: "npm:0.11.0" tsup: "npm:^8.2.4" + zustand: "npm:^5" peerDependencies: graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 graphql-ws: ">= 4.5.0" From 92dd841806b84928769634d35d5313bf1696338c Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 18:32:59 +0200 Subject: [PATCH 26/73] upd --- packages/graphiql-react/src/utility/tabs.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/graphiql-react/src/utility/tabs.spec.ts b/packages/graphiql-react/src/utility/tabs.spec.ts index 5f8ed10106..7ec8b559c8 100644 --- a/packages/graphiql-react/src/utility/tabs.spec.ts +++ b/packages/graphiql-react/src/utility/tabs.spec.ts @@ -6,6 +6,10 @@ import { } from './tabs'; import { STORAGE_KEY } from '../constants'; +const storageStore = { + getState: () => ({ storage: null }), +}; + describe('createTab', () => { it('creates with default title', () => { expect(createTab({})).toEqual( From aa8ed008322bc023deab2c259ff3e779274a781d Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 18:53:17 +0200 Subject: [PATCH 27/73] fix type errors --- .../graphiql-toolkit/src/storage/history.ts | 8 +++-- .../graphiql-toolkit/src/storage/query.ts | 34 +++++++------------ 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/graphiql-toolkit/src/storage/history.ts b/packages/graphiql-toolkit/src/storage/history.ts index c855c48f80..3bfcad8824 100644 --- a/packages/graphiql-toolkit/src/storage/history.ts +++ b/packages/graphiql-toolkit/src/storage/history.ts @@ -5,7 +5,7 @@ import { QueryStore, QueryStoreItem } from './query'; const MAX_QUERY_SIZE = 100000; export class HistoryStore { - queries: QueryStoreItem[]; + queries: QueryStoreItem[] = []; history: QueryStore; favorite: QueryStore; @@ -21,7 +21,11 @@ export class HistoryStore { // favorites are not automatically deleted, so there's no need for a max length this.favorite = new QueryStore('favorites', this.storage, null); - this.queries = [...this.history.fetchAll(), ...this.favorite.fetchAll()]; + void Promise.all([this.history.fetchAll(), this.favorite.fetchAll()]).then( + ([history, favorite]) => { + this.queries = [...history, ...favorite]; + }, + ); } private shouldSaveQuery( diff --git a/packages/graphiql-toolkit/src/storage/query.ts b/packages/graphiql-toolkit/src/storage/query.ts index 1338130067..26432625c0 100644 --- a/packages/graphiql-toolkit/src/storage/query.ts +++ b/packages/graphiql-toolkit/src/storage/query.ts @@ -10,14 +10,18 @@ export type QueryStoreItem = { }; export class QueryStore { - items: QueryStoreItem[]; + items: QueryStoreItem[] = []; constructor( private key: string, - private storage: PersistStorage, + private storage: PersistStorage<{ + [key: string]: QueryStoreItem[]; + }>, private maxSize: number | null = null, ) { - this.items = this.fetchAll(); + void this.fetchAll().then(items => { + this.items = items; + }); } get length() { @@ -80,10 +84,10 @@ export class QueryStore { return this.items.at(-1); } - fetchAll() { - const raw = this.storage.getItem(this.key); + async fetchAll() { + const raw = await this.storage.getItem(this.key); if (raw) { - return JSON.parse(raw)[this.key] as QueryStoreItem[]; + return raw.state[this.key]; } return []; } @@ -94,24 +98,10 @@ export class QueryStore { if (this.maxSize && items.length > this.maxSize) { items.shift(); } - - for (let attempts = 0; attempts < 5; attempts++) { - const response = this.storage.setItem( - this.key, - JSON.stringify({ [this.key]: items }), - ); - if (!response?.error) { - this.items = items; - } else if (response.isQuotaError && this.maxSize) { - // Only try to delete last items on LRU stores - items.shift(); - } else { - return; // We don't know what happened in this case, so just bailing out - } - } + this.storage.setItem(this.key, items); } save() { - this.storage.setItem(this.key, JSON.stringify({ [this.key]: this.items })); + this.storage.setItem(this.key, this.items); } } From 618387103b546518b79135b5ee262f1ece81b9c8 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 18:59:05 +0200 Subject: [PATCH 28/73] add ts-expect --- packages/graphiql-toolkit/src/storage/query.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/graphiql-toolkit/src/storage/query.ts b/packages/graphiql-toolkit/src/storage/query.ts index 26432625c0..672fc440e6 100644 --- a/packages/graphiql-toolkit/src/storage/query.ts +++ b/packages/graphiql-toolkit/src/storage/query.ts @@ -98,10 +98,16 @@ export class QueryStore { if (this.maxSize && items.length > this.maxSize) { items.shift(); } - this.storage.setItem(this.key, items); + this.storage.setItem(this.key, { + // @ts-expect-error -- fixme + state: items, + }); } save() { - this.storage.setItem(this.key, this.items); + this.storage.setItem(this.key, { + // @ts-expect-error -- fixme + state: this.items, + }); } } From 39b2a8a083f362c9bf5df80a54327f0a56ec21b6 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 19:02:00 +0200 Subject: [PATCH 29/73] upd --- packages/graphiql-react/src/deprecated.ts | 8 ++++---- packages/graphiql-react/src/stores/storage.ts | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/graphiql-react/src/deprecated.ts b/packages/graphiql-react/src/deprecated.ts index 9faa27b9ac..6ab53badab 100644 --- a/packages/graphiql-react/src/deprecated.ts +++ b/packages/graphiql-react/src/deprecated.ts @@ -1,7 +1,5 @@ -/* eslint-disable unicorn/prefer-export-from */ import { useGraphiQL, useGraphiQLActions } from './components'; import { pick } from './utility'; -import { useStorage } from './stores'; /** * @deprecated Use `const { prettifyEditors } = useGraphiQLActions()` instead. @@ -71,6 +69,8 @@ export function useSchemaContext() { } /** - * @deprecated Use `const storage = useStorage()` instead. + * @deprecated Use `const storage = useGraphiQL(state => state.storage)` instead. */ -export const useStorageContext = useStorage; +export function useStorageContext() { + return useGraphiQL(state => state.storage); +} diff --git a/packages/graphiql-react/src/stores/storage.ts b/packages/graphiql-react/src/stores/storage.ts index d47b17b56f..71f9b002e0 100644 --- a/packages/graphiql-react/src/stores/storage.ts +++ b/packages/graphiql-react/src/stores/storage.ts @@ -2,6 +2,8 @@ import type { PersistStorage } from 'zustand/middleware'; import type { StateCreator } from 'zustand'; import type { SlicesWithActions, Theme } from '../types'; +export type Storage = PersistStorage + interface GraphiQLPersistedState { theme?: Theme; visiblePlugin?: string; @@ -13,7 +15,7 @@ export interface StorageSlice { * @default createJSONStorage(() => localStorage) * @see https://zustand.docs.pmnd.rs/integrations/persisting-store-data#createjsonstorage */ - storage: PersistStorage; + storage: Storage; } export interface StorageProps { From a90f9320a4f08947450dbdd162346beab2710e62 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 19:03:19 +0200 Subject: [PATCH 30/73] upd --- packages/graphiql-react/src/utility/tabs.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/graphiql-react/src/utility/tabs.ts b/packages/graphiql-react/src/utility/tabs.ts index afaf985a0f..23ed514dfb 100644 --- a/packages/graphiql-react/src/utility/tabs.ts +++ b/packages/graphiql-react/src/utility/tabs.ts @@ -93,7 +93,7 @@ export function getDefaultTabState({ shouldPersistHeaders?: boolean; }) { const { storage } = storageStore.getState(); - const storedState = storage.get(STORAGE_KEY.tabs); + const storedState = storage.getItem(STORAGE_KEY.tabs); try { if (!storedState) { throw new Error('Storage for tabs is empty'); @@ -274,10 +274,10 @@ export function fuzzyExtractOperationName(str: string): string | null { export function clearHeadersFromTabs() { const { storage } = storageStore.getState(); - const persistedTabs = storage.get(STORAGE_KEY.tabs); + const persistedTabs = storage.getItem(STORAGE_KEY.tabs); if (persistedTabs) { const parsedTabs = JSON.parse(persistedTabs); - storage.set( + storage.setItem( STORAGE_KEY.tabs, JSON.stringify(parsedTabs, (key, value) => key === 'headers' ? null : value, From 3a1273ec8488e6d11a37997d2c6a9389167d9b06 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 19:04:33 +0200 Subject: [PATCH 31/73] upd --- packages/graphiql-react/src/stores/storage.ts | 2 +- packages/graphiql-react/src/utility/hooks.ts | 2 +- packages/graphiql-react/src/utility/tabs.spec.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/graphiql-react/src/stores/storage.ts b/packages/graphiql-react/src/stores/storage.ts index 71f9b002e0..16f7c9fb0a 100644 --- a/packages/graphiql-react/src/stores/storage.ts +++ b/packages/graphiql-react/src/stores/storage.ts @@ -2,7 +2,7 @@ import type { PersistStorage } from 'zustand/middleware'; import type { StateCreator } from 'zustand'; import type { SlicesWithActions, Theme } from '../types'; -export type Storage = PersistStorage +export type Storage = PersistStorage; interface GraphiQLPersistedState { theme?: Theme; diff --git a/packages/graphiql-react/src/utility/hooks.ts b/packages/graphiql-react/src/utility/hooks.ts index b2c3aae456..dfaab0d486 100644 --- a/packages/graphiql-react/src/utility/hooks.ts +++ b/packages/graphiql-react/src/utility/hooks.ts @@ -28,7 +28,7 @@ export function useChangeHandler( if (storageKey === null) { return; } - storage.set(storageKey, value); + storage.setItem(storageKey, value); }); const updateTab = debounce(100, (value: string) => { updateActiveTabValues({ [tabProperty]: value }); diff --git a/packages/graphiql-react/src/utility/tabs.spec.ts b/packages/graphiql-react/src/utility/tabs.spec.ts index 7ec8b559c8..74836fb4e6 100644 --- a/packages/graphiql-react/src/utility/tabs.spec.ts +++ b/packages/graphiql-react/src/utility/tabs.spec.ts @@ -174,10 +174,10 @@ describe('clearHeadersFromTabs', () => { }, headers: '{ "authorization": "secret" }', }; - storage.set(STORAGE_KEY.tabs, JSON.stringify(stateWithHeaders)); + storage.setItem(STORAGE_KEY.tabs, JSON.stringify(stateWithHeaders)); clearHeadersFromTabs(); - expect(JSON.parse(storage.get(STORAGE_KEY.tabs)!)).toEqual({ + expect(JSON.parse(storage.getItem(STORAGE_KEY.tabs)!)).toEqual({ ...stateWithHeaders, headers: null, }); From 7ac40a9c3568c4a7f8cc01f85dbec365a78b24a4 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 19:10:09 +0200 Subject: [PATCH 32/73] upd --- docs/migration/graphiql-5.0.0.md | 2 +- packages/graphiql-react/README.md | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/migration/graphiql-5.0.0.md b/docs/migration/graphiql-5.0.0.md index 1b500b78de..4ee89604d5 100644 --- a/docs/migration/graphiql-5.0.0.md +++ b/docs/migration/graphiql-5.0.0.md @@ -138,7 +138,7 @@ function App() { > Clicking on a reference in the Query editor now works by holding `Cmd` on macOS or `Ctrl` on Windows/Linux. - `usePrettifyEditors`, `useCopyQuery`, `useMergeQuery`, `useExecutionContext`, `usePluginContext`, `useSchemaContext`, `useStorageContext` hooks are deprecated. -- Add new `useGraphiQL` and `useGraphiQLActions` hooks instead. See updated [README](../../packages/graphiql-react/README.md#available-stores) for more details about them. +- Add new `useGraphiQL` and `useGraphiQLActions` hooks instead. See updated [README](../../packages/graphiql-react/README.md#core-hooks) for more details about them. - remove `useSynchronizeValue` hook - fix `defaultQuery` with empty string does not result in an empty default query - fix `defaultQuery`, when is set will only be used for the first tab. When opening more tabs, the query editor will start out empty diff --git a/packages/graphiql-react/README.md b/packages/graphiql-react/README.md index 51e0eeec5a..2f61ac5c23 100644 --- a/packages/graphiql-react/README.md +++ b/packages/graphiql-react/README.md @@ -81,28 +81,26 @@ Further details on how to use `@graphiql/react` can be found in the reference implementation of a GraphQL IDE - Graph*i*QL - in the [`graphiql` package](https://github.com/graphql/graphiql/blob/main/packages/graphiql/src/components/GraphiQL.tsx). -## Available Stores +## Core Hooks GraphiQL uses a set of state management stores, each responsible for a specific part of the IDE's behavior. These stores contain all logic related to state management and can be accessed via custom React hooks. -### Core Hooks - -- **`useStorage`**: Provides a storage API that can be used to persist state in the browser (by default using `localStorage`). -- **`useTheme`**: Manages the current theme and provides a method to update it. - **`useGraphiQL`**: Access the current state. - **`useGraphiQLActions`**: Trigger actions that mutate the state. This hook **never** rerenders. The `useGraphiQLActions` hook **exposes all actions** across store slices. The `useGraphiQL` hook **provides access to the following store slices**: -| Store Slice | Responsibilities | -| ----------- | -------------------------------------------------------------------------------- | -| `editor` | Manages **query**, **variables**, **headers**, and **response** editors and tabs | -| `execution` | Handles the execution of GraphQL requests | -| `plugin` | Manages plugins and the currently active plugin | -| `schema` | Fetches, validates, and stores the GraphQL schema | +| Store Slice | Responsibilities | +| ----------- | --------------------------------------------------------------------------------------------------------- | +| `storage` | Provides a storage API that can be used to persist state in the browser (by default using `localStorage`) | +| `theme` | Manages the current theme and provides a method to update it. | +| `editor` | Manages **query**, **variables**, **headers**, and **response** editors and tabs | +| `execution` | Handles the execution of GraphQL requests | +| `schema` | Fetches, validates, and stores the GraphQL schema | +| `plugin` | Manages plugins and the currently active plugin | ### Usage Example From a919c2a33ec16ee4bbb4e286a07fd3851c358e05 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 19:10:45 +0200 Subject: [PATCH 33/73] upd --- docs/migration/graphiql-5.0.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migration/graphiql-5.0.0.md b/docs/migration/graphiql-5.0.0.md index 4ee89604d5..1472b66f54 100644 --- a/docs/migration/graphiql-5.0.0.md +++ b/docs/migration/graphiql-5.0.0.md @@ -145,7 +145,7 @@ function App() { - fix execute query shortcut in query editor, run it even there are no operations in query editor - fix plugin store, save last opened plugin in storage - remove `onClickReference` from variable editor -- fix shortcut text per OS for default query and in run query in execute query button's tooltip +- fix shortcut text per OS for a default query and in run query in execute query button's tooltip The `ToolbarMenu` component has changed. From a3f887f2ad8a04e29b0de82a5370bc75ce9504df Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 19:18:23 +0200 Subject: [PATCH 34/73] upd --- .changeset/little-impalas-applaud.md | 8 ++++++++ packages/graphiql-react/src/components/provider.tsx | 1 + packages/graphiql-react/src/utility/tabs.ts | 4 +++- packages/graphiql/src/cdn.ts | 6 +----- 4 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 .changeset/little-impalas-applaud.md diff --git a/.changeset/little-impalas-applaud.md b/.changeset/little-impalas-applaud.md new file mode 100644 index 0000000000..26491389fb --- /dev/null +++ b/.changeset/little-impalas-applaud.md @@ -0,0 +1,8 @@ +--- +#'@graphiql/plugin-history': minor +#'@graphiql/toolkit': minor +#'@graphiql/react': minor +#'graphiql': minor +--- + +remove `createLocalStorage` from `@graphiql/toolkit` diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 9c332371a7..22cda75fca 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -158,6 +158,7 @@ useEffect(() => { props.initialHeaders ?? storage!.getItem(STORAGE_KEY.headers); const { tabs, activeTabIndex } = getDefaultTabState({ + storage, defaultHeaders, defaultQuery, defaultTabs, diff --git a/packages/graphiql-react/src/utility/tabs.ts b/packages/graphiql-react/src/utility/tabs.ts index 23ed514dfb..f9c0e03896 100644 --- a/packages/graphiql-react/src/utility/tabs.ts +++ b/packages/graphiql-react/src/utility/tabs.ts @@ -1,6 +1,7 @@ 'use no memo'; // can't figure why it isn't optimized import { STORAGE_KEY } from '../constants'; +import type { Storage } from '../stores/storage'; const storageStore = { getState: () => ({ storage: null }), @@ -70,6 +71,7 @@ export type TabsState = { }; export function getDefaultTabState({ + storage, defaultQuery, defaultHeaders, headers, @@ -84,6 +86,7 @@ export function getDefaultTabState({ ], shouldPersistHeaders, }: { + storage: Storage; defaultQuery: string; defaultHeaders?: string; headers: string | null; @@ -92,7 +95,6 @@ export function getDefaultTabState({ variables: string | null; shouldPersistHeaders?: boolean; }) { - const { storage } = storageStore.getState(); const storedState = storage.getItem(STORAGE_KEY.tabs); try { if (!storedState) { diff --git a/packages/graphiql/src/cdn.ts b/packages/graphiql/src/cdn.ts index 84a1296ba6..2e02a63c80 100644 --- a/packages/graphiql/src/cdn.ts +++ b/packages/graphiql/src/cdn.ts @@ -6,7 +6,7 @@ */ import { version } from 'react'; import * as GraphiQLReact from '@graphiql/react'; -import { createGraphiQLFetcher, createLocalStorage } from '@graphiql/toolkit'; +import { createGraphiQLFetcher } from '@graphiql/toolkit'; import * as GraphQL from 'graphql'; import { GraphiQL } from './GraphiQL'; import './setup-workers/vite'; @@ -32,10 +32,6 @@ export default Object.assign(GraphiQL, { * This function is needed in order to easily create a fetcher function. */ createFetcher: createGraphiQLFetcher, - /** - * This function is needed in order to easily generate a custom storage namespace - */ - createLocalStorage, /** * We also add the complete `graphiql-js` exports so that this instance of * `graphiql-js` can be reused from plugin CDN bundles. From f21fa94c752406a858f97a7828cd798b4f74a7d6 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 19:40:29 +0200 Subject: [PATCH 35/73] upd --- packages/graphiql-react/src/stores/storage.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/graphiql-react/src/stores/storage.ts b/packages/graphiql-react/src/stores/storage.ts index 16f7c9fb0a..471e061350 100644 --- a/packages/graphiql-react/src/stores/storage.ts +++ b/packages/graphiql-react/src/stores/storage.ts @@ -1,13 +1,6 @@ -import type { PersistStorage } from 'zustand/middleware'; import type { StateCreator } from 'zustand'; -import type { SlicesWithActions, Theme } from '../types'; - -export type Storage = PersistStorage; - -interface GraphiQLPersistedState { - theme?: Theme; - visiblePlugin?: string; -} +import type { StateStorage } from 'zustand/middleware'; +import type { SlicesWithActions } from '../types'; export interface StorageSlice { /** @@ -15,7 +8,7 @@ export interface StorageSlice { * @default createJSONStorage(() => localStorage) * @see https://zustand.docs.pmnd.rs/integrations/persisting-store-data#createjsonstorage */ - storage: Storage; + storage: StateStorage; } export interface StorageProps { From 9640b26fdcae440fd360f95148a385c64d9e50bd Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 19:46:08 +0200 Subject: [PATCH 36/73] upd --- packages/graphiql-react/src/utility/resize.ts | 49 ++++++++++--------- packages/graphiql-react/src/utility/tabs.ts | 4 +- .../graphiql-toolkit/src/storage/history.ts | 4 +- .../graphiql-toolkit/src/storage/query.ts | 18 ++----- 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/packages/graphiql-react/src/utility/resize.ts b/packages/graphiql-react/src/utility/resize.ts index 77c830a286..594dcb23d5 100644 --- a/packages/graphiql-react/src/utility/resize.ts +++ b/packages/graphiql-react/src/utility/resize.ts @@ -11,10 +11,12 @@ interface UseDragResizeArgs { * @default 1 */ defaultSizeRelation?: number; + /** * The direction in which the two halves should be resizable. */ direction: 'horizontal' | 'vertical'; + /** * Choose one of the two halves that should initially be hidden. */ @@ -33,12 +35,14 @@ interface UseDragResizeArgs { * @default 100 */ sizeThresholdFirst?: number; + /** * The minimum width in pixels for the second half. If it is resized to a * width smaller than this threshold, the half will be hidden. * @default 100 */ sizeThresholdSecond?: number; + /** * A key for which the state of resizing is persisted in storage (if storage * is available). @@ -58,16 +62,7 @@ export function useDragResize({ const storage = useGraphiQL(state => state.storage); const [hiddenElement, setHiddenElement] = useState( - () => { - const storedValue = storageKey && storage.getItem(storageKey); - if (storedValue === HIDE_FIRST || initiallyHidden === 'first') { - return 'first'; - } - if (storedValue === HIDE_SECOND || initiallyHidden === 'second') { - return 'second'; - } - return null; - }, + null, ); const firstRef = useRef(null); @@ -80,20 +75,30 @@ export function useDragResize({ * Set initial flex values */ useEffect(() => { - const storedValue = - (storageKey && storage.getItem(storageKey)) || defaultFlexRef.current; - - if (firstRef.current) { - firstRef.current.style.flex = - storedValue === HIDE_FIRST || storedValue === HIDE_SECOND - ? defaultFlexRef.current - : storedValue; - } + async function init() { + const $storedValue = storageKey && (await storage.getItem(storageKey)); + const initialHiddenElement = + $storedValue === HIDE_FIRST || initiallyHidden === 'first' + ? 'first' + : $storedValue === HIDE_SECOND || initiallyHidden === 'second' + ? 'second' + : null; + setHiddenElement(initialHiddenElement); - if (secondRef.current) { - secondRef.current.style.flex = '1'; + if (firstRef.current) { + const storedValue = $storedValue || defaultFlexRef.current; + firstRef.current.style.flex = + storedValue === HIDE_FIRST || storedValue === HIDE_SECOND + ? defaultFlexRef.current + : storedValue; + } + if (secondRef.current) { + secondRef.current.style.flex = '1'; + } } - }, [direction, storage, storageKey]); + + void init(); + }, [direction, storage, storageKey, initiallyHidden]); /** * Hide and show items when the state changes diff --git a/packages/graphiql-react/src/utility/tabs.ts b/packages/graphiql-react/src/utility/tabs.ts index f9c0e03896..aab94d7dfe 100644 --- a/packages/graphiql-react/src/utility/tabs.ts +++ b/packages/graphiql-react/src/utility/tabs.ts @@ -1,7 +1,7 @@ 'use no memo'; // can't figure why it isn't optimized +import type { StateStorage } from 'zustand/middleware'; import { STORAGE_KEY } from '../constants'; -import type { Storage } from '../stores/storage'; const storageStore = { getState: () => ({ storage: null }), @@ -86,7 +86,7 @@ export function getDefaultTabState({ ], shouldPersistHeaders, }: { - storage: Storage; + storage: StateStorage; defaultQuery: string; defaultHeaders?: string; headers: string | null; diff --git a/packages/graphiql-toolkit/src/storage/history.ts b/packages/graphiql-toolkit/src/storage/history.ts index 3bfcad8824..afbf80fc0d 100644 --- a/packages/graphiql-toolkit/src/storage/history.ts +++ b/packages/graphiql-toolkit/src/storage/history.ts @@ -1,5 +1,5 @@ import { parse } from 'graphql'; -import type { PersistStorage } from 'zustand/middleware'; +import type { StateStorage } from 'zustand/middleware'; import { QueryStore, QueryStoreItem } from './query'; const MAX_QUERY_SIZE = 100000; @@ -10,7 +10,7 @@ export class HistoryStore { favorite: QueryStore; constructor( - private storage: PersistStorage, + private storage: StateStorage, private maxHistoryLength: number, ) { this.history = new QueryStore( diff --git a/packages/graphiql-toolkit/src/storage/query.ts b/packages/graphiql-toolkit/src/storage/query.ts index 672fc440e6..09441b6a9c 100644 --- a/packages/graphiql-toolkit/src/storage/query.ts +++ b/packages/graphiql-toolkit/src/storage/query.ts @@ -1,4 +1,4 @@ -import type { PersistStorage } from 'zustand/middleware'; +import type { StateStorage } from 'zustand/middleware'; export type QueryStoreItem = { query?: string; @@ -14,9 +14,7 @@ export class QueryStore { constructor( private key: string, - private storage: PersistStorage<{ - [key: string]: QueryStoreItem[]; - }>, + private storage: StateStorage, private maxSize: number | null = null, ) { void this.fetchAll().then(items => { @@ -87,7 +85,7 @@ export class QueryStore { async fetchAll() { const raw = await this.storage.getItem(this.key); if (raw) { - return raw.state[this.key]; + return raw; } return []; } @@ -98,16 +96,10 @@ export class QueryStore { if (this.maxSize && items.length > this.maxSize) { items.shift(); } - this.storage.setItem(this.key, { - // @ts-expect-error -- fixme - state: items, - }); + this.storage.setItem(this.key, this.items); } save() { - this.storage.setItem(this.key, { - // @ts-expect-error -- fixme - state: this.items, - }); + this.storage.setItem(this.key, this.items); } } From 4d0c8506f9e2801d4d233e054433d4c26d20a008 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 20:23:37 +0200 Subject: [PATCH 37/73] upd --- packages/graphiql-react/src/components/provider.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 22cda75fca..6296f665f5 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -150,12 +150,15 @@ useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- false positive if (storeRef.current === null) { function getInitialState() { + if (storage === undefined) { + throw new TypeError('Unexpected `storage` prop is undefined.'); + } // We only need to compute it lazily during the initial render. - const query = props.initialQuery ?? storage!.getItem(STORAGE_KEY.query); + const query = props.initialQuery ?? storage.getItem(STORAGE_KEY.query); const variables = - props.initialVariables ?? storage!.getItem(STORAGE_KEY.variables); + props.initialVariables ?? storage.getItem(STORAGE_KEY.variables); const headers = - props.initialHeaders ?? storage!.getItem(STORAGE_KEY.headers); + props.initialHeaders ?? storage.getItem(STORAGE_KEY.headers); const { tabs, activeTabIndex } = getDefaultTabState({ storage, @@ -168,11 +171,11 @@ useEffect(() => { variables, }); - const isStored = storage!.getItem(STORAGE_KEY.persistHeaders) !== null; + const isStored = storage.getItem(STORAGE_KEY.persistHeaders) !== null; const $shouldPersistHeaders = shouldPersistHeaders !== false && isStored - ? storage!.getItem(STORAGE_KEY.persistHeaders) === 'true' + ? storage.getItem(STORAGE_KEY.persistHeaders) === 'true' : shouldPersistHeaders; const store = create()( From 25d80e09aa28583be0d2fbafffdfb8aaa2a39116 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 20:31:52 +0200 Subject: [PATCH 38/73] upd resize --- packages/graphiql-react/src/utility/resize.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/graphiql-react/src/utility/resize.ts b/packages/graphiql-react/src/utility/resize.ts index 594dcb23d5..482816fe36 100644 --- a/packages/graphiql-react/src/utility/resize.ts +++ b/packages/graphiql-react/src/utility/resize.ts @@ -60,22 +60,16 @@ export function useDragResize({ storageKey, }: UseDragResizeArgs) { const storage = useGraphiQL(state => state.storage); - const [hiddenElement, setHiddenElement] = useState( null, ); - const firstRef = useRef(null); const dragBarRef = useRef(null); const secondRef = useRef(null); - const defaultFlexRef = useRef(`${defaultSizeRelation}`); - /** - * Set initial flex values - */ useEffect(() => { - async function init() { + async function initFlexValues() { const $storedValue = storageKey && (await storage.getItem(storageKey)); const initialHiddenElement = $storedValue === HIDE_FIRST || initiallyHidden === 'first' @@ -97,8 +91,8 @@ export function useDragResize({ } } - void init(); - }, [direction, storage, storageKey, initiallyHidden]); + void initFlexValues(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps -- only on mount /** * Hide and show items when the state changes @@ -120,7 +114,7 @@ export function useDragResize({ } } - function show(element: HTMLDivElement) { + async function show(element: HTMLDivElement) { element.style.left = ''; element.style.position = ''; element.style.opacity = ''; @@ -128,7 +122,7 @@ export function useDragResize({ if (!storageKey) { return; } - const storedValue = storage.getItem(storageKey); + const storedValue = await storage.getItem(storageKey); if ( firstRef.current && storedValue !== HIDE_FIRST && @@ -144,7 +138,7 @@ export function useDragResize({ if (id === hiddenElement) { hide(element); } else { - show(element); + void show(element); } } } From 6166ef731463db0597626b8027a7fc20de3ac37b Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 20:40:25 +0200 Subject: [PATCH 39/73] upd provider --- .../src/components/provider.tsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 6296f665f5..09dd3e33ef 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -149,33 +149,36 @@ useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- false positive if (storeRef.current === null) { - function getInitialState() { + async function getInitialState() { if (storage === undefined) { throw new TypeError('Unexpected `storage` prop is undefined.'); } // We only need to compute it lazily during the initial render. - const query = props.initialQuery ?? storage.getItem(STORAGE_KEY.query); + const query = + props.initialQuery ?? (await storage.getItem(STORAGE_KEY.query)); const variables = - props.initialVariables ?? storage.getItem(STORAGE_KEY.variables); + props.initialVariables ?? + (await storage.getItem(STORAGE_KEY.variables)); const headers = - props.initialHeaders ?? storage.getItem(STORAGE_KEY.headers); + props.initialHeaders ?? (await storage.getItem(STORAGE_KEY.headers)); const { tabs, activeTabIndex } = getDefaultTabState({ - storage, defaultHeaders, defaultQuery, defaultTabs, headers, query, shouldPersistHeaders, + storage, variables, }); - const isStored = storage.getItem(STORAGE_KEY.persistHeaders) !== null; + const isStored = + (await storage.getItem(STORAGE_KEY.persistHeaders)) !== null; const $shouldPersistHeaders = shouldPersistHeaders !== false && isStored - ? storage.getItem(STORAGE_KEY.persistHeaders) === 'true' + ? (await storage.getItem(STORAGE_KEY.persistHeaders)) === 'true' : shouldPersistHeaders; const store = create()( @@ -253,6 +256,10 @@ useEffect(() => { visiblePlugin: state.visiblePlugin, }), storage, + // setItem: debounce(500, (...args) => { + // console.log('calling setItem') + // return storage.setItem(...args) + // }), }, ), ); From dec8b86f11e5d74b23ee0b518de78a6181a8be0e Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 20:44:27 +0200 Subject: [PATCH 40/73] upd provider --- packages/graphiql-react/src/components/provider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 09dd3e33ef..cce5070705 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -85,7 +85,6 @@ export const GraphiQLProvider: FC = ({ ...props }) => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime check if (!fetcher) { throw new TypeError( 'The `GraphiQLProvider` component requires a `fetcher` function to be passed as prop.', From a01ca5398f552da825d60c9ad5ca7eef887ebd68 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 20:46:05 +0200 Subject: [PATCH 41/73] upd provider --- packages/graphiql-react/src/components/provider.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index cce5070705..18683099bb 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -83,6 +83,10 @@ export const GraphiQLProvider: FC = ({ storage = createJSONStorage(() => localStorage), + initialQuery, + initialVariables, + initialHeaders, + ...props }) => { if (!fetcher) { @@ -153,13 +157,11 @@ useEffect(() => { throw new TypeError('Unexpected `storage` prop is undefined.'); } // We only need to compute it lazily during the initial render. - const query = - props.initialQuery ?? (await storage.getItem(STORAGE_KEY.query)); + const query = initialQuery ?? (await storage.getItem(STORAGE_KEY.query)); const variables = - props.initialVariables ?? - (await storage.getItem(STORAGE_KEY.variables)); + initialVariables ?? (await storage.getItem(STORAGE_KEY.variables)); const headers = - props.initialHeaders ?? (await storage.getItem(STORAGE_KEY.headers)); + initialHeaders ?? (await storage.getItem(STORAGE_KEY.headers)); const { tabs, activeTabIndex } = getDefaultTabState({ defaultHeaders, From fd7114cdc226b41988ad3c5626712bfd2d4e0941 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 20:51:27 +0200 Subject: [PATCH 42/73] upd toolkit/query.ts --- packages/graphiql-toolkit/src/storage/query.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/graphiql-toolkit/src/storage/query.ts b/packages/graphiql-toolkit/src/storage/query.ts index 09441b6a9c..1c3cac8fdd 100644 --- a/packages/graphiql-toolkit/src/storage/query.ts +++ b/packages/graphiql-toolkit/src/storage/query.ts @@ -85,7 +85,8 @@ export class QueryStore { async fetchAll() { const raw = await this.storage.getItem(this.key); if (raw) { - return raw; + // @ts-expect-error -- fixme: test what type is `raw` + return raw as QueryStoreItem[]; } return []; } @@ -96,10 +97,12 @@ export class QueryStore { if (this.maxSize && items.length > this.maxSize) { items.shift(); } + // @ts-expect-error -- fixme this.storage.setItem(this.key, this.items); } save() { + // @ts-expect-error -- fixme this.storage.setItem(this.key, this.items); } } From 928b463c06f014e8d3809cd5ec27d6e5cfc2d946 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 21:06:40 +0200 Subject: [PATCH 43/73] upd --- .../src/components/provider.tsx | 15 ++++++------ packages/graphiql/src/GraphiQL.tsx | 23 +++++++++++-------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 18683099bb..301c2c7de7 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -152,16 +152,16 @@ useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- false positive if (storeRef.current === null) { - async function getInitialState() { + // TODO: make `async` and `await storage.getItem`... + function getInitialState() { if (storage === undefined) { throw new TypeError('Unexpected `storage` prop is undefined.'); } // We only need to compute it lazily during the initial render. - const query = initialQuery ?? (await storage.getItem(STORAGE_KEY.query)); + const query = initialQuery ?? storage.getItem(STORAGE_KEY.query); const variables = - initialVariables ?? (await storage.getItem(STORAGE_KEY.variables)); - const headers = - initialHeaders ?? (await storage.getItem(STORAGE_KEY.headers)); + initialVariables ?? storage.getItem(STORAGE_KEY.variables); + const headers = initialHeaders ?? storage.getItem(STORAGE_KEY.headers); const { tabs, activeTabIndex } = getDefaultTabState({ defaultHeaders, @@ -174,12 +174,11 @@ useEffect(() => { variables, }); - const isStored = - (await storage.getItem(STORAGE_KEY.persistHeaders)) !== null; + const isStored = storage.getItem(STORAGE_KEY.persistHeaders) !== null; const $shouldPersistHeaders = shouldPersistHeaders !== false && isStored - ? (await storage.getItem(STORAGE_KEY.persistHeaders)) === 'true' + ? storage.getItem(STORAGE_KEY.persistHeaders) === 'true' : shouldPersistHeaders; const store = create()( diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index d8638e087f..e1ade738dc 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -244,18 +244,23 @@ const GraphiQLInterface: FC = ({ direction: 'horizontal', storageKey: 'editorFlex', }); + const [initiallyHiddenEditorTools] = useState<'second' | undefined>(() => { + if ( + defaultEditorToolsVisibility === 'variables' || + defaultEditorToolsVisibility === 'headers' + ) { + return; + } + if (typeof defaultEditorToolsVisibility === 'boolean') { + return defaultEditorToolsVisibility ? undefined : 'second'; + } + return initialVariables || initialHeaders ? undefined : 'second'; + }); + const editorToolsResize = useDragResize({ defaultSizeRelation: 3, direction: 'vertical', - initiallyHidden: ((d: typeof defaultEditorToolsVisibility) => { - if (d === 'variables' || d === 'headers') { - return; - } - if (typeof d === 'boolean') { - return d ? undefined : 'second'; - } - return initialVariables || initialHeaders ? undefined : 'second'; - })(defaultEditorToolsVisibility), + initiallyHidden: initiallyHiddenEditorTools, sizeThresholdSecond: 60, storageKey: 'secondaryEditorFlex', }); From 9306b8a81fa75223bf97dde33a946dea3d1fa698 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 22 Jun 2025 22:01:15 +0200 Subject: [PATCH 44/73] upd [skip ci] --- packages/graphiql-react/src/components/provider.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 301c2c7de7..e0ca23bd3a 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -251,10 +251,13 @@ useEffect(() => { console.info('Hydration with storage finished'); }; }, - partialize: state => ({ - theme: state.theme, - visiblePlugin: state.visiblePlugin, - }), + partialize(state) { + console.log('partialize', state) + return { + theme: state.theme, + visiblePlugin: state.visiblePlugin, + }; + }, storage, // setItem: debounce(500, (...args) => { // console.log('calling setItem') From 607f9496ddb8d21b192ea8a9853e9fe510bad5bc Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 01:14:15 +0200 Subject: [PATCH 45/73] upd [skip ci] --- packages/graphiql-react/src/components/response-editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphiql-react/src/components/response-editor.tsx b/packages/graphiql-react/src/components/response-editor.tsx index 8188d4efda..38cca27eb8 100644 --- a/packages/graphiql-react/src/components/response-editor.tsx +++ b/packages/graphiql-react/src/components/response-editor.tsx @@ -45,7 +45,7 @@ export const ResponseEditor: FC = ({ const ref = useRef(null!); useEffect(() => { if (fetchError) { - responseEditor?.setValue(fetchError); + responseEditor?.setValue(formatError({ message: fetchError })); } if (validationErrors.length) { responseEditor?.setValue(formatError(validationErrors)); From bab8f93d073a99a7f21fe7c6200cded0592911c4 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 01:20:49 +0200 Subject: [PATCH 46/73] get default tabs state --- .../src/components/provider.tsx | 4 +- packages/graphiql-react/src/utility/tabs.ts | 118 +++++++++--------- 2 files changed, 59 insertions(+), 63 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index e0ca23bd3a..a11599a877 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -169,8 +169,6 @@ useEffect(() => { defaultTabs, headers, query, - shouldPersistHeaders, - storage, variables, }); @@ -254,6 +252,8 @@ useEffect(() => { partialize(state) { console.log('partialize', state) return { + activeTabIndex: state.activeTabIndex, + tabs: state.tabs, theme: state.theme, visiblePlugin: state.visiblePlugin, }; diff --git a/packages/graphiql-react/src/utility/tabs.ts b/packages/graphiql-react/src/utility/tabs.ts index aab94d7dfe..ef27e46301 100644 --- a/packages/graphiql-react/src/utility/tabs.ts +++ b/packages/graphiql-react/src/utility/tabs.ts @@ -71,7 +71,6 @@ export type TabsState = { }; export function getDefaultTabState({ - storage, defaultQuery, defaultHeaders, headers, @@ -84,72 +83,69 @@ export function getDefaultTabState({ headers: headers ?? defaultHeaders, }, ], - shouldPersistHeaders, }: { - storage: StateStorage; - defaultQuery: string; defaultHeaders?: string; - headers: string | null; + defaultQuery: string; defaultTabs?: TabDefinition[]; + headers: string | null; query: string | null; variables: string | null; - shouldPersistHeaders?: boolean; }) { - const storedState = storage.getItem(STORAGE_KEY.tabs); - try { - if (!storedState) { - throw new Error('Storage for tabs is empty'); - } - const parsed = JSON.parse(storedState); - // if headers are not persisted, do not derive the hash using default headers state - // or else you will get new tabs on every refresh - const headersForHash = shouldPersistHeaders ? headers : undefined; - if (isTabsState(parsed)) { - const expectedHash = hashFromTabContents({ - query, - variables, - headers: headersForHash, - }); - let matchingTabIndex = -1; - - for (let index = 0; index < parsed.tabs.length; index++) { - const tab = parsed.tabs[index]!; - tab.hash = hashFromTabContents({ - query: tab.query, - variables: tab.variables, - headers: tab.headers, - }); - if (tab.hash === expectedHash) { - matchingTabIndex = index; - } - } - - if (matchingTabIndex >= 0) { - parsed.activeTabIndex = matchingTabIndex; - } else { - const operationName = query ? fuzzyExtractOperationName(query) : null; - parsed.tabs.push({ - id: guid(), - hash: expectedHash, - title: operationName || DEFAULT_TITLE, - query, - variables, - headers, - operationName, - response: null, - }); - parsed.activeTabIndex = parsed.tabs.length - 1; - } - - return parsed; - } - throw new Error('Storage for tabs is invalid'); - } catch { - return { - activeTabIndex: 0, - tabs: defaultTabs.map(createTab), - }; - } + // const storedState = storage.getItem(STORAGE_KEY.tabs); + // try { + // if (!storedState) { + // throw new Error('Storage for tabs is empty'); + // } + // const parsed = JSON.parse(storedState); + // // if headers are not persisted, do not derive the hash using default headers state + // // or else you will get new tabs on every refresh + // const headersForHash = shouldPersistHeaders ? headers : undefined; + // if (isTabsState(parsed)) { + // const expectedHash = hashFromTabContents({ + // query, + // variables, + // headers: headersForHash, + // }); + // let matchingTabIndex = -1; + // + // for (let index = 0; index < parsed.tabs.length; index++) { + // const tab = parsed.tabs[index]!; + // tab.hash = hashFromTabContents({ + // query: tab.query, + // variables: tab.variables, + // headers: tab.headers, + // }); + // if (tab.hash === expectedHash) { + // matchingTabIndex = index; + // } + // } + // + // if (matchingTabIndex >= 0) { + // parsed.activeTabIndex = matchingTabIndex; + // } else { + // const operationName = query ? fuzzyExtractOperationName(query) : null; + // parsed.tabs.push({ + // id: guid(), + // hash: expectedHash, + // title: operationName || DEFAULT_TITLE, + // query, + // variables, + // headers, + // operationName, + // response: null, + // }); + // parsed.activeTabIndex = parsed.tabs.length - 1; + // } + // + // return parsed; + // } + // throw new Error('Storage for tabs is invalid'); + // } catch { + return { + activeTabIndex: 0, + tabs: defaultTabs.map(createTab), + }; + // } } function isTabsState(obj: any): obj is TabsState { From fa298dd94af86e7239057131b96279b67bfd6ddc Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 01:23:30 +0200 Subject: [PATCH 47/73] tabs cleanup --- .../src/components/provider.tsx | 3 +- packages/graphiql-react/src/utility/tabs.ts | 47 +------------------ 2 files changed, 3 insertions(+), 47 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index a11599a877..d4181eed99 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -250,7 +250,8 @@ useEffect(() => { }; }, partialize(state) { - console.log('partialize', state) + // eslint-disable-next-line no-console + console.log('partialize', state); return { activeTabIndex: state.activeTabIndex, tabs: state.tabs, diff --git a/packages/graphiql-react/src/utility/tabs.ts b/packages/graphiql-react/src/utility/tabs.ts index ef27e46301..ade63149e4 100644 --- a/packages/graphiql-react/src/utility/tabs.ts +++ b/packages/graphiql-react/src/utility/tabs.ts @@ -3,10 +3,6 @@ import type { StateStorage } from 'zustand/middleware'; import { STORAGE_KEY } from '../constants'; -const storageStore = { - getState: () => ({ storage: null }), -}; - export interface TabDefinition { /** * The contents of the query editor of this tab. @@ -148,46 +144,6 @@ export function getDefaultTabState({ // } } -function isTabsState(obj: any): obj is TabsState { - return ( - obj && - typeof obj === 'object' && - !Array.isArray(obj) && - hasNumberKey(obj, 'activeTabIndex') && - 'tabs' in obj && - Array.isArray(obj.tabs) && - obj.tabs.every(isTabState) - ); -} - -function isTabState(obj: any): obj is TabState { - // We don't persist the hash, so we skip the check here - return ( - obj && - typeof obj === 'object' && - !Array.isArray(obj) && - hasStringKey(obj, 'id') && - hasStringKey(obj, 'title') && - hasStringOrNullKey(obj, 'query') && - hasStringOrNullKey(obj, 'variables') && - hasStringOrNullKey(obj, 'headers') && - hasStringOrNullKey(obj, 'operationName') && - hasStringOrNullKey(obj, 'response') - ); -} - -function hasNumberKey(obj: Record, key: string) { - return key in obj && typeof obj[key] === 'number'; -} - -function hasStringKey(obj: Record, key: string) { - return key in obj && typeof obj[key] === 'string'; -} - -function hasStringOrNullKey(obj: Record, key: string) { - return key in obj && (typeof obj[key] === 'string' || obj[key] === null); -} - export function serializeTabState( tabState: TabsState, shouldPersistHeaders = false, @@ -270,8 +226,7 @@ export function fuzzyExtractOperationName(str: string): string | null { return match?.[2] ?? null; } -export function clearHeadersFromTabs() { - const { storage } = storageStore.getState(); +export function clearHeadersFromTabs(storage: StateStorage) { const persistedTabs = storage.getItem(STORAGE_KEY.tabs); if (persistedTabs) { const parsedTabs = JSON.parse(persistedTabs); From 3b24b9675379cf1f0b12328114a654d802eea7ab Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 01:24:21 +0200 Subject: [PATCH 48/73] upd --- packages/graphiql-react/src/components/provider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index d4181eed99..c3f52e8e2b 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -89,6 +89,7 @@ export const GraphiQLProvider: FC = ({ ...props }) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime check if (!fetcher) { throw new TypeError( 'The `GraphiQLProvider` component requires a `fetcher` function to be passed as prop.', From 3252b1324700e0d80e5e1bfa1c8b86eda1403083 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 01:30:22 +0200 Subject: [PATCH 49/73] remove storeTabs --- .../src/components/provider.tsx | 5 ---- packages/graphiql-react/src/stores/editor.ts | 30 ++++--------------- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index c3f52e8e2b..6ed5899f13 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -261,15 +261,10 @@ useEffect(() => { }; }, storage, - // setItem: debounce(500, (...args) => { - // console.log('calling setItem') - // return storage.setItem(...args) - // }), }, ), ); const { actions } = store.getState(); - actions.storeTabs({ activeTabIndex, tabs }); actions.setPlugins(plugins); return store; diff --git a/packages/graphiql-react/src/stores/editor.ts b/packages/graphiql-react/src/stores/editor.ts index 43b2b3460e..46eb33180f 100644 --- a/packages/graphiql-react/src/stores/editor.ts +++ b/packages/graphiql-react/src/stores/editor.ts @@ -17,7 +17,7 @@ import { serializeTabState, } from '../utility/tabs'; import type { SlicesWithActions, MonacoEditor } from '../types'; -import { debounce, formatJSONC } from '../utility'; +import { formatJSONC } from '../utility'; import { STORAGE_KEY } from '../constants'; export interface EditorSlice extends TabsState { @@ -198,8 +198,6 @@ export interface EditorActions { */ setShouldPersistHeaders(persist: boolean): void; - storeTabs(tabsState: TabsState): void; - setOperationFacts(facts: { documentAST?: DocumentNode; operationName?: string; @@ -337,7 +335,7 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { const $actions: EditorActions = { addTab() { - set(({ defaultHeaders, onTabChange, tabs, activeTabIndex, actions }) => { + set(({ defaultHeaders, onTabChange, tabs, activeTabIndex }) => { // Make sure the current tab stores the latest values const updatedValues = synchronizeActiveTabValues({ tabs, @@ -347,7 +345,6 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { tabs: [...updatedValues.tabs, createTab({ headers: defaultHeaders })], activeTabIndex: updatedValues.tabs.length, }; - actions.storeTabs(updated); setEditorValues(updated.tabs[updated.activeTabIndex]!); onTabChange?.(updated); return updated; @@ -356,24 +353,19 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { changeTab(index) { set(({ actions, onTabChange, tabs }) => { actions.stop(); - const updated = { - tabs, - activeTabIndex: index, - }; - actions.storeTabs(updated); + const updated = { tabs, activeTabIndex: index }; setEditorValues(updated.tabs[updated.activeTabIndex]!); onTabChange?.(updated); return updated; }); }, moveTab(newOrder) { - set(({ onTabChange, actions, tabs, activeTabIndex }) => { + set(({ onTabChange, tabs, activeTabIndex }) => { const activeTab = tabs[activeTabIndex]!; const updated = { tabs: newOrder, activeTabIndex: newOrder.indexOf(activeTab), }; - actions.storeTabs(updated); setEditorValues(updated.tabs[updated.activeTabIndex]!); onTabChange?.(updated); return updated; @@ -388,19 +380,17 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { tabs: tabs.filter((_tab, i) => index !== i), activeTabIndex: Math.max(activeTabIndex - 1, 0), }; - actions.storeTabs(updated); setEditorValues(updated.tabs[updated.activeTabIndex]!); onTabChange?.(updated); return updated; }); }, updateActiveTabValues(partialTab) { - set(({ activeTabIndex, tabs, onTabChange, actions }) => { + set(({ activeTabIndex, tabs, onTabChange }) => { const updated = setPropertiesInActiveTab( { tabs, activeTabIndex }, partialTab, ); - actions.storeTabs(updated); onTabChange?.(updated); return updated; }); @@ -433,19 +423,11 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { storage.setItem(STORAGE_KEY.tabs, serializedTabs); } else { storage.setItem(STORAGE_KEY.headers, ''); - clearHeadersFromTabs(); + clearHeadersFromTabs(storage); } storage.setItem(STORAGE_KEY.persistHeaders, persist.toString()); set({ shouldPersistHeaders: persist }); }, - storeTabs({ tabs, activeTabIndex }) { - const { storage } = get(); - const { shouldPersistHeaders } = get(); - const store = debounce(500, (value: string) => { - storage.setItem(STORAGE_KEY.tabs, value); - }); - store(serializeTabState({ tabs, activeTabIndex }, shouldPersistHeaders)); - }, setOperationFacts({ documentAST, operationName, operations }) { set({ documentAST, From ed5567e1ba3341a374408e0c41ac26cdeacba45f Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 01:58:10 +0200 Subject: [PATCH 50/73] upd --- packages/graphiql-react/src/stores/editor.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/graphiql-react/src/stores/editor.ts b/packages/graphiql-react/src/stores/editor.ts index 46eb33180f..373f81817c 100644 --- a/packages/graphiql-react/src/stores/editor.ts +++ b/packages/graphiql-react/src/stores/editor.ts @@ -429,11 +429,7 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { set({ shouldPersistHeaders: persist }); }, setOperationFacts({ documentAST, operationName, operations }) { - set({ - documentAST, - operationName, - operations, - }); + set({ documentAST, operationName, operations }); }, async copyQuery() { const { queryEditor, onCopyQuery } = get(); From 12ab3d5dee8e7adb5e61a5cff261803da8c3c2c8 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 01:58:56 +0200 Subject: [PATCH 51/73] upd --- packages/graphiql-react/src/components/provider.tsx | 7 +++---- packages/graphiql-react/src/utility/tabs.ts | 12 ++++-------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 6ed5899f13..b216f3eb3c 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -159,10 +159,9 @@ useEffect(() => { throw new TypeError('Unexpected `storage` prop is undefined.'); } // We only need to compute it lazily during the initial render. - const query = initialQuery ?? storage.getItem(STORAGE_KEY.query); - const variables = - initialVariables ?? storage.getItem(STORAGE_KEY.variables); - const headers = initialHeaders ?? storage.getItem(STORAGE_KEY.headers); + const query = initialQuery; + const variables = initialVariables; + const headers = initialHeaders; const { tabs, activeTabIndex } = getDefaultTabState({ defaultHeaders, diff --git a/packages/graphiql-react/src/utility/tabs.ts b/packages/graphiql-react/src/utility/tabs.ts index ade63149e4..8a42d9cfa2 100644 --- a/packages/graphiql-react/src/utility/tabs.ts +++ b/packages/graphiql-react/src/utility/tabs.ts @@ -83,16 +83,12 @@ export function getDefaultTabState({ defaultHeaders?: string; defaultQuery: string; defaultTabs?: TabDefinition[]; - headers: string | null; - query: string | null; - variables: string | null; + headers?: string; + query?: string; + variables?: string; }) { - // const storedState = storage.getItem(STORAGE_KEY.tabs); // try { - // if (!storedState) { - // throw new Error('Storage for tabs is empty'); - // } - // const parsed = JSON.parse(storedState); + // const parsed = storedState; // // if headers are not persisted, do not derive the hash using default headers state // // or else you will get new tabs on every refresh // const headersForHash = shouldPersistHeaders ? headers : undefined; From b69bfc53fc8b1bd040c93985efca7a8a027fbee7 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 02:14:53 +0200 Subject: [PATCH 52/73] upd --- packages/graphiql-react/src/components/provider.tsx | 10 ++++++++-- .../graphiql-react/src/components/response-editor.tsx | 1 + packages/graphiql-react/src/utility/tabs.ts | 3 +++ packages/graphiql/src/GraphiQL.tsx | 3 +++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index b216f3eb3c..c6a381b089 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -251,10 +251,16 @@ useEffect(() => { }, partialize(state) { // eslint-disable-next-line no-console - console.log('partialize', state); + console.log( + 'partialize', + state.shouldPersistHeaders, + state.tabs.map(t => t.headers), + ); return { activeTabIndex: state.activeTabIndex, - tabs: state.tabs, + tabs: state.shouldPersistHeaders + ? state.tabs + : state.tabs.map(tab => ({ ...tab, headers: null })), theme: state.theme, visiblePlugin: state.visiblePlugin, }; diff --git a/packages/graphiql-react/src/components/response-editor.tsx b/packages/graphiql-react/src/components/response-editor.tsx index 38cca27eb8..93261e55a2 100644 --- a/packages/graphiql-react/src/components/response-editor.tsx +++ b/packages/graphiql-react/src/components/response-editor.tsx @@ -21,6 +21,7 @@ type ResponseTooltipType = ComponentType<{ * A position in the editor. */ position: Position; + /** * Word that has been hovered over. */ diff --git a/packages/graphiql-react/src/utility/tabs.ts b/packages/graphiql-react/src/utility/tabs.ts index 8a42d9cfa2..fb69dc27c6 100644 --- a/packages/graphiql-react/src/utility/tabs.ts +++ b/packages/graphiql-react/src/utility/tabs.ts @@ -8,10 +8,12 @@ export interface TabDefinition { * The contents of the query editor of this tab. */ query: string | null; + /** * The contents of the variable editor of this tab. */ variables?: string | null; + /** * The contents of the headers editor of this tab. */ @@ -59,6 +61,7 @@ export type TabsState = { * A list of state objects for each tab. */ tabs: TabState[]; + /** * The index of the currently active tab with regards to the `tabs` list of * this object. diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index e1ade738dc..f1bed4f377 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -151,6 +151,7 @@ interface GraphiQLInterfaceProps 'forcedTheme' | 'showPersistHeadersSettings' > { children?: ReactNode; + /** * Set the default state for the editor tools. * - `false` hides the editor tools @@ -161,11 +162,13 @@ interface GraphiQLInterfaceProps * editors has contents. */ defaultEditorToolsVisibility?: boolean | 'variables' | 'headers'; + /** * Toggle if the headers' editor should be shown inside the editor tools. * @default true */ isHeadersEditorEnabled?: boolean; + /** * Additional class names which will be appended to the container element. */ From 7ad875f50585e0bbad3ad003dad5829fb3540b6f Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 02:20:35 +0200 Subject: [PATCH 53/73] remove `clearHeadersFromTabs` and `serializeTabState` --- packages/graphiql-react/src/stores/editor.ts | 20 ++----------- packages/graphiql-react/src/utility/hooks.ts | 16 +--------- .../graphiql-react/src/utility/tabs.spec.ts | 27 ----------------- packages/graphiql-react/src/utility/tabs.ts | 29 ------------------- 4 files changed, 3 insertions(+), 89 deletions(-) diff --git a/packages/graphiql-react/src/stores/editor.ts b/packages/graphiql-react/src/stores/editor.ts index 373f81817c..a3848a068e 100644 --- a/packages/graphiql-react/src/stores/editor.ts +++ b/packages/graphiql-react/src/stores/editor.ts @@ -13,12 +13,9 @@ import { TabDefinition, TabsState, TabState, - clearHeadersFromTabs, - serializeTabState, } from '../utility/tabs'; import type { SlicesWithActions, MonacoEditor } from '../types'; import { formatJSONC } from '../utility'; -import { STORAGE_KEY } from '../constants'; export interface EditorSlice extends TabsState { /** @@ -412,21 +409,8 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { return { operationName }; }); }, - setShouldPersistHeaders(persist) { - const { headerEditor, tabs, activeTabIndex, storage } = get(); - if (persist) { - storage.setItem(STORAGE_KEY.headers, headerEditor?.getValue() ?? ''); - const serializedTabs = serializeTabState( - { tabs, activeTabIndex }, - true, - ); - storage.setItem(STORAGE_KEY.tabs, serializedTabs); - } else { - storage.setItem(STORAGE_KEY.headers, ''); - clearHeadersFromTabs(storage); - } - storage.setItem(STORAGE_KEY.persistHeaders, persist.toString()); - set({ shouldPersistHeaders: persist }); + setShouldPersistHeaders(shouldPersistHeaders) { + set({ shouldPersistHeaders }); }, setOperationFacts({ documentAST, operationName, operations }) { set({ documentAST, operationName, operations }); diff --git a/packages/graphiql-react/src/utility/hooks.ts b/packages/graphiql-react/src/utility/hooks.ts index dfaab0d486..9138a39811 100644 --- a/packages/graphiql-react/src/utility/hooks.ts +++ b/packages/graphiql-react/src/utility/hooks.ts @@ -4,13 +4,8 @@ import { debounce } from './debounce'; import type { editor as monacoEditor } from '../monaco-editor'; import { useGraphiQL, useGraphiQLActions } from '../components'; -const storageStore = { - getState: () => ({ storage: null }), -}; - export function useChangeHandler( callback: ((value: string) => void) | undefined, - storageKey: string | null, tabProperty: 'variables' | 'headers', ) { const { updateActiveTabValues } = useGraphiQLActions(); @@ -22,21 +17,12 @@ export function useChangeHandler( if (!editor) { return; } - const { storage } = storageStore.getState(); - - const store = debounce(500, (value: string) => { - if (storageKey === null) { - return; - } - storage.setItem(storageKey, value); - }); const updateTab = debounce(100, (value: string) => { updateActiveTabValues({ [tabProperty]: value }); }); const handleChange = (_event: monacoEditor.IModelContentChangedEvent) => { const newValue = editor.getValue(); - store(newValue); updateTab(newValue); callback?.(newValue); }; @@ -44,7 +30,7 @@ export function useChangeHandler( return () => { disposable.dispose(); }; - }, [callback, editor, storageKey, tabProperty, updateActiveTabValues]); + }, [callback, editor, tabProperty, updateActiveTabValues]); } // https://react.dev/learn/you-might-not-need-an-effect diff --git a/packages/graphiql-react/src/utility/tabs.spec.ts b/packages/graphiql-react/src/utility/tabs.spec.ts index 74836fb4e6..2ea2bdc5a3 100644 --- a/packages/graphiql-react/src/utility/tabs.spec.ts +++ b/packages/graphiql-react/src/utility/tabs.spec.ts @@ -2,13 +2,7 @@ import { createTab, fuzzyExtractOperationName, getDefaultTabState, - clearHeadersFromTabs, } from './tabs'; -import { STORAGE_KEY } from '../constants'; - -const storageStore = { - getState: () => ({ storage: null }), -}; describe('createTab', () => { it('creates with default title', () => { @@ -162,24 +156,3 @@ describe('getDefaultTabState', () => { }); }); }); - -describe('clearHeadersFromTabs', () => { - it('preserves tab state except for headers', () => { - const { storage } = storageStore.getState(); - const stateWithHeaders = { - operationName: 'test', - query: 'query test {\n test {\n id\n }\n}', - test: { - a: 'test', - }, - headers: '{ "authorization": "secret" }', - }; - storage.setItem(STORAGE_KEY.tabs, JSON.stringify(stateWithHeaders)); - clearHeadersFromTabs(); - - expect(JSON.parse(storage.getItem(STORAGE_KEY.tabs)!)).toEqual({ - ...stateWithHeaders, - headers: null, - }); - }); -}); diff --git a/packages/graphiql-react/src/utility/tabs.ts b/packages/graphiql-react/src/utility/tabs.ts index fb69dc27c6..a205a090d0 100644 --- a/packages/graphiql-react/src/utility/tabs.ts +++ b/packages/graphiql-react/src/utility/tabs.ts @@ -1,8 +1,5 @@ 'use no memo'; // can't figure why it isn't optimized -import type { StateStorage } from 'zustand/middleware'; -import { STORAGE_KEY } from '../constants'; - export interface TabDefinition { /** * The contents of the query editor of this tab. @@ -143,19 +140,6 @@ export function getDefaultTabState({ // } } -export function serializeTabState( - tabState: TabsState, - shouldPersistHeaders = false, -) { - return JSON.stringify(tabState, (key, value) => - key === 'hash' || - key === 'response' || - (!shouldPersistHeaders && key === 'headers') - ? null - : value, - ); -} - export function createTab({ query = null, variables = null, @@ -225,17 +209,4 @@ export function fuzzyExtractOperationName(str: string): string | null { return match?.[2] ?? null; } -export function clearHeadersFromTabs(storage: StateStorage) { - const persistedTabs = storage.getItem(STORAGE_KEY.tabs); - if (persistedTabs) { - const parsedTabs = JSON.parse(persistedTabs); - storage.setItem( - STORAGE_KEY.tabs, - JSON.stringify(parsedTabs, (key, value) => - key === 'headers' ? null : value, - ), - ); - } -} - const DEFAULT_TITLE = ''; From 9503865417c63f36c08f1ba517df6e124888339e Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 02:35:33 +0200 Subject: [PATCH 54/73] save showPersistHeadersSettings in storage --- packages/graphiql-react/src/components/provider.tsx | 1 + packages/graphiql/src/GraphiQL.tsx | 9 +++++---- packages/graphiql/src/ui/sidebar.tsx | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index c6a381b089..74401fef84 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -258,6 +258,7 @@ useEffect(() => { ); return { activeTabIndex: state.activeTabIndex, + shouldPersistHeaders: state.shouldPersistHeaders, tabs: state.shouldPersistHeaders ? state.tabs : state.tabs.map(tab => ({ ...tab, headers: null })), diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index f1bed4f377..4ad0410898 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -67,11 +67,13 @@ const GraphiQL_: FC = ({ responseTooltip, defaultEditorToolsVisibility, isHeadersEditorEnabled, - showPersistHeadersSettings, forcedTheme, confirmCloseTab, className, + shouldPersistHeaders, + showPersistHeadersSettings = Boolean(shouldPersistHeaders), + children, ...props }) => { @@ -98,9 +100,7 @@ const GraphiQL_: FC = ({ throw new TypeError('The `readOnly` prop has been removed.'); } const interfaceProps: GraphiQLInterfaceProps = { - // TODO check if `showPersistHeadersSettings` prop is needed, or we can just use `shouldPersistHeaders` instead - showPersistHeadersSettings: - showPersistHeadersSettings ?? props.shouldPersistHeaders !== false, + showPersistHeadersSettings, onEditQuery, onEditVariables, onEditHeaders, @@ -120,6 +120,7 @@ const GraphiQL_: FC = ({ diff --git a/packages/graphiql/src/ui/sidebar.tsx b/packages/graphiql/src/ui/sidebar.tsx index 85ff1a6935..93cf925e0e 100644 --- a/packages/graphiql/src/ui/sidebar.tsx +++ b/packages/graphiql/src/ui/sidebar.tsx @@ -232,7 +232,7 @@ export const Sidebar: FC = ({
- {showPersistHeadersSettings ? ( + {showPersistHeadersSettings && (
@@ -265,7 +265,7 @@ export const Sidebar: FC = ({
- ) : null} + )} {!forcedTheme && (
From ba0eda9cdbeffe38f69a42738bac59b0da8a769e Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 02:38:13 +0200 Subject: [PATCH 55/73] rm --- packages/graphiql-react/src/components/provider.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 74401fef84..8ea4a18683 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -278,15 +278,6 @@ useEffect(() => { storeRef.current = getInitialState(); } - // TODO: - // const lastShouldPersistHeadersProp = useRef(undefined); - // useEffect(() => { - // const propValue = shouldPersistHeaders; - // if (lastShouldPersistHeadersProp.current !== propValue) { - // editorStore.getState().setShouldPersistHeaders(propValue); - // lastShouldPersistHeadersProp.current = propValue; - // } - // }, [shouldPersistHeaders]); // Execution sync useDidUpdate(() => { From fdeb156d533e7691a0cac2ae4b40257f7660fc5e Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 02:42:39 +0200 Subject: [PATCH 56/73] fix clear storage --- packages/graphiql/src/ui/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphiql/src/ui/sidebar.tsx b/packages/graphiql/src/ui/sidebar.tsx index 93cf925e0e..dc9d20fdb5 100644 --- a/packages/graphiql/src/ui/sidebar.tsx +++ b/packages/graphiql/src/ui/sidebar.tsx @@ -103,7 +103,7 @@ export const Sidebar: FC = ({ function handleClearData() { try { - storage.clear(); + storage.removeItem('graphiql:theme'); setClearStorageStatus('success'); } catch { setClearStorageStatus('error'); From 510ce6d87ced9777971f1ecce4d931a1f4474963 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 02:57:29 +0200 Subject: [PATCH 57/73] onChangeEditor --- .../src/components/variable-editor.tsx | 14 ++++-- packages/graphiql-react/src/utility/hooks.ts | 50 +++++++++---------- packages/graphiql-react/src/utility/index.ts | 2 +- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/packages/graphiql-react/src/components/variable-editor.tsx b/packages/graphiql-react/src/components/variable-editor.tsx index 11df4cc104..60d65816d6 100644 --- a/packages/graphiql-react/src/components/variable-editor.tsx +++ b/packages/graphiql-react/src/components/variable-editor.tsx @@ -1,11 +1,11 @@ import { FC, useEffect, useRef } from 'react'; import { useGraphiQL, useGraphiQLActions } from './provider'; import type { EditorProps } from '../types'; -import { KEY_BINDINGS, STORAGE_KEY, VARIABLE_URI } from '../constants'; +import { KEY_BINDINGS, VARIABLE_URI } from '../constants'; import { getOrCreateModel, createEditor, - useChangeHandler, + onChangeEditor, onEditorContainerKeyDown, cleanupDisposables, cn, @@ -23,10 +23,10 @@ export const VariableEditor: FC = ({ onEdit, ...props }) => { - const { setEditor, run, prettifyEditors, mergeQuery } = useGraphiQLActions(); + const { setEditor, run, prettifyEditors, mergeQuery, updateActiveTabValues } = + useGraphiQLActions(); const initialVariables = useGraphiQL(state => state.initialVariables); const ref = useRef(null!); - useChangeHandler(onEdit, STORAGE_KEY.variables, 'variables'); useEffect(() => { const model = getOrCreateModel({ uri: VARIABLE_URI, @@ -35,6 +35,12 @@ export const VariableEditor: FC = ({ const editor = createEditor(ref, { model }); setEditor({ variableEditor: editor }); const disposables = [ + onChangeEditor({ + onEdit, + tabProperty: 'variables', + updateActiveTabValues, + model, + }), editor.addAction({ ...KEY_BINDINGS.runQuery, run }), editor.addAction({ ...KEY_BINDINGS.prettify, run: prettifyEditors }), editor.addAction({ ...KEY_BINDINGS.mergeFragments, run: mergeQuery }), diff --git a/packages/graphiql-react/src/utility/hooks.ts b/packages/graphiql-react/src/utility/hooks.ts index 9138a39811..2e94b7e5dd 100644 --- a/packages/graphiql-react/src/utility/hooks.ts +++ b/packages/graphiql-react/src/utility/hooks.ts @@ -1,36 +1,32 @@ // eslint-disable-next-line @typescript-eslint/no-restricted-imports -- TODO: check why query builder update only 1st field https://github.com/graphql/graphiql/issues/3836 import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { debounce } from './debounce'; -import type { editor as monacoEditor } from '../monaco-editor'; +import type { editor as monacoEditor, IDisposable } from '../monaco-editor'; import { useGraphiQL, useGraphiQLActions } from '../components'; -export function useChangeHandler( - callback: ((value: string) => void) | undefined, - tabProperty: 'variables' | 'headers', -) { - const { updateActiveTabValues } = useGraphiQLActions(); - const editor = useGraphiQL( - state => - state[tabProperty === 'variables' ? 'variableEditor' : 'headerEditor'], - ); - useEffect(() => { - if (!editor) { - return; - } - const updateTab = debounce(100, (value: string) => { - updateActiveTabValues({ [tabProperty]: value }); - }); +export function onChangeEditor({ + onEdit, + tabProperty, + updateActiveTabValues, + model, +}: { + onEdit?: (value: string) => void; + tabProperty: 'variables' | 'headers'; + updateActiveTabValues: ReturnType< + typeof useGraphiQLActions + >['updateActiveTabValues']; + model: monacoEditor.ITextModel; +}): IDisposable { + const updateTab = debounce(100, (value: string) => { + updateActiveTabValues({ [tabProperty]: value }); + }); - const handleChange = (_event: monacoEditor.IModelContentChangedEvent) => { - const newValue = editor.getValue(); - updateTab(newValue); - callback?.(newValue); - }; - const disposable = editor.getModel()!.onDidChangeContent(handleChange); - return () => { - disposable.dispose(); - }; - }, [callback, editor, tabProperty, updateActiveTabValues]); + const handleChange = (_event: monacoEditor.IModelContentChangedEvent) => { + const newValue = model.getValue(); + updateTab(newValue); + onEdit?.(newValue); + }; + return model.onDidChangeContent(handleChange); } // https://react.dev/learn/you-might-not-need-an-effect diff --git a/packages/graphiql-react/src/utility/index.ts b/packages/graphiql-react/src/utility/index.ts index d52f006c9c..e93f31a736 100644 --- a/packages/graphiql-react/src/utility/index.ts +++ b/packages/graphiql-react/src/utility/index.ts @@ -17,6 +17,6 @@ export { useOperationsEditorState, useVariablesEditorState, useHeadersEditorState, - useChangeHandler, + onChangeEditor, useDidUpdate, } from './hooks'; From 897e40c4b1f8fff55b251d64c6cdd9a50162ff1b Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 02:59:22 +0200 Subject: [PATCH 58/73] onChangeEditor header editor --- .../src/components/header-editor.tsx | 23 +++++++++---------- packages/graphiql-react/src/utility/hooks.ts | 7 +++--- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/graphiql-react/src/components/header-editor.tsx b/packages/graphiql-react/src/components/header-editor.tsx index 9ecee47119..14139d5503 100644 --- a/packages/graphiql-react/src/components/header-editor.tsx +++ b/packages/graphiql-react/src/components/header-editor.tsx @@ -1,13 +1,12 @@ import { FC, useEffect, useRef } from 'react'; import { useGraphiQL, useGraphiQLActions } from './provider'; import type { EditorProps } from '../types'; -import { HEADER_URI, KEY_BINDINGS, STORAGE_KEY } from '../constants'; +import { HEADER_URI, KEY_BINDINGS } from '../constants'; import { getOrCreateModel, createEditor, - useChangeHandler, + onChangeEditor, onEditorContainerKeyDown, - pick, cleanupDisposables, cn, } from '../utility'; @@ -21,21 +20,21 @@ interface HeaderEditorProps extends EditorProps { } export const HeaderEditor: FC = ({ onEdit, ...props }) => { - const { setEditor, run, prettifyEditors, mergeQuery } = useGraphiQLActions(); - const { initialHeaders, shouldPersistHeaders } = useGraphiQL( - pick('initialHeaders', 'shouldPersistHeaders'), - ); + const { setEditor, run, prettifyEditors, mergeQuery, updateActiveTabValues } = + useGraphiQLActions(); + const initialHeaders = useGraphiQL(state => state.initialHeaders); const ref = useRef(null!); - useChangeHandler( - onEdit, - shouldPersistHeaders ? STORAGE_KEY.headers : null, - 'headers', - ); useEffect(() => { const model = getOrCreateModel({ uri: HEADER_URI, value: initialHeaders }); const editor = createEditor(ref, { model }); setEditor({ headerEditor: editor }); const disposables = [ + onChangeEditor({ + onEdit, + tabProperty: 'headers', + updateActiveTabValues, + model, + }), editor.addAction({ ...KEY_BINDINGS.runQuery, run }), editor.addAction({ ...KEY_BINDINGS.prettify, run: prettifyEditors }), editor.addAction({ ...KEY_BINDINGS.mergeFragments, run: mergeQuery }), diff --git a/packages/graphiql-react/src/utility/hooks.ts b/packages/graphiql-react/src/utility/hooks.ts index 2e94b7e5dd..a5375c1b70 100644 --- a/packages/graphiql-react/src/utility/hooks.ts +++ b/packages/graphiql-react/src/utility/hooks.ts @@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { debounce } from './debounce'; import type { editor as monacoEditor, IDisposable } from '../monaco-editor'; -import { useGraphiQL, useGraphiQLActions } from '../components'; +import { useGraphiQL } from '../components'; +import type { EditorActions } from '../stores'; export function onChangeEditor({ onEdit, @@ -12,9 +13,7 @@ export function onChangeEditor({ }: { onEdit?: (value: string) => void; tabProperty: 'variables' | 'headers'; - updateActiveTabValues: ReturnType< - typeof useGraphiQLActions - >['updateActiveTabValues']; + updateActiveTabValues: EditorActions['updateActiveTabValues']; model: monacoEditor.ITextModel; }): IDisposable { const updateTab = debounce(100, (value: string) => { From 69d423191e4d2e5304f41c23b55f22ca4e07785e Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 03:15:54 +0200 Subject: [PATCH 59/73] upd resize hook --- packages/graphiql-react/src/components/provider.tsx | 10 +--------- packages/graphiql-react/src/constants.ts | 10 ---------- packages/graphiql/src/GraphiQL.tsx | 6 +++--- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 8ea4a18683..6b61e05ef2 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -153,7 +153,6 @@ useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- false positive if (storeRef.current === null) { - // TODO: make `async` and `await storage.getItem`... function getInitialState() { if (storage === undefined) { throw new TypeError('Unexpected `storage` prop is undefined.'); @@ -172,13 +171,6 @@ useEffect(() => { variables, }); - const isStored = storage.getItem(STORAGE_KEY.persistHeaders) !== null; - - const $shouldPersistHeaders = - shouldPersistHeaders !== false && isStored - ? storage.getItem(STORAGE_KEY.persistHeaders) === 'true' - : shouldPersistHeaders; - const store = create()( persist( (...args) => { @@ -195,7 +187,7 @@ useEffect(() => { onEditOperationName, onPrettifyQuery, onTabChange, - shouldPersistHeaders: $shouldPersistHeaders, + shouldPersistHeaders, tabs, })(...args); const executionSlice = createExecutionSlice({ diff --git a/packages/graphiql-react/src/constants.ts b/packages/graphiql-react/src/constants.ts index d52f87ad77..25f91c98f8 100644 --- a/packages/graphiql-react/src/constants.ts +++ b/packages/graphiql-react/src/constants.ts @@ -42,16 +42,6 @@ export const KEY_MAP = Object.freeze({ }, }); -export const STORAGE_KEY = { - headers: 'headers', - visiblePlugin: 'visiblePlugin', - query: 'query', - variables: 'variables', - tabs: 'tabState', - persistHeaders: 'shouldPersistHeaders', - theme: 'theme', -} as const; - export const DEFAULT_QUERY = `# Welcome to GraphiQL # # GraphiQL is an in-browser tool for writing, validating, and testing diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index 4ad0410898..876754be6e 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -242,11 +242,11 @@ const GraphiQLInterface: FC = ({ } }, sizeThresholdSecond: 200, - storageKey: 'docExplorerFlex', + storageKey: 'flex:plugin', }); const editorResize = useDragResize({ direction: 'horizontal', - storageKey: 'editorFlex', + storageKey: 'flex:response', }); const [initiallyHiddenEditorTools] = useState<'second' | undefined>(() => { if ( @@ -266,7 +266,7 @@ const GraphiQLInterface: FC = ({ direction: 'vertical', initiallyHidden: initiallyHiddenEditorTools, sizeThresholdSecond: 60, - storageKey: 'secondaryEditorFlex', + storageKey: 'flex:editor-tools', }); const [activeSecondaryEditor, setActiveSecondaryEditor] = useState< From f2761d73f67e6b3ba0645706daee84c385152817 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 03:17:25 +0200 Subject: [PATCH 60/73] upd resize hook --- packages/graphiql-react/src/utility/resize.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/graphiql-react/src/utility/resize.ts b/packages/graphiql-react/src/utility/resize.ts index 482816fe36..1dee251eca 100644 --- a/packages/graphiql-react/src/utility/resize.ts +++ b/packages/graphiql-react/src/utility/resize.ts @@ -47,7 +47,7 @@ interface UseDragResizeArgs { * A key for which the state of resizing is persisted in storage (if storage * is available). */ - storageKey?: string; + storageKey: string; } export function useDragResize({ @@ -57,8 +57,9 @@ export function useDragResize({ onHiddenElementChange, sizeThresholdFirst = 100, sizeThresholdSecond = 100, - storageKey, + storageKey: key, }: UseDragResizeArgs) { + const storageKey = `graphiql:${key}`; const storage = useGraphiQL(state => state.storage); const [hiddenElement, setHiddenElement] = useState( null, @@ -118,10 +119,6 @@ export function useDragResize({ element.style.left = ''; element.style.position = ''; element.style.opacity = ''; - - if (!storageKey) { - return; - } const storedValue = await storage.getItem(storageKey); if ( firstRef.current && @@ -149,9 +146,7 @@ export function useDragResize({ return; } const store = debounce(500, (value: string) => { - if (storageKey) { - storage.setItem(storageKey, value); - } + storage.setItem(storageKey, value); }); function setHiddenElementWithCallback(element: ResizableElement | null) { From 2fc411cf0ba4117c94dd61fff94d76212f52ed7e Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 03:20:36 +0200 Subject: [PATCH 61/73] upd query editor --- packages/graphiql-react/src/components/provider.tsx | 6 +----- .../graphiql-react/src/components/query-editor.tsx | 13 ++----------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 6b61e05ef2..0f3593008c 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -27,11 +27,7 @@ import { isSchema, validateSchema, } from 'graphql'; -import { - DEFAULT_PRETTIFY_QUERY, - DEFAULT_QUERY, - STORAGE_KEY, -} from '../constants'; +import { DEFAULT_PRETTIFY_QUERY, DEFAULT_QUERY } from '../constants'; import { getDefaultTabState } from '../utility/tabs'; import { EDITOR_THEME } from '../utility/create-editor'; diff --git a/packages/graphiql-react/src/components/query-editor.tsx b/packages/graphiql-react/src/components/query-editor.tsx index 80f24ce75d..030dbdc6d4 100644 --- a/packages/graphiql-react/src/components/query-editor.tsx +++ b/packages/graphiql-react/src/components/query-editor.tsx @@ -13,12 +13,7 @@ import { cn, } from '../utility'; import type { MonacoEditor, EditorProps, SchemaReference } from '../types'; -import { - KEY_BINDINGS, - MONACO_GRAPHQL_API, - QUERY_URI, - STORAGE_KEY, -} from '../constants'; +import { KEY_BINDINGS, MONACO_GRAPHQL_API, QUERY_URI } from '../constants'; import { type editor as monacoEditor, languages, @@ -68,7 +63,6 @@ export const QueryEditor: FC = ({ operations, operationName, externalFragments, - storage, } = useGraphiQL( pick( 'initialQuery', @@ -77,7 +71,6 @@ export const QueryEditor: FC = ({ 'operations', 'operationName', 'externalFragments', - 'storage', ), ); const ref = useRef(null!); @@ -214,9 +207,7 @@ export const QueryEditor: FC = ({ // We don't use the generic `useChangeHandler` hook here because we want to // have additional logic that updates the operation facts that we save in `editorStore` const handleChange = debounce(100, () => { - const query = editor.getValue(); - storage.setItem(STORAGE_KEY.query, query); - + const query = model.getValue(); const operationFacts = getAndUpdateOperationFacts(editor); // Invoke callback props only after the operation facts have been updated onEdit?.(query, operationFacts?.documentAST); From 19300c941a968550066298736ae0a91f67481623 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 11:57:05 +0200 Subject: [PATCH 62/73] upd --- .changeset/little-impalas-applaud.md | 4 ++++ packages/graphiql-react/src/deprecated.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/.changeset/little-impalas-applaud.md b/.changeset/little-impalas-applaud.md index 26491389fb..a6116e28b7 100644 --- a/.changeset/little-impalas-applaud.md +++ b/.changeset/little-impalas-applaud.md @@ -6,3 +6,7 @@ --- remove `createLocalStorage` from `@graphiql/toolkit` + +deprecate `useStorage` and `useTheme` hooks, use `useGraphiQLActions` and `useGraphiQL` hooks instead. + +remove `StorageAPI`, replace it with `persist` and `createJSONStorage` from `zustand/middleware` diff --git a/packages/graphiql-react/src/deprecated.ts b/packages/graphiql-react/src/deprecated.ts index 6ab53badab..f7a8336cab 100644 --- a/packages/graphiql-react/src/deprecated.ts +++ b/packages/graphiql-react/src/deprecated.ts @@ -74,3 +74,17 @@ export function useSchemaContext() { export function useStorageContext() { return useGraphiQL(state => state.storage); } + +/** + * @deprecated Use `const storage = useGraphiQL(state => state.storage)` instead. + */ +export const useStorage = useStorageContext; // eslint-disable-line @typescript-eslint/no-deprecated + +/** + * @deprecated Use `useGraphiQLActions` and `useGraphiQL` hooks instead. + */ +export function useTheme() { + const { setTheme } = useGraphiQLActions(); + const theme = useGraphiQL(state => state.theme); + return { setTheme, theme }; +} From de16d06f6509bf6427b9e02c326bb552c1344c88 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 12:01:48 +0200 Subject: [PATCH 63/73] upd --- .../src/components/header-editor.tsx | 14 +++++----- .../src/components/variable-editor.tsx | 14 +++++----- packages/graphiql-react/src/utility/hooks.ts | 26 ------------------- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/packages/graphiql-react/src/components/header-editor.tsx b/packages/graphiql-react/src/components/header-editor.tsx index 14139d5503..fcf08085ea 100644 --- a/packages/graphiql-react/src/components/header-editor.tsx +++ b/packages/graphiql-react/src/components/header-editor.tsx @@ -5,7 +5,7 @@ import { HEADER_URI, KEY_BINDINGS } from '../constants'; import { getOrCreateModel, createEditor, - onChangeEditor, + debounce, onEditorContainerKeyDown, cleanupDisposables, cn, @@ -28,12 +28,14 @@ export const HeaderEditor: FC = ({ onEdit, ...props }) => { const model = getOrCreateModel({ uri: HEADER_URI, value: initialHeaders }); const editor = createEditor(ref, { model }); setEditor({ headerEditor: editor }); + const updateTab = debounce(100, (headers: string) => { + updateActiveTabValues({ headers }); + }); const disposables = [ - onChangeEditor({ - onEdit, - tabProperty: 'headers', - updateActiveTabValues, - model, + model.onDidChangeContent(() => { + const newValue = model.getValue(); + updateTab(newValue); + onEdit?.(newValue); }), editor.addAction({ ...KEY_BINDINGS.runQuery, run }), editor.addAction({ ...KEY_BINDINGS.prettify, run: prettifyEditors }), diff --git a/packages/graphiql-react/src/components/variable-editor.tsx b/packages/graphiql-react/src/components/variable-editor.tsx index 60d65816d6..66ce8a2150 100644 --- a/packages/graphiql-react/src/components/variable-editor.tsx +++ b/packages/graphiql-react/src/components/variable-editor.tsx @@ -5,7 +5,7 @@ import { KEY_BINDINGS, VARIABLE_URI } from '../constants'; import { getOrCreateModel, createEditor, - onChangeEditor, + debounce, onEditorContainerKeyDown, cleanupDisposables, cn, @@ -34,12 +34,14 @@ export const VariableEditor: FC = ({ }); const editor = createEditor(ref, { model }); setEditor({ variableEditor: editor }); + const updateTab = debounce(100, (variables: string) => { + updateActiveTabValues({ variables }); + }); const disposables = [ - onChangeEditor({ - onEdit, - tabProperty: 'variables', - updateActiveTabValues, - model, + model.onDidChangeContent(() => { + const newValue = model.getValue(); + updateTab(newValue); + onEdit?.(newValue); }), editor.addAction({ ...KEY_BINDINGS.runQuery, run }), editor.addAction({ ...KEY_BINDINGS.prettify, run: prettifyEditors }), diff --git a/packages/graphiql-react/src/utility/hooks.ts b/packages/graphiql-react/src/utility/hooks.ts index a5375c1b70..571b4f8271 100644 --- a/packages/graphiql-react/src/utility/hooks.ts +++ b/packages/graphiql-react/src/utility/hooks.ts @@ -1,32 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-restricted-imports -- TODO: check why query builder update only 1st field https://github.com/graphql/graphiql/issues/3836 import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { debounce } from './debounce'; -import type { editor as monacoEditor, IDisposable } from '../monaco-editor'; import { useGraphiQL } from '../components'; -import type { EditorActions } from '../stores'; - -export function onChangeEditor({ - onEdit, - tabProperty, - updateActiveTabValues, - model, -}: { - onEdit?: (value: string) => void; - tabProperty: 'variables' | 'headers'; - updateActiveTabValues: EditorActions['updateActiveTabValues']; - model: monacoEditor.ITextModel; -}): IDisposable { - const updateTab = debounce(100, (value: string) => { - updateActiveTabValues({ [tabProperty]: value }); - }); - - const handleChange = (_event: monacoEditor.IModelContentChangedEvent) => { - const newValue = model.getValue(); - updateTab(newValue); - onEdit?.(newValue); - }; - return model.onDidChangeContent(handleChange); -} // https://react.dev/learn/you-might-not-need-an-effect export const useEditorState = ( From a2024b9fac46e08ba7cc6c654ca1570c1e15b19b Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 12:04:38 +0200 Subject: [PATCH 64/73] upd --- .../graphiql-react/src/components/header-editor.tsx | 12 +++++------- .../graphiql-react/src/components/query-editor.tsx | 3 --- .../src/components/variable-editor.tsx | 12 +++++------- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/graphiql-react/src/components/header-editor.tsx b/packages/graphiql-react/src/components/header-editor.tsx index fcf08085ea..248942b0c8 100644 --- a/packages/graphiql-react/src/components/header-editor.tsx +++ b/packages/graphiql-react/src/components/header-editor.tsx @@ -28,15 +28,13 @@ export const HeaderEditor: FC = ({ onEdit, ...props }) => { const model = getOrCreateModel({ uri: HEADER_URI, value: initialHeaders }); const editor = createEditor(ref, { model }); setEditor({ headerEditor: editor }); - const updateTab = debounce(100, (headers: string) => { - updateActiveTabValues({ headers }); + const handleChange = debounce(100, () => { + const value = model.getValue(); + updateActiveTabValues({ headers: value }); + onEdit?.(value); }); const disposables = [ - model.onDidChangeContent(() => { - const newValue = model.getValue(); - updateTab(newValue); - onEdit?.(newValue); - }), + model.onDidChangeContent(handleChange), editor.addAction({ ...KEY_BINDINGS.runQuery, run }), editor.addAction({ ...KEY_BINDINGS.prettify, run: prettifyEditors }), editor.addAction({ ...KEY_BINDINGS.mergeFragments, run: mergeQuery }), diff --git a/packages/graphiql-react/src/components/query-editor.tsx b/packages/graphiql-react/src/components/query-editor.tsx index 030dbdc6d4..26c13d07fa 100644 --- a/packages/graphiql-react/src/components/query-editor.tsx +++ b/packages/graphiql-react/src/components/query-editor.tsx @@ -203,9 +203,6 @@ export const QueryEditor: FC = ({ const model = getOrCreateModel({ uri: QUERY_URI, value: initialQuery }); const editor = createEditor(ref, { model }); setEditor({ queryEditor: editor }); - - // We don't use the generic `useChangeHandler` hook here because we want to - // have additional logic that updates the operation facts that we save in `editorStore` const handleChange = debounce(100, () => { const query = model.getValue(); const operationFacts = getAndUpdateOperationFacts(editor); diff --git a/packages/graphiql-react/src/components/variable-editor.tsx b/packages/graphiql-react/src/components/variable-editor.tsx index 66ce8a2150..a3fe0c9271 100644 --- a/packages/graphiql-react/src/components/variable-editor.tsx +++ b/packages/graphiql-react/src/components/variable-editor.tsx @@ -34,15 +34,13 @@ export const VariableEditor: FC = ({ }); const editor = createEditor(ref, { model }); setEditor({ variableEditor: editor }); - const updateTab = debounce(100, (variables: string) => { - updateActiveTabValues({ variables }); + const handleChange = debounce(100, () => { + const value = model.getValue(); + updateActiveTabValues({ variables: value }); + onEdit?.(value); }); const disposables = [ - model.onDidChangeContent(() => { - const newValue = model.getValue(); - updateTab(newValue); - onEdit?.(newValue); - }), + model.onDidChangeContent(handleChange), editor.addAction({ ...KEY_BINDINGS.runQuery, run }), editor.addAction({ ...KEY_BINDINGS.prettify, run: prettifyEditors }), editor.addAction({ ...KEY_BINDINGS.mergeFragments, run: mergeQuery }), From 686cfe93fe9f3f2d83be1ca0b4a59a3033c058c9 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 12:17:23 +0200 Subject: [PATCH 65/73] remove unused CodeMirror CSS classes from GraphiQL 4 --- .../src/components/markdown-content/index.css | 41 +---- .../graphiql-react/src/style/codemirror.css | 159 ------------------ packages/graphiql-react/src/style/fold.css | 22 --- packages/graphiql-react/src/style/hint.css | 71 -------- packages/graphiql-react/src/style/info.css | 60 ------- packages/graphiql-react/src/style/jump.css | 5 - packages/graphiql-react/src/style/lint.css | 93 ---------- packages/graphiql-react/src/style/root.css | 6 - packages/graphiql/cypress/e2e/lint.cy.ts | 16 -- 9 files changed, 3 insertions(+), 470 deletions(-) delete mode 100644 packages/graphiql-react/src/style/codemirror.css delete mode 100644 packages/graphiql-react/src/style/fold.css delete mode 100644 packages/graphiql-react/src/style/hint.css delete mode 100644 packages/graphiql-react/src/style/info.css delete mode 100644 packages/graphiql-react/src/style/jump.css delete mode 100644 packages/graphiql-react/src/style/lint.css diff --git a/packages/graphiql-react/src/components/markdown-content/index.css b/packages/graphiql-react/src/components/markdown-content/index.css index c08f0866c1..d9cbba7719 100644 --- a/packages/graphiql-react/src/components/markdown-content/index.css +++ b/packages/graphiql-react/src/components/markdown-content/index.css @@ -7,11 +7,7 @@ */ .graphiql-markdown-description, -.graphiql-markdown-deprecation, -.CodeMirror-hint-information-description, -.CodeMirror-hint-information-deprecation-reason, -.CodeMirror-info .info-description, -.CodeMirror-info .info-deprecation { +.graphiql-markdown-deprecation { & blockquote { margin-left: 0; margin-right: 0; @@ -68,9 +64,7 @@ } } -.graphiql-markdown-description, -.CodeMirror-hint-information-description, -.CodeMirror-info .info-description { +.graphiql-markdown-description { & a { color: hsl(var(--color-primary)); text-decoration: none; @@ -95,9 +89,7 @@ } } -.graphiql-markdown-deprecation, -.CodeMirror-hint-information-deprecation-reason, -.CodeMirror-info .info-deprecation { +.graphiql-markdown-deprecation { & a { color: hsl(var(--color-warning)); text-decoration: underline; @@ -120,30 +112,3 @@ .graphiql-markdown-preview > :not(:first-child) { display: none; } - -/** - * We show deprecations in the following places: - * - In the hint tooltip when typing in the query editor. - * - In the info tooltip when hovering over a field in the query editor. - */ - -.CodeMirror-hint-information-deprecation, -.CodeMirror-info .info-deprecation { - background-color: hsla(var(--color-warning), var(--alpha-background-light)); - border: 1px solid hsl(var(--color-warning)); - border-radius: var(--border-radius-4); - color: hsl(var(--color-warning)); - margin-top: var(--px-12); - padding: var(--px-6) var(--px-8); -} - -.CodeMirror-hint-information-deprecation-label, -.CodeMirror-info .info-deprecation-label { - font-size: var(--font-size-hint); - font-weight: var(--font-weight-medium); -} - -.CodeMirror-hint-information-deprecation-reason, -.CodeMirror-info .info-deprecation-reason { - margin-top: var(--px-6); -} diff --git a/packages/graphiql-react/src/style/codemirror.css b/packages/graphiql-react/src/style/codemirror.css deleted file mode 100644 index ad674f8074..0000000000 --- a/packages/graphiql-react/src/style/codemirror.css +++ /dev/null @@ -1,159 +0,0 @@ -/* No padding around line numbers */ -.graphiql-container .CodeMirror-linenumber { - padding: 0; -} - -/* No border between gutter and editor */ -.graphiql-container .CodeMirror-gutters { - border: none; -} - -/** - * Editor theme - */ - -.cm-s-graphiql { - /* Default to punctuation */ - color: hsla(var(--color-neutral), var(--alpha-tertiary)); - - /* OperationType, `fragment`, `on` */ - & .cm-keyword { - color: hsl(var(--color-primary)); - } - /* Name (OperationDefinition), FragmentName */ - & .cm-def { - color: hsl(var(--color-tertiary)); - } - /* Punctuator (except `$` and `@`) */ - & .cm-punctuation { - color: hsla(var(--color-neutral), var(--alpha-tertiary)); - } - /* Variable */ - & .cm-variable { - color: hsl(var(--color-secondary)); - } - /* NamedType */ - & .cm-atom { - color: hsl(var(--color-tertiary)); - } - /* IntValue, FloatValue */ - & .cm-number { - color: hsl(var(--color-success)); - } - /* StringValue */ - & .cm-string { - color: hsl(var(--color-warning)); - } - /* BooleanValue */ - & .cm-builtin { - color: hsl(var(--color-success)); - } - /* EnumValue */ - & .cm-string-2 { - color: hsl(var(--color-secondary)); - } - /* Name (ObjectField, Argument) */ - & .cm-attribute { - color: hsl(var(--color-tertiary)); - } - /* Name (Directive) */ - & .cm-meta { - color: hsl(var(--color-tertiary)); - } - /* Name (Alias, Field without Alias) */ - & .cm-property { - color: hsl(var(--color-info)); - } - /* Name (Field with Alias) */ - & .cm-qualifier { - color: hsl(var(--color-secondary)); - } - /* Comment */ - & .cm-comment { - color: hsla(var(--color-neutral), var(--alpha-secondary)); - } - /* Whitespace */ - & .cm-ws { - color: hsla(var(--color-neutral), var(--alpha-tertiary)); - } - /* Invalid characters */ - & .cm-invalidchar { - color: hsl(var(--color-error)); - } - - /* Cursor */ - & .CodeMirror-cursor { - border-left: 2px solid hsla(var(--color-neutral), var(--alpha-secondary)); - } - - /* Color for line numbers and fold-gutters */ - & .CodeMirror-linenumber { - color: hsla(var(--color-neutral), var(--alpha-tertiary)); - } -} - -/* Matching bracket colors */ -.graphiql-container div.CodeMirror span.CodeMirror-matchingbracket, -.graphiql-container div.CodeMirror span.CodeMirror-nonmatchingbracket { - color: hsl(var(--color-warning)); -} - -/* Selected text blocks */ -.graphiql-container .CodeMirror-selected, -.graphiql-container .CodeMirror-focused .CodeMirror-selected { - background: hsla(var(--color-neutral), var(--alpha-background-heavy)); -} - -/* Position the search dialog */ -.graphiql-container .CodeMirror-dialog { - background: inherit; - color: inherit; - left: 0; - right: 0; - overflow: hidden; - padding: var(--px-2) var(--px-6); - position: absolute; - z-index: 6; -} -.graphiql-container .CodeMirror-dialog-top { - border-bottom: 1px solid - hsla(var(--color-neutral), var(--alpha-background-heavy)); - padding-bottom: var(--px-12); - top: 0; -} -.graphiql-container .CodeMirror-dialog-bottom { - border-top: 1px solid - hsla(var(--color-neutral), var(--alpha-background-heavy)); - bottom: 0; - padding-top: var(--px-12); -} - -/* Hide the search hint */ -.graphiql-container .CodeMirror-search-hint { - display: none; -} - -/* Style the input field for searching */ -.graphiql-container .CodeMirror-dialog input { - border: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); - border-radius: var(--border-radius-4); - padding: var(--px-4); -} -.graphiql-container .CodeMirror-dialog input:focus { - outline: hsl(var(--color-primary)) solid 2px; -} - -/* Set the highlight color for search results */ -.graphiql-container .cm-searching { - background-color: hsla(var(--color-warning), var(--alpha-background-light)); - /** - * When cycling through search results, CodeMirror overlays the current - * selection with another element that has the .CodeMirror-selected class - * applied. This adds another background color (see above), but this extra - * box does not quite match the height of this element. To match them up we - * add some extra padding here. (Note that this doesn't affect the line - * height of the CodeMirror editor as all line wrappers have a fixed height.) - */ - padding-bottom: 1.5px; - padding-top: 0.5px; -} diff --git a/packages/graphiql-react/src/style/fold.css b/packages/graphiql-react/src/style/fold.css deleted file mode 100644 index ce87eb3d8b..0000000000 --- a/packages/graphiql-react/src/style/fold.css +++ /dev/null @@ -1,22 +0,0 @@ -.CodeMirror-foldgutter { - width: var(--px-12); -} - -.CodeMirror-foldmarker { - background-color: hsl(var(--color-info)); - border-radius: var(--border-radius-4); - color: hsl(var(--color-base)); - font-family: inherit; - margin: 0 var(--px-4); - padding: 0 var(--px-8); - text-shadow: none; -} - -.CodeMirror-foldgutter-open, -.CodeMirror-foldgutter-folded { - color: hsla(var(--color-neutral), var(--alpha-tertiary)); - - &::after { - margin: 0 var(--px-2); - } -} diff --git a/packages/graphiql-react/src/style/hint.css b/packages/graphiql-react/src/style/hint.css deleted file mode 100644 index 239cf258d7..0000000000 --- a/packages/graphiql-react/src/style/hint.css +++ /dev/null @@ -1,71 +0,0 @@ -/* Popup styles */ -.CodeMirror-hints { - background: hsl(var(--color-base)); - border: var(--popover-border); - border-radius: var(--border-radius-8); - box-shadow: var(--popover-box-shadow); - display: grid; - font-family: var(--font-family); - font-size: var(--font-size-body); - grid-template-columns: auto fit-content(300px); - /* By default this is equals exactly 8 items including margins */ - max-height: 264px; - padding: 0; -} - -/* Autocomplete items */ -.CodeMirror-hint { - border-radius: var(--border-radius-4); - color: hsla(var(--color-neutral), var(--alpha-secondary)); - grid-column: 1 / 2; - margin: var(--px-4); - /* Override element style added by codemirror */ - padding: var(--px-6) var(--px-8) !important; - - &:not(:first-child) { - margin-top: 0; - } -} -li.CodeMirror-hint-active { - background: hsla(var(--color-primary), var(--alpha-background-medium)); - color: hsl(var(--color-primary)); -} - -/* Sidebar with additional information */ -.CodeMirror-hint-information { - border-left: 1px solid - hsla(var(--color-neutral), var(--alpha-background-heavy)); - grid-column: 2 / 3; - grid-row: 1 / 99999; - /* Same as the popup */ - max-height: 264px; - overflow: auto; - padding: var(--px-12); -} -.CodeMirror-hint-information-header { - display: flex; - align-items: baseline; -} -.CodeMirror-hint-information-field-name { - font-size: var(--font-size-h4); - font-weight: var(--font-weight-medium); -} -.CodeMirror-hint-information-type-name-pill { - border: 1px solid hsla(var(--color-neutral), var(--alpha-tertiary)); - border-radius: var(--border-radius-4); - color: hsla(var(--color-neutral), var(--alpha-secondary)); - margin-left: var(--px-6); - padding: var(--px-4); -} -.CodeMirror-hint-information-type-name { - color: inherit; - text-decoration: none; - - &:hover { - text-decoration: underline dotted; - } -} -.CodeMirror-hint-information-description { - color: hsla(var(--color-neutral), var(--alpha-secondary)); - margin-top: var(--px-12); -} diff --git a/packages/graphiql-react/src/style/info.css b/packages/graphiql-react/src/style/info.css deleted file mode 100644 index a1a84335c9..0000000000 --- a/packages/graphiql-react/src/style/info.css +++ /dev/null @@ -1,60 +0,0 @@ -/* Popup styles */ -.CodeMirror-info { - background-color: hsl(var(--color-base)); - border: var(--popover-border); - border-radius: var(--border-radius-8); - box-shadow: var(--popover-box-shadow); - color: hsl(var(--color-neutral)); - max-height: 300px; - max-width: 400px; - opacity: 0; - overflow: auto; - padding: var(--px-12); - position: fixed; - transition: opacity 0.15s; - z-index: 10; - - /* Link styles */ - & a { - color: inherit; - text-decoration: none; - - &:hover { - text-decoration: underline dotted; - } - } - - /* Align elements in header */ - & .CodeMirror-info-header { - display: flex; - align-items: baseline; - } - - /* Main elements */ - & .CodeMirror-info-header { - & > .type-name, - & > .field-name, - & > .arg-name, - & > .directive-name, - & > .enum-value { - font-size: var(--font-size-h4); - font-weight: var(--font-weight-medium); - } - } - - /* Type names */ - & .type-name-pill { - border: 1px solid hsla(var(--color-neutral), var(--alpha-tertiary)); - border-radius: var(--border-radius-4); - color: hsla(var(--color-neutral), var(--alpha-secondary)); - margin-left: var(--px-6); - padding: var(--px-4); - } - - /* Descriptions */ - & .info-description { - color: hsla(var(--color-neutral), var(--alpha-secondary)); - margin-top: var(--px-12); - overflow: hidden; - } -} diff --git a/packages/graphiql-react/src/style/jump.css b/packages/graphiql-react/src/style/jump.css deleted file mode 100644 index 7ba8e49c21..0000000000 --- a/packages/graphiql-react/src/style/jump.css +++ /dev/null @@ -1,5 +0,0 @@ -/* Underline the clickable token */ -.CodeMirror-jump-token { - text-decoration: underline dotted; - cursor: pointer; -} diff --git a/packages/graphiql-react/src/style/lint.css b/packages/graphiql-react/src/style/lint.css deleted file mode 100644 index 372534550c..0000000000 --- a/packages/graphiql-react/src/style/lint.css +++ /dev/null @@ -1,93 +0,0 @@ -/* Text styles */ -.CodeMirror-lint-mark-error, -.CodeMirror-lint-mark-warning { - background-repeat: repeat-x; - /** - * The following two are very specific to the font size, so we use - * "magic values" instead of variables. - */ - background-size: 10px 3px; - background-position: 0 95%; -} -.cm-s-graphiql .CodeMirror-lint-mark-error { - color: hsl(var(--color-error)); -} -.CodeMirror-lint-mark-error { - background-image: linear-gradient( - 45deg, - transparent 65%, - hsl(var(--color-error)) 80%, - transparent 90% - ), - linear-gradient( - 135deg, - transparent 5%, - hsl(var(--color-error)) 15%, - transparent 25% - ), - linear-gradient( - 135deg, - transparent 45%, - hsl(var(--color-error)) 55%, - transparent 65% - ), - linear-gradient( - 45deg, - transparent 25%, - hsl(var(--color-error)) 35%, - transparent 50% - ); -} -.cm-s-graphiql .CodeMirror-lint-mark-warning { - color: hsl(var(--color-warning)); -} -.CodeMirror-lint-mark-warning { - background-image: linear-gradient( - 45deg, - transparent 65%, - hsl(var(--color-warning)) 80%, - transparent 90% - ), - linear-gradient( - 135deg, - transparent 5%, - hsl(var(--color-warning)) 15%, - transparent 25% - ), - linear-gradient( - 135deg, - transparent 45%, - hsl(var(--color-warning)) 55%, - transparent 65% - ), - linear-gradient( - 45deg, - transparent 25%, - hsl(var(--color-warning)) 35%, - transparent 50% - ); -} - -/* Popup styles */ -.CodeMirror-lint-tooltip { - background-color: hsl(var(--color-base)); - border: var(--popover-border); - border-radius: var(--border-radius-8); - box-shadow: var(--popover-box-shadow); - font-size: var(--font-size-body); - font-family: var(--font-family); - max-width: 600px; - overflow: hidden; - padding: var(--px-12); -} -.CodeMirror-lint-message-error, -.CodeMirror-lint-message-warning { - background-image: none; - padding: 0; -} -.CodeMirror-lint-message-error { - color: hsl(var(--color-error)); -} -.CodeMirror-lint-message-warning { - color: hsl(var(--color-warning)); -} diff --git a/packages/graphiql-react/src/style/root.css b/packages/graphiql-react/src/style/root.css index 915e656ca5..77910c1797 100644 --- a/packages/graphiql-react/src/style/root.css +++ b/packages/graphiql-react/src/style/root.css @@ -1,11 +1,5 @@ @import 'auto-insertion.css'; -@import 'codemirror.css'; @import 'editor.css'; -@import 'fold.css'; -@import 'hint.css'; -@import 'info.css'; -@import 'jump.css'; -@import 'lint.css'; /* a very simple box-model reset, intentionally does not include pseudo elements */ .graphiql-container * { diff --git a/packages/graphiql/cypress/e2e/lint.cy.ts b/packages/graphiql/cypress/e2e/lint.cy.ts index 8a87737de6..bf5a95678b 100644 --- a/packages/graphiql/cypress/e2e/lint.cy.ts +++ b/packages/graphiql/cypress/e2e/lint.cy.ts @@ -1,22 +1,6 @@ import { version as graphqlVersion } from 'graphql'; describe('Linting', () => { - it('Does not mark valid fields', () => { - cy.visitWithOp({ - query: /* GraphQL */ ` - { - myAlias: id - test { - id - } - } - `, - }) - .contains('myAlias') - .should('not.have.class', 'CodeMirror-lint-mark') - .and('not.have.class', 'CodeMirror-lint-mark-error'); - }); - it('Marks invalid fields as error', () => { cy.visitWithOp({ query: /* GraphQL */ ` From ab810450c259e8674241941e97948c4c551a3eaf Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 12:17:42 +0200 Subject: [PATCH 66/73] changeset --- .changeset/little-impalas-applaud.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/little-impalas-applaud.md b/.changeset/little-impalas-applaud.md index a6116e28b7..43a9b4119c 100644 --- a/.changeset/little-impalas-applaud.md +++ b/.changeset/little-impalas-applaud.md @@ -10,3 +10,5 @@ remove `createLocalStorage` from `@graphiql/toolkit` deprecate `useStorage` and `useTheme` hooks, use `useGraphiQLActions` and `useGraphiQL` hooks instead. remove `StorageAPI`, replace it with `persist` and `createJSONStorage` from `zustand/middleware` + +remove unused CodeMirror CSS classes from GraphiQL 4 From 0a3734f2752db45918966f1671f63f780645fbe2 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 12:26:30 +0200 Subject: [PATCH 67/73] upd resize --- packages/graphiql-react/src/utility/index.ts | 1 - packages/graphiql-react/src/utility/resize.ts | 17 +++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/graphiql-react/src/utility/index.ts b/packages/graphiql-react/src/utility/index.ts index e93f31a736..ba2500184e 100644 --- a/packages/graphiql-react/src/utility/index.ts +++ b/packages/graphiql-react/src/utility/index.ts @@ -17,6 +17,5 @@ export { useOperationsEditorState, useVariablesEditorState, useHeadersEditorState, - onChangeEditor, useDidUpdate, } from './hooks'; diff --git a/packages/graphiql-react/src/utility/resize.ts b/packages/graphiql-react/src/utility/resize.ts index 1dee251eca..3704a606bf 100644 --- a/packages/graphiql-react/src/utility/resize.ts +++ b/packages/graphiql-react/src/utility/resize.ts @@ -59,15 +59,15 @@ export function useDragResize({ sizeThresholdSecond = 100, storageKey: key, }: UseDragResizeArgs) { - const storageKey = `graphiql:${key}`; const storage = useGraphiQL(state => state.storage); + const storageKey = `graphiql:${key}`; const [hiddenElement, setHiddenElement] = useState( null, ); const firstRef = useRef(null); const dragBarRef = useRef(null); const secondRef = useRef(null); - const defaultFlexRef = useRef(`${defaultSizeRelation}`); + const defaultFlex = String(defaultSizeRelation); useEffect(() => { async function initFlexValues() { @@ -81,10 +81,10 @@ export function useDragResize({ setHiddenElement(initialHiddenElement); if (firstRef.current) { - const storedValue = $storedValue || defaultFlexRef.current; + const storedValue = $storedValue || defaultFlex; firstRef.current.style.flex = storedValue === HIDE_FIRST || storedValue === HIDE_SECOND - ? defaultFlexRef.current + ? defaultFlex : storedValue; } if (secondRef.current) { @@ -125,7 +125,7 @@ export function useDragResize({ storedValue !== HIDE_FIRST && storedValue !== HIDE_SECOND ) { - firstRef.current.style.flex = storedValue || defaultFlexRef.current; + firstRef.current.style.flex = storedValue || defaultFlex; } } @@ -139,7 +139,7 @@ export function useDragResize({ } } } - }, [hiddenElement, storage, storageKey]); + }, [defaultFlex, hiddenElement, storage, storageKey]); useEffect(() => { if (!dragBarRef.current || !firstRef.current || !secondRef.current) { @@ -227,9 +227,9 @@ export function useDragResize({ function reset() { if (firstRef.current) { - firstRef.current.style.flex = defaultFlexRef.current; + firstRef.current.style.flex = defaultFlex; } - store(defaultFlexRef.current); + store(defaultFlex); setHiddenElementWithCallback(null); } @@ -240,6 +240,7 @@ export function useDragResize({ dragBarContainer.removeEventListener('dblclick', reset); }; }, [ + defaultFlex, direction, onHiddenElementChange, sizeThresholdFirst, From 22c167e6cffb937f586953dcf8342ea21ed2aaf2 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 13:27:23 +0200 Subject: [PATCH 68/73] upd toolkit --- .../src/storage/__tests__/query.spec.ts | 49 ++++++++----------- .../graphiql-toolkit/src/storage/query.ts | 30 +++++++----- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/packages/graphiql-toolkit/src/storage/__tests__/query.spec.ts b/packages/graphiql-toolkit/src/storage/__tests__/query.spec.ts index 7339635cac..647d4d3b29 100644 --- a/packages/graphiql-toolkit/src/storage/__tests__/query.spec.ts +++ b/packages/graphiql-toolkit/src/storage/__tests__/query.spec.ts @@ -5,7 +5,7 @@ class StorageMock { shouldThrow: () => boolean; // @ts-expect-error count: number; - map = {}; + map: Record = {}; // @ts-expect-error storage: Storage; @@ -13,37 +13,26 @@ class StorageMock { this.shouldThrow = shouldThrow; } - set(key: string, value: string) { + setItem(key: string, value: string) { this.count++; - if (this.shouldThrow()) { - return { - error: new Error('boom'), - isQuotaError: true, - }; + throw new Error('boom'); } - // @ts-expect-error this.map[key] = value; - - return { - error: null, - isQuotaError: false, - }; } - get(key: string) { - // @ts-expect-error + getItem(key: string) { return this.map[key] || null; } } describe('QueryStore', () => { + const storage = createJSONStorage(() => localStorage); + describe('with no max items', () => { - it('can push multiple items', () => { - const store = new QueryStore( - 'normal', - createJSONStorage(() => localStorage), - ); + it('can push multiple items', async () => { + // @ts-expect-error -- fixme + const store = await QueryStore.create('normal', storage); for (let i = 0; i < 100; i++) { store.push({ query: `item${i}` }); @@ -52,10 +41,13 @@ describe('QueryStore', () => { expect(store.items.length).toBe(100); }); - it('will fail silently on quota error', () => { + it('will fail silently on quota error', async () => { let i = 0; - // @ts-expect-error - const store = new QueryStore('normal', new StorageMock(() => i > 4)); + const store = await QueryStore.create( + 'normal', + // @ts-expect-error + new StorageMock(() => i > 4), + ); for (; i < 10; i++) { store.push({ query: `item${i}` }); @@ -68,8 +60,9 @@ describe('QueryStore', () => { }); describe('with max items', () => { - it('can push a limited number of items', () => { - const store = new QueryStore('limited', new StorageAPI(), 20); + it('can push a limited number of items', async () => { + // @ts-expect-error -- fixme + const store = await QueryStore.create('limited', storage, 20); for (let i = 0; i < 100; i++) { store.push({ query: `item${i}` }); @@ -81,10 +74,10 @@ describe('QueryStore', () => { expect(store.items[19].query).toBe('item99'); }); - it('tries to remove on quota error until it succeeds', () => { + it('tries to remove on quota error until it succeeds', async () => { let shouldThrow: () => boolean; let retryCounter = 0; - const store = new QueryStore( + const store = await QueryStore.create( 'normal', // @ts-expect-error new StorageMock(() => { @@ -114,7 +107,7 @@ describe('QueryStore', () => { expect(store.items[7].query).toBe('finalItem'); }); - it('tries to remove a maximum of 5 times', () => { + it('tries to remove a maximum of 5 times', async () => { let shouldThrow: () => boolean; let retryCounter = 0; const store = new QueryStore( diff --git a/packages/graphiql-toolkit/src/storage/query.ts b/packages/graphiql-toolkit/src/storage/query.ts index 1c3cac8fdd..c9a7136d81 100644 --- a/packages/graphiql-toolkit/src/storage/query.ts +++ b/packages/graphiql-toolkit/src/storage/query.ts @@ -16,10 +16,14 @@ export class QueryStore { private key: string, private storage: StateStorage, private maxSize: number | null = null, - ) { - void this.fetchAll().then(items => { - this.items = items; - }); + ) {} + + static async create( + ...args: ConstructorParameters + ): Promise { + const store = new this(...args); + store.items = await store.fetchAll(); + return store; } get length() { @@ -83,12 +87,11 @@ export class QueryStore { } async fetchAll() { - const raw = await this.storage.getItem(this.key); - if (raw) { - // @ts-expect-error -- fixme: test what type is `raw` - return raw as QueryStoreItem[]; + const items = await this.storage.getItem(this.key); + if (!items) { + return []; } - return []; + return items as unknown as QueryStoreItem[]; } push(item: QueryStoreItem) { @@ -97,12 +100,13 @@ export class QueryStore { if (this.maxSize && items.length > this.maxSize) { items.shift(); } - // @ts-expect-error -- fixme - this.storage.setItem(this.key, this.items); + try { + this.storage.setItem(this.key, JSON.stringify(this.items)); + this.items = items; + } catch {} } save() { - // @ts-expect-error -- fixme - this.storage.setItem(this.key, this.items); + this.storage.setItem(this.key, JSON.stringify(this.items)); } } From 55ba6ee1bb1ddf1b341aed0cb19cafc8b8e95a36 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 15:40:06 +0200 Subject: [PATCH 69/73] upd --- packages/graphiql-react/src/components/dialog/index.css | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/graphiql-react/src/components/dialog/index.css b/packages/graphiql-react/src/components/dialog/index.css index 256b85b487..58fa1711f9 100644 --- a/packages/graphiql-react/src/components/dialog/index.css +++ b/packages/graphiql-react/src/components/dialog/index.css @@ -2,12 +2,6 @@ position: fixed; inset: 0; background-color: hsla(var(--color-neutral), var(--alpha-background-heavy)); - /** - * CodeMirror has a `z-index` set for the container of the scrollbar of the - * editor, so we have to add one here to make sure that the dialog is shown - * above the editor scrollbar (if they are visible). - */ - z-index: 10; } .graphiql-dialog { From e224c48190c93c5aff3142c53336855646659b9c Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 15:42:56 +0200 Subject: [PATCH 70/73] upd --- .../src/components/provider.tsx | 27 ++++++------------- packages/graphiql-react/src/stores/editor.ts | 19 ++++++++++--- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 0f3593008c..97eb72ed97 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -153,20 +153,14 @@ useEffect(() => { if (storage === undefined) { throw new TypeError('Unexpected `storage` prop is undefined.'); } - // We only need to compute it lazily during the initial render. - const query = initialQuery; - const variables = initialVariables; - const headers = initialHeaders; - const { tabs, activeTabIndex } = getDefaultTabState({ defaultHeaders, defaultQuery, defaultTabs, - headers, - query, - variables, + headers: initialHeaders, + query: initialQuery, + variables: initialVariables, }); - const store = create()( persist( (...args) => { @@ -175,10 +169,6 @@ useEffect(() => { defaultHeaders, defaultQuery, externalFragments: getExternalFragments(externalFragments), - initialHeaders: headers ?? defaultHeaders ?? '', - initialQuery: - query ?? (activeTabIndex === 0 ? tabs[0]!.query : null) ?? '', - initialVariables: variables ?? '', onCopyQuery, onEditOperationName, onPrettifyQuery, @@ -227,6 +217,11 @@ useEffect(() => { const theme = state.theme === undefined ? defaultTheme : state.theme; state.actions.setTheme(theme); + state.actions.setInitialValues({ + initialHeaders, + initialQuery, + initialVariables, + }) } if (error) { // eslint-disable-next-line no-console @@ -238,12 +233,6 @@ useEffect(() => { }; }, partialize(state) { - // eslint-disable-next-line no-console - console.log( - 'partialize', - state.shouldPersistHeaders, - state.tabs.map(t => t.headers), - ); return { activeTabIndex: state.activeTabIndex, shouldPersistHeaders: state.shouldPersistHeaders, diff --git a/packages/graphiql-react/src/stores/editor.ts b/packages/graphiql-react/src/stores/editor.ts index a3848a068e..b9e8041c39 100644 --- a/packages/graphiql-react/src/stores/editor.ts +++ b/packages/graphiql-react/src/stores/editor.ts @@ -215,6 +215,12 @@ export interface EditorActions { * Prettify query, variable and header editors. */ prettifyEditors: () => Promise; + + setInitialValues: (values: { + initialQuery?: string; + initialVariables?: string; + initialHeaders?: string; + }) => void; } export interface EditorProps @@ -270,9 +276,6 @@ type CreateEditorSlice = ( | 'shouldPersistHeaders' | 'tabs' | 'activeTabIndex' - | 'initialQuery' - | 'initialVariables' - | 'initialHeaders' | 'onEditOperationName' | 'externalFragments' | 'onTabChange' @@ -331,6 +334,16 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { } const $actions: EditorActions = { + setInitialValues({ initialHeaders, initialQuery, initialVariables }) { + set(({ tabs, activeTabIndex }) => { + const activeTab = tabs[activeTabIndex]!; + return { + initialHeaders: initialHeaders ?? activeTab.headers ?? '', + initialQuery: initialQuery ?? activeTab.query ?? '', + initialVariables: initialVariables ?? activeTab.variables ?? '', + }; + }); + }, addTab() { set(({ defaultHeaders, onTabChange, tabs, activeTabIndex }) => { // Make sure the current tab stores the latest values From 3c0407b7d150f8936dbbfda5fa6f49133917428f Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 15:43:59 +0200 Subject: [PATCH 71/73] prettier --- packages/graphiql-react/src/components/provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 97eb72ed97..492b5ad391 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -221,7 +221,7 @@ useEffect(() => { initialHeaders, initialQuery, initialVariables, - }) + }); } if (error) { // eslint-disable-next-line no-console From e6a1a0764bbf1ddfe1fb2b0d05a86bbe72d9b002 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 15:59:28 +0200 Subject: [PATCH 72/73] upd --- .../graphiql-react/src/components/provider.tsx | 1 + packages/graphiql-react/src/stores/editor.ts | 3 +++ packages/graphiql-react/src/stores/storage.ts | 17 ++++++++++++++--- .../graphiql-react/src/utility/tabs.spec.ts | 12 +----------- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 492b5ad391..30b88f9412 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -192,6 +192,7 @@ useEffect(() => { schemaDescription, })(...args); const themeSlice = createThemeSlice({ editorTheme })(...args); + // @ts-expect-error -- fixme const storageSlice = createStorageSlice({ storage })(...args); return { ...editorSlice, diff --git a/packages/graphiql-react/src/stores/editor.ts b/packages/graphiql-react/src/stores/editor.ts index b9e8041c39..17c1da6ab1 100644 --- a/packages/graphiql-react/src/stores/editor.ts +++ b/packages/graphiql-react/src/stores/editor.ts @@ -505,6 +505,9 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { return { ...initial, + initialQuery: '', + initialVariables: '', + initialHeaders: '', actions: $actions, }; }; diff --git a/packages/graphiql-react/src/stores/storage.ts b/packages/graphiql-react/src/stores/storage.ts index 471e061350..6061b4bb9e 100644 --- a/packages/graphiql-react/src/stores/storage.ts +++ b/packages/graphiql-react/src/stores/storage.ts @@ -1,6 +1,17 @@ import type { StateCreator } from 'zustand'; -import type { StateStorage } from 'zustand/middleware'; -import type { SlicesWithActions } from '../types'; +import type { PersistStorage, StateStorage } from 'zustand/middleware'; +import type { SlicesWithActions, Theme } from '../types'; +import type { TabState } from '../utility/tabs'; + +export type Storage = PersistStorage; + +interface GraphiQLPersistedState { + activeTabIndex: number; + shouldPersistHeaders: boolean; + tabs: TabState[]; + theme?: Theme; + visiblePlugin?: string; +} export interface StorageSlice { /** @@ -12,7 +23,7 @@ export interface StorageSlice { } export interface StorageProps { - storage?: StorageSlice['storage']; + storage?: Storage; } type CreateStorageSlice = ( diff --git a/packages/graphiql-react/src/utility/tabs.spec.ts b/packages/graphiql-react/src/utility/tabs.spec.ts index 2ea2bdc5a3..9abd85690a 100644 --- a/packages/graphiql-react/src/utility/tabs.spec.ts +++ b/packages/graphiql-react/src/utility/tabs.spec.ts @@ -101,14 +101,7 @@ describe('fuzzyExtractionOperationTitle', () => { describe('getDefaultTabState', () => { it('returns default tab', () => { - expect( - getDefaultTabState({ - defaultQuery: '# Default', - headers: null, - query: null, - variables: null, - }), - ).toEqual({ + expect(getDefaultTabState({ defaultQuery: '# Default' })).toEqual({ activeTabIndex: 0, tabs: [ expect.objectContaining({ @@ -123,7 +116,6 @@ describe('getDefaultTabState', () => { expect( getDefaultTabState({ defaultQuery: '# Default', - headers: null, defaultTabs: [ { headers: null, @@ -136,8 +128,6 @@ describe('getDefaultTabState', () => { variables: null, }, ], - query: null, - variables: null, }), ).toEqual({ activeTabIndex: 0, From e15b7afb93f89f310af8734e90ed724be14d0739 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 23 Jun 2025 16:18:05 +0200 Subject: [PATCH 73/73] fix response editor --- packages/graphiql-react/src/components/response-editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphiql-react/src/components/response-editor.tsx b/packages/graphiql-react/src/components/response-editor.tsx index 93261e55a2..9815d0cc7c 100644 --- a/packages/graphiql-react/src/components/response-editor.tsx +++ b/packages/graphiql-react/src/components/response-editor.tsx @@ -46,7 +46,7 @@ export const ResponseEditor: FC = ({ const ref = useRef(null!); useEffect(() => { if (fetchError) { - responseEditor?.setValue(formatError({ message: fetchError })); + responseEditor?.setValue(fetchError); } if (validationErrors.length) { responseEditor?.setValue(formatError(validationErrors));