From 4fb231fb9619544974d81be9a2e7d92e37ab7426 Mon Sep 17 00:00:00 2001 From: Mathias Klippinge Date: Thu, 15 Aug 2024 13:40:51 +0200 Subject: [PATCH] Allow GraphiQL apps control over closing tabs (#3563) * Allow GraphiQL apps control over closing tabs * Add changeset * Code review + lint * Adds e2e spec for controlling closing of tabs * Apply suggestions from code review * some changes from my git stash * rollback * cspell * fix * rm some previous code * fix tests --------- Co-authored-by: Dimitri POSTOLOV --- .changeset/empty-lobsters-breathe.md | 5 ++ packages/graphiql/cypress/e2e/tabs.cy.ts | 24 ++++++ packages/graphiql/resources/renderExample.js | 21 ++--- packages/graphiql/src/components/GraphiQL.tsx | 86 ++++++++++++++----- resources/custom-words.txt | 1 + 5 files changed, 105 insertions(+), 32 deletions(-) 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..155d7a0f4bf --- /dev/null +++ b/.changeset/empty-lobsters-breathe.md @@ -0,0 +1,5 @@ +--- +'graphiql': minor +--- + +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 4ad14f76d14..13cdba3f066 100644 --- a/packages/graphiql/cypress/e2e/tabs.cy.ts +++ b/packages/graphiql/cypress/e2e/tabs.cy.ts @@ -78,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', 0); + }); + }); }); diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js index 017eea99397..208d6accc2d 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. @@ -48,6 +42,11 @@ function onTabChange(tabsState) { updateURL(); } +function confirmCloseTab(index) { + // eslint-disable-next-line no-alert + return confirm(`Are you sure you want to close tab with index ${index}?`); +} + function updateURL() { const newSearch = Object.entries(parameters) .filter(([_key, value]) => value) @@ -91,6 +90,8 @@ root.render( isHeadersEditorEnabled: true, shouldPersistHeaders: true, inputValueDeprecation: GraphQLVersion.includes('15.5') ? undefined : true, + confirmCloseTab: + parameters.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 cbc85c95775..c108f69624d 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -101,9 +101,9 @@ export type GraphiQLProps = Omit & * * @see https://github.com/graphql/graphiql#usage */ - export function GraphiQL({ dangerouslyAssumeSchemaIsValid, + confirmCloseTab, defaultQuery, defaultTabs, externalFragments, @@ -168,6 +168,7 @@ export function GraphiQL({ variables={variables} > | 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 }); @@ -408,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) { @@ -470,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 ( @@ -482,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 ( - ); @@ -571,22 +623,12 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { > { - executionContext.stop(); - editorContext.changeTab(index); - }} + id={`${TAB_CLASS_PREFIX}${index}`} + onClick={handleTabClick} > {tab.title} - { - if (editorContext.activeTabIndex === index) { - executionContext.stop(); - } - editorContext.closeTab(index); - }} - /> + ))} {addTab} @@ -601,9 +643,9 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) {