Skip to content

Commit cf0ebba

Browse files
v6: Remove the legacy editor toolbar and corner logo (#4398)
## Summary - Remove the legacy vertical editor toolbar and the floating "GraphiQL" corner logo. Branding now lives only in the top bar, and editor actions live only in the tab strip. - Move Merge Fragments, previously available only in the old toolbar, into the tab-strip actions alongside Prettify, Copy, and Save, so the action isn't lost. - The composable `Toolbar`/`Logo` render-prop slots still work for embedders. Only the default rendering is removed. - Enlarge the tab-strip action buttons so they're easier to click. - Position the action buttons at the right edge of the query editor instead of the far right of the response pane, so they stay next to the query and follow the editor/response divider as it's resized. The sessions area is split into an editor column and a response column divided by the drag bar; the tab strip lives in the editor column. ## Test plan - [x] Open GraphiQL. There's no floating vertical button column and no corner logo over the editor; branding appears once, in the top bar. - [x] The tab strip shows Prettify, Merge Fragments, Copy, and Save, each exactly once. - [x] The action buttons sit at the right edge of the query editor (by the editor/response divider), not over the response pane, and follow the divider when it's dragged. - [x] Write a query that references a fragment, click Merge Fragments, and confirm the fragment gets inlined into the operation. - [x] Prettify reformats the query, Copy copies it, and the single Run button in the top bar still executes it. - [x] The Prettify, Merge, and Copy keyboard shortcuts still work. - [x] The tab-strip action buttons read as comfortably sized, not cramped, and stay evenly spaced and aligned with the tab labels. Refs: #4219
1 parent 0557a68 commit cf0ebba

8 files changed

Lines changed: 386 additions & 156 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'graphiql': minor
3+
---
4+
5+
Remove the duplicated legacy editor toolbar and corner logo. Branding now appears only in the top bar, and the editor actions (prettify, merge fragments, copy, save) live together in the tab strip. Merge Fragments, previously only in the old toolbar, moves into the tab-strip actions.
6+
7+
The action buttons now sit at the right edge of the query editor rather than the far right of the response pane, so they stay next to the query you're editing and follow the editor/response divider as you resize it.

packages/graphiql/cypress/e2e/init.cy.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ describe('GraphiQL On Initialization', () => {
3232
'#graphiql',
3333
'.graphiql-container',
3434
'.graphiql-sessions',
35+
'.graphiql-editor-column',
3536
'.graphiql-editors',
37+
'.graphiql-response-column',
3638
'.graphiql-response',
3739
'.graphiql-editor-tool',
3840
];
@@ -43,6 +45,18 @@ describe('GraphiQL On Initialization', () => {
4345
}
4446
});
4547

48+
it('Places the action buttons on the editor side of the split', () => {
49+
cy.visit('/');
50+
// The prettify/merge/copy/save buttons belong to the query editor, so they
51+
// live in the editor column rather than floating over the response pane.
52+
cy.get('.graphiql-editor-column .graphiql-tab-strip-actions')
53+
.find('.graphiql-tab-strip-action')
54+
.should('have.length.at.least', 3);
55+
cy.get('.graphiql-response-column .graphiql-tab-strip-actions').should(
56+
'not.exist',
57+
);
58+
});
59+
4660
it('Executes a GraphQL query over HTTP that has the expected result', () => {
4761
cy.visitWithOp({ query: testQuery });
4862
cy.clickExecuteQuery();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
describe('GraphiQL Merge Fragments', () => {
2+
it('merges a fragment into the operation when clicked', () => {
3+
const query = `fragment IdFragment on Test {
4+
id
5+
}
6+
7+
query TestQuery {
8+
...IdFragment
9+
}`;
10+
11+
cy.visitWithOp({ query });
12+
cy.clickMergeFragments();
13+
14+
cy.get(
15+
'.graphiql-query-editor .view-lines.monaco-mouse-cursor-text',
16+
).should(element => {
17+
const text = element.get(0).innerText;
18+
expect(text).to.not.contain('fragment IdFragment');
19+
expect(text).to.not.contain('...IdFragment');
20+
expect(text).to.contain('id');
21+
});
22+
});
23+
});

packages/graphiql/cypress/support/commands.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ declare namespace Cypress {
7070

7171
clickPrettify(): Chainable<Element>;
7272

73+
clickMergeFragments(): Chainable<Element>;
74+
7375
assertHasValues(op: Op): Chainable<Element>;
7476

7577
assertQueryResult(expectedResult: MockResult): Chainable<Element>;
@@ -141,11 +143,15 @@ Cypress.Commands.add('activateOperation', (operationName: string) => {
141143
});
142144

143145
Cypress.Commands.add('clickExecuteQuery', () => {
144-
cy.get('.graphiql-execute-button').click();
146+
cy.get('[aria-label="Run query"]').click();
145147
});
146148

147149
Cypress.Commands.add('clickPrettify', () => {
148-
cy.get('[aria-label="Prettify query (Shift-Ctrl-P)"]').click();
150+
cy.get('[aria-label="Prettify query"]').click();
151+
});
152+
153+
Cypress.Commands.add('clickMergeFragments', () => {
154+
cy.get('[aria-label="Merge fragments into query"]').click();
149155
});
150156

151157
Cypress.Commands.add('visitWithOp', ({ query, variables, variablesString }) => {

packages/graphiql/src/GraphiQL.spec.tsx

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,9 @@ describe('GraphiQL', () => {
384384
const { container } = render(<GraphiQL fetcher={noOpFetcher} />);
385385

386386
const dragBar = container.querySelector('.graphiql-horizontal-drag-bar')!;
387-
const editors =
388-
container.querySelector<HTMLDivElement>('.graphiql-editors')!;
387+
const editorColumn = container.querySelector<HTMLDivElement>(
388+
'.graphiql-editor-column',
389+
)!;
389390

390391
act(() => {
391392
fireEvent.mouseDown(dragBar, {
@@ -403,7 +404,7 @@ describe('GraphiQL', () => {
403404

404405
await waitFor(() => {
405406
// 700 / (900 - 700) = 3.5
406-
expect(editors.style.flexGrow).toEqual('3.5');
407+
expect(editorColumn.style.flexGrow).toEqual('3.5');
407408
});
408409

409410
clientWidthSpy.mockRestore();
@@ -626,21 +627,23 @@ describe('GraphiQL', () => {
626627
</>
627628
);
628629

629-
const { container, getByRole } = render(
630+
const { container, queryByRole } = render(
630631
<GraphiQL fetcher={noOpFetcher}>{myFragment}</GraphiQL>,
631632
);
632633

633634
await waitFor(() => {
634635
expect(
635636
container.querySelector('.graphiql-container'),
636637
).toBeInTheDocument();
637-
expect(container.querySelector('.graphiql-logo')).toBeInTheDocument();
638-
expect(getByRole('toolbar')).toBeInTheDocument();
638+
expect(
639+
container.querySelector('.graphiql-logo'),
640+
).not.toBeInTheDocument();
641+
expect(queryByRole('toolbar')).not.toBeInTheDocument();
639642
});
640643
});
641644

642645
it('properly ignores non-override children components', async () => {
643-
const { container, getByRole } = render(
646+
const { container, queryByRole } = render(
644647
<GraphiQL fetcher={noOpFetcher}>
645648
<MyFunctionalComponent />
646649
</GraphiQL>,
@@ -650,8 +653,10 @@ describe('GraphiQL', () => {
650653
expect(
651654
container.querySelector('.graphiql-container'),
652655
).toBeInTheDocument();
653-
expect(container.querySelector('.graphiql-logo')).toBeInTheDocument();
654-
expect(getByRole('toolbar')).toBeInTheDocument();
656+
expect(
657+
container.querySelector('.graphiql-logo'),
658+
).not.toBeInTheDocument();
659+
expect(queryByRole('toolbar')).not.toBeInTheDocument();
655660
});
656661
});
657662

@@ -663,7 +668,7 @@ describe('GraphiQL', () => {
663668
}
664669
}
665670

666-
const { container, getByRole } = render(
671+
const { container, queryByRole } = render(
667672
<GraphiQL fetcher={noOpFetcher}>
668673
<MyClassComponent />
669674
</GraphiQL>,
@@ -673,8 +678,10 @@ describe('GraphiQL', () => {
673678
expect(
674679
container.querySelector('.graphiql-container'),
675680
).toBeInTheDocument();
676-
expect(container.querySelector('.graphiql-logo')).toBeInTheDocument();
677-
expect(getByRole('toolbar')).toBeInTheDocument();
681+
expect(
682+
container.querySelector('.graphiql-logo'),
683+
).not.toBeInTheDocument();
684+
expect(queryByRole('toolbar')).not.toBeInTheDocument();
678685
});
679686
});
680687

@@ -731,6 +738,83 @@ describe('GraphiQL', () => {
731738
});
732739
});
733740

741+
describe('tab strip actions', () => {
742+
it('renders exactly one prettify, merge, copy, and save action, and no legacy toolbar or logo', async () => {
743+
const { container } = render(
744+
<GraphiQL fetcher={noOpFetcher} onSaveQuery={() => {}} />,
745+
);
746+
747+
await waitFor(() => {
748+
expect(
749+
container.querySelectorAll('[aria-label="Prettify query"]'),
750+
).toHaveLength(1);
751+
expect(
752+
container.querySelectorAll(
753+
'[aria-label="Merge fragments into query"]',
754+
),
755+
).toHaveLength(1);
756+
expect(
757+
container.querySelectorAll('[aria-label="Copy query"]'),
758+
).toHaveLength(1);
759+
expect(
760+
container.querySelectorAll('[aria-label="Save query"]'),
761+
).toHaveLength(1);
762+
expect(
763+
container.querySelector('.graphiql-toolbar'),
764+
).not.toBeInTheDocument();
765+
expect(
766+
container.querySelector('.graphiql-logo'),
767+
).not.toBeInTheDocument();
768+
});
769+
});
770+
771+
it('merges fragments into the operation when Merge Fragments is clicked', async () => {
772+
let queryEditor: MonacoEditor;
773+
let documentAST: unknown;
774+
775+
const HookConsumer: FC = () => {
776+
const $queryEditor = useGraphiQL(state => state.queryEditor);
777+
const $documentAST = useGraphiQL(state => state.documentAST);
778+
useEffect(() => {
779+
queryEditor = $queryEditor!;
780+
documentAST = $documentAST;
781+
}, [$queryEditor, $documentAST]);
782+
return null;
783+
};
784+
785+
const { getByLabelText } = render(
786+
<GraphiQL fetcher={noOpFetcher}>
787+
<HookConsumer />
788+
</GraphiQL>,
789+
);
790+
791+
const query = `fragment NameFragment on Query { q }
792+
query TestQuery { ...NameFragment }`;
793+
794+
await waitFor(() => {
795+
expect(queryEditor).toBeTruthy();
796+
});
797+
act(() => {
798+
queryEditor.setValue(query);
799+
});
800+
801+
await waitFor(() => {
802+
expect(queryEditor.getValue()).toContain('...NameFragment');
803+
// The editor debounces content-change handling before it updates the
804+
// document AST that `mergeQuery` reads from.
805+
expect(documentAST).toBeTruthy();
806+
});
807+
808+
fireEvent.click(getByLabelText('Merge fragments into query'));
809+
810+
await waitFor(() => {
811+
const merged = queryEditor.getValue();
812+
expect(merged).not.toContain('...NameFragment');
813+
expect(merged).toContain('q');
814+
});
815+
}, 15_000);
816+
});
817+
734818
it('should support multiple instances', async () => {
735819
const queryEditors: Record<0 | 1, MonacoEditor> = Object.create(null);
736820

0 commit comments

Comments
 (0)