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

Initial tabs #2821

Merged
merged 3 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/olive-tips-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@graphiql/react": minor
"graphiql": minor
---

Initial tabs support
88 changes: 87 additions & 1 deletion packages/graphiql-react/src/editor/__tests__/tabs.spec.ts
Original file line number Diff line number Diff line change
@@ -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: '<untitled>',
}),
);
});

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', () => {
Expand Down Expand Up @@ -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: '<untitled>',
}),
],
});
});

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',
}),
],
});
});
});
17 changes: 17 additions & 0 deletions packages/graphiql-react/src/editor/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
createTab,
getDefaultTabState,
setPropertiesInActiveTab,
TabDefinition,
TabsState,
TabState,
useSetEditorValues,
Expand Down Expand Up @@ -168,6 +169,21 @@ 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.
*
* @example
* ```tsx
* <GraphiQL
* initialTabs={[
* { query: 'query myExampleQuery {}' },
* { query: '{ id }' }
* ]}
* />
*```
*/
avaly marked this conversation as resolved.
Show resolved Hide resolved
initialTabs?: TabDefinition[];
/**
* Invoked when the operation name changes. Possible triggers are:
* - Editing the contents of the query editor
Expand Down Expand Up @@ -257,6 +273,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) {
query,
variables,
headers,
initialTabs: props.initialTabs,
defaultQuery: props.defaultQuery || DEFAULT_QUERY,
storage,
});
Expand Down
47 changes: 27 additions & 20 deletions packages/graphiql-react/src/editor/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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),
};
}
}
Expand Down Expand Up @@ -244,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 ?? '');
Expand All @@ -261,11 +268,11 @@ export function createTab({
query = null,
variables = null,
headers = null,
}: Partial<Pick<TabState, 'query' | 'variables' | 'headers'>> = {}): TabState {
}: Partial<TabDefinition> = {}): TabState {
return {
id: guid(),
hash: hashFromTabContents({ query, variables, headers }),
title: DEFAULT_TITLE,
title: (query && fuzzyExtractOperationName(query)) || DEFAULT_TITLE,
query,
variables,
headers,
Expand Down Expand Up @@ -311,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('|');
}
Expand Down
2 changes: 2 additions & 0 deletions packages/graphiql-react/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function GraphiQLProvider({
fetcher,
getDefaultFieldNames,
headers,
initialTabs,
inputValueDeprecation,
introspectionQueryName,
maxHistoryLength,
Expand All @@ -54,6 +55,7 @@ export function GraphiQLProvider({
defaultQuery={defaultQuery}
externalFragments={externalFragments}
headers={headers}
initialTabs={initialTabs}
onEditOperationName={onEditOperationName}
onTabChange={onTabChange}
query={query}
Expand Down
2 changes: 2 additions & 0 deletions packages/graphiql/src/components/GraphiQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export function GraphiQL({
fetcher,
getDefaultFieldNames,
headers,
initialTabs,
inputValueDeprecation,
introspectionQueryName,
maxHistoryLength,
Expand Down Expand Up @@ -133,6 +134,7 @@ export function GraphiQL({
externalFragments={externalFragments}
fetcher={fetcher}
headers={headers}
initialTabs={initialTabs}
inputValueDeprecation={inputValueDeprecation}
introspectionQueryName={introspectionQueryName}
maxHistoryLength={maxHistoryLength}
Expand Down
22 changes: 22 additions & 0 deletions packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<GraphiQL fetcher={noOpFetcher} />);

Expand All @@ -668,6 +669,7 @@ describe('GraphiQL', () => {
container.querySelectorAll('.graphiql-tab .graphiql-tab-close'),
).toHaveLength(3);
});

it('close button removes a tab', () => {
const { container } = render(<GraphiQL fetcher={noOpFetcher} />);

Expand All @@ -687,5 +689,25 @@ describe('GraphiQL', () => {
container.querySelectorAll('.graphiql-tab .graphiql-tab-close'),
).toHaveLength(0);
});

it('shows initial tabs', () => {
const { container } = render(
<GraphiQL
fetcher={noOpFetcher}
initialTabs={[
{
query: 'query Person { person { name } }',
},
{
query: 'query Image { image }',
},
]}
/>,
);

expect(
container.querySelectorAll('.graphiql-tabs .graphiql-tab'),
).toHaveLength(2);
});
});
});