From c3571a716337419e6b69219d8daca6fc07add983 Mon Sep 17 00:00:00 2001 From: Valentin Agachi Date: Sun, 16 Oct 2022 15:48:20 +0200 Subject: [PATCH 1/3] Initial tabs --- .../src/editor/__tests__/tabs.spec.ts | 88 ++++++++++++++++++- .../graphiql-react/src/editor/context.tsx | 7 ++ packages/graphiql-react/src/editor/tabs.ts | 39 ++++---- packages/graphiql-react/src/provider.tsx | 2 + packages/graphiql/src/components/GraphiQL.tsx | 2 + .../components/__tests__/GraphiQL.spec.tsx | 22 +++++ 6 files changed, 143 insertions(+), 17 deletions(-) diff --git a/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts b/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts index a5cef55fbcc..410180afb6a 100644 --- a/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts +++ b/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts @@ -1,4 +1,30 @@ -import { fuzzyExtractOperationName } from '../tabs'; +import { + createTab, + fuzzyExtractOperationName, + getDefaultTabState, +} from '../tabs'; + +describe('createTab', () => { + it('creates with default title', () => { + expect(createTab({})).toEqual( + expect.objectContaining({ + id: expect.any(String), + hash: expect.any(String), + title: '', + }), + ); + }); + + it('creates with title from query', () => { + expect(createTab({ query: 'query Foo {}' })).toEqual( + expect.objectContaining({ + id: expect.any(String), + hash: expect.any(String), + title: 'Foo', + }), + ); + }); +}); describe('fuzzyExtractionOperationTitle', () => { describe('without prefix', () => { @@ -55,3 +81,63 @@ describe('fuzzyExtractionOperationTitle', () => { ).toBeNull(); }); }); + +describe('getDefaultTabState', () => { + it('returns default tab', () => { + expect( + getDefaultTabState({ + defaultQuery: '# Default', + headers: null, + query: null, + variables: null, + storage: null, + }), + ).toEqual({ + activeTabIndex: 0, + tabs: [ + expect.objectContaining({ + query: '# Default', + title: '', + }), + ], + }); + }); + + it('returns initial tabs', () => { + expect( + getDefaultTabState({ + defaultQuery: '# Default', + headers: null, + initialTabs: [ + { + headers: null, + query: 'query Person { person { name } }', + variables: '{"id":"foo"}', + }, + { + headers: '{"x-header":"foo"}', + query: 'query Image { image }', + variables: null, + }, + ], + query: null, + variables: null, + storage: null, + }), + ).toEqual({ + activeTabIndex: 0, + tabs: [ + expect.objectContaining({ + query: 'query Person { person { name } }', + title: 'Person', + variables: '{"id":"foo"}', + }), + expect.objectContaining({ + headers: '{"x-header":"foo"}', + query: 'query Image { image }', + title: 'Image', + }), + ], + }); + }); +}); diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index 1fa5147edd8..5b0ea5b2f9a 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -18,6 +18,7 @@ import { createTab, getDefaultTabState, setPropertiesInActiveTab, + TabDefinition, TabsState, TabState, useSetEditorValues, @@ -168,6 +169,11 @@ export type EditorContextProviderProps = { * typing in the editor. */ headers?: string; + /** + * This prop can be used to defined the initial set of tabs with their queries, + * variables and headers. + */ + initialTabs?: TabDefinition[]; /** * Invoked when the operation name changes. Possible triggers are: * - Editing the contents of the query editor @@ -257,6 +263,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { query, variables, headers, + initialTabs: props.initialTabs, defaultQuery: props.defaultQuery || DEFAULT_QUERY, storage, }); diff --git a/packages/graphiql-react/src/editor/tabs.ts b/packages/graphiql-react/src/editor/tabs.ts index 75127d049c6..ab8b54d1acc 100644 --- a/packages/graphiql-react/src/editor/tabs.ts +++ b/packages/graphiql-react/src/editor/tabs.ts @@ -5,10 +5,25 @@ import debounce from '../utility/debounce'; import { CodeMirrorEditorWithOperationFacts } from './context'; import { CodeMirrorEditor } from './types'; +export type 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. + */ + headers: string | null; +}; + /** * This object describes the state of a single tab. */ -export type TabState = { +export type TabState = TabDefinition & { /** * A GUID value generated when the tab was created. */ @@ -23,18 +38,6 @@ export type TabState = { * The title of the tab shown in the tab element. */ title: string; - /** - * 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. - */ - headers: string | null; /** * The operation name derived from the contents of the query editor of this * tab. @@ -64,12 +67,14 @@ export type TabsState = { export function getDefaultTabState({ defaultQuery, headers, + initialTabs, query, variables, storage, }: { defaultQuery: string; headers: string | null; + initialTabs?: TabDefinition[]; query: string | null; variables: string | null; storage: StorageAPI | null; @@ -120,7 +125,9 @@ export function getDefaultTabState({ } catch (err) { return { activeTabIndex: 0, - tabs: [createTab({ query: query ?? defaultQuery, variables, headers })], + tabs: ( + initialTabs || [{ query: query ?? defaultQuery, variables, headers }] + ).map(createTab), }; } } @@ -261,11 +268,11 @@ export function createTab({ query = null, variables = null, headers = null, -}: Partial> = {}): TabState { +}: Partial = {}): TabState { return { id: guid(), hash: hashFromTabContents({ query, variables, headers }), - title: DEFAULT_TITLE, + title: (query && fuzzyExtractOperationName(query)) || DEFAULT_TITLE, query, variables, headers, diff --git a/packages/graphiql-react/src/provider.tsx b/packages/graphiql-react/src/provider.tsx index d881c3072bf..d846f8b0873 100644 --- a/packages/graphiql-react/src/provider.tsx +++ b/packages/graphiql-react/src/provider.tsx @@ -28,6 +28,7 @@ export function GraphiQLProvider({ fetcher, getDefaultFieldNames, headers, + initialTabs, inputValueDeprecation, introspectionQueryName, maxHistoryLength, @@ -54,6 +55,7 @@ export function GraphiQLProvider({ defaultQuery={defaultQuery} externalFragments={externalFragments} headers={headers} + initialTabs={initialTabs} onEditOperationName={onEditOperationName} onTabChange={onTabChange} query={query} diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index a7dcb8d6db3..8c073a72abe 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -98,6 +98,7 @@ export function GraphiQL({ fetcher, getDefaultFieldNames, headers, + initialTabs, inputValueDeprecation, introspectionQueryName, maxHistoryLength, @@ -133,6 +134,7 @@ export function GraphiQL({ externalFragments={externalFragments} fetcher={fetcher} headers={headers} + initialTabs={initialTabs} inputValueDeprecation={inputValueDeprecation} introspectionQueryName={introspectionQueryName} maxHistoryLength={maxHistoryLength} diff --git a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx index 6cc668258d7..280dc592c60 100644 --- a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx +++ b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx @@ -651,6 +651,7 @@ describe('GraphiQL', () => { container.querySelectorAll('.graphiql-tabs .graphiql-tab'), ).toHaveLength(3); }); + it('each tab has a close button when multiple tabs are open', () => { const { container } = render(); @@ -668,6 +669,7 @@ describe('GraphiQL', () => { container.querySelectorAll('.graphiql-tab .graphiql-tab-close'), ).toHaveLength(3); }); + it('close button removes a tab', () => { const { container } = render(); @@ -687,5 +689,25 @@ describe('GraphiQL', () => { container.querySelectorAll('.graphiql-tab .graphiql-tab-close'), ).toHaveLength(0); }); + + it('shows initial tabs', () => { + const { container } = render( + , + ); + + expect( + container.querySelectorAll('.graphiql-tabs .graphiql-tab'), + ).toHaveLength(2); + }); }); }); From 333f124bea021792e30f54745da7e9d061eb5081 Mon Sep 17 00:00:00 2001 From: Valentin Agachi Date: Sun, 16 Oct 2022 15:55:39 +0200 Subject: [PATCH 2/3] Create olive-tips-deliver.md --- .changeset/olive-tips-deliver.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/olive-tips-deliver.md diff --git a/.changeset/olive-tips-deliver.md b/.changeset/olive-tips-deliver.md new file mode 100644 index 00000000000..5cfeb982250 --- /dev/null +++ b/.changeset/olive-tips-deliver.md @@ -0,0 +1,6 @@ +--- +"@graphiql/react": minor +"graphiql": minor +--- + +Initial tabs support From 05bed1d19f1a8eb4b38d329ccb3d805f45ca6943 Mon Sep 17 00:00:00 2001 From: Valentin Agachi Date: Mon, 17 Oct 2022 17:14:44 +0200 Subject: [PATCH 3/3] fixup! Initial tabs --- packages/graphiql-react/src/editor/context.tsx | 10 ++++++++++ packages/graphiql-react/src/editor/tabs.ts | 12 ++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index 5b0ea5b2f9a..f7a4e761216 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -172,6 +172,16 @@ export type EditorContextProviderProps = { /** * This prop can be used to defined the initial set of tabs with their queries, * variables and headers. + * + * @example + * ```tsx + * + *``` */ initialTabs?: TabDefinition[]; /** diff --git a/packages/graphiql-react/src/editor/tabs.ts b/packages/graphiql-react/src/editor/tabs.ts index ab8b54d1acc..ec0c8b8cbcf 100644 --- a/packages/graphiql-react/src/editor/tabs.ts +++ b/packages/graphiql-react/src/editor/tabs.ts @@ -13,11 +13,11 @@ export type TabDefinition = { /** * The contents of the variable editor of this tab. */ - variables: string | null; + variables?: string | null; /** * The contents of the headers editor of this tab. */ - headers: string | null; + headers?: string | null; }; /** @@ -251,8 +251,8 @@ export function useSetEditorValues({ response, }: { query: string | null; - variables: string | null; - headers: string | null; + variables?: string | null; + headers?: string | null; response: string | null; }) => { queryEditor?.setValue(query ?? ''); @@ -318,8 +318,8 @@ function guid(): string { function hashFromTabContents(args: { query: string | null; - variables: string | null; - headers: string | null; + variables?: string | null; + headers?: string | null; }): string { return [args.query ?? '', args.variables ?? '', args.headers ?? ''].join('|'); }