Skip to content

Commit

Permalink
fix: new util to manage historyStore outside of query history compone…
Browse files Browse the repository at this point in the history
…nt (#1914)
  • Loading branch information
harshithpabbati authored Jul 10, 2021
1 parent 04fad79 commit eb2d91f
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 132 deletions.
6 changes: 6 additions & 0 deletions .changeset/short-mirrors-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'graphiql': minor
---

fix: history can now be saved even when query history panel is not opened
feat: create a new maxHistoryLength prop to allow more than 20 queries in history panel
1 change: 1 addition & 0 deletions packages/graphiql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ For more details on props, see the [API Docs](https://graphiql-test.netlify.app/
| `onEditHeaders` | `Function` | called when the request headers editor changes. The argument to the function will be the headers string. |
| `onEditOperationName` | `Function` | called when the operation name to be executed changes. |
| `onToggleDocs` | `Function` | called when the docs will be toggled. The argument to the function will be a boolean whether the docs are now open or closed. |
| `maxHistoryLength` | `number` | **Default:** 20. allows you to increase the number of queries in the history component | 20 |

### Children (this pattern will be dropped in 2.0.0)

Expand Down
21 changes: 20 additions & 1 deletion packages/graphiql/src/components/GraphiQL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import type {
Unsubscribable,
FetcherResultPayload,
} from '@graphiql/toolkit';
import HistoryStore from '../utility/HistoryStore';

const DEFAULT_DOC_EXPLORER_WIDTH = 350;

Expand Down Expand Up @@ -123,6 +124,7 @@ export type GraphiQLProps = {
readOnly?: boolean;
docExplorerOpen?: boolean;
toolbar?: GraphiQLToolbarConfig;
maxHistoryLength?: number;
};

export type GraphiQLState = {
Expand All @@ -147,6 +149,7 @@ export type GraphiQLState = {
variableToType?: VariableToType;
operations?: OperationDefinitionNode[];
documentAST?: DocumentNode;
maxHistoryLength: number;
};

/**
Expand Down Expand Up @@ -185,6 +188,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
variableEditorComponent: Maybe<VariableEditor>;
headerEditorComponent: Maybe<HeaderEditor>;
_queryHistory: Maybe<QueryHistory>;
_historyStore: Maybe<HistoryStore>;
editorBarComponent: Maybe<HTMLDivElement>;
queryEditorComponent: Maybe<QueryEditor>;
resultViewerElement: Maybe<HTMLElement>;
Expand All @@ -200,6 +204,10 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
// Cache the storage instance
this._storage = new StorageAPI(props.storage);

const maxHistoryLength = props.maxHistoryLength ?? 20;

this._historyStore = new HistoryStore(this._storage, maxHistoryLength);

// Disable setState when the component is not mounted
this.componentIsMounted = false;

Expand Down Expand Up @@ -285,6 +293,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
DEFAULT_DOC_EXPLORER_WIDTH,
isWaitingForResponse: false,
subscription: null,
maxHistoryLength,
...queryFacts,
};
}
Expand Down Expand Up @@ -493,6 +502,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
variables={this.state.variables}
onSelectQuery={this.handleSelectHistoryQuery}
storage={this._storage}
maxHistoryLength={this.state.maxHistoryLength}
queryID={this._editorQueryID}>
<button
className="docExplorerHide"
Expand Down Expand Up @@ -1062,12 +1072,21 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
this._storage.set('operationName', operationName as string);

if (this._queryHistory) {
this._queryHistory.updateHistory(
this._queryHistory.onUpdateHistory(
editedQuery,
variables,
headers,
operationName,
);
} else {
if (this._historyStore) {
this._historyStore.updateHistory(
editedQuery,
variables,
headers,
operationName,
);
}
}

// when dealing with defer or stream, we need to aggregate results
Expand Down
177 changes: 46 additions & 131 deletions packages/graphiql/src/components/QueryHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,15 @@
* LICENSE file in the root directory of this source tree.
*/

import { parse } from 'graphql';
import React from 'react';
import QueryStore, { QueryStoreItem } from '../utility/QueryStore';
import { QueryStoreItem } from '../utility/QueryStore';
import HistoryQuery, {
HandleEditLabelFn,
HandleToggleFavoriteFn,
HandleSelectQueryFn,
HandleToggleFavoriteFn,
} from './HistoryQuery';
import StorageAPI from '../utility/StorageAPI';

const MAX_QUERY_SIZE = 100000;
const MAX_HISTORY_LENGTH = 20;

const shouldSaveQuery = (
query?: string,
variables?: string,
headers?: string,
lastQuerySaved?: QueryStoreItem,
) => {
if (!query) {
return false;
}

try {
parse(query);
} catch (e) {
return false;
}

// Don't try to save giant queries
if (query.length > MAX_QUERY_SIZE) {
return false;
}
if (!lastQuerySaved) {
return true;
}
if (JSON.stringify(query) === JSON.stringify(lastQuerySaved.query)) {
if (
JSON.stringify(variables) === JSON.stringify(lastQuerySaved.variables)
) {
if (JSON.stringify(headers) === JSON.stringify(lastQuerySaved.headers)) {
return false;
}
if (headers && !lastQuerySaved.headers) {
return false;
}
}
if (variables && !lastQuerySaved.variables) {
return false;
}
}
return true;
};
import HistoryStore from '../utility/HistoryStore';

type QueryHistoryProps = {
query?: string;
Expand All @@ -67,6 +23,7 @@ type QueryHistoryProps = {
queryID?: number;
onSelectQuery: HandleSelectQueryFn;
storage: StorageAPI;
maxHistoryLength: number;
};

type QueryHistoryState = {
Expand All @@ -77,129 +34,87 @@ export class QueryHistory extends React.Component<
QueryHistoryProps,
QueryHistoryState
> {
historyStore: QueryStore;
favoriteStore: QueryStore;
historyStore: HistoryStore;

constructor(props: QueryHistoryProps) {
super(props);
this.historyStore = new QueryStore(
'queries',
props.storage,
MAX_HISTORY_LENGTH,
this.historyStore = new HistoryStore(
this.props.storage,
this.props.maxHistoryLength,
);
// favorites are not automatically deleted, so there's no need for a max length
this.favoriteStore = new QueryStore('favorites', props.storage, null);
const historyQueries = this.historyStore.fetchAll();
const favoriteQueries = this.favoriteStore.fetchAll();
const queries = historyQueries.concat(favoriteQueries);
const queries = this.historyStore.queries;
this.state = { queries };
}

render() {
const queries = this.state.queries.slice().reverse();
const queryNodes = queries.map((query, i) => {
return (
<HistoryQuery
handleEditLabel={this.editLabel}
handleToggleFavorite={this.toggleFavorite}
key={`${i}:${query.label || query.query}`}
onSelect={this.props.onSelectQuery}
{...query}
/>
);
});
return (
<section aria-label="History">
<div className="history-title-bar">
<div className="history-title">{'History'}</div>
<div className="doc-explorer-rhs">{this.props.children}</div>
</div>
<ul className="history-contents">{queryNodes}</ul>
</section>
);
}

// Public API
updateHistory = (
onUpdateHistory = (
query?: string,
variables?: string,
headers?: string,
operationName?: string,
) => {
if (
shouldSaveQuery(
query,
variables,
headers,
this.historyStore.fetchRecent(),
)
) {
this.historyStore.push({
query,
variables,
headers,
operationName,
});
const historyQueries = this.historyStore.items;
const favoriteQueries = this.favoriteStore.items;
const queries = historyQueries.concat(favoriteQueries);
this.setState({
queries,
});
}
this.historyStore.updateHistory(query, variables, headers, operationName);
this.setState({ queries: this.historyStore.queries });
};

// Public API
toggleFavorite: HandleToggleFavoriteFn = (
onHandleEditLabel: HandleEditLabelFn = (
query,
variables,
headers,
operationName,
label,
favorite,
) => {
const item: QueryStoreItem = {
this.historyStore.editLabel(
query,
variables,
headers,
operationName,
label,
};
if (!this.favoriteStore.contains(item)) {
item.favorite = true;
this.favoriteStore.push(item);
} else if (favorite) {
item.favorite = false;
this.favoriteStore.delete(item);
}
this.setState({
queries: [...this.historyStore.items, ...this.favoriteStore.items],
});
favorite,
);
this.setState({ queries: this.historyStore.queries });
};

// Public API
editLabel: HandleEditLabelFn = (
onToggleFavorite: HandleToggleFavoriteFn = (
query,
variables,
headers,
operationName,
label,
favorite,
) => {
const item = {
this.historyStore.toggleFavorite(
query,
variables,
headers,
operationName,
label,
};
if (favorite) {
this.favoriteStore.edit({ ...item, favorite });
} else {
this.historyStore.edit(item);
}
this.setState({
queries: [...this.historyStore.items, ...this.favoriteStore.items],
});
favorite,
);
this.setState({ queries: this.historyStore.queries });
};

render() {
const queries = this.state.queries.slice().reverse();
const queryNodes = queries.map((query, i) => {
return (
<HistoryQuery
handleEditLabel={this.onHandleEditLabel}
handleToggleFavorite={this.onToggleFavorite}
key={`${i}:${query.label || query.query}`}
onSelect={this.props.onSelectQuery}
{...query}
/>
);
});
return (
<section aria-label="History">
<div className="history-title-bar">
<div className="history-title">{'History'}</div>
<div className="doc-explorer-rhs">{this.props.children}</div>
</div>
<ul className="history-contents">{queryNodes}</ul>
</section>
);
}
}
15 changes: 15 additions & 0 deletions packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,21 @@ describe('GraphiQL', () => {
expect(container.querySelector('.historyPaneWrap')).not.toBeInTheDocument();
});

it('will save history item even when history panel is closed', () => {
const { getByTitle, container } = render(
<GraphiQL
query={mockQuery1}
variables={mockVariables1}
headers={mockHeaders1}
operationName={mockOperationName1}
fetcher={noOpFetcher}
/>,
);
fireEvent.click(getByTitle('Execute Query (Ctrl-Enter)'));
fireEvent.click(getByTitle('Show History'));
expect(container.querySelectorAll('.history-contents li')).toHaveLength(1);
});

it('adds a history item when the execute query function button is clicked', () => {
const { getByTitle, container } = render(
<GraphiQL
Expand Down
Loading

0 comments on commit eb2d91f

Please sign in to comment.