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

feat: add query composer #84

Merged
merged 7 commits into from
Mar 30, 2024
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
1,064 changes: 1,057 additions & 7 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"sort-package-json": "^2.7.0"
},
"dependencies": {
"@graphiql/plugin-explorer": "^1.0.3",
"@wordpress/components": "^27.0.0",
"@wordpress/data": "^9.22.0",
"@wordpress/element": "^5.23.0",
Expand Down
17 changes: 14 additions & 3 deletions src/components/Editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import React from 'react';
import { GraphiQL } from 'graphiql';
import { useDispatch, useSelect } from '@wordpress/data';
import { explorerPlugin } from '@graphiql/plugin-explorer';

import { PrettifyButton } from './toolbarButtons/PrettifyButton';
import { CopyQueryButton } from './toolbarButtons/CopyQueryButton';
import { MergeFragmentsButton } from './toolbarButtons/MergeFragmentsButton';
import { ShareDocumentButton } from './toolbarButtons/ShareDocumentButton';


import 'graphiql/graphiql.min.css';

const fetcher = async ( graphQLParams ) => {
Expand Down Expand Up @@ -35,6 +37,10 @@ const toolbarButtons = {
share: ShareDocumentButton,
};

const explorer = explorerPlugin();

import '../../styles/explorer.css';

export function Editor() {
const query = useSelect( ( select ) => {
return select( 'wpgraphql-ide' ).getQuery();
Expand All @@ -47,8 +53,13 @@ export function Editor() {
const { setDrawerOpen } = useDispatch( 'wpgraphql-ide' );

return (
<>
<GraphiQL query={ query } fetcher={ fetcher }>
// this id is used to scope styles to the app.
<span id="wpgraphql-ide-app">
<GraphiQL
query={ query }
fetcher={ fetcher }
plugins={[ explorer ]}
>
<GraphiQL.Toolbar>
{ Object.entries( toolbarButtons ).map(
( [ key, Button ] ) => (
Expand All @@ -72,6 +83,6 @@ export function Editor() {
) }
</GraphiQL.Logo>
</GraphiQL>
</>
</span>
);
}
15 changes: 3 additions & 12 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// WordPress dependencies for hooks, data handling, and component rendering.
import { createHooks } from '@wordpress/hooks';
import { register, dispatch } from '@wordpress/data';
import { register } from '@wordpress/data';
import { createRoot } from '@wordpress/element';
import * as GraphQL from "graphql/index.js";

// Local imports including the store configuration and the main App component.
import { store } from './store';
Expand All @@ -10,24 +11,14 @@ import { App } from './App';
// Register the store with wp.data to make it available throughout the plugin.
register( store );

/**
* Registers a plugin within the WPGraphQL IDE ecosystem.
*
* @param {string} name - The name of the plugin to register.
* @param {Object} config - The configuration object for the plugin.
*/
function registerPlugin( name, config ) {
dispatch( store ).registerPlugin( name, config );
}

// Create a central event hook system for the WPGraphQL IDE.
export const hooks = createHooks();

// Expose a global variable for the IDE, facilitating extension through external scripts.
window.WPGraphQLIDE = {
registerPlugin,
hooks,
store,
GraphQL
};

/**
Expand Down
Empty file added styles/explorer.css
Empty file.
78 changes: 78 additions & 0 deletions styles/wpgraphql-ide.css
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,81 @@ body.graphql_page_graphql-ide #wpfooter {
body.graphql_page_graphql-ide .AppRoot {
height: 100%;
}

/**
* GraphiQL Explorer Styles
*/
#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap {
width: 100% !important;

}

#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .doc-explorer-title {
font-weight: var(--font-weight-medium);
font-size: var(--font-size-h2);
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .doc-explorer-title-bar {
padding: var(--px-16);
padding-bottom: 0;
}

#wpgraphql-ide-app .graphiql-plugin .doc-explorer-rhs {
display: none!important;
}

#wpgraphql-ide-app .graphiql-plugin .doc-explorer-contents {
height: 100%;
}

#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root {
padding: 0!important;
}

#wpgraphql-ide-app .graphiql-plugin:has(div.docExplorerWrap) {
padding: 0!important;
}

#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root > div:first-of-type {
padding: 20px!important;
flex: 3 1 0!important;
}

#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root input {
width: auto!important;
padding: 0 8px;
line-height: 2;
min-height: 30px;
box-shadow: 0 0 0 transparent;
border-radius: 4px;
border: 1px solid
hsla(var(--color-neutral), var(--alpha-background-heavy))!important;
background: transparent;
}

#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root .graphiql-explorer-actions {
border-top: 1px solid
hsla(var(--color-neutral), var(--alpha-background-heavy))!important;
}
#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root .graphiql-explorer-actions > span:first-of-type{
padding: 10px;
}

#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root>div>div {
border-bottom: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy))!important;
margin-bottom: 15px!important;
}

#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root>div>div:last-of-type {
border-bottom: none!important;
margin-bottom: 0!important;
}

#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root .graphiql-explorer-actions > select {
color: hsl(var(--color-primary));
border: 1px solid
hsla(var(--color-neutral), var(--alpha-background-heavy))!important;
background: transparent;
}
139 changes: 139 additions & 0 deletions tests/e2e/specs/query-composer.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {loginToWordPressAdmin, openDrawer, typeQuery, visitAdminFacingPage} from "../utils";
import {expect, test } from "@wordpress/e2e-test-utils-playwright";

// Login to WordPress before each test
test.beforeEach( async ( { page } ) => {
await loginToWordPressAdmin( page );
} );

async function navigateToGraphiQLAndOpenQueryExplorer({ page }) {
await expect( page.locator( '.graphiql-container' ) ).toBeHidden();
await openDrawer( page );
await page.click( '[aria-label="Show GraphiQL Explorer"]' );
await expect( page.locator( '.docExplorerWrap' ) ).toBeVisible();
}

test.describe( 'GraphiQL Query Composer', () => {

test( 'Clicking the Query Composer button opens and closes the Query Composer', async ( { page } ) => {
await visitAdminFacingPage( page );
await expect( page.locator( '.graphiql-container' ) ).toBeHidden();
await openDrawer( page );
await expect( page.locator( '.graphiql-container' ) ).toBeVisible();

// query composer should be hidden by default
await expect( page.locator( '.docExplorerWrap' ) ).toBeHidden();

// open query composer and check if it is visible
await page.click( '[aria-label="Show GraphiQL Explorer"]' );
await expect( page.locator( '.docExplorerWrap' ) ).toBeVisible();

// close query composer and check if it is hidden
await page.click( '[aria-label="Hide GraphiQL Explorer"]' );
await expect( page.locator( '.docExplorerWrap' ) ).toBeHidden();

});

test( 'Changing the name of an operation in the explorer updates the query editor', async ( { page } ) => {
await navigateToGraphiQLAndOpenQueryExplorer( { page });

const firstQueryOperationNameInput = await page.locator( '.graphiql-explorer-root>div>div:first-of-type input' );

const queryEditor = await page.locator('[aria-label="Query Editor"] .CodeMirror' );

await expect( queryEditor ).not.toContainText( 'NewQueryName' );

// focus on the input field
await firstQueryOperationNameInput.fill( 'NewQueryName' );
await expect( queryEditor ).toContainText( 'NewQueryName' );

});

test( 'Selecting a field in the explorer adds that field to the query', async ( { page } ) => {

await navigateToGraphiQLAndOpenQueryExplorer( { page });

const firstFieldSelector = '.graphiql-explorer-root>div>div>div.graphiql-explorer-node:nth-of-type(2) > span';
const firstField = await page.locator( firstFieldSelector );
await expect( firstField ).toBeVisible();
const fieldName = await page.locator( firstFieldSelector ).getAttribute( 'data-field-name' );

const queryEditor = await page.locator('[aria-label="Query Editor"] .CodeMirror' );
await expect( queryEditor ).not.toContainText( fieldName );
await firstField.click();
await expect( queryEditor ).toContainText( fieldName );

});

test( 'Selecting a field in the explorer that has arguments and filling in arguments updates the query', async ( { page } ) => {

await navigateToGraphiQLAndOpenQueryExplorer( { page });

const fieldSelector = '.graphiql-explorer-root>div>div>div.graphiql-explorer-contentNode';
const field = await page.locator( `${fieldSelector}>span` );
await expect( field ).toBeVisible();
const fieldName = await field.getAttribute( 'data-field-name' );

const queryEditor = await page.locator('[aria-label="Query Editor"] .CodeMirror' );
await expect( queryEditor ).not.toContainText( fieldName );
await expect( queryEditor ).not.toContainText( 'id:' );
await expect( queryEditor ).not.toContainText( '123' );
await field.click();
await expect( queryEditor ).toContainText( fieldName );


const idArgumentField = await page.locator( `${fieldSelector}>div.graphiql-explorer-contentNode div[data-arg-name="id"]` );
await idArgumentField.click();
const idArgumentFieldInput = await page.locator( `${fieldSelector}>div.graphiql-explorer-contentNode div[data-arg-name="id"] input` );
await idArgumentFieldInput.fill( '123' );
await expect( queryEditor ).toContainText( 'id: "123"' );


});

test( 'Deleting a query from the explorer removes it from the document', async ( { page } ) => {

await navigateToGraphiQLAndOpenQueryExplorer( { page });

const fieldSelector = '.graphiql-explorer-root>div>div>div.graphiql-explorer-contentNode';
const field = await page.locator( `${fieldSelector}>span` );
await expect( field ).toBeVisible();
const fieldName = await field.getAttribute( 'data-field-name' );

const queryEditor = await page.locator('[aria-label="Query Editor"] .CodeMirror' );
await expect( queryEditor ).not.toContainText( fieldName );
await field.click();
await expect( queryEditor ).toContainText( fieldName );

const firstQueryOperationTitleBar = await page.locator( '.graphiql-explorer-root>div>div>div.graphiql-operation-title-bar' );
await firstQueryOperationTitleBar.hover();
const deleteButton = await page.locator( '.graphiql-explorer-root>div>div>div.graphiql-operation-title-bar button:first-of-type' );
await deleteButton.click();
await expect( queryEditor ).not.toContainText( fieldName );

});

test( 'Copying a query from the explorer adds a copy to the document', async ( { page } ) => {

await navigateToGraphiQLAndOpenQueryExplorer( { page });

const fieldSelector = '.graphiql-explorer-root>div>div>div.graphiql-explorer-contentNode';
const field = await page.locator( `${fieldSelector}>span` );
await expect( field ).toBeVisible();
const fieldName = await field.getAttribute( 'data-field-name' );

const queryEditor = await page.locator('[aria-label="Query Editor"] .CodeMirror' );
await expect( queryEditor ).not.toContainText( fieldName );
await field.click();
await expect( queryEditor ).toContainText( fieldName );

const firstQueryOperationTitleBar = await page.locator( '.graphiql-explorer-root>div>div>div.graphiql-operation-title-bar' );
await firstQueryOperationTitleBar.hover();
const copyButton = await page.locator( '.graphiql-explorer-root>div>div>div.graphiql-operation-title-bar button:nth-of-type(2)' );
await copyButton.click();
await expect( queryEditor ).toContainText( `MyQuery` );
await expect( queryEditor ).toContainText( `MyQueryCopy` );

});

});
2 changes: 2 additions & 0 deletions tests/e2e/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export async function openDrawer( page ) {
state: 'visible',
} );
}

await page.waitForLoadState( 'networkidle' );
}

export async function closeDrawer( page ) {
Expand Down
Loading