From 36624564d4b7b775861bcb081e1655640843a56c Mon Sep 17 00:00:00 2001 From: harshithpabbati Date: Fri, 5 Feb 2021 10:57:02 +0530 Subject: [PATCH] feat: [RFC] Introduce Query Params --- .../src/api/actions/browserActions.ts | 18 ++++ .../src/api/hooks/index.ts | 1 + .../src/api/hooks/useQueryParams.ts | 6 ++ .../graphiql-2-rfc-context/src/api/index.ts | 1 + .../api/providers/GraphiQLBrowserProvider.tsx | 93 +++++++++++++++++++ .../api/providers/GraphiQLSchemaProvider.tsx | 2 +- .../graphiql-2-rfc-context/src/api/types.ts | 5 + .../src/components/GraphiQL.tsx | 24 +++-- .../src/components/QueryEditor.tsx | 11 ++- .../src/components/VariableEditor.tsx | 9 +- packages/graphiql-toolkit/src/index.ts | 53 +++++++++++ 11 files changed, 209 insertions(+), 14 deletions(-) create mode 100644 packages/graphiql-2-rfc-context/src/api/actions/browserActions.ts create mode 100644 packages/graphiql-2-rfc-context/src/api/hooks/useQueryParams.ts create mode 100644 packages/graphiql-2-rfc-context/src/api/providers/GraphiQLBrowserProvider.tsx diff --git a/packages/graphiql-2-rfc-context/src/api/actions/browserActions.ts b/packages/graphiql-2-rfc-context/src/api/actions/browserActions.ts new file mode 100644 index 00000000000..cc1fe2ab870 --- /dev/null +++ b/packages/graphiql-2-rfc-context/src/api/actions/browserActions.ts @@ -0,0 +1,18 @@ +export enum BrowserActionTypes { + QueryStringParamsChanged = 'QueryStringParamsChanged', +} + +export type BrowserAction = QueryStringParamsChangedAction; + +export const queryStringParamsChangedAction = ( + parameter: string, + value: string, +) => + ({ + type: BrowserActionTypes.QueryStringParamsChanged, + payload: { parameter, value }, + } as const); + +export type QueryStringParamsChangedAction = ReturnType< + typeof queryStringParamsChangedAction +>; diff --git a/packages/graphiql-2-rfc-context/src/api/hooks/index.ts b/packages/graphiql-2-rfc-context/src/api/hooks/index.ts index 868c56a394d..cad48b2ca0a 100644 --- a/packages/graphiql-2-rfc-context/src/api/hooks/index.ts +++ b/packages/graphiql-2-rfc-context/src/api/hooks/index.ts @@ -10,3 +10,4 @@ export * from './useOperation'; export * from './useQueryFacts'; export * from './useSchema'; export * from './useValueRef'; +export * from './useQueryParams'; diff --git a/packages/graphiql-2-rfc-context/src/api/hooks/useQueryParams.ts b/packages/graphiql-2-rfc-context/src/api/hooks/useQueryParams.ts new file mode 100644 index 00000000000..52a2553c7d9 --- /dev/null +++ b/packages/graphiql-2-rfc-context/src/api/hooks/useQueryParams.ts @@ -0,0 +1,6 @@ +import { useBrowserContext } from '../providers/GraphiQLBrowserProvider'; + +export default function useQueryParams(name: string) { + const { queryStringParams } = useBrowserContext(); + return queryStringParams[name]; +} diff --git a/packages/graphiql-2-rfc-context/src/api/index.ts b/packages/graphiql-2-rfc-context/src/api/index.ts index af5583a50f7..9daf4380001 100644 --- a/packages/graphiql-2-rfc-context/src/api/index.ts +++ b/packages/graphiql-2-rfc-context/src/api/index.ts @@ -9,5 +9,6 @@ export * from './providers/GraphiQLEditorsProvider'; export * from './providers/GraphiQLSessionProvider'; export * from './providers/GraphiQLSchemaProvider'; +export * from './providers/GraphiQLBrowserProvider'; export * from './hooks'; export * from './types'; diff --git a/packages/graphiql-2-rfc-context/src/api/providers/GraphiQLBrowserProvider.tsx b/packages/graphiql-2-rfc-context/src/api/providers/GraphiQLBrowserProvider.tsx new file mode 100644 index 00000000000..af2b27922c6 --- /dev/null +++ b/packages/graphiql-2-rfc-context/src/api/providers/GraphiQLBrowserProvider.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { parseQueryStringURL, updateQueryStringURL } from 'graphiql-toolkit'; +import { BrowserState } from '../types'; + +import { + BrowserAction, + BrowserActionTypes, + queryStringParamsChangedAction, +} from '../actions/browserActions'; + +export type BrowserReducer = React.Reducer; +export interface BrowserHandlers { + changeQueryStringParams: (parameter: string, value: string) => void; + dispatch: React.Dispatch; +} + +export const initialBrowserState: BrowserState = { + queryStringParams: { + operation: + parseQueryStringURL(window.location.search).operation || + parseQueryStringURL(window.location.search).query, + variables: parseQueryStringURL(window.location.search).variables, + operationName: parseQueryStringURL(window.location.search).operationName, + }, + queryStringParamsURL: '', +}; + +export const initialBrowserContextState: BrowserState & BrowserHandlers = { + changeQueryStringParams: () => null, + dispatch: () => null, + ...initialBrowserState, +}; + +export const BrowserContext = React.createContext< + BrowserState & BrowserHandlers +>(initialBrowserContextState); + +export const useBrowserContext = () => React.useContext(BrowserContext); + +const browserReducer: BrowserReducer = (state, action) => { + switch (action.type) { + case BrowserActionTypes.QueryStringParamsChanged: { + const { parameter, value } = action.payload; + return { + ...state, + queryStringParams: { + ...state.queryStringParams, + [parameter]: value, + }, + queryStringParamsURL: updateQueryStringURL( + state.queryStringParams, + parameter, + value, + ), + }; + } + default: { + return state; + } + } +}; + +export type BrowserProviderProps = { + children: React.ReactNode; +}; + +export function BrowserProvider({ children }: BrowserProviderProps) { + const [state, dispatch] = React.useReducer( + browserReducer, + initialBrowserState, + ); + + const changeQueryStringParams = React.useCallback( + (parameter: string, value: string) => + dispatch(queryStringParamsChangedAction(parameter, value)), + [dispatch], + ); + + React.useEffect(() => { + history.replaceState(null, document.title, state.queryStringParamsURL); + }, [state.queryStringParamsURL]); + + return ( + + {children} + + ); +} diff --git a/packages/graphiql-2-rfc-context/src/api/providers/GraphiQLSchemaProvider.tsx b/packages/graphiql-2-rfc-context/src/api/providers/GraphiQLSchemaProvider.tsx index ca8ec1031b6..26f59f7e399 100644 --- a/packages/graphiql-2-rfc-context/src/api/providers/GraphiQLSchemaProvider.tsx +++ b/packages/graphiql-2-rfc-context/src/api/providers/GraphiQLSchemaProvider.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * */ - +/* global monaco */ import React, { useCallback } from 'react'; import { GraphQLSchema } from 'graphql'; import { SchemaConfig, Fetcher } from '../../types'; diff --git a/packages/graphiql-2-rfc-context/src/api/types.ts b/packages/graphiql-2-rfc-context/src/api/types.ts index 1d58b28ceea..ac0928b2fb1 100644 --- a/packages/graphiql-2-rfc-context/src/api/types.ts +++ b/packages/graphiql-2-rfc-context/src/api/types.ts @@ -38,3 +38,8 @@ export type SessionState = { subscription?: Unsubscribable | null; operationName?: string; // current operation name }; + +export type BrowserState = { + queryStringParams: any; + queryStringParamsURL: string; +}; diff --git a/packages/graphiql-2-rfc-context/src/components/GraphiQL.tsx b/packages/graphiql-2-rfc-context/src/components/GraphiQL.tsx index dfab415ed11..8992ee4b38d 100644 --- a/packages/graphiql-2-rfc-context/src/components/GraphiQL.tsx +++ b/packages/graphiql-2-rfc-context/src/components/GraphiQL.tsx @@ -32,6 +32,8 @@ import { SessionProvider, SessionContext, } from '../api/providers/GraphiQLSessionProvider'; +import { BrowserProvider } from '../api/providers/GraphiQLBrowserProvider'; + import { getFetcher } from '../api/common'; import { Unsubscribable, Fetcher, ReactNodeLike } from '../types'; @@ -126,16 +128,18 @@ export const GraphiQL: React.FC = props => { - - - {props.children} - - + + + + {props.children} + + + diff --git a/packages/graphiql-2-rfc-context/src/components/QueryEditor.tsx b/packages/graphiql-2-rfc-context/src/components/QueryEditor.tsx index 58ca1758740..d7a816e1f4d 100644 --- a/packages/graphiql-2-rfc-context/src/components/QueryEditor.tsx +++ b/packages/graphiql-2-rfc-context/src/components/QueryEditor.tsx @@ -1,3 +1,4 @@ +/* global monaco */ /** @jsx jsx */ /** * Copyright (c) 2021 GraphQL Contributors. @@ -15,6 +16,7 @@ import EditorWrapper from '../components/common/EditorWrapper'; import { useSessionContext } from '../api/providers/GraphiQLSessionProvider'; import { useEditorsContext } from '../api/providers/GraphiQLEditorsProvider'; +import { useBrowserContext } from '../api/providers/GraphiQLBrowserProvider'; export type QueryEditorProps = { onEdit?: (value: string) => void; @@ -37,6 +39,7 @@ export function QueryEditor(props: QueryEditorProps) { const [ignoreChangeEvent, setIgnoreChangeEvent] = React.useState(false); const cachedValueRef = React.useRef(props.operation ?? ''); const session = useSessionContext(); + const browser = useBrowserContext(); const { loadEditor } = useEditorsContext(); @@ -49,6 +52,7 @@ export function QueryEditor(props: QueryEditorProps) { React.useEffect(() => { require('monaco-graphql/esm/monaco.contribution'); + session.changeOperation(browser.queryStringParams.operation); // Lazily require to ensure requiring GraphiQL outside of a Browser context // does not produce an error. @@ -88,7 +92,12 @@ export function QueryEditor(props: QueryEditorProps) { editor.setValue(thisValue); } setIgnoreChangeEvent(false); - }, [session, session.operation, session.operation.text]); + }, [ + session, + session.operation, + session.operation.text, + browser.queryStringParams.operation, + ]); React.useEffect(() => { const editor = editorRef.current; diff --git a/packages/graphiql-2-rfc-context/src/components/VariableEditor.tsx b/packages/graphiql-2-rfc-context/src/components/VariableEditor.tsx index 412f0c7feb3..8f441da07a6 100644 --- a/packages/graphiql-2-rfc-context/src/components/VariableEditor.tsx +++ b/packages/graphiql-2-rfc-context/src/components/VariableEditor.tsx @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - +/* global monaco */ /** @jsx jsx */ import { jsx } from 'theme-ui'; import { GraphQLType } from 'graphql'; @@ -15,6 +15,7 @@ import EditorWrapper from '../components/common/EditorWrapper'; import { useEditorsContext } from '../api/providers/GraphiQLEditorsProvider'; import { useSessionContext } from '../api/providers/GraphiQLSessionProvider'; +import { useBrowserContext } from '../api/providers/GraphiQLBrowserProvider'; import type { EditorOptions } from '../types'; // import useQueryFacts from '../api/hooks/useQueryFacts'; @@ -45,6 +46,7 @@ export type VariableEditorProps = { */ export function VariableEditor(props: VariableEditorProps) { const session = useSessionContext(); + const browser = useBrowserContext(); // const queryFacts = useQueryFacts(); const [ignoreChangeEvent, setIgnoreChangeEvent] = React.useState(false); const editorRef = React.useRef(); @@ -54,6 +56,7 @@ export function VariableEditor(props: VariableEditorProps) { // const variableToType = queryFacts?.variableToType React.useEffect(() => { + session.changeVariables(browser.queryStringParams.variables); // Lazily require to ensure requiring GraphiQL outside of a Browser context // does not produce an error. @@ -89,6 +92,7 @@ export function VariableEditor(props: VariableEditorProps) { if (!ignoreChangeEvent) { cachedValueRef.current = editor.getValue(); session.changeVariables(cachedValueRef.current); + browser.changeQueryStringParams('variables', cachedValueRef.current); } }); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -104,7 +108,8 @@ export function VariableEditor(props: VariableEditorProps) { // user-input changes which could otherwise result in an infinite // event loop. setIgnoreChangeEvent(true); - if (session.variables.text !== cachedValueRef.current) { + const vars = session.variables.text; + if (vars && vars !== cachedValueRef.current) { const thisValue = session.variables.text || ''; cachedValueRef.current = thisValue; editor.setValue(thisValue); diff --git a/packages/graphiql-toolkit/src/index.ts b/packages/graphiql-toolkit/src/index.ts index 9690aedea3c..a7d40668da9 100644 --- a/packages/graphiql-toolkit/src/index.ts +++ b/packages/graphiql-toolkit/src/index.ts @@ -1,3 +1,56 @@ +/** + * Copyright (c) 2020 GraphQL Contributors. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + export * from './types'; // TODO: move the most useful utilities from graphiql to here + +export function parseQueryStringURL(searchURL: string) { + const parameters: any = {}; + searchURL + .substr(1) + .split('&') + .forEach(function (entry) { + const eq = entry.indexOf('='); + if (eq >= 0) { + parameters[decodeURIComponent(entry.slice(0, eq))] = decodeURIComponent( + entry.slice(eq + 1), + ); + } + }); + return parameters; +} + +export function updateQueryStringURL( + parameters: any, + parameter: string, + value: string, +) { + parameters[parameter] = value; + let newSearch: string = ''; + if ( + parameters.operationName !== undefined || + parameters.operation !== undefined || + parameters.variables !== undefined + ) { + newSearch = + '?' + + Object.keys(parameters) + .filter(function (key) { + return Boolean(parameters[key]); + }) + .map(function (key) { + return ( + encodeURIComponent(key) + + '=' + + encodeURIComponent(parameters?.[key]) + ); + }) + .join('&'); + } + return newSearch; +}