Skip to content

Commit eb2d91f

Browse files
fix: new util to manage historyStore outside of query history component (#1914)
1 parent 04fad79 commit eb2d91f

File tree

6 files changed

+246
-132
lines changed

6 files changed

+246
-132
lines changed

.changeset/short-mirrors-occur.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'graphiql': minor
3+
---
4+
5+
fix: history can now be saved even when query history panel is not opened
6+
feat: create a new maxHistoryLength prop to allow more than 20 queries in history panel

packages/graphiql/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ For more details on props, see the [API Docs](https://graphiql-test.netlify.app/
216216
| `onEditHeaders` | `Function` | called when the request headers editor changes. The argument to the function will be the headers string. |
217217
| `onEditOperationName` | `Function` | called when the operation name to be executed changes. |
218218
| `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. |
219+
| `maxHistoryLength` | `number` | **Default:** 20. allows you to increase the number of queries in the history component | 20 |
219220

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

packages/graphiql/src/components/GraphiQL.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import type {
6464
Unsubscribable,
6565
FetcherResultPayload,
6666
} from '@graphiql/toolkit';
67+
import HistoryStore from '../utility/HistoryStore';
6768

6869
const DEFAULT_DOC_EXPLORER_WIDTH = 350;
6970

@@ -123,6 +124,7 @@ export type GraphiQLProps = {
123124
readOnly?: boolean;
124125
docExplorerOpen?: boolean;
125126
toolbar?: GraphiQLToolbarConfig;
127+
maxHistoryLength?: number;
126128
};
127129

128130
export type GraphiQLState = {
@@ -147,6 +149,7 @@ export type GraphiQLState = {
147149
variableToType?: VariableToType;
148150
operations?: OperationDefinitionNode[];
149151
documentAST?: DocumentNode;
152+
maxHistoryLength: number;
150153
};
151154

152155
/**
@@ -185,6 +188,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
185188
variableEditorComponent: Maybe<VariableEditor>;
186189
headerEditorComponent: Maybe<HeaderEditor>;
187190
_queryHistory: Maybe<QueryHistory>;
191+
_historyStore: Maybe<HistoryStore>;
188192
editorBarComponent: Maybe<HTMLDivElement>;
189193
queryEditorComponent: Maybe<QueryEditor>;
190194
resultViewerElement: Maybe<HTMLElement>;
@@ -200,6 +204,10 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
200204
// Cache the storage instance
201205
this._storage = new StorageAPI(props.storage);
202206

207+
const maxHistoryLength = props.maxHistoryLength ?? 20;
208+
209+
this._historyStore = new HistoryStore(this._storage, maxHistoryLength);
210+
203211
// Disable setState when the component is not mounted
204212
this.componentIsMounted = false;
205213

@@ -285,6 +293,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
285293
DEFAULT_DOC_EXPLORER_WIDTH,
286294
isWaitingForResponse: false,
287295
subscription: null,
296+
maxHistoryLength,
288297
...queryFacts,
289298
};
290299
}
@@ -493,6 +502,7 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
493502
variables={this.state.variables}
494503
onSelectQuery={this.handleSelectHistoryQuery}
495504
storage={this._storage}
505+
maxHistoryLength={this.state.maxHistoryLength}
496506
queryID={this._editorQueryID}>
497507
<button
498508
className="docExplorerHide"
@@ -1062,12 +1072,21 @@ export class GraphiQL extends React.Component<GraphiQLProps, GraphiQLState> {
10621072
this._storage.set('operationName', operationName as string);
10631073

10641074
if (this._queryHistory) {
1065-
this._queryHistory.updateHistory(
1075+
this._queryHistory.onUpdateHistory(
10661076
editedQuery,
10671077
variables,
10681078
headers,
10691079
operationName,
10701080
);
1081+
} else {
1082+
if (this._historyStore) {
1083+
this._historyStore.updateHistory(
1084+
editedQuery,
1085+
variables,
1086+
headers,
1087+
operationName,
1088+
);
1089+
}
10711090
}
10721091

10731092
// when dealing with defer or stream, we need to aggregate results

packages/graphiql/src/components/QueryHistory.tsx

Lines changed: 46 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,15 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import { parse } from 'graphql';
98
import React from 'react';
10-
import QueryStore, { QueryStoreItem } from '../utility/QueryStore';
9+
import { QueryStoreItem } from '../utility/QueryStore';
1110
import HistoryQuery, {
1211
HandleEditLabelFn,
13-
HandleToggleFavoriteFn,
1412
HandleSelectQueryFn,
13+
HandleToggleFavoriteFn,
1514
} from './HistoryQuery';
1615
import StorageAPI from '../utility/StorageAPI';
17-
18-
const MAX_QUERY_SIZE = 100000;
19-
const MAX_HISTORY_LENGTH = 20;
20-
21-
const shouldSaveQuery = (
22-
query?: string,
23-
variables?: string,
24-
headers?: string,
25-
lastQuerySaved?: QueryStoreItem,
26-
) => {
27-
if (!query) {
28-
return false;
29-
}
30-
31-
try {
32-
parse(query);
33-
} catch (e) {
34-
return false;
35-
}
36-
37-
// Don't try to save giant queries
38-
if (query.length > MAX_QUERY_SIZE) {
39-
return false;
40-
}
41-
if (!lastQuerySaved) {
42-
return true;
43-
}
44-
if (JSON.stringify(query) === JSON.stringify(lastQuerySaved.query)) {
45-
if (
46-
JSON.stringify(variables) === JSON.stringify(lastQuerySaved.variables)
47-
) {
48-
if (JSON.stringify(headers) === JSON.stringify(lastQuerySaved.headers)) {
49-
return false;
50-
}
51-
if (headers && !lastQuerySaved.headers) {
52-
return false;
53-
}
54-
}
55-
if (variables && !lastQuerySaved.variables) {
56-
return false;
57-
}
58-
}
59-
return true;
60-
};
16+
import HistoryStore from '../utility/HistoryStore';
6117

6218
type QueryHistoryProps = {
6319
query?: string;
@@ -67,6 +23,7 @@ type QueryHistoryProps = {
6723
queryID?: number;
6824
onSelectQuery: HandleSelectQueryFn;
6925
storage: StorageAPI;
26+
maxHistoryLength: number;
7027
};
7128

7229
type QueryHistoryState = {
@@ -77,129 +34,87 @@ export class QueryHistory extends React.Component<
7734
QueryHistoryProps,
7835
QueryHistoryState
7936
> {
80-
historyStore: QueryStore;
81-
favoriteStore: QueryStore;
37+
historyStore: HistoryStore;
8238

8339
constructor(props: QueryHistoryProps) {
8440
super(props);
85-
this.historyStore = new QueryStore(
86-
'queries',
87-
props.storage,
88-
MAX_HISTORY_LENGTH,
41+
this.historyStore = new HistoryStore(
42+
this.props.storage,
43+
this.props.maxHistoryLength,
8944
);
90-
// favorites are not automatically deleted, so there's no need for a max length
91-
this.favoriteStore = new QueryStore('favorites', props.storage, null);
92-
const historyQueries = this.historyStore.fetchAll();
93-
const favoriteQueries = this.favoriteStore.fetchAll();
94-
const queries = historyQueries.concat(favoriteQueries);
45+
const queries = this.historyStore.queries;
9546
this.state = { queries };
9647
}
9748

98-
render() {
99-
const queries = this.state.queries.slice().reverse();
100-
const queryNodes = queries.map((query, i) => {
101-
return (
102-
<HistoryQuery
103-
handleEditLabel={this.editLabel}
104-
handleToggleFavorite={this.toggleFavorite}
105-
key={`${i}:${query.label || query.query}`}
106-
onSelect={this.props.onSelectQuery}
107-
{...query}
108-
/>
109-
);
110-
});
111-
return (
112-
<section aria-label="History">
113-
<div className="history-title-bar">
114-
<div className="history-title">{'History'}</div>
115-
<div className="doc-explorer-rhs">{this.props.children}</div>
116-
</div>
117-
<ul className="history-contents">{queryNodes}</ul>
118-
</section>
119-
);
120-
}
121-
122-
// Public API
123-
updateHistory = (
49+
onUpdateHistory = (
12450
query?: string,
12551
variables?: string,
12652
headers?: string,
12753
operationName?: string,
12854
) => {
129-
if (
130-
shouldSaveQuery(
131-
query,
132-
variables,
133-
headers,
134-
this.historyStore.fetchRecent(),
135-
)
136-
) {
137-
this.historyStore.push({
138-
query,
139-
variables,
140-
headers,
141-
operationName,
142-
});
143-
const historyQueries = this.historyStore.items;
144-
const favoriteQueries = this.favoriteStore.items;
145-
const queries = historyQueries.concat(favoriteQueries);
146-
this.setState({
147-
queries,
148-
});
149-
}
55+
this.historyStore.updateHistory(query, variables, headers, operationName);
56+
this.setState({ queries: this.historyStore.queries });
15057
};
15158

152-
// Public API
153-
toggleFavorite: HandleToggleFavoriteFn = (
59+
onHandleEditLabel: HandleEditLabelFn = (
15460
query,
15561
variables,
15662
headers,
15763
operationName,
15864
label,
15965
favorite,
16066
) => {
161-
const item: QueryStoreItem = {
67+
this.historyStore.editLabel(
16268
query,
16369
variables,
16470
headers,
16571
operationName,
16672
label,
167-
};
168-
if (!this.favoriteStore.contains(item)) {
169-
item.favorite = true;
170-
this.favoriteStore.push(item);
171-
} else if (favorite) {
172-
item.favorite = false;
173-
this.favoriteStore.delete(item);
174-
}
175-
this.setState({
176-
queries: [...this.historyStore.items, ...this.favoriteStore.items],
177-
});
73+
favorite,
74+
);
75+
this.setState({ queries: this.historyStore.queries });
17876
};
17977

180-
// Public API
181-
editLabel: HandleEditLabelFn = (
78+
onToggleFavorite: HandleToggleFavoriteFn = (
18279
query,
18380
variables,
18481
headers,
18582
operationName,
18683
label,
18784
favorite,
18885
) => {
189-
const item = {
86+
this.historyStore.toggleFavorite(
19087
query,
19188
variables,
19289
headers,
19390
operationName,
19491
label,
195-
};
196-
if (favorite) {
197-
this.favoriteStore.edit({ ...item, favorite });
198-
} else {
199-
this.historyStore.edit(item);
200-
}
201-
this.setState({
202-
queries: [...this.historyStore.items, ...this.favoriteStore.items],
203-
});
92+
favorite,
93+
);
94+
this.setState({ queries: this.historyStore.queries });
20495
};
96+
97+
render() {
98+
const queries = this.state.queries.slice().reverse();
99+
const queryNodes = queries.map((query, i) => {
100+
return (
101+
<HistoryQuery
102+
handleEditLabel={this.onHandleEditLabel}
103+
handleToggleFavorite={this.onToggleFavorite}
104+
key={`${i}:${query.label || query.query}`}
105+
onSelect={this.props.onSelectQuery}
106+
{...query}
107+
/>
108+
);
109+
});
110+
return (
111+
<section aria-label="History">
112+
<div className="history-title-bar">
113+
<div className="history-title">{'History'}</div>
114+
<div className="doc-explorer-rhs">{this.props.children}</div>
115+
</div>
116+
<ul className="history-contents">{queryNodes}</ul>
117+
</section>
118+
);
119+
}
205120
}

packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,21 @@ describe('GraphiQL', () => {
175175
expect(container.querySelector('.historyPaneWrap')).not.toBeInTheDocument();
176176
});
177177

178+
it('will save history item even when history panel is closed', () => {
179+
const { getByTitle, container } = render(
180+
<GraphiQL
181+
query={mockQuery1}
182+
variables={mockVariables1}
183+
headers={mockHeaders1}
184+
operationName={mockOperationName1}
185+
fetcher={noOpFetcher}
186+
/>,
187+
);
188+
fireEvent.click(getByTitle('Execute Query (Ctrl-Enter)'));
189+
fireEvent.click(getByTitle('Show History'));
190+
expect(container.querySelectorAll('.history-contents li')).toHaveLength(1);
191+
});
192+
178193
it('adds a history item when the execute query function button is clicked', () => {
179194
const { getByTitle, container } = render(
180195
<GraphiQL

0 commit comments

Comments
 (0)