Skip to content

Commit

Permalink
move tab state to editor context (#2452)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasheyenbrock authored May 31, 2022
1 parent 8241f56 commit ee0fd8b
Show file tree
Hide file tree
Showing 15 changed files with 531 additions and 509 deletions.
6 changes: 6 additions & 0 deletions .changeset/heavy-kangaroos-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphiql/react': minor
'graphiql': patch
---

Move tab state from `graphiql` into editor context from `@graphiql/react`
Original file line number Diff line number Diff line change
@@ -1,57 +1,57 @@
import { fuzzyExtractOperationTitle } from '../fuzzyExtractOperationTitle';
import { fuzzyExtractOperationName } from '../tabs';

describe('fuzzyExtractionOperationTitle', () => {
describe('without prefix', () => {
it('should extract query names', () => {
expect(fuzzyExtractOperationTitle('query MyExampleQuery() {}')).toEqual(
expect(fuzzyExtractOperationName('query MyExampleQuery() {}')).toEqual(
'MyExampleQuery',
);
});
it('should extract query names with special characters', () => {
expect(fuzzyExtractOperationTitle('query My_ExampleQuery() {}')).toEqual(
expect(fuzzyExtractOperationName('query My_ExampleQuery() {}')).toEqual(
'My_ExampleQuery',
);
});
it('should extract query names with numbers', () => {
expect(fuzzyExtractOperationTitle('query My_3xampleQuery() {}')).toEqual(
expect(fuzzyExtractOperationName('query My_3xampleQuery() {}')).toEqual(
'My_3xampleQuery',
);
});
it('should extract mutation names with numbers', () => {
expect(
fuzzyExtractOperationTitle('mutation My_3xampleQuery() {}'),
fuzzyExtractOperationName('mutation My_3xampleQuery() {}'),
).toEqual('My_3xampleQuery');
});
});
describe('with space prefix', () => {
it('should extract query names', () => {
expect(fuzzyExtractOperationTitle(' query MyExampleQuery() {}')).toEqual(
expect(fuzzyExtractOperationName(' query MyExampleQuery() {}')).toEqual(
'MyExampleQuery',
);
});
it('should extract query names with special characters', () => {
expect(fuzzyExtractOperationTitle(' query My_ExampleQuery() {}')).toEqual(
expect(fuzzyExtractOperationName(' query My_ExampleQuery() {}')).toEqual(
'My_ExampleQuery',
);
});
it('should extract query names with numbers', () => {
expect(fuzzyExtractOperationTitle(' query My_3xampleQuery() {}')).toEqual(
expect(fuzzyExtractOperationName(' query My_3xampleQuery() {}')).toEqual(
'My_3xampleQuery',
);
});
it('should extract mutation names with numbers', () => {
expect(
fuzzyExtractOperationTitle(' mutation My_3xampleQuery() {}'),
fuzzyExtractOperationName(' mutation My_3xampleQuery() {}'),
).toEqual('My_3xampleQuery');
});
});

it('should return null for anonymous queries', () => {
expect(fuzzyExtractOperationTitle('{}')).toEqual('<untitled>');
expect(fuzzyExtractOperationName('{}')).toBeNull();
});
it('should not extract query names with comments', () => {
expect(fuzzyExtractOperationTitle('# query My_3xampleQuery() {}')).toEqual(
'<untitled>',
);
expect(
fuzzyExtractOperationName('# query My_3xampleQuery() {}'),
).toBeNull();
});
});
150 changes: 138 additions & 12 deletions packages/graphiql-react/src/editor/context.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { DocumentNode, OperationDefinitionNode } from 'graphql';
import { VariableToType } from 'graphql-language-service';
import { ReactNode, useMemo, useState } from 'react';
import { ReactNode, useCallback, useMemo, useRef, useState } from 'react';

import { useStorageContext } from '../storage';
import { createContextHook, createNullableContext } from '../utility/context';
import { STORAGE_KEY as STORAGE_KEY_HEADERS } from './header-editor';
import { useSynchronizeValue } from './hooks';
import { STORAGE_KEY_QUERY } from './query-editor';
import {
emptyTab,
getDefaultTabState,
setPropertiesInActiveTab,
TabsState,
TabState,
useSetEditorValues,
useStoreTabs,
useSynchronizeActiveTabValues,
} from './tabs';
import { CodeMirrorEditor } from './types';
import { STORAGE_KEY as STORAGE_KEY_VARIABLES } from './variable-editor';

Expand All @@ -18,6 +28,15 @@ export type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & {
};

export type EditorContextType = {
activeTabIndex: number;
tabs: TabState[];
addTab(): void;
changeTab(index: number): void;
closeTab(index: number): void;
updateActiveTabValues(
partialTab: Partial<Omit<TabState, 'id' | 'hash' | 'title'>>,
): void;

headerEditor: CodeMirrorEditor | null;
queryEditor: CodeMirrorEditorWithOperationFacts | null;
responseEditor: CodeMirrorEditor | null;
Expand All @@ -26,6 +45,7 @@ export type EditorContextType = {
setQueryEditor(newEditor: CodeMirrorEditorWithOperationFacts): void;
setResponseEditor(newEditor: CodeMirrorEditor): void;
setVariableEditor(newEditor: CodeMirrorEditor): void;

initialHeaders: string;
initialQuery: string;
initialVariables: string;
Expand All @@ -39,7 +59,9 @@ type EditorContextProviderProps = {
children: ReactNode;
defaultQuery?: string;
headers?: string;
onTabChange?(tabs: TabsState): void;
query?: string;
shouldPersistHeaders?: boolean;
variables?: string;
};

Expand All @@ -65,19 +87,111 @@ export function EditorContextProvider(props: EditorContextProviderProps) {

// We store this in state but never update it. By passing a function we only
// need to compute it lazily during the initial render.
const [initialValues] = useState(() => ({
initialHeaders: props.headers ?? storage?.get(STORAGE_KEY_HEADERS) ?? '',
initialQuery:
props.query ??
storage?.get(STORAGE_KEY_QUERY) ??
props.defaultQuery ??
DEFAULT_QUERY,
initialVariables:
props.variables ?? storage?.get(STORAGE_KEY_VARIABLES) ?? '',
const [storedEditorValues] = useState(() => ({
headers: props.headers ?? storage?.get(STORAGE_KEY_HEADERS) ?? null,
query: props.query ?? storage?.get(STORAGE_KEY_QUERY) ?? null,
variables: props.variables ?? storage?.get(STORAGE_KEY_VARIABLES) ?? null,
}));

const [tabState, setTabState] = useState<TabsState>(() =>
getDefaultTabState({ ...storedEditorValues, storage }),
);

const storeTabs = useStoreTabs({
storage,
shouldPersistHeaders: props.shouldPersistHeaders,
});
const synchronizeActiveTabValues = useSynchronizeActiveTabValues({
queryEditor,
variableEditor,
headerEditor,
responseEditor,
});
const setEditorValues = useSetEditorValues({
queryEditor,
variableEditor,
headerEditor,
responseEditor,
});
const { onTabChange } = props;

const addTab = useCallback<EditorContextType['addTab']>(() => {
setTabState(current => {
// Make sure the current tab stores the latest values
const updatedValues = synchronizeActiveTabValues(current);
const updated = {
tabs: [...updatedValues.tabs, emptyTab()],
activeTabIndex: updatedValues.tabs.length,
};
storeTabs(updated);
setEditorValues(updated.tabs[updated.activeTabIndex]);
onTabChange?.(updated);
return updated;
});
}, [onTabChange, setEditorValues, storeTabs, synchronizeActiveTabValues]);

const changeTab = useCallback<EditorContextType['changeTab']>(
index => {
setTabState(current => {
const updated = {
...synchronizeActiveTabValues(current),
activeTabIndex: index,
};
storeTabs(updated);
setEditorValues(updated.tabs[updated.activeTabIndex]);
onTabChange?.(updated);
return updated;
});
},
[onTabChange, setEditorValues, storeTabs, synchronizeActiveTabValues],
);

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;
});
},
[onTabChange, setEditorValues, storeTabs],
);

const updateActiveTabValues = useCallback<
EditorContextType['updateActiveTabValues']
>(
partialTab => {
setTabState(current => {
const updated = setPropertiesInActiveTab(current, partialTab);
storeTabs(updated);
onTabChange?.(updated);
return updated;
});
},
[onTabChange, storeTabs],
);

const defaultQuery =
tabState.activeTabIndex > 0 ? '' : props.defaultQuery ?? DEFAULT_QUERY;
const initialValues = useRef({
initialHeaders: storedEditorValues.headers ?? '',
initialQuery: storedEditorValues.query ?? defaultQuery,
initialVariables: storedEditorValues.variables ?? '',
});

const value = useMemo<EditorContextType>(
() => ({
...tabState,
addTab,
changeTab,
closeTab,
updateActiveTabValues,

headerEditor,
queryEditor,
responseEditor,
Expand All @@ -86,9 +200,21 @@ export function EditorContextProvider(props: EditorContextProviderProps) {
setQueryEditor,
setResponseEditor,
setVariableEditor,
...initialValues,

...initialValues.current,
}),
[headerEditor, initialValues, queryEditor, responseEditor, variableEditor],
[
tabState,
addTab,
changeTab,
closeTab,
updateActiveTabValues,

headerEditor,
queryEditor,
responseEditor,
variableEditor,
],
);

return (
Expand Down
2 changes: 2 additions & 0 deletions packages/graphiql-react/src/editor/header-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export function useHeaderEditor({
headerEditor,
onEdit,
shouldPersistHeaders ? STORAGE_KEY : null,
'headers',
useHeaderEditor,
);

useCompletion(headerEditor);
Expand Down
20 changes: 18 additions & 2 deletions packages/graphiql-react/src/editor/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ export function useChangeHandler(
editor: CodeMirrorEditor | null,
callback: EditCallback | undefined,
storageKey: string | null,
tabProperty: 'variables' | 'headers',
caller: Function,
) {
const { updateActiveTabValues } = useEditorContext({ nonNull: true, caller });
const storage = useStorageContext();

useEffect(() => {
if (!editor) {
return;
Expand All @@ -43,14 +47,26 @@ export function useChangeHandler(
storage.set(storageKey, value);
});

const updateTab = debounce(100, (value: string) => {
updateActiveTabValues({ [tabProperty]: value });
});

const handleChange = (editorInstance: CodeMirrorEditor) => {
const newValue = editorInstance.getValue();
callback?.(newValue);
store(newValue);
updateTab(newValue);
callback?.(newValue);
};
editor.on('change', handleChange);
return () => editor.off('change', handleChange);
}, [callback, editor, storage, storageKey]);
}, [
callback,
editor,
storage,
storageKey,
tabProperty,
updateActiveTabValues,
]);
}

export function useCompletion(editor: CodeMirrorEditor | null) {
Expand Down
2 changes: 2 additions & 0 deletions packages/graphiql-react/src/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
ResponseTooltipType,
UseResponseEditorArgs,
} from './response-editor';
import type { TabsState } from './tabs';
import type { UseVariableEditorArgs } from './variable-editor';

export {
Expand All @@ -44,6 +45,7 @@ export {
export type {
EditorContextType,
ResponseTooltipType,
TabsState,
UseHeaderEditorArgs,
UseQueryEditorArgs,
UseResponseEditorArgs,
Expand Down
Loading

0 comments on commit ee0fd8b

Please sign in to comment.