From 5d381c57b88951a3a253f26139bdf7a830fa18b0 Mon Sep 17 00:00:00 2001 From: Mathias Klippinge Date: Wed, 13 Mar 2024 14:57:21 +0100 Subject: [PATCH 01/11] Allow GraphiQL apps control over closing tabs --- .../graphiql-react/src/editor/context.tsx | 30 +++++++++++++++++++ packages/graphiql-react/src/provider.tsx | 2 ++ packages/graphiql/src/components/GraphiQL.tsx | 14 ++++++--- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index b592dd680da..ecfcbe850bf 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -55,6 +55,14 @@ export type EditorContextType = TabsState & { * @param index The index of the tab that should be switched to. */ changeTab(index: number): void; + /** + * When the user clicks a close tab button, this function is invoked with + * the index of the tab that is about to be closed. It returns a promise + * that should resolve to `true` (meaning the tab may be closed) or `false` + * (meaning the tab may not be closed). + * @param index The index of the tab that should be closed. + */ + closeTabConfirmation(index: number): Promise; /** * Move a tab to a new spot. * @param newOrder The new order for the tabs. @@ -212,6 +220,14 @@ export type EditorContextProviderProps = { * @param operationName The operation name after it has been changed. */ onEditOperationName?(operationName: string): void; + /** + * When the user clicks a close tab button, this function is invoked with + * the index of the tab that is about to be closed. It returns a promise + * that should resolve to `true` (meaning the tab may be closed) or `false` + * (meaning the tab may not be closed). + * @param index The index of the tab that should be closed. + */ + confirmCloseTab?(index: number): Promise; /** * Invoked when the state of the tabs changes. Possible triggers are: * - Updating any editor contents inside the currently active tab @@ -423,6 +439,19 @@ export function EditorContextProvider(props: EditorContextProviderProps) { [onTabChange, setEditorValues, storeTabs], ); + const closeTabConfirmation = useCallback< + EditorContextType['closeTabConfirmation'] + >( + async index => { + if (props.confirmCloseTab) { + const confirmation = await props.confirmCloseTab(index); + return confirmation; + } + return true; + }, + [props.confirmCloseTab], + ); + const closeTab = useCallback( index => { setTabState(current => { @@ -498,6 +527,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { addTab, changeTab, moveTab, + closeTabConfirmation, closeTab, updateActiveTabValues, diff --git a/packages/graphiql-react/src/provider.tsx b/packages/graphiql-react/src/provider.tsx index ead1907de8e..6e19c013658 100644 --- a/packages/graphiql-react/src/provider.tsx +++ b/packages/graphiql-react/src/provider.tsx @@ -22,6 +22,7 @@ export type GraphiQLProviderProps = EditorContextProviderProps & export function GraphiQLProvider({ children, + confirmCloseTab, dangerouslyAssumeSchemaIsValid, defaultQuery, defaultHeaders, @@ -53,6 +54,7 @@ export function GraphiQLProvider({ & export function GraphiQL({ dangerouslyAssumeSchemaIsValid, + confirmCloseTab, defaultQuery, defaultTabs, externalFragments, @@ -140,6 +141,7 @@ export function GraphiQL({ return ( { - if (editorContext.activeTabIndex === index) { - executionContext.stop(); + onClick={async () => { + if ( + await editorContext.closeTabConfirmation(index) + ) { + if (editorContext.activeTabIndex === index) { + executionContext.stop(); + } + editorContext.closeTab(index); } - editorContext.closeTab(index); }} /> From 51dab39d23724c209eeeaa0f1cc28082c6b2388e Mon Sep 17 00:00:00 2001 From: Mathias Klippinge Date: Tue, 19 Mar 2024 16:42:40 +0100 Subject: [PATCH 02/11] Add changeset --- .changeset/empty-lobsters-breathe.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/empty-lobsters-breathe.md diff --git a/.changeset/empty-lobsters-breathe.md b/.changeset/empty-lobsters-breathe.md new file mode 100644 index 00000000000..4baf84a4412 --- /dev/null +++ b/.changeset/empty-lobsters-breathe.md @@ -0,0 +1,6 @@ +--- +'graphiql': minor +'@graphiql/react': minor +--- + +Allow control of closing tabs From 8175e0082961fbbc2d16d99b56742f68c4ae426e Mon Sep 17 00:00:00 2001 From: Mathias Klippinge Date: Wed, 20 Mar 2024 13:36:27 +0100 Subject: [PATCH 03/11] Code review + lint --- .../graphiql-react/src/editor/context.tsx | 47 ++++++++++++------- packages/graphiql/src/components/GraphiQL.tsx | 11 +---- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index ecfcbe850bf..1fc33d31fa9 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -37,6 +37,7 @@ import { } from './tabs'; import { CodeMirrorEditor } from './types'; import { STORAGE_KEY as STORAGE_KEY_VARIABLES } from './variable-editor'; +import { useExecutionContext } from '../execution'; export type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & { documentAST: DocumentNode | null; @@ -279,6 +280,7 @@ export type EditorContextProviderProps = { export function EditorContextProvider(props: EditorContextProviderProps) { const storage = useStorageContext(); + const executionContext = useExecutionContext(); const [headerEditor, setHeaderEditor] = useState( null, ); @@ -376,7 +378,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { headerEditor, responseEditor, }); - const { onTabChange, defaultHeaders, children } = props; + const { onTabChange, confirmCloseTab, defaultHeaders, children } = props; const setEditorValues = useSetEditorValues({ queryEditor, variableEditor, @@ -443,29 +445,41 @@ export function EditorContextProvider(props: EditorContextProviderProps) { EditorContextType['closeTabConfirmation'] >( async index => { - if (props.confirmCloseTab) { - const confirmation = await props.confirmCloseTab(index); + if (confirmCloseTab) { + const confirmation = await confirmCloseTab(index); return confirmation; } return true; }, - [props.confirmCloseTab], + [confirmCloseTab], ); const closeTab = useCallback( - index => { - setTabState(current => { - const updated = { - tabs: current.tabs.filter((_tab, i) => index !== i), - activeTabIndex: Math.max(current.activeTabIndex - 1, 0), - }; - storeTabs(updated); - setEditorValues(updated.tabs[updated.activeTabIndex]); - onTabChange?.(updated); - return updated; - }); + async index => { + if (await closeTabConfirmation(index)) { + if (index === tabState.activeTabIndex) { + executionContext?.stop(); + } + setTabState(current => { + const updated = { + tabs: current.tabs.filter((_tab, i) => index !== i), + activeTabIndex: Math.max(current.activeTabIndex - 1, 0), + }; + storeTabs(updated); + setEditorValues(updated.tabs[updated.activeTabIndex]); + onTabChange?.(updated); + return updated; + }); + } }, - [onTabChange, setEditorValues, storeTabs], + [ + onTabChange, + setEditorValues, + storeTabs, + closeTabConfirmation, + tabState.activeTabIndex, + executionContext, + ], ); const updateActiveTabValues = useCallback< @@ -558,6 +572,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { addTab, changeTab, moveTab, + closeTabConfirmation, closeTab, updateActiveTabValues, diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 4d791adabb5..36f50914b93 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -582,16 +582,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { {tab.title} { - if ( - await editorContext.closeTabConfirmation(index) - ) { - if (editorContext.activeTabIndex === index) { - executionContext.stop(); - } - editorContext.closeTab(index); - } - }} + onClick={() => editorContext.closeTab(index)} /> ))} From 0387001a28990d54a8d5b2b387bb32a56f051bc1 Mon Sep 17 00:00:00 2001 From: Mathias Klippinge Date: Wed, 14 Aug 2024 09:29:22 +0200 Subject: [PATCH 04/11] Adds e2e spec for controlling closing of tabs --- packages/graphiql/cypress/e2e/tabs.cy.ts | 26 +++++++++++++++++++- packages/graphiql/resources/renderExample.js | 9 +++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/graphiql/cypress/e2e/tabs.cy.ts b/packages/graphiql/cypress/e2e/tabs.cy.ts index 4ad14f76d14..1b3528db886 100644 --- a/packages/graphiql/cypress/e2e/tabs.cy.ts +++ b/packages/graphiql/cypress/e2e/tabs.cy.ts @@ -1,5 +1,23 @@ describe('Tabs', () => { it('Should store editor contents when switching between tabs', () => { + let count = 0; + + cy.on('window:confirm', str => { + count += 1; + switch (count) { + case 1: + expect(str).to.eq('Are you sure you want to close this tab?'); + // reject the initial attempt to close the tab + return false; + case 2: + expect(str).to.eq('Are you sure you want to close this tab?'); + // approve the second attempt to close the tab + return true; + default: + return true; + } + }); + cy.visit('/?query='); // Assert that no tab visible when there's only one session @@ -64,7 +82,13 @@ describe('Tabs', () => { response: { data: { image: '/images/logo.svg' } }, }); - // Close tab + // Close tab (this will get rejected) + cy.get('#graphiql-session-tab-1 + .graphiql-tab-close').click(); + + // Tab is still visible + cy.get('#graphiql-session-tab-1 + .graphiql-tab-close').should('exist'); + + // Close tab (this will get accepted) cy.get('#graphiql-session-tab-1 + .graphiql-tab-close').click(); // Assert that no tab visible when there's only one session diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js index b2fc32c7d25..162465b529d 100644 --- a/packages/graphiql/resources/renderExample.js +++ b/packages/graphiql/resources/renderExample.js @@ -48,6 +48,14 @@ function onTabChange(tabsState) { updateURL(); } +async function confirmCloseTab(_index) { + // eslint-disable-next-line no-alert + if (window.confirm('Are you sure you want to close this tab?')) { + return true; + } + return false; +} + function updateURL() { const newSearch = Object.entries(parameters) .filter(([_key, value]) => value) @@ -101,6 +109,7 @@ root.render( isHeadersEditorEnabled: true, shouldPersistHeaders: true, inputValueDeprecation: GraphQLVersion.includes('15.5') ? undefined : true, + confirmCloseTab, onTabChange, forcedTheme: parameters.forcedTheme, }), From 4bfb724e279cdb595ecd6bf9c850329187cbff41 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 15 Aug 2024 10:30:50 +0200 Subject: [PATCH 05/11] Apply suggestions from code review --- .changeset/empty-lobsters-breathe.md | 2 +- packages/graphiql/cypress/e2e/tabs.cy.ts | 24 ++++++++++++++++++++ packages/graphiql/resources/renderExample.js | 13 ++++++----- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/.changeset/empty-lobsters-breathe.md b/.changeset/empty-lobsters-breathe.md index 4baf84a4412..69b24dd7c46 100644 --- a/.changeset/empty-lobsters-breathe.md +++ b/.changeset/empty-lobsters-breathe.md @@ -3,4 +3,4 @@ '@graphiql/react': minor --- -Allow control of closing tabs +Add new prop `confirmCloseTab` to allow control of closing tabs diff --git a/packages/graphiql/cypress/e2e/tabs.cy.ts b/packages/graphiql/cypress/e2e/tabs.cy.ts index 1b3528db886..aaad3a0355b 100644 --- a/packages/graphiql/cypress/e2e/tabs.cy.ts +++ b/packages/graphiql/cypress/e2e/tabs.cy.ts @@ -1,4 +1,28 @@ describe('Tabs', () => { + describe('confirmCloseTab()', () => { + it('should keep tab when `Cancel` was clicked', () => { + cy.on('window:confirm', () => false); + cy.visit('/?confirmCloseTab=true'); + + cy.get('.graphiql-tab-add').click(); + + cy.get('.graphiql-tab-button + .graphiql-tab-close').eq(1).click(); + + cy.get('.graphiql-tab-button').should('have.length', 2); + }); + + it('should close tab when `OK` was clicked', () => { + cy.on('window:confirm', () => true); + cy.visit('/?confirmCloseTab=true'); + + cy.get('.graphiql-tab-add').click(); + + cy.get('.graphiql-tab-button + .graphiql-tab-close').eq(1).click(); + + cy.get('.graphiql-tab-button').should('have.length', 1); + }); + }) + it('Should store editor contents when switching between tabs', () => { let count = 0; diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js index 162465b529d..ac45284b7db 100644 --- a/packages/graphiql/resources/renderExample.js +++ b/packages/graphiql/resources/renderExample.js @@ -48,14 +48,15 @@ function onTabChange(tabsState) { updateURL(); } -async function confirmCloseTab(_index) { +function confirmCloseTab(index) { // eslint-disable-next-line no-alert - if (window.confirm('Are you sure you want to close this tab?')) { - return true; - } - return false; + return confirm(`Are you sure you want to close tab with index ${index}?`); } +const searchParams = Object.fromEntries( + new URLSearchParams(location.search).entries(), +); + function updateURL() { const newSearch = Object.entries(parameters) .filter(([_key, value]) => value) @@ -109,7 +110,7 @@ root.render( isHeadersEditorEnabled: true, shouldPersistHeaders: true, inputValueDeprecation: GraphQLVersion.includes('15.5') ? undefined : true, - confirmCloseTab, + confirmCloseTab: searchParams.confirmCloseTab === 'true' ? confirmCloseTab : undefined, onTabChange, forcedTheme: parameters.forcedTheme, }), From 9d1de61b8f8397fa28080f5f1850d062738f94db Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 15 Aug 2024 10:35:50 +0200 Subject: [PATCH 06/11] some changes from my git stash --- .changeset/empty-lobsters-breathe.md | 1 - .../graphiql-react/src/editor/context.tsx | 57 +++++-------- packages/graphiql/cypress/e2e/tabs.cy.ts | 74 ++++++----------- packages/graphiql/resources/renderExample.js | 3 +- packages/graphiql/src/components/GraphiQL.tsx | 81 ++++++++++++++----- 5 files changed, 111 insertions(+), 105 deletions(-) diff --git a/.changeset/empty-lobsters-breathe.md b/.changeset/empty-lobsters-breathe.md index 69b24dd7c46..155d7a0f4bf 100644 --- a/.changeset/empty-lobsters-breathe.md +++ b/.changeset/empty-lobsters-breathe.md @@ -1,6 +1,5 @@ --- 'graphiql': minor -'@graphiql/react': minor --- Add new prop `confirmCloseTab` to allow control of closing tabs diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index 1fc33d31fa9..1ee0a8eeead 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -37,7 +37,6 @@ import { } from './tabs'; import { CodeMirrorEditor } from './types'; import { STORAGE_KEY as STORAGE_KEY_VARIABLES } from './variable-editor'; -import { useExecutionContext } from '../execution'; export type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & { documentAST: DocumentNode | null; @@ -88,7 +87,7 @@ export type EditorContextType = TabsState & { ): void; /** - * The CodeMirror editor instance for the headers editor. + * The CodeMirror editor instance for the headers' editor. */ headerEditor: CodeMirrorEditor | null; /** @@ -102,11 +101,11 @@ export type EditorContextType = TabsState & { */ responseEditor: CodeMirrorEditor | null; /** - * The CodeMirror editor instance for the variables editor. + * The CodeMirror editor instance for the variables' editor. */ variableEditor: CodeMirrorEditor | null; /** - * Set the CodeMirror editor instance for the headers editor. + * Set the CodeMirror editor instance for the headers' editor. */ setHeaderEditor(newEditor: CodeMirrorEditor): void; /** @@ -118,7 +117,7 @@ export type EditorContextType = TabsState & { */ setResponseEditor(newEditor: CodeMirrorEditor): void; /** - * Set the CodeMirror editor instance for the variables editor. + * Set the CodeMirror editor instance for the variables' editor. */ setVariableEditor(newEditor: CodeMirrorEditor): void; @@ -216,7 +215,7 @@ export type EditorContextProviderProps = { /** * Invoked when the operation name changes. Possible triggers are: * - Editing the contents of the query editor - * - Selecting a operation for execution in a document that contains multiple + * - Selecting an operation for execution in a document that contains multiple * operation definitions * @param operationName The operation name after it has been changed. */ @@ -280,7 +279,6 @@ export type EditorContextProviderProps = { export function EditorContextProvider(props: EditorContextProviderProps) { const storage = useStorageContext(); - const executionContext = useExecutionContext(); const [headerEditor, setHeaderEditor] = useState( null, ); @@ -378,7 +376,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { headerEditor, responseEditor, }); - const { onTabChange, confirmCloseTab, defaultHeaders, children } = props; + const { onTabChange, defaultHeaders, children } = props; const setEditorValues = useSetEditorValues({ queryEditor, variableEditor, @@ -445,41 +443,29 @@ export function EditorContextProvider(props: EditorContextProviderProps) { EditorContextType['closeTabConfirmation'] >( async index => { - if (confirmCloseTab) { - const confirmation = await confirmCloseTab(index); + if (props.confirmCloseTab) { + const confirmation = await props.confirmCloseTab(index); return confirmation; } return true; }, - [confirmCloseTab], + [props.confirmCloseTab], ); const closeTab = useCallback( - async index => { - if (await closeTabConfirmation(index)) { - if (index === tabState.activeTabIndex) { - executionContext?.stop(); - } - setTabState(current => { - const updated = { - tabs: current.tabs.filter((_tab, i) => index !== i), - activeTabIndex: Math.max(current.activeTabIndex - 1, 0), - }; - storeTabs(updated); - setEditorValues(updated.tabs[updated.activeTabIndex]); - onTabChange?.(updated); - return updated; - }); - } + index => { + setTabState(current => { + const updated = { + tabs: current.tabs.filter((_tab, i) => index !== i), + activeTabIndex: Math.max(current.activeTabIndex - 1, 0), + }; + storeTabs(updated); + setEditorValues(updated.tabs[updated.activeTabIndex]); + onTabChange?.(updated); + return updated; + }); }, - [ - onTabChange, - setEditorValues, - storeTabs, - closeTabConfirmation, - tabState.activeTabIndex, - executionContext, - ], + [onTabChange, setEditorValues, storeTabs], ); const updateActiveTabValues = useCallback< @@ -572,7 +558,6 @@ export function EditorContextProvider(props: EditorContextProviderProps) { addTab, changeTab, moveTab, - closeTabConfirmation, closeTab, updateActiveTabValues, diff --git a/packages/graphiql/cypress/e2e/tabs.cy.ts b/packages/graphiql/cypress/e2e/tabs.cy.ts index aaad3a0355b..2a66d152501 100644 --- a/packages/graphiql/cypress/e2e/tabs.cy.ts +++ b/packages/graphiql/cypress/e2e/tabs.cy.ts @@ -1,47 +1,5 @@ describe('Tabs', () => { - describe('confirmCloseTab()', () => { - it('should keep tab when `Cancel` was clicked', () => { - cy.on('window:confirm', () => false); - cy.visit('/?confirmCloseTab=true'); - - cy.get('.graphiql-tab-add').click(); - - cy.get('.graphiql-tab-button + .graphiql-tab-close').eq(1).click(); - - cy.get('.graphiql-tab-button').should('have.length', 2); - }); - - it('should close tab when `OK` was clicked', () => { - cy.on('window:confirm', () => true); - cy.visit('/?confirmCloseTab=true'); - - cy.get('.graphiql-tab-add').click(); - - cy.get('.graphiql-tab-button + .graphiql-tab-close').eq(1).click(); - - cy.get('.graphiql-tab-button').should('have.length', 1); - }); - }) - it('Should store editor contents when switching between tabs', () => { - let count = 0; - - cy.on('window:confirm', str => { - count += 1; - switch (count) { - case 1: - expect(str).to.eq('Are you sure you want to close this tab?'); - // reject the initial attempt to close the tab - return false; - case 2: - expect(str).to.eq('Are you sure you want to close this tab?'); - // approve the second attempt to close the tab - return true; - default: - return true; - } - }); - cy.visit('/?query='); // Assert that no tab visible when there's only one session @@ -106,13 +64,7 @@ describe('Tabs', () => { response: { data: { image: '/images/logo.svg' } }, }); - // Close tab (this will get rejected) - cy.get('#graphiql-session-tab-1 + .graphiql-tab-close').click(); - - // Tab is still visible - cy.get('#graphiql-session-tab-1 + .graphiql-tab-close').should('exist'); - - // Close tab (this will get accepted) + // Close tab cy.get('#graphiql-session-tab-1 + .graphiql-tab-close').click(); // Assert that no tab visible when there's only one session @@ -126,4 +78,28 @@ describe('Tabs', () => { response: { data: { id: 'abc123' } }, }); }); + + describe('confirmCloseTab()', () => { + it('should keep tab when `Cancel` was clicked', () => { + cy.on('window:confirm', () => false); + cy.visit('/?confirmCloseTab=true'); + + cy.get('.graphiql-tab-add').click(); + + cy.get('.graphiql-tab-button + .graphiql-tab-close').eq(1).click(); + + cy.get('.graphiql-tab-button').should('have.length', 2); + }); + + it('should close tab when `OK` was clicked', () => { + cy.on('window:confirm', () => true); + cy.visit('/?confirmCloseTab=true'); + + cy.get('.graphiql-tab-add').click(); + + cy.get('.graphiql-tab-button + .graphiql-tab-close').eq(1).click(); + + cy.get('.graphiql-tab-button').should('have.length', 1); + }); + }); }); diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js index ac45284b7db..b1fdcd5c641 100644 --- a/packages/graphiql/resources/renderExample.js +++ b/packages/graphiql/resources/renderExample.js @@ -110,7 +110,8 @@ root.render( isHeadersEditorEnabled: true, shouldPersistHeaders: true, inputValueDeprecation: GraphQLVersion.includes('15.5') ? undefined : true, - confirmCloseTab: searchParams.confirmCloseTab === 'true' ? confirmCloseTab : undefined, + confirmCloseTab: + searchParams.confirmCloseTab === 'true' ? confirmCloseTab : undefined, onTabChange, forcedTheme: parameters.forcedTheme, }), diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 36f50914b93..c108f69624d 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -101,7 +101,6 @@ export type GraphiQLProps = Omit & * * @see https://github.com/graphql/graphiql#usage */ - export function GraphiQL({ dangerouslyAssumeSchemaIsValid, confirmCloseTab, @@ -141,7 +140,6 @@ export function GraphiQL({ return ( | boolean; }; const THEMES = ['light', 'dark', 'system'] as const; +const TAB_CLASS_PREFIX = 'graphiql-session-tab-'; + export function GraphiQLInterface(props: GraphiQLInterfaceProps) { const isHeadersEditorEnabled = props.isHeadersEditorEnabled ?? true; const editorContext = useEditorContext({ nonNull: true }); @@ -410,9 +419,9 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { ); const handlePluginClick: MouseEventHandler = useCallback( - e => { + event => { const context = pluginContext!; - const pluginIndex = Number(e.currentTarget.dataset.index!); + const pluginIndex = Number(event.currentTarget.dataset.index!); const plugin = context.plugins.find((_, index) => pluginIndex === index)!; const isVisible = plugin === context.visiblePlugin; if (isVisible) { @@ -472,6 +481,48 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { ); const className = props.className ? ` ${props.className}` : ''; + const confirmClose = props.confirmCloseTab; + + const handleTabClose: MouseEventHandler = useCallback( + async event => { + const tabButton = event.currentTarget + .previousSibling as HTMLButtonElement; + const index = Number(tabButton.id.replace(TAB_CLASS_PREFIX, '')); + + /** TODO: + * Move everything after into `editorContext.closeTab` once zustand will be used instead of + * React context, since now we can't use execution context inside editor context, since editor + * context is used in execution context. + */ + const shouldCloseTab = confirmClose ? await confirmClose(index) : true; + + if (!shouldCloseTab) { + return; + } + + if (editorContext.activeTabIndex === index) { + executionContext.stop(); + } + editorContext.closeTab(index); + }, + [confirmClose, editorContext, executionContext], + ); + + const handleTabClick: MouseEventHandler = useCallback( + event => { + const index = Number( + event.currentTarget.id.replace(TAB_CLASS_PREFIX, ''), + ); + /** TODO: + * Move everything after into `editorContext.changeTab` once zustand will be used instead of + * React context, since now we can't use execution context inside editor context, since editor + * context is used in execution context. + */ + executionContext.stop(); + editorContext.changeTab(index); + }, + [editorContext, executionContext], + ); return ( @@ -484,7 +535,6 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { {pluginContext?.plugins.map((plugin, index) => { const isVisible = plugin === pluginContext.visiblePlugin; const label = `${isVisible ? 'Hide' : 'Show'} ${plugin.title}`; - const Icon = plugin.icon; return ( - ); @@ -573,17 +623,12 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { > { - executionContext.stop(); - editorContext.changeTab(index); - }} + id={`${TAB_CLASS_PREFIX}${index}`} + onClick={handleTabClick} > {tab.title} - editorContext.closeTab(index)} - /> + ))} {addTab} @@ -598,9 +643,9 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) {
Date: Thu, 15 Aug 2024 10:42:31 +0200 Subject: [PATCH 07/11] rollback --- .../graphiql-react/src/editor/context.tsx | 43 +++---------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index 1ee0a8eeead..26f4d232459 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -55,14 +55,6 @@ export type EditorContextType = TabsState & { * @param index The index of the tab that should be switched to. */ changeTab(index: number): void; - /** - * When the user clicks a close tab button, this function is invoked with - * the index of the tab that is about to be closed. It returns a promise - * that should resolve to `true` (meaning the tab may be closed) or `false` - * (meaning the tab may not be closed). - * @param index The index of the tab that should be closed. - */ - closeTabConfirmation(index: number): Promise; /** * Move a tab to a new spot. * @param newOrder The new order for the tabs. @@ -87,7 +79,7 @@ export type EditorContextType = TabsState & { ): void; /** - * The CodeMirror editor instance for the headers' editor. + * The CodeMirror editor instance for the headers editor. */ headerEditor: CodeMirrorEditor | null; /** @@ -101,11 +93,11 @@ export type EditorContextType = TabsState & { */ responseEditor: CodeMirrorEditor | null; /** - * The CodeMirror editor instance for the variables' editor. + * The CodeMirror editor instance for the variables editor. */ variableEditor: CodeMirrorEditor | null; /** - * Set the CodeMirror editor instance for the headers' editor. + * Set the CodeMirror editor instance for the headers editor. */ setHeaderEditor(newEditor: CodeMirrorEditor): void; /** @@ -117,7 +109,7 @@ export type EditorContextType = TabsState & { */ setResponseEditor(newEditor: CodeMirrorEditor): void; /** - * Set the CodeMirror editor instance for the variables' editor. + * Set the CodeMirror editor instance for the variables editor. */ setVariableEditor(newEditor: CodeMirrorEditor): void; @@ -215,19 +207,11 @@ export type EditorContextProviderProps = { /** * Invoked when the operation name changes. Possible triggers are: * - Editing the contents of the query editor - * - Selecting an operation for execution in a document that contains multiple + * - Selecting a operation for execution in a document that contains multiple * operation definitions * @param operationName The operation name after it has been changed. */ onEditOperationName?(operationName: string): void; - /** - * When the user clicks a close tab button, this function is invoked with - * the index of the tab that is about to be closed. It returns a promise - * that should resolve to `true` (meaning the tab may be closed) or `false` - * (meaning the tab may not be closed). - * @param index The index of the tab that should be closed. - */ - confirmCloseTab?(index: number): Promise; /** * Invoked when the state of the tabs changes. Possible triggers are: * - Updating any editor contents inside the currently active tab @@ -376,14 +360,13 @@ export function EditorContextProvider(props: EditorContextProviderProps) { headerEditor, responseEditor, }); - const { onTabChange, defaultHeaders, children } = props; const setEditorValues = useSetEditorValues({ queryEditor, variableEditor, headerEditor, responseEditor, - defaultHeaders, }); + const { onTabChange, defaultHeaders, children } = props; const addTab = useCallback(() => { setTabState(current => { @@ -439,19 +422,6 @@ export function EditorContextProvider(props: EditorContextProviderProps) { [onTabChange, setEditorValues, storeTabs], ); - const closeTabConfirmation = useCallback< - EditorContextType['closeTabConfirmation'] - >( - async index => { - if (props.confirmCloseTab) { - const confirmation = await props.confirmCloseTab(index); - return confirmation; - } - return true; - }, - [props.confirmCloseTab], - ); - const closeTab = useCallback( index => { setTabState(current => { @@ -527,7 +497,6 @@ export function EditorContextProvider(props: EditorContextProviderProps) { addTab, changeTab, moveTab, - closeTabConfirmation, closeTab, updateActiveTabValues, From 1fea3a6222858d1403873c428a21de2e10f8aa42 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 15 Aug 2024 10:43:29 +0200 Subject: [PATCH 08/11] cspell --- resources/custom-words.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/custom-words.txt b/resources/custom-words.txt index a1a3f38758d..ce224a9e305 100644 --- a/resources/custom-words.txt +++ b/resources/custom-words.txt @@ -224,6 +224,7 @@ wgutils wincent wonka yoshiakis +zustand zdravo Здорово أهلاً From efa7d477570aad7b89f6d40f9815bbc7f25916ed Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 15 Aug 2024 10:44:53 +0200 Subject: [PATCH 09/11] fix --- packages/graphiql-react/src/editor/context.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index 26f4d232459..b592dd680da 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -360,13 +360,14 @@ export function EditorContextProvider(props: EditorContextProviderProps) { headerEditor, responseEditor, }); + const { onTabChange, defaultHeaders, children } = props; const setEditorValues = useSetEditorValues({ queryEditor, variableEditor, headerEditor, responseEditor, + defaultHeaders, }); - const { onTabChange, defaultHeaders, children } = props; const addTab = useCallback(() => { setTabState(current => { From 51dd93e6867ce582080514e3405c9eaa19f58dec Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Thu, 15 Aug 2024 10:51:06 +0200 Subject: [PATCH 10/11] rm some previous code --- packages/graphiql-react/src/provider.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/graphiql-react/src/provider.tsx b/packages/graphiql-react/src/provider.tsx index 6e19c013658..ead1907de8e 100644 --- a/packages/graphiql-react/src/provider.tsx +++ b/packages/graphiql-react/src/provider.tsx @@ -22,7 +22,6 @@ export type GraphiQLProviderProps = EditorContextProviderProps & export function GraphiQLProvider({ children, - confirmCloseTab, dangerouslyAssumeSchemaIsValid, defaultQuery, defaultHeaders, @@ -54,7 +53,6 @@ export function GraphiQLProvider({ Date: Thu, 15 Aug 2024 13:28:12 +0200 Subject: [PATCH 11/11] fix tests --- packages/graphiql/cypress/e2e/tabs.cy.ts | 2 +- packages/graphiql/resources/renderExample.js | 20 +++++--------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/graphiql/cypress/e2e/tabs.cy.ts b/packages/graphiql/cypress/e2e/tabs.cy.ts index 2a66d152501..13cdba3f066 100644 --- a/packages/graphiql/cypress/e2e/tabs.cy.ts +++ b/packages/graphiql/cypress/e2e/tabs.cy.ts @@ -99,7 +99,7 @@ describe('Tabs', () => { cy.get('.graphiql-tab-button + .graphiql-tab-close').eq(1).click(); - cy.get('.graphiql-tab-button').should('have.length', 1); + cy.get('.graphiql-tab-button').should('have.length', 0); }); }); }); diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js index b1fdcd5c641..99c1fffea90 100644 --- a/packages/graphiql/resources/renderExample.js +++ b/packages/graphiql/resources/renderExample.js @@ -8,20 +8,14 @@ * * It is used by: * - the netlify demo - * - end to end tests + * - end-to-end tests * - webpack dev server */ // Parse the search string to get url parameters. -const parameters = {}; -for (const entry of window.location.search.slice(1).split('&')) { - const eq = entry.indexOf('='); - if (eq >= 0) { - parameters[decodeURIComponent(entry.slice(0, eq))] = decodeURIComponent( - entry.slice(eq + 1), - ); - } -} +const parameters = Object.fromEntries( + new URLSearchParams(location.search).entries(), +); // When the query and variables string is edited, update the URL bar so // that it can be easily shared. @@ -53,10 +47,6 @@ function confirmCloseTab(index) { return confirm(`Are you sure you want to close tab with index ${index}?`); } -const searchParams = Object.fromEntries( - new URLSearchParams(location.search).entries(), -); - function updateURL() { const newSearch = Object.entries(parameters) .filter(([_key, value]) => value) @@ -111,7 +101,7 @@ root.render( shouldPersistHeaders: true, inputValueDeprecation: GraphQLVersion.includes('15.5') ? undefined : true, confirmCloseTab: - searchParams.confirmCloseTab === 'true' ? confirmCloseTab : undefined, + parameters.confirmCloseTab === 'true' ? confirmCloseTab : undefined, onTabChange, forcedTheme: parameters.forcedTheme, }),