From e04dce446543da7615dece29f236c8a56d36be99 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Wed, 27 Mar 2024 16:33:44 -0600 Subject: [PATCH 1/7] - scaffold initial query-composer plugin --- src/components/Editor.jsx | 11 +++++++- src/query-composer/README.md | 4 +++ src/query-composer/index.js | 39 ++++++++++++++++++++++++++ tests/e2e/specs/query-composer.spec.js | 30 ++++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/query-composer/README.md create mode 100644 src/query-composer/index.js create mode 100644 tests/e2e/specs/query-composer.spec.js diff --git a/src/components/Editor.jsx b/src/components/Editor.jsx index 2a34c72..934862c 100644 --- a/src/components/Editor.jsx +++ b/src/components/Editor.jsx @@ -3,11 +3,14 @@ import React from 'react'; import { GraphiQL } from 'graphiql'; import { useDispatch, useSelect } from '@wordpress/data'; +import { queryComposerPlugin } from '../query-composer'; + 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 ) => { @@ -35,6 +38,8 @@ const toolbarButtons = { share: ShareDocumentButton, }; +const queryComposer = queryComposerPlugin(); + export function Editor() { const query = useSelect( ( select ) => { return select( 'wpgraphql-ide' ).getQuery(); @@ -48,7 +53,11 @@ export function Editor() { return ( <> - + { Object.entries( toolbarButtons ).map( ( [ key, Button ] ) => ( diff --git a/src/query-composer/README.md b/src/query-composer/README.md new file mode 100644 index 0000000..7a40eaf --- /dev/null +++ b/src/query-composer/README.md @@ -0,0 +1,4 @@ +# Query Composer + +This directory contains code related to the GraphiQL Query Composer, a feature that allows users to compose queries using a visual interface. + diff --git a/src/query-composer/index.js b/src/query-composer/index.js new file mode 100644 index 0000000..8b74438 --- /dev/null +++ b/src/query-composer/index.js @@ -0,0 +1,39 @@ +export const queryComposerPlugin = ( props ) => { + + console.log( { + queryComposerPlugin: props, + }) + + return { + title: 'Query Composer', + icon: () => { + + + + + + }, + content: () => ( +
+

Query Composer

+
{JSON.stringify(props, null, 2)}
+
+ ), + } + +} diff --git a/tests/e2e/specs/query-composer.spec.js b/tests/e2e/specs/query-composer.spec.js new file mode 100644 index 0000000..8dea0d5 --- /dev/null +++ b/tests/e2e/specs/query-composer.spec.js @@ -0,0 +1,30 @@ +import {loginToWordPressAdmin, openDrawer, 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 ); +} ); + +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( '.graphiql-query-composer' ) ).toBeHidden(); + + // open query composer and check if it is visible + await page.click( '[aria-label="Show Query Composer"]' ); + await expect( page.locator( '.graphiql-query-composer' ) ).toBeVisible(); + + // close query composer and check if it is hidden + await page.click( '[aria-label="Hide Query Composer"]' ); + await expect( page.locator( '.graphiql-query-composer' ) ).toBeHidden(); + + }); + +}); From a9af55d184713f00950b846969c28d2016954f0f Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 28 Mar 2024 11:06:09 -0600 Subject: [PATCH 2/7] - initial work on re-building the composer --- package-lock.json | 1042 ++++++++++++++++++++++++++++++++++- package.json | 3 + src/index.js | 15 +- src/query-composer/index.js | 5 +- 4 files changed, 1044 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 25226ee..703489a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,15 @@ "@wordpress/element": "^5.23.0", "@wordpress/hooks": "^3.49.0", "@wordpress/icons": "^9.43.0", + "antd": "^5.15.4", "copy-to-clipboard": "^3.3.3", "graphiql": "^3.0.10", "graphql": "^16.8.1", "graphql-ws": "^5.14.2", "lz-string": "^1.5.0", "react": "^18.2.0", + "styled-components": "^6.1.8", + "use-error-boundary": "^2.0.6", "vaul": "^0.9.0" }, "devDependencies": { @@ -64,6 +67,81 @@ "node": ">=6.0.0" } }, + "node_modules/@ant-design/colors": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.0.2.tgz", + "integrity": "sha512-7KJkhTiPiLHSu+LmMJnehfJ6242OCxSlR3xHVBecYxnMW8MS/878NXct1GqYARyL59fyeFdKRxXTfvR9SnDgJg==", + "dependencies": { + "@ctrl/tinycolor": "^3.6.1" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.18.5", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.18.5.tgz", + "integrity": "sha512-Ub4n3d+MAX/qtE5S9PM8iOn5ocU7GUAIC4Adc2X8UCMXnsRRfpJBHsBdtQ1qoAuaQ7lU2M1BTCuJ+fkv4fOWiw==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.0.13" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs/node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "node_modules/@ant-design/cssinjs/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "node_modules/@ant-design/icons": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.3.5.tgz", + "integrity": "sha512-Vyv/OsKz56BsKBtcRlLP6G8RGaRW43f7G5dK3XNPCaeV4YyehLVaITuNKi2YJG9hMVURkBdzdGhveNQlnKTFqw==", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.11.2", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" + }, + "node_modules/@ant-design/react-slick": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.0.2.tgz", + "integrity": "sha512-Wj8onxL/T8KQLFFiCA4t8eIRGpRR+UPgOdac2sYzonv+i0n3kXHmvHLLiOYL655DQx2Umii9Y9nNgL7ssu5haQ==", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, "node_modules/@ariakit/core": { "version": "0.3.11", "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.11.tgz", @@ -1881,8 +1959,9 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "license": "MIT", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1983,6 +2062,14 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "engines": { + "node": ">=10" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "dev": true, @@ -4068,6 +4155,118 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@rc-component/color-picker": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.5.3.tgz", + "integrity": "sha512-+tGGH3nLmYXTalVe0L8hSZNs73VTP5ueSHwUlDC77KKRaN7G4DS4wcpG5DTDzdcV/Yas+rzA6UGgIyzd8fS4cw==", + "dependencies": { + "@babel/runtime": "^7.23.6", + "@ctrl/tinycolor": "^3.6.1", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.14.2.tgz", + "integrity": "sha512-A75DZ8LVvahBIvxooj3Gvf2sxe+CGOkmzPNX7ek0i0AJHyKZ1HXe5ieIGo3m0FMdZfVOlbCJ952Duq8VKAHk6g==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.0.0.tgz", + "integrity": "sha512-niwKADPdY5dhdIblV6uwSayVivwo2uUISfJqri+/ovYQcH/omxDYBJKo755QKeoIIsWptxnRpgr7reEnNEZGFg==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.38.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@sentry/core": { "version": "6.19.7", "dev": true, @@ -5135,6 +5334,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" + }, "node_modules/@types/tapable": { "version": "1.0.12", "dev": true, @@ -6860,6 +7064,69 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antd": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.15.4.tgz", + "integrity": "sha512-79eLOQW1DG92yzulx+ValfHFjvPnaaI41BffGquAnzx42Ws3eEcKofsa2jNRyJN5NWr9I5wqvABDq9rRRfGGsg==", + "dependencies": { + "@ant-design/colors": "^7.0.2", + "@ant-design/cssinjs": "^1.18.5", + "@ant-design/icons": "^5.3.5", + "@ant-design/react-slick": "~1.0.2", + "@babel/runtime": "^7.24.1", + "@ctrl/tinycolor": "^3.6.1", + "@rc-component/color-picker": "~1.5.3", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/tour": "~1.14.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.10", + "qrcode.react": "^3.1.0", + "rc-cascader": "~3.24.0", + "rc-checkbox": "~3.2.0", + "rc-collapse": "~3.7.3", + "rc-dialog": "~9.4.0", + "rc-drawer": "~7.1.0", + "rc-dropdown": "~4.2.0", + "rc-field-form": "~1.42.1", + "rc-image": "~7.6.0", + "rc-input": "~1.4.5", + "rc-input-number": "~9.0.0", + "rc-mentions": "~2.11.1", + "rc-menu": "~9.13.0", + "rc-motion": "^2.9.0", + "rc-notification": "~5.3.0", + "rc-pagination": "~4.0.4", + "rc-picker": "~4.3.0", + "rc-progress": "~3.5.1", + "rc-rate": "~2.12.0", + "rc-resize-observer": "^1.4.0", + "rc-segmented": "~2.3.0", + "rc-select": "~14.13.0", + "rc-slider": "~10.5.0", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.42.0", + "rc-tabs": "~14.1.1", + "rc-textarea": "~1.6.3", + "rc-tooltip": "~6.2.0", + "rc-tree": "~5.8.5", + "rc-tree-select": "~5.19.0", + "rc-upload": "~4.5.2", + "rc-util": "^5.39.1", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/anymatch": { "version": "3.1.3", "dev": true, @@ -6957,6 +7224,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-tree-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", + "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==" + }, "node_modules/array-union": { "version": "2.1.0", "dev": true, @@ -7108,6 +7380,11 @@ "node": ">=8" } }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, "node_modules/asynciterator.prototype": { "version": "1.0.0", "dev": true, @@ -7847,6 +8124,14 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "dev": true, @@ -8963,6 +9248,14 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "7.1.1", "dev": true, @@ -9061,6 +9354,16 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "2.3.1", "dev": true, @@ -9279,6 +9582,11 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debounce": { "version": "1.2.1", "dev": true, @@ -15095,6 +15403,14 @@ "dev": true, "license": "MIT" }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json2php": { "version": "0.0.7", "dev": true, @@ -16726,7 +17042,6 @@ }, "node_modules/nanoid": { "version": "3.3.7", - "dev": true, "funding": [ { "type": "github", @@ -17738,7 +18053,6 @@ }, "node_modules/picocolors": { "version": "1.0.0", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -18540,7 +18854,6 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -18819,6 +19132,14 @@ ], "license": "MIT" }, + "node_modules/qrcode.react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz", + "integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/qs": { "version": "6.11.0", "dev": true, @@ -18919,6 +19240,578 @@ "node": ">=0.10.0" } }, + "node_modules/rc-cascader": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.24.0.tgz", + "integrity": "sha512-NwkYsVULA61S085jbOYbq8Z7leyIxVmLwf+71mWLjA3kCfUf/rAKC0WfjQbqBDaLGlU9d4z1EzyPaHBKLYWv6A==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "array-tree-filter": "^2.1.0", + "classnames": "^2.3.1", + "rc-select": "~14.13.0", + "rc-tree": "~5.8.1", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.2.0.tgz", + "integrity": "sha512-8inzw4y9dAhZmv/Ydl59Qdy5tdp9CKg4oPVcRigi+ga/yKPZS5m5SyyQPtYSgbcqHRYOdUhiPSeKfktc76du1A==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.7.3.tgz", + "integrity": "sha512-60FJcdTRn0X5sELF18TANwtVi7FtModq649H11mYF1jh83DniMoM4MqY627sEKRCTm4+WXfGDcB7hY5oW6xhyw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.4.0.tgz", + "integrity": "sha512-AScCexaLACvf8KZRqCPz12BJ8olszXOS4lKlkMyzDQHS1m0zj1KZMYgmMCh39ee0Dcv8kyrj8mTqxuLyhH+QuQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.1.0.tgz", + "integrity": "sha512-nBE1rF5iZvpavoyqhSSz2mk/yANltA7g3aF0U45xkx381n3we/RKs9cJfNKp9mSWCedOKWt9FLEwZDaAaOGn2w==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.0.tgz", + "integrity": "sha512-odM8Ove+gSh0zU27DUj5cG1gNKg7mLWBYzB5E4nNLrLwBmYEgYP43vHKDGOVZcJSVElQBI0+jTQgjnq0NfLjng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.42.1.tgz", + "integrity": "sha512-SqiEmWNP+I61Lt80+ofPvT+3l8Ij6vb35IS+x14gheVnCJN0SRnOwEgsqCEB5FslT7xqjUqDnU845hRZ1jzlAA==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "async-validator": "^4.1.0", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.6.0.tgz", + "integrity": "sha512-tL3Rvd1sS+frZQ01i+tkeUPaOeFz2iG9/scAt/Cfs0hyCRVA/w0Pu1J/JxIX8blalvmHE0bZQRYdOmRAzWu4Hg==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.4.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.4.5.tgz", + "integrity": "sha512-AjzykhwnwYTRSwwgCu70CGKBIAv6bP2nqnFptnNTprph/TF1BAs0Qxl91mie/BR6n827WIJB6ZjaRf9iiMwAfw==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.0.0.tgz", + "integrity": "sha512-RfcDBDdWFFetouWFXBA+WPEC8LzBXyngr9b+yTLVIygfFu7HiLRGn/s/v9wwno94X7KFvnb28FNynMGj9XJlDQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.4.0", + "rc-util": "^5.28.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.11.1.tgz", + "integrity": "sha512-upb4AK1SRFql7qGnbLEvJqLMugVVIyjmwBJW9L0eLoN9po4JmJZaBzmKA4089fNtsU8k6l/tdZiVafyooeKnLw==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.4.0", + "rc-menu": "~9.13.0", + "rc-textarea": "~1.6.1", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.13.0.tgz", + "integrity": "sha512-1l8ooCB3HcYJKCltC/s7OxRKRjgymdl9htrCeGZcXNaMct0RxZRK6OPV3lPhVksIvAGMgzPd54ClpZ5J4b8cZA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.0.tgz", + "integrity": "sha512-XIU2+xLkdIr1/h6ohPZXyPBMvOmuyFZQ/T0xnawz+Rh+gh4FINcnZmMT5UTIj6hgI0VLDjTaPeRd+smJeSPqiQ==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.3.0.tgz", + "integrity": "sha512-WCf0uCOkZ3HGfF0p1H4Sgt7aWfipxORWTPp7o6prA3vxwtWhtug3GfpYls1pnBp4WA+j8vGIi5c2/hQRpGzPcQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.3.2.tgz", + "integrity": "sha512-nsUm78jkYAoPygDAcGZeC2VwIg/IBGSodtOY3pMof4W3M9qRJgqaDYm03ZayHlde3I6ipliAxbN0RUcGf5KOzw==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-4.0.4.tgz", + "integrity": "sha512-GGrLT4NgG6wgJpT/hHIpL9nELv27A1XbSZzECIuQBQTVSf4xGKxWr6I/jhpRPauYEWEbWVw22ObG6tJQqwJqWQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.3.0.tgz", + "integrity": "sha512-bQNB/+NdW55jlQ5lPnNqF5J90Tq4SihLbAF7tzPBvGDJyoYmDgwLm4FN0ZB3Ot9i1v6vJY/1mgqZZTT9jbYc5w==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.38.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.5.1.tgz", + "integrity": "sha512-V6Amx6SbLRwPin/oD+k1vbPrO8+9Qf8zW1T8A7o83HdNafEVvAxPV5YsgtKFP+Ud5HghLj33zKOcEHrcrUGkfw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.12.0.tgz", + "integrity": "sha512-g092v5iZCdVzbjdn28FzvWebK2IutoVoiTeqoLTj9WM7SjA/gOJIw5/JFZMRyJYYVe1jLAU2UhAfstIpCNRozg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.0.tgz", + "integrity": "sha512-PnMVyRid9JLxFavTjeDXEXo65HCRqbmLBw9xX9gfC4BZiSzbLXKzW3jPz+J0P71pLbD5tBMTT+mkstV5gD0c9Q==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.38.0", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.3.0.tgz", + "integrity": "sha512-I3FtM5Smua/ESXutFfb8gJ8ZPcvFR+qUgeeGFQHBOvRiRKyAk4aBE5nfqrxXx+h8/vn60DQjOt6i4RNtrbOobg==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.13.0", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.13.0.tgz", + "integrity": "sha512-ew34FsaqHokK4dxVrcIxSYrgWJ2XJYlkk32eiOIiEo3GkHUExdCzmozMYaUc2P67c5QJRUvvY0uqCs3QG67h5A==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.5.0.tgz", + "integrity": "sha512-xiYght50cvoODZYI43v3Ylsqiw14+D7ELsgzR40boDZaya1HFa1Etnv9MDkQE8X/UrXAffwv2AcNAhslgYuDTw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.27.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.42.0", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.42.0.tgz", + "integrity": "sha512-GwHV9Zs3HvWxBkoXatO/IeKoElzy3Ojf3dcyw1Rj3cyQVb+ZHtexslKdyzsrKRPJ0mUa62BoX+ZAg3zgTEql8w==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.37.0", + "rc-virtual-list": "^3.11.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-14.1.1.tgz", + "integrity": "sha512-5nOr9PVpJy2SWHTLgv1+kESDOb0tFzl0cYU9r9d8LfL0Wg9i/n1B558rmkxdQHgBwMqxmwoyPSAbQROxMQe8nw==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.13.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.6.3.tgz", + "integrity": "sha512-8k7+8Y2GJ/cQLiClFMg8kUXOOdvcFQrnGeSchOvI2ZMIVvX5a3zQpLxoODL0HTrvU63fPkRmMuqaEcOF9dQemA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.4.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.2.0.tgz", + "integrity": "sha512-iS/3iOAvtDh9GIx1ulY7EFUXUtktFccNLsARo3NPgLf0QW9oT0w3dA9cYWlhqAKmD+uriEwdWz1kH0Qs4zk2Aw==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.8.5", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.8.5.tgz", + "integrity": "sha512-PRfcZtVDNkR7oh26RuNe1hpw11c1wfgzwmPFL0lnxGnYefe9lDAO6cg5wJKIAwyXFVt5zHgpjYmaz0CPy1ZtKg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.19.0.tgz", + "integrity": "sha512-f4l5EsmSGF3ggj76YTzKNPY9SnXfFaer7ZccTSGb3urUf54L+cCqyT+UsPr+S5TAr8mZSxJ7g3CgkCe+cVQ6sw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-select": "~14.13.0", + "rc-tree": "~5.8.1", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.5.2.tgz", + "integrity": "sha512-QO3ne77DwnAPKFn0bA5qJM81QBjQi0e0NHdkvpFyY73Bea2NfITiotqJqVjHgeYPOJu5lLVR32TNGP084aSoXA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.39.1", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.39.1.tgz", + "integrity": "sha512-OW/ERynNDgNr4y0oiFmtes3rbEamXw7GHGbkbNd9iRr7kgT03T6fT0b9WpJ3mbxKhyOcAHnGcIoh5u/cjrC2OQ==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-virtual-list": { + "version": "3.11.4", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.11.4.tgz", + "integrity": "sha512-NbBi0fvyIu26gP69nQBiWgUMTPX3mr4FcuBQiVqagU0BnuX8WQkiivnMs105JROeuUIFczLrlgUhLQwTWV1XDA==", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/re-resizable": { "version": "6.9.11", "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.11.tgz", @@ -18961,7 +19854,6 @@ }, "node_modules/react-is": { "version": "18.2.0", - "dev": true, "license": "MIT" }, "node_modules/react-refresh": { @@ -19354,6 +20246,11 @@ "dev": true, "license": "MIT" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "1.22.8", "license": "MIT", @@ -19700,6 +20597,19 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/scroll-into-view-if-needed/node_modules/compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, "node_modules/select": { "version": "1.1.2", "license": "MIT" @@ -19976,6 +20886,11 @@ "node": ">=0.10.0" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "1.2.0", "dev": true, @@ -20258,7 +21173,6 @@ }, "node_modules/source-map-js": { "version": "1.0.2", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -20498,6 +21412,11 @@ "node": ">=0.6.19" } }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" + }, "node_modules/string-length": { "version": "4.0.2", "dev": true, @@ -20675,6 +21594,93 @@ "tslib": "^2.1.0" } }, + "node_modules/styled-components": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz", + "integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.1", + "@emotion/unitless": "0.8.0", + "@types/stylis": "4.2.0", + "css-to-react-native": "3.2.0", + "csstype": "3.1.2", + "postcss": "8.4.31", + "shallowequal": "1.1.0", + "stylis": "4.3.1", + "tslib": "2.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/styled-components/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/styled-components/node_modules/@emotion/unitless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + }, + "node_modules/styled-components/node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/stylehacks": { "version": "6.0.2", "dev": true, @@ -21185,6 +22191,14 @@ "dev": true, "license": "MIT" }, + "node_modules/throttle-debounce": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", + "engines": { + "node": ">=12.22" + } + }, "node_modules/through": { "version": "2.3.8", "dev": true, @@ -21714,6 +22728,20 @@ } } }, + "node_modules/use-error-boundary": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/use-error-boundary/-/use-error-boundary-2.0.6.tgz", + "integrity": "sha512-AWCVKSAanLe6R/on/ZkHYtGKfXs8BQX6z/TUGYqtvkajLqQyrGKJJscbahtq8OyN8L3LqTRjJWx4gCOLmfIObw==", + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/use-lilius": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/use-lilius/-/use-lilius-2.0.4.tgz", diff --git a/package.json b/package.json index a38bf6f..68c7ffd 100644 --- a/package.json +++ b/package.json @@ -41,12 +41,15 @@ "@wordpress/element": "^5.23.0", "@wordpress/hooks": "^3.49.0", "@wordpress/icons": "^9.43.0", + "antd": "^5.15.4", "copy-to-clipboard": "^3.3.3", "graphiql": "^3.0.10", "graphql": "^16.8.1", "graphql-ws": "^5.14.2", "lz-string": "^1.5.0", "react": "^18.2.0", + "styled-components": "^6.1.8", + "use-error-boundary": "^2.0.6", "vaul": "^0.9.0" }, "lint-staged": { diff --git a/src/index.js b/src/index.js index ccc7407..ddea706 100644 --- a/src/index.js +++ b/src/index.js @@ -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'; @@ -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 }; /** diff --git a/src/query-composer/index.js b/src/query-composer/index.js index 8b74438..af5e856 100644 --- a/src/query-composer/index.js +++ b/src/query-composer/index.js @@ -1,3 +1,5 @@ +import { Composer } from "./components/Composer"; + export const queryComposerPlugin = ( props ) => { console.log( { @@ -30,8 +32,7 @@ export const queryComposerPlugin = ( props ) => { }, content: () => (
-

Query Composer

-
{JSON.stringify(props, null, 2)}
+
), } From 799dec38d322759d1daad235978e5e88c26f62d5 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 28 Mar 2024 16:41:49 -0600 Subject: [PATCH 3/7] - initial work getting the Operation Panels working with the Panel component from @wordpress/components --- src/query-composer/components/Composer.js | 51 ++ .../components/OperationPanel.js | 63 +++ src/query-composer/components/QueryBuilder.js | 92 ++++ src/query-composer/components/RootView.js | 6 + src/query-composer/utils/utils.js | 477 ++++++++++++++++++ 5 files changed, 689 insertions(+) create mode 100644 src/query-composer/components/Composer.js create mode 100644 src/query-composer/components/OperationPanel.js create mode 100644 src/query-composer/components/QueryBuilder.js create mode 100644 src/query-composer/components/RootView.js create mode 100644 src/query-composer/utils/utils.js diff --git a/src/query-composer/components/Composer.js b/src/query-composer/components/Composer.js new file mode 100644 index 0000000..416cff5 --- /dev/null +++ b/src/query-composer/components/Composer.js @@ -0,0 +1,51 @@ +import {useOperationsEditorState, useOptimisticState, useSchemaContext} from "@graphiql/react"; +import {QueryBuilder} from "./QueryBuilder"; + +/** + * This is the wrapping component around the GraphiQL Explorer / Query Builder + * + * This provides the wrapping markup and sets up the initial visible state + * + * @param props + * @returns {JSX.Element|null} + * @constructor + */ +const Wrapper = ({ children }) => { + return ( +
+
+
Query Composer
+
+
+ {children} +
+
+ ); +}; + +export const Composer = (props) => { + + const { schema } = useSchemaContext(); + const [operationsString, handleEditOperations] = useOptimisticState( + useOperationsEditorState(), + ); + + if ( ! schema ) { + return ( + +
+ No Schema Available +
+
+ ); + } + + return ( + + + + ) +} diff --git a/src/query-composer/components/OperationPanel.js b/src/query-composer/components/OperationPanel.js new file mode 100644 index 0000000..15270cd --- /dev/null +++ b/src/query-composer/components/OperationPanel.js @@ -0,0 +1,63 @@ +import {GraphQLObjectType} from "graphql"; +import {RootView} from "./RootView"; + +export const OperationPanel = ({ index, operationName, operationType, relevantOperations, schema, operation, queryFields, mutationFields, subscriptionFields, getDefaultFieldNames, getDefaultScalarArgValue }) => { + + const fragmentType = + operation.kind === "FragmentDefinition" && + operation.typeCondition.kind === "NamedType" && + schema.getType(operation.typeCondition.name.value); + + const fragmentFields = fragmentType instanceof GraphQLObjectType ? fragmentType.getFields() : {}; + + const fields = + operationType === "query" + ? queryFields + : operationType === "mutation" + ? mutationFields + : operationType === "subscription" + ? subscriptionFields + : operation.kind === "FragmentDefinition" + ? fragmentFields + : null; + + const fragmentTypeName = + operation.kind === "FragmentDefinition" + ? operation.typeCondition.name.value + : null; + + const availableFragments = relevantOperations.reduce((acc, operation) => { + if (operation.kind === "FragmentDefinition") { + const fragmentTypeName = operation.typeCondition.name.value; + const existingFragmentsForType = acc[fragmentTypeName] || []; + const newFragmentsForType = [ + ...existingFragmentsForType, + operation, + ].sort((a, b) => a.name.value.localeCompare(b.name.value)); + return { + ...acc, + [fragmentTypeName]: newFragmentsForType, + }; + } + + return acc; + }, {}); + + return ( + + ); + +} diff --git a/src/query-composer/components/QueryBuilder.js b/src/query-composer/components/QueryBuilder.js new file mode 100644 index 0000000..9f85cd9 --- /dev/null +++ b/src/query-composer/components/QueryBuilder.js @@ -0,0 +1,92 @@ +import {defaultGetDefaultFieldNames, defaultGetDefaultScalarArgValue, memoizeParseQuery, DEFAULT_DOCUMENT} from "../utils/utils"; +import { useRef } from '@wordpress/element'; +import {Panel, PanelBody, PanelRow} from '@wordpress/components'; +import {OperationPanel} from "./OperationPanel"; + +export const QueryBuilder = (props) => { + + const { schema, query } = props; + const queryType = schema.getQueryType(); + const mutationType = schema.getMutationType(); + const subscriptionType = schema.getSubscriptionType(); + + let container = useRef(null); + + if ( ! queryType && ! mutationType && ! subscriptionType ) { + return ( +
+ No Query Type Found +
+ ); + } + + const queryFields = queryType ? queryType.getFields() : {}; + const mutationFields = mutationType ? mutationType.getFields() : {}; + const subscriptionFields = subscriptionType ? subscriptionType.getFields() : {}; + + const getDefaultFieldNames = + props.getDefaultFieldNames || defaultGetDefaultFieldNames; + + const getDefaultScalarArgValue = + props.getDefaultScalarArgValue || defaultGetDefaultScalarArgValue; + + const parsedQuery = memoizeParseQuery(query); + const definitions = parsedQuery.definitions; + + const _relevantOperations = definitions + .map((definition) => { + if (definition.kind === "FragmentDefinition") { + return definition; + } else if (definition.kind === "OperationDefinition") { + return definition; + } else { + return null; + } + }) + .filter(Boolean); + + const relevantOperations = + // If we don't have any relevant definitions from the parsed document, + // then at least show an expanded Query selection + _relevantOperations.length === 0 + ? DEFAULT_DOCUMENT.definitions + : _relevantOperations; + + return ( + { container = node }}> + {relevantOperations.map((operation, index) => { + const operationType = + operation.kind === "FragmentDefinition" + ? "fragment" + : (operation && operation.operation) || "query"; + const operationName = operation.name && operation.name.value || `Untitled ${operationType}`; + + return ( + + + + + + ) + })} + + {/**/} + {/* */} + {/* {JSON.stringify(parsedQuery, null, 2)}*/} + {/* */} + {/**/} + + ) +} diff --git a/src/query-composer/components/RootView.js b/src/query-composer/components/RootView.js new file mode 100644 index 0000000..b869630 --- /dev/null +++ b/src/query-composer/components/RootView.js @@ -0,0 +1,6 @@ +export const RootView = (props) => { + return ( +
{JSON.stringify(props, null, 2)}
+ ) +} + diff --git a/src/query-composer/utils/utils.js b/src/query-composer/utils/utils.js new file mode 100644 index 0000000..14b1959 --- /dev/null +++ b/src/query-composer/utils/utils.js @@ -0,0 +1,477 @@ +import { + parse, + isNonNullType, + isLeafType, + isWrappingType, + isScalarType, + isRequiredInputField, + isInputObjectType, + isEnumType, +} from "graphql"; + +import * as React from "react"; + +let parseQueryMemoize = null; + +/** + * Set a default operation if no operation is present in the Query document + * + * @type {{selectionSet: {selections: [], kind: string}, variableDefinitions: [], directives: [], kind: string, name: {kind: string, value: string}, operation: string}} + */ +const DEFAULT_OPERATION = { + kind: "OperationDefinition", + operation: "query", + variableDefinitions: [], + name: { + kind: "Name", + value: "NewQuery", + }, + directives: [], + selectionSet: { + kind: "SelectionSet", + selections: [], + }, +}; + +/** + * Parse the GraphQL Query document + * + * @param query + * @returns {Error|null|*} + */ +const parseQuery = (query) => { + try { + if (!query.trim()) { + return null; + } + return parse( + query, + // Tell graphql to not bother track locations when parsing, we don't need + // it and it's a tiny bit more expensive. + { noLocation: true } + ); + } catch (e) { + return new Error(e); + } +}; + +export const defaultGetDefaultFieldNames = (type) => { + const fields = type.getFields(); + + // Is there an `id` field? + if (fields["id"]) { + const res = ["id"]; + if (fields["email"]) { + res.push("email"); + } else if (fields["name"]) { + res.push("name"); + } + return res; + } + + // Is there an `edges` field? + if (fields["edges"]) { + return ["edges"]; + } + + // Is there an `node` field? + if (fields["node"]) { + return ["node"]; + } + + if (fields["nodes"]) { + return ["nodes"]; + } + + // Include all leaf-type fields. + const leafFieldNames = []; + Object.keys(fields).forEach((fieldName) => { + if (isLeafType(fields[fieldName].type)) { + leafFieldNames.push(fieldName); + } + }); + + if (!leafFieldNames.length) { + // No leaf fields, add typename so that the query stays valid + return ["__typename"]; + } + return leafFieldNames.slice(0, 2); // Prevent too many fields from being added +}; + +export const defaultColors = { + keyword: "#B11A04", + // OperationName, FragmentName + def: "#D2054E", + // FieldName + property: "#1F61A0", + // FieldAlias + qualifier: "#1C92A9", + // ArgumentName and ObjectFieldName + attribute: "#8B2BB9", + number: "#2882F9", + string: "#D64292", + // Boolean + builtin: "#D47509", + // Enum + string2: "#0B7FC7", + variable: "#397D13", + // Type + atom: "#CA9800", +}; + +export const defaultArrowOpen = ( + + + +); + +export const defaultArrowClosed = ( + + + +); + +export const defaultCheckboxChecked = ( + + + +); + +export const defaultCheckboxUnchecked = ( + + + +); + +export const defaultStyles = { + buttonStyle: { + fontSize: "1.2em", + padding: "0px", + backgroundColor: "white", + border: "none", + margin: "5px 0px", + height: "40px", + width: "100%", + display: "block", + maxWidth: "none", + }, + + actionButtonStyle: { + padding: "0px", + backgroundColor: "white", + border: "none", + margin: "0px", + maxWidth: "none", + height: "15px", + width: "15px", + display: "inline-block", + fontSize: "smaller", + }, + + explorerActionsStyle: { + margin: "4px -8px -8px", + paddingLeft: "8px", + bottom: "0px", + width: "100%", + textAlign: "center", + background: "none", + borderTop: "none", + borderBottom: "none", + }, +}; + +/** + * Set a default query document if no operations are present in the editor + * + * @type {{kind: string, definitions: [{selectionSet: {selections: *[], kind: string}, variableDefinitions: *[], directives: *[], kind: string, name: {kind: string, value: string}, operation: string}]}} + */ +export const DEFAULT_DOCUMENT = { + kind: "Document", + definitions: [DEFAULT_OPERATION], +}; + +/** + * Memoize the parsed query + * + * @param query + * @returns {{kind: string, definitions: {selectionSet: {selections: *[], kind: string}, variableDefinitions: *[], directives: *[], kind: string, name: {kind: string, value: string}, operation: string}[]}|*} + */ +export const memoizeParseQuery = (query) => { + + if (parseQueryMemoize && parseQueryMemoize[0] === query) { + return parseQueryMemoize[1]; + } else { + const result = parseQuery(query); + + if (!result) { + return DEFAULT_DOCUMENT; + } else if (result instanceof Error) { + if (parseQueryMemoize) { + return parseQueryMemoize[1] ?? ''; + } else { + return DEFAULT_DOCUMENT; + } + } else { + parseQueryMemoize = [query, result]; + return result; + } + } +}; + +// Capitalize a string +export const capitalize = (string) => { + return string.charAt(0).toUpperCase() + string.slice(1); +}; + +export const getDefaultFieldNames = (type) => { + const fields = type.getFields(); + + // Is there an `id` field? + if (fields["id"]) { + const res = ["id"]; + if (fields["email"]) { + res.push("email"); + } else if (fields["name"]) { + res.push("name"); + } + return res; + } + + // Is there an `edges` field? + if (fields["edges"]) { + return ["edges"]; + } + + // Is there an `node` field? + if (fields["node"]) { + return ["node"]; + } + + if (fields["nodes"]) { + return ["nodes"]; + } + + // Include all leaf-type fields. + const leafFieldNames = []; + Object.keys(fields).forEach((fieldName) => { + if (isLeafType(fields[fieldName].type)) { + leafFieldNames.push(fieldName); + } + }); + + if (!leafFieldNames.length) { + // No leaf fields, add typename so that the query stays valid + return ["__typename"]; + } + return leafFieldNames.slice(0, 2); // Prevent too many fields from being added +}; + +export const isRequiredArgument = (arg) => { + return isNonNullType(arg.type) && arg.defaultValue === undefined; +}; + +export const unwrapOutputType = (outputType) => { + let unwrappedType = outputType; + while (isWrappingType(unwrappedType)) { + unwrappedType = unwrappedType.ofType; + } + return unwrappedType; +}; + +export const unwrapInputType = (inputType) => { + let unwrappedType = inputType; + while (isWrappingType(unwrappedType)) { + unwrappedType = unwrappedType.ofType; + } + return unwrappedType; +}; + +export const coerceArgValue = (argType, value) => { + // Handle the case where we're setting a variable as the value + if (typeof value !== "string" && value.kind === "VariableDefinition") { + return value.variable; + } else if (isScalarType(argType)) { + try { + switch (argType.name) { + case "String": + return { + kind: "StringValue", + value: String(argType.parseValue(value)), + }; + case "Float": + return { + kind: "FloatValue", + value: String(argType.parseValue(parseFloat(value))), + }; + case "Int": + return { + kind: "IntValue", + value: String(argType.parseValue(parseInt(value, 10))), + }; + case "Boolean": + try { + const parsed = JSON.parse(value); + if (typeof parsed === "boolean") { + return { kind: "BooleanValue", value: parsed }; + } else { + return { kind: "BooleanValue", value: false }; + } + } catch (e) { + return { + kind: "BooleanValue", + value: false, + }; + } + default: + return { + kind: "StringValue", + value: String(argType.parseValue(value)), + }; + } + } catch (e) { + console.error("error coercing arg value", e, value); + return { kind: "StringValue", value: value }; + } + } else { + try { + const parsedValue = argType.parseValue(value); + if (parsedValue) { + return { kind: "EnumValue", value: String(parsedValue) }; + } else { + return { kind: "EnumValue", value: argType.getValues()[0].name }; + } + } catch (e) { + return { kind: "EnumValue", value: argType.getValues()[0].name }; + } + } +}; + +export const defaultInputObjectFields = ( + getDefaultScalarArgValue, + makeDefaultArg, + parentField, + fields +) => { + const nodes = []; + for (const field of fields) { + if ( + isRequiredInputField(field) || + (makeDefaultArg && makeDefaultArg(parentField, field)) + ) { + const fieldType = unwrapInputType(field.type); + if (isInputObjectType(fieldType)) { + const fields = fieldType.getFields(); + nodes.push({ + kind: "ObjectField", + name: { kind: "Name", value: field.name }, + value: { + kind: "ObjectValue", + fields: defaultInputObjectFields( + getDefaultScalarArgValue, + makeDefaultArg, + parentField, + Object.keys(fields).map((k) => fields[k]) + ), + }, + }); + } else if (isLeafType(fieldType)) { + nodes.push({ + kind: "ObjectField", + name: { kind: "Name", value: field.name }, + value: getDefaultScalarArgValue(parentField, field, fieldType), + }); + } + } + } + return nodes; +}; + +export const defaultArgs = ( + getDefaultScalarArgValue, + makeDefaultArg, + field +) => { + const args = []; + for (const arg of field.args) { + if ( + isRequiredArgument(arg) || + (makeDefaultArg && makeDefaultArg(field, arg)) + ) { + const argType = unwrapInputType(arg.type); + if (isInputObjectType(argType)) { + const fields = argType.getFields(); + args.push({ + kind: "Argument", + name: { kind: "Name", value: arg.name }, + value: { + kind: "ObjectValue", + fields: defaultInputObjectFields( + getDefaultScalarArgValue, + makeDefaultArg, + field, + Object.keys(fields).map((k) => fields[k]) + ), + }, + }); + } else if (isLeafType(argType)) { + args.push({ + kind: "Argument", + name: { kind: "Name", value: arg.name }, + value: getDefaultScalarArgValue(field, arg, argType), + }); + } + } + } + return args; +}; + +export const defaultValue = (argType) => { + if (isEnumType(argType)) { + return { kind: "EnumValue", value: argType.getValues()[0].name }; + } else { + switch (argType.name) { + case "String": + return { kind: "StringValue", value: "" }; + case "Float": + return { kind: "FloatValue", value: "1.5" }; + case "Int": + return { kind: "IntValue", value: "10" }; + case "Boolean": + return { kind: "BooleanValue", value: false }; + default: + return { kind: "StringValue", value: "" }; + } + } +}; + +export const defaultGetDefaultScalarArgValue = (parentField, arg, argType) => { + return defaultValue(argType); +}; + +export const isRunShortcut = (event) => { + return event.ctrlKey && event.key === "Enter"; +}; + +export const canRunOperation = (operationName) => { + return operationName !== "FragmentDefinition"; +}; From d417136e2a8ef525349582be5e261223e3a9fed5 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 28 Mar 2024 20:21:45 -0600 Subject: [PATCH 4/7] - implement the Query Explorer plugin instead of re-building the components - adjust styles --- package-lock.json | 25 + package.json | 1 + src/components/Editor.jsx | 9 +- src/query-composer/README.md | 4 - src/query-composer/components/Composer.js | 51 -- .../components/OperationPanel.js | 63 --- src/query-composer/components/QueryBuilder.js | 92 ---- src/query-composer/components/RootView.js | 6 - src/query-composer/index.js | 40 -- src/query-composer/utils/utils.js | 477 ------------------ styles/explorer.css | 0 styles/wpgraphql-ide.css | 78 +++ 12 files changed, 109 insertions(+), 737 deletions(-) delete mode 100644 src/query-composer/README.md delete mode 100644 src/query-composer/components/Composer.js delete mode 100644 src/query-composer/components/OperationPanel.js delete mode 100644 src/query-composer/components/QueryBuilder.js delete mode 100644 src/query-composer/components/RootView.js delete mode 100644 src/query-composer/index.js delete mode 100644 src/query-composer/utils/utils.js create mode 100644 styles/explorer.css diff --git a/package-lock.json b/package-lock.json index 703489a..d622869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "@graphiql/plugin-explorer": "^1.0.3", "@wordpress/components": "^27.0.0", "@wordpress/data": "^9.22.0", "@wordpress/element": "^5.23.0", @@ -2431,6 +2432,30 @@ "version": "0.2.1", "license": "MIT" }, + "node_modules/@graphiql/plugin-explorer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@graphiql/plugin-explorer/-/plugin-explorer-1.0.3.tgz", + "integrity": "sha512-dKgdNXut/CaxfU8lcFDi1Jb/5f7NnCHa62E7xlNUNYRzieZ7+Bka0rvspgxzXPYevviigzmrtPGtHd5SeaZ57g==", + "dependencies": { + "graphiql-explorer": "^0.9.0" + }, + "peerDependencies": { + "@graphiql/react": "^0.20.3", + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/@graphiql/plugin-explorer/node_modules/graphiql-explorer": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/graphiql-explorer/-/graphiql-explorer-0.9.0.tgz", + "integrity": "sha512-fZC/wsuatqiQDO2otchxriFO0LaWIo/ovF/CQJ1yOudmY0P7pzDiP+l9CEHUiWbizk3e99x6DQG4XG1VxA+d6A==", + "peerDependencies": { + "graphql": "^0.6.0 || ^0.7.0 || ^0.8.0-b || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0", + "react": "^15.6.0 || ^16.0.0", + "react-dom": "^15.6.0 || ^16.0.0" + } + }, "node_modules/@graphiql/react": { "version": "0.20.3", "license": "MIT", diff --git a/package.json b/package.json index 68c7ffd..fcf0f9a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Editor.jsx b/src/components/Editor.jsx index 934862c..8d3dc99 100644 --- a/src/components/Editor.jsx +++ b/src/components/Editor.jsx @@ -2,8 +2,7 @@ import React from 'react'; import { GraphiQL } from 'graphiql'; import { useDispatch, useSelect } from '@wordpress/data'; - -import { queryComposerPlugin } from '../query-composer'; +import { explorerPlugin } from '@graphiql/plugin-explorer'; import { PrettifyButton } from './toolbarButtons/PrettifyButton'; import { CopyQueryButton } from './toolbarButtons/CopyQueryButton'; @@ -38,7 +37,9 @@ const toolbarButtons = { share: ShareDocumentButton, }; -const queryComposer = queryComposerPlugin(); +const explorer = explorerPlugin(); + +import '../../styles/explorer.css'; export function Editor() { const query = useSelect( ( select ) => { @@ -56,7 +57,7 @@ export function Editor() { { Object.entries( toolbarButtons ).map( diff --git a/src/query-composer/README.md b/src/query-composer/README.md deleted file mode 100644 index 7a40eaf..0000000 --- a/src/query-composer/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Query Composer - -This directory contains code related to the GraphiQL Query Composer, a feature that allows users to compose queries using a visual interface. - diff --git a/src/query-composer/components/Composer.js b/src/query-composer/components/Composer.js deleted file mode 100644 index 416cff5..0000000 --- a/src/query-composer/components/Composer.js +++ /dev/null @@ -1,51 +0,0 @@ -import {useOperationsEditorState, useOptimisticState, useSchemaContext} from "@graphiql/react"; -import {QueryBuilder} from "./QueryBuilder"; - -/** - * This is the wrapping component around the GraphiQL Explorer / Query Builder - * - * This provides the wrapping markup and sets up the initial visible state - * - * @param props - * @returns {JSX.Element|null} - * @constructor - */ -const Wrapper = ({ children }) => { - return ( -
-
-
Query Composer
-
-
- {children} -
-
- ); -}; - -export const Composer = (props) => { - - const { schema } = useSchemaContext(); - const [operationsString, handleEditOperations] = useOptimisticState( - useOperationsEditorState(), - ); - - if ( ! schema ) { - return ( - -
- No Schema Available -
-
- ); - } - - return ( - - - - ) -} diff --git a/src/query-composer/components/OperationPanel.js b/src/query-composer/components/OperationPanel.js deleted file mode 100644 index 15270cd..0000000 --- a/src/query-composer/components/OperationPanel.js +++ /dev/null @@ -1,63 +0,0 @@ -import {GraphQLObjectType} from "graphql"; -import {RootView} from "./RootView"; - -export const OperationPanel = ({ index, operationName, operationType, relevantOperations, schema, operation, queryFields, mutationFields, subscriptionFields, getDefaultFieldNames, getDefaultScalarArgValue }) => { - - const fragmentType = - operation.kind === "FragmentDefinition" && - operation.typeCondition.kind === "NamedType" && - schema.getType(operation.typeCondition.name.value); - - const fragmentFields = fragmentType instanceof GraphQLObjectType ? fragmentType.getFields() : {}; - - const fields = - operationType === "query" - ? queryFields - : operationType === "mutation" - ? mutationFields - : operationType === "subscription" - ? subscriptionFields - : operation.kind === "FragmentDefinition" - ? fragmentFields - : null; - - const fragmentTypeName = - operation.kind === "FragmentDefinition" - ? operation.typeCondition.name.value - : null; - - const availableFragments = relevantOperations.reduce((acc, operation) => { - if (operation.kind === "FragmentDefinition") { - const fragmentTypeName = operation.typeCondition.name.value; - const existingFragmentsForType = acc[fragmentTypeName] || []; - const newFragmentsForType = [ - ...existingFragmentsForType, - operation, - ].sort((a, b) => a.name.value.localeCompare(b.name.value)); - return { - ...acc, - [fragmentTypeName]: newFragmentsForType, - }; - } - - return acc; - }, {}); - - return ( - - ); - -} diff --git a/src/query-composer/components/QueryBuilder.js b/src/query-composer/components/QueryBuilder.js deleted file mode 100644 index 9f85cd9..0000000 --- a/src/query-composer/components/QueryBuilder.js +++ /dev/null @@ -1,92 +0,0 @@ -import {defaultGetDefaultFieldNames, defaultGetDefaultScalarArgValue, memoizeParseQuery, DEFAULT_DOCUMENT} from "../utils/utils"; -import { useRef } from '@wordpress/element'; -import {Panel, PanelBody, PanelRow} from '@wordpress/components'; -import {OperationPanel} from "./OperationPanel"; - -export const QueryBuilder = (props) => { - - const { schema, query } = props; - const queryType = schema.getQueryType(); - const mutationType = schema.getMutationType(); - const subscriptionType = schema.getSubscriptionType(); - - let container = useRef(null); - - if ( ! queryType && ! mutationType && ! subscriptionType ) { - return ( -
- No Query Type Found -
- ); - } - - const queryFields = queryType ? queryType.getFields() : {}; - const mutationFields = mutationType ? mutationType.getFields() : {}; - const subscriptionFields = subscriptionType ? subscriptionType.getFields() : {}; - - const getDefaultFieldNames = - props.getDefaultFieldNames || defaultGetDefaultFieldNames; - - const getDefaultScalarArgValue = - props.getDefaultScalarArgValue || defaultGetDefaultScalarArgValue; - - const parsedQuery = memoizeParseQuery(query); - const definitions = parsedQuery.definitions; - - const _relevantOperations = definitions - .map((definition) => { - if (definition.kind === "FragmentDefinition") { - return definition; - } else if (definition.kind === "OperationDefinition") { - return definition; - } else { - return null; - } - }) - .filter(Boolean); - - const relevantOperations = - // If we don't have any relevant definitions from the parsed document, - // then at least show an expanded Query selection - _relevantOperations.length === 0 - ? DEFAULT_DOCUMENT.definitions - : _relevantOperations; - - return ( - { container = node }}> - {relevantOperations.map((operation, index) => { - const operationType = - operation.kind === "FragmentDefinition" - ? "fragment" - : (operation && operation.operation) || "query"; - const operationName = operation.name && operation.name.value || `Untitled ${operationType}`; - - return ( - - - - - - ) - })} - - {/**/} - {/* */} - {/* {JSON.stringify(parsedQuery, null, 2)}*/} - {/* */} - {/**/} - - ) -} diff --git a/src/query-composer/components/RootView.js b/src/query-composer/components/RootView.js deleted file mode 100644 index b869630..0000000 --- a/src/query-composer/components/RootView.js +++ /dev/null @@ -1,6 +0,0 @@ -export const RootView = (props) => { - return ( -
{JSON.stringify(props, null, 2)}
- ) -} - diff --git a/src/query-composer/index.js b/src/query-composer/index.js deleted file mode 100644 index af5e856..0000000 --- a/src/query-composer/index.js +++ /dev/null @@ -1,40 +0,0 @@ -import { Composer } from "./components/Composer"; - -export const queryComposerPlugin = ( props ) => { - - console.log( { - queryComposerPlugin: props, - }) - - return { - title: 'Query Composer', - icon: () => { - - - - - - }, - content: () => ( -
- -
- ), - } - -} diff --git a/src/query-composer/utils/utils.js b/src/query-composer/utils/utils.js deleted file mode 100644 index 14b1959..0000000 --- a/src/query-composer/utils/utils.js +++ /dev/null @@ -1,477 +0,0 @@ -import { - parse, - isNonNullType, - isLeafType, - isWrappingType, - isScalarType, - isRequiredInputField, - isInputObjectType, - isEnumType, -} from "graphql"; - -import * as React from "react"; - -let parseQueryMemoize = null; - -/** - * Set a default operation if no operation is present in the Query document - * - * @type {{selectionSet: {selections: [], kind: string}, variableDefinitions: [], directives: [], kind: string, name: {kind: string, value: string}, operation: string}} - */ -const DEFAULT_OPERATION = { - kind: "OperationDefinition", - operation: "query", - variableDefinitions: [], - name: { - kind: "Name", - value: "NewQuery", - }, - directives: [], - selectionSet: { - kind: "SelectionSet", - selections: [], - }, -}; - -/** - * Parse the GraphQL Query document - * - * @param query - * @returns {Error|null|*} - */ -const parseQuery = (query) => { - try { - if (!query.trim()) { - return null; - } - return parse( - query, - // Tell graphql to not bother track locations when parsing, we don't need - // it and it's a tiny bit more expensive. - { noLocation: true } - ); - } catch (e) { - return new Error(e); - } -}; - -export const defaultGetDefaultFieldNames = (type) => { - const fields = type.getFields(); - - // Is there an `id` field? - if (fields["id"]) { - const res = ["id"]; - if (fields["email"]) { - res.push("email"); - } else if (fields["name"]) { - res.push("name"); - } - return res; - } - - // Is there an `edges` field? - if (fields["edges"]) { - return ["edges"]; - } - - // Is there an `node` field? - if (fields["node"]) { - return ["node"]; - } - - if (fields["nodes"]) { - return ["nodes"]; - } - - // Include all leaf-type fields. - const leafFieldNames = []; - Object.keys(fields).forEach((fieldName) => { - if (isLeafType(fields[fieldName].type)) { - leafFieldNames.push(fieldName); - } - }); - - if (!leafFieldNames.length) { - // No leaf fields, add typename so that the query stays valid - return ["__typename"]; - } - return leafFieldNames.slice(0, 2); // Prevent too many fields from being added -}; - -export const defaultColors = { - keyword: "#B11A04", - // OperationName, FragmentName - def: "#D2054E", - // FieldName - property: "#1F61A0", - // FieldAlias - qualifier: "#1C92A9", - // ArgumentName and ObjectFieldName - attribute: "#8B2BB9", - number: "#2882F9", - string: "#D64292", - // Boolean - builtin: "#D47509", - // Enum - string2: "#0B7FC7", - variable: "#397D13", - // Type - atom: "#CA9800", -}; - -export const defaultArrowOpen = ( - - - -); - -export const defaultArrowClosed = ( - - - -); - -export const defaultCheckboxChecked = ( - - - -); - -export const defaultCheckboxUnchecked = ( - - - -); - -export const defaultStyles = { - buttonStyle: { - fontSize: "1.2em", - padding: "0px", - backgroundColor: "white", - border: "none", - margin: "5px 0px", - height: "40px", - width: "100%", - display: "block", - maxWidth: "none", - }, - - actionButtonStyle: { - padding: "0px", - backgroundColor: "white", - border: "none", - margin: "0px", - maxWidth: "none", - height: "15px", - width: "15px", - display: "inline-block", - fontSize: "smaller", - }, - - explorerActionsStyle: { - margin: "4px -8px -8px", - paddingLeft: "8px", - bottom: "0px", - width: "100%", - textAlign: "center", - background: "none", - borderTop: "none", - borderBottom: "none", - }, -}; - -/** - * Set a default query document if no operations are present in the editor - * - * @type {{kind: string, definitions: [{selectionSet: {selections: *[], kind: string}, variableDefinitions: *[], directives: *[], kind: string, name: {kind: string, value: string}, operation: string}]}} - */ -export const DEFAULT_DOCUMENT = { - kind: "Document", - definitions: [DEFAULT_OPERATION], -}; - -/** - * Memoize the parsed query - * - * @param query - * @returns {{kind: string, definitions: {selectionSet: {selections: *[], kind: string}, variableDefinitions: *[], directives: *[], kind: string, name: {kind: string, value: string}, operation: string}[]}|*} - */ -export const memoizeParseQuery = (query) => { - - if (parseQueryMemoize && parseQueryMemoize[0] === query) { - return parseQueryMemoize[1]; - } else { - const result = parseQuery(query); - - if (!result) { - return DEFAULT_DOCUMENT; - } else if (result instanceof Error) { - if (parseQueryMemoize) { - return parseQueryMemoize[1] ?? ''; - } else { - return DEFAULT_DOCUMENT; - } - } else { - parseQueryMemoize = [query, result]; - return result; - } - } -}; - -// Capitalize a string -export const capitalize = (string) => { - return string.charAt(0).toUpperCase() + string.slice(1); -}; - -export const getDefaultFieldNames = (type) => { - const fields = type.getFields(); - - // Is there an `id` field? - if (fields["id"]) { - const res = ["id"]; - if (fields["email"]) { - res.push("email"); - } else if (fields["name"]) { - res.push("name"); - } - return res; - } - - // Is there an `edges` field? - if (fields["edges"]) { - return ["edges"]; - } - - // Is there an `node` field? - if (fields["node"]) { - return ["node"]; - } - - if (fields["nodes"]) { - return ["nodes"]; - } - - // Include all leaf-type fields. - const leafFieldNames = []; - Object.keys(fields).forEach((fieldName) => { - if (isLeafType(fields[fieldName].type)) { - leafFieldNames.push(fieldName); - } - }); - - if (!leafFieldNames.length) { - // No leaf fields, add typename so that the query stays valid - return ["__typename"]; - } - return leafFieldNames.slice(0, 2); // Prevent too many fields from being added -}; - -export const isRequiredArgument = (arg) => { - return isNonNullType(arg.type) && arg.defaultValue === undefined; -}; - -export const unwrapOutputType = (outputType) => { - let unwrappedType = outputType; - while (isWrappingType(unwrappedType)) { - unwrappedType = unwrappedType.ofType; - } - return unwrappedType; -}; - -export const unwrapInputType = (inputType) => { - let unwrappedType = inputType; - while (isWrappingType(unwrappedType)) { - unwrappedType = unwrappedType.ofType; - } - return unwrappedType; -}; - -export const coerceArgValue = (argType, value) => { - // Handle the case where we're setting a variable as the value - if (typeof value !== "string" && value.kind === "VariableDefinition") { - return value.variable; - } else if (isScalarType(argType)) { - try { - switch (argType.name) { - case "String": - return { - kind: "StringValue", - value: String(argType.parseValue(value)), - }; - case "Float": - return { - kind: "FloatValue", - value: String(argType.parseValue(parseFloat(value))), - }; - case "Int": - return { - kind: "IntValue", - value: String(argType.parseValue(parseInt(value, 10))), - }; - case "Boolean": - try { - const parsed = JSON.parse(value); - if (typeof parsed === "boolean") { - return { kind: "BooleanValue", value: parsed }; - } else { - return { kind: "BooleanValue", value: false }; - } - } catch (e) { - return { - kind: "BooleanValue", - value: false, - }; - } - default: - return { - kind: "StringValue", - value: String(argType.parseValue(value)), - }; - } - } catch (e) { - console.error("error coercing arg value", e, value); - return { kind: "StringValue", value: value }; - } - } else { - try { - const parsedValue = argType.parseValue(value); - if (parsedValue) { - return { kind: "EnumValue", value: String(parsedValue) }; - } else { - return { kind: "EnumValue", value: argType.getValues()[0].name }; - } - } catch (e) { - return { kind: "EnumValue", value: argType.getValues()[0].name }; - } - } -}; - -export const defaultInputObjectFields = ( - getDefaultScalarArgValue, - makeDefaultArg, - parentField, - fields -) => { - const nodes = []; - for (const field of fields) { - if ( - isRequiredInputField(field) || - (makeDefaultArg && makeDefaultArg(parentField, field)) - ) { - const fieldType = unwrapInputType(field.type); - if (isInputObjectType(fieldType)) { - const fields = fieldType.getFields(); - nodes.push({ - kind: "ObjectField", - name: { kind: "Name", value: field.name }, - value: { - kind: "ObjectValue", - fields: defaultInputObjectFields( - getDefaultScalarArgValue, - makeDefaultArg, - parentField, - Object.keys(fields).map((k) => fields[k]) - ), - }, - }); - } else if (isLeafType(fieldType)) { - nodes.push({ - kind: "ObjectField", - name: { kind: "Name", value: field.name }, - value: getDefaultScalarArgValue(parentField, field, fieldType), - }); - } - } - } - return nodes; -}; - -export const defaultArgs = ( - getDefaultScalarArgValue, - makeDefaultArg, - field -) => { - const args = []; - for (const arg of field.args) { - if ( - isRequiredArgument(arg) || - (makeDefaultArg && makeDefaultArg(field, arg)) - ) { - const argType = unwrapInputType(arg.type); - if (isInputObjectType(argType)) { - const fields = argType.getFields(); - args.push({ - kind: "Argument", - name: { kind: "Name", value: arg.name }, - value: { - kind: "ObjectValue", - fields: defaultInputObjectFields( - getDefaultScalarArgValue, - makeDefaultArg, - field, - Object.keys(fields).map((k) => fields[k]) - ), - }, - }); - } else if (isLeafType(argType)) { - args.push({ - kind: "Argument", - name: { kind: "Name", value: arg.name }, - value: getDefaultScalarArgValue(field, arg, argType), - }); - } - } - } - return args; -}; - -export const defaultValue = (argType) => { - if (isEnumType(argType)) { - return { kind: "EnumValue", value: argType.getValues()[0].name }; - } else { - switch (argType.name) { - case "String": - return { kind: "StringValue", value: "" }; - case "Float": - return { kind: "FloatValue", value: "1.5" }; - case "Int": - return { kind: "IntValue", value: "10" }; - case "Boolean": - return { kind: "BooleanValue", value: false }; - default: - return { kind: "StringValue", value: "" }; - } - } -}; - -export const defaultGetDefaultScalarArgValue = (parentField, arg, argType) => { - return defaultValue(argType); -}; - -export const isRunShortcut = (event) => { - return event.ctrlKey && event.key === "Enter"; -}; - -export const canRunOperation = (operationName) => { - return operationName !== "FragmentDefinition"; -}; diff --git a/styles/explorer.css b/styles/explorer.css new file mode 100644 index 0000000..e69de29 diff --git a/styles/wpgraphql-ide.css b/styles/wpgraphql-ide.css index 379d325..20973cc 100644 --- a/styles/wpgraphql-ide.css +++ b/styles/wpgraphql-ide.css @@ -152,3 +152,81 @@ body.graphql_page_graphql-ide #wpfooter { body.graphql_page_graphql-ide .AppRoot { height: 100%; } + +/** + * GraphiQL Explorer Styles + */ +#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap { + width: 100% !important; + +} + +#wpgraphql-ide-root .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-root .graphiql-plugin .docExplorerWrap .doc-explorer-title-bar { + padding: var(--px-16); + padding-bottom: 0; +} + +#wpgraphql-ide-root .graphiql-plugin .doc-explorer-rhs { + display: none!important; +} + +#wpgraphql-ide-root .graphiql-plugin .doc-explorer-contents { + height: 100%; +} + +#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root { + padding: 0!important; +} + +#wpgraphql-ide-root .graphiql-plugin:has(div.docExplorerWrap) { + padding: 0!important; +} + +#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root > div:first-of-type { + padding: 20px!important; + flex: 3 1 0!important; +} + +#wpgraphql-ide-root .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-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root .graphiql-explorer-actions { + border-top: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy))!important; +} +#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root .graphiql-explorer-actions > span:first-of-type{ + padding: 10px; +} + +#wpgraphql-ide-root .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-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root>div>div:last-of-type { + border-bottom: none!important; + margin-bottom: 0!important; +} + +#wpgraphql-ide-root .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; +} From 156cb84712aeed8361503eff083779147bb19591 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 28 Mar 2024 20:37:04 -0600 Subject: [PATCH 5/7] - update dependencies, styles --- package-lock.json | 3 --- package.json | 3 --- src/components/Editor.jsx | 5 +++-- styles/wpgraphql-ide.css | 28 ++++++++++++++-------------- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index d622869..208ef8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,15 +11,12 @@ "@wordpress/element": "^5.23.0", "@wordpress/hooks": "^3.49.0", "@wordpress/icons": "^9.43.0", - "antd": "^5.15.4", "copy-to-clipboard": "^3.3.3", "graphiql": "^3.0.10", "graphql": "^16.8.1", "graphql-ws": "^5.14.2", "lz-string": "^1.5.0", "react": "^18.2.0", - "styled-components": "^6.1.8", - "use-error-boundary": "^2.0.6", "vaul": "^0.9.0" }, "devDependencies": { diff --git a/package.json b/package.json index fcf0f9a..bc89592 100644 --- a/package.json +++ b/package.json @@ -42,15 +42,12 @@ "@wordpress/element": "^5.23.0", "@wordpress/hooks": "^3.49.0", "@wordpress/icons": "^9.43.0", - "antd": "^5.15.4", "copy-to-clipboard": "^3.3.3", "graphiql": "^3.0.10", "graphql": "^16.8.1", "graphql-ws": "^5.14.2", "lz-string": "^1.5.0", "react": "^18.2.0", - "styled-components": "^6.1.8", - "use-error-boundary": "^2.0.6", "vaul": "^0.9.0" }, "lint-staged": { diff --git a/src/components/Editor.jsx b/src/components/Editor.jsx index 8d3dc99..a4342e2 100644 --- a/src/components/Editor.jsx +++ b/src/components/Editor.jsx @@ -53,7 +53,8 @@ export function Editor() { const { setDrawerOpen } = useDispatch( 'wpgraphql-ide' ); return ( - <> + // this id is used to scope styles to the app. + - + ); } diff --git a/styles/wpgraphql-ide.css b/styles/wpgraphql-ide.css index 20973cc..62f5ed8 100644 --- a/styles/wpgraphql-ide.css +++ b/styles/wpgraphql-ide.css @@ -156,45 +156,45 @@ body.graphql_page_graphql-ide .AppRoot { /** * GraphiQL Explorer Styles */ -#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap { +#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap { width: 100% !important; } -#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap .doc-explorer-title { +#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-root .graphiql-plugin .docExplorerWrap .doc-explorer-title-bar { +#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .doc-explorer-title-bar { padding: var(--px-16); padding-bottom: 0; } -#wpgraphql-ide-root .graphiql-plugin .doc-explorer-rhs { +#wpgraphql-ide-app .graphiql-plugin .doc-explorer-rhs { display: none!important; } -#wpgraphql-ide-root .graphiql-plugin .doc-explorer-contents { +#wpgraphql-ide-app .graphiql-plugin .doc-explorer-contents { height: 100%; } -#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root { +#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root { padding: 0!important; } -#wpgraphql-ide-root .graphiql-plugin:has(div.docExplorerWrap) { +#wpgraphql-ide-app .graphiql-plugin:has(div.docExplorerWrap) { padding: 0!important; } -#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root > div:first-of-type { +#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root > div:first-of-type { padding: 20px!important; flex: 3 1 0!important; } -#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root input { +#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root input { width: auto!important; padding: 0 8px; line-height: 2; @@ -206,25 +206,25 @@ body.graphql_page_graphql-ide .AppRoot { background: transparent; } -#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root .graphiql-explorer-actions { +#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-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root .graphiql-explorer-actions > span:first-of-type{ +#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root .graphiql-explorer-actions > span:first-of-type{ padding: 10px; } -#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root>div>div { +#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-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root>div>div:last-of-type { +#wpgraphql-ide-app .graphiql-plugin .docExplorerWrap .graphiql-explorer-root>div>div:last-of-type { border-bottom: none!important; margin-bottom: 0!important; } -#wpgraphql-ide-root .graphiql-plugin .docExplorerWrap .graphiql-explorer-root .graphiql-explorer-actions > select { +#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; From 3ac798dd2d33c6eba3b817e5b180327aacc2056a Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 28 Mar 2024 20:39:12 -0600 Subject: [PATCH 6/7] - update test --- tests/e2e/specs/query-composer.spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/e2e/specs/query-composer.spec.js b/tests/e2e/specs/query-composer.spec.js index 8dea0d5..cf25d84 100644 --- a/tests/e2e/specs/query-composer.spec.js +++ b/tests/e2e/specs/query-composer.spec.js @@ -15,15 +15,15 @@ test.describe( 'GraphiQL Query Composer', () => { await expect( page.locator( '.graphiql-container' ) ).toBeVisible(); // query composer should be hidden by default - await expect( page.locator( '.graphiql-query-composer' ) ).toBeHidden(); + await expect( page.locator( '.docExplorerWrap' ) ).toBeHidden(); // open query composer and check if it is visible - await page.click( '[aria-label="Show Query Composer"]' ); - await expect( page.locator( '.graphiql-query-composer' ) ).toBeVisible(); + 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 Query Composer"]' ); - await expect( page.locator( '.graphiql-query-composer' ) ).toBeHidden(); + await page.click( '[aria-label="Hide GraphiQL Explorer"]' ); + await expect( page.locator( '.docExplorerWrap' ) ).toBeHidden(); }); From 7eecfb5018dee3e2a387f891cf989c8cfdbf5124 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Fri, 29 Mar 2024 15:25:29 -0600 Subject: [PATCH 7/7] - add tests --- tests/e2e/specs/query-composer.spec.js | 111 ++++++++++++++++++++++++- tests/e2e/utils.js | 2 + 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/tests/e2e/specs/query-composer.spec.js b/tests/e2e/specs/query-composer.spec.js index cf25d84..a08bbe9 100644 --- a/tests/e2e/specs/query-composer.spec.js +++ b/tests/e2e/specs/query-composer.spec.js @@ -1,4 +1,4 @@ -import {loginToWordPressAdmin, openDrawer, visitAdminFacingPage} from "../utils"; +import {loginToWordPressAdmin, openDrawer, typeQuery, visitAdminFacingPage} from "../utils"; import {expect, test } from "@wordpress/e2e-test-utils-playwright"; // Login to WordPress before each test @@ -6,6 +6,13 @@ 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 } ) => { @@ -27,4 +34,106 @@ test.describe( 'GraphiQL Query Composer', () => { }); + 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` ); + + }); + }); diff --git a/tests/e2e/utils.js b/tests/e2e/utils.js index beb7eaf..79dacfa 100644 --- a/tests/e2e/utils.js +++ b/tests/e2e/utils.js @@ -140,6 +140,8 @@ export async function openDrawer( page ) { state: 'visible', } ); } + + await page.waitForLoadState( 'networkidle' ); } export async function closeDrawer( page ) {