Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow GraphiQL apps control over closing tabs #3563

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/empty-lobsters-breathe.md
@@ -0,0 +1,6 @@
---
'graphiql': minor
'@graphiql/react': minor
---

Allow control of closing tabs
71 changes: 58 additions & 13 deletions packages/graphiql-react/src/editor/context.tsx
Expand Up @@ -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;
Expand All @@ -55,6 +56,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<boolean>;
/**
* Move a tab to a new spot.
* @param newOrder The new order for the tabs.
Expand Down Expand Up @@ -212,6 +221,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<boolean>;
/**
* Invoked when the state of the tabs changes. Possible triggers are:
* - Updating any editor contents inside the currently active tab
Expand Down Expand Up @@ -263,6 +280,7 @@ export type EditorContextProviderProps = {

export function EditorContextProvider(props: EditorContextProviderProps) {
const storage = useStorageContext();
const executionContext = useExecutionContext();
const [headerEditor, setHeaderEditor] = useState<CodeMirrorEditor | null>(
null,
);
Expand Down Expand Up @@ -366,7 +384,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) {
headerEditor,
responseEditor,
});
const { onTabChange, defaultHeaders, children } = props;
const { onTabChange, confirmCloseTab, defaultHeaders, children } = props;

const addTab = useCallback<EditorContextType['addTab']>(() => {
setTabState(current => {
Expand Down Expand Up @@ -422,20 +440,45 @@ export function EditorContextProvider(props: EditorContextProviderProps) {
[onTabChange, setEditorValues, storeTabs],
);

const closeTabConfirmation = useCallback<
EditorContextType['closeTabConfirmation']
>(
async index => {
if (confirmCloseTab) {
const confirmation = await confirmCloseTab(index);
return confirmation;
}
return true;
},
[confirmCloseTab],
);

const closeTab = useCallback<EditorContextType['closeTab']>(
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<
Expand Down Expand Up @@ -497,6 +540,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) {
addTab,
changeTab,
moveTab,
closeTabConfirmation,
closeTab,
updateActiveTabValues,

Expand Down Expand Up @@ -527,6 +571,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) {
addTab,
changeTab,
moveTab,
closeTabConfirmation,
closeTab,
updateActiveTabValues,

Expand Down
2 changes: 2 additions & 0 deletions packages/graphiql-react/src/provider.tsx
Expand Up @@ -22,6 +22,7 @@ export type GraphiQLProviderProps = EditorContextProviderProps &

export function GraphiQLProvider({
children,
confirmCloseTab,
dangerouslyAssumeSchemaIsValid,
defaultQuery,
defaultHeaders,
Expand Down Expand Up @@ -53,6 +54,7 @@ export function GraphiQLProvider({
<StorageContextProvider storage={storage}>
<HistoryContextProvider maxHistoryLength={maxHistoryLength}>
<EditorContextProvider
confirmCloseTab={confirmCloseTab}
defaultQuery={defaultQuery}
defaultHeaders={defaultHeaders}
defaultTabs={defaultTabs}
Expand Down
9 changes: 3 additions & 6 deletions packages/graphiql/src/components/GraphiQL.tsx
Expand Up @@ -102,6 +102,7 @@ export type GraphiQLProps = Omit<GraphiQLProviderProps, 'children'> &

export function GraphiQL({
dangerouslyAssumeSchemaIsValid,
confirmCloseTab,
defaultQuery,
defaultTabs,
externalFragments,
Expand Down Expand Up @@ -138,6 +139,7 @@ export function GraphiQL({

return (
<GraphiQLProvider
confirmCloseTab={confirmCloseTab}
getDefaultFieldNames={getDefaultFieldNames}
dangerouslyAssumeSchemaIsValid={dangerouslyAssumeSchemaIsValid}
defaultQuery={defaultQuery}
Expand Down Expand Up @@ -545,12 +547,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) {
{tab.title}
</Tab.Button>
<Tab.Close
onClick={() => {
if (editorContext.activeTabIndex === index) {
executionContext.stop();
}
editorContext.closeTab(index);
}}
onClick={() => editorContext.closeTab(index)}
/>
</Tab>
))}
Expand Down