Skip to content

Commit 43ab0cf

Browse files
authored
Merge pull request #22 from swiftype/jasonstoltz/fix-back-button
Fixed browser back button and miscellaneous changes
2 parents c10c866 + fef704b commit 43ab0cf

File tree

8 files changed

+190
-72
lines changed

8 files changed

+190
-72
lines changed

.circleci/config.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Javascript Node CircleCI 2.0 configuration file
2+
#
3+
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
4+
#
5+
version: 2
6+
jobs:
7+
build:
8+
docker:
9+
# specify the version you desire here
10+
- image: circleci/node:7.10
11+
12+
# Specify service dependencies here if necessary
13+
# CircleCI maintains a library of pre-built images
14+
# documented at https://circleci.com/docs/2.0/circleci-images/
15+
# - image: circleci/mongo:3.4.4
16+
17+
working_directory: ~/repo
18+
19+
steps:
20+
- checkout
21+
22+
# Download and cache dependencies
23+
- restore_cache:
24+
keys:
25+
- v1-dependencies-{{ checksum "package.json" }}
26+
# fallback to using the latest cache if no exact match is found
27+
- v1-dependencies-
28+
29+
- run: npm install
30+
31+
- save_cache:
32+
paths:
33+
- node_modules
34+
key: v1-dependencies-{{ checksum "package.json" }}
35+
36+
- run: ./bin/ensure_engine_config.sh
37+
38+
# run tests!
39+
- run: npm test

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# App Search Reference UI
22

3+
[![CircleCI](https://circleci.com/gh/swiftype/app-search-reference-ui-react.svg?style=svg)](https://circleci.com/gh/swiftype/app-search-reference-ui-react)
4+
35
The Reference UI is a configurable, generic UI meant to work with
46
any [App Search](https://www.elastic.co/cloud/search-lib-service) Engine. It
57
can serve as a simple demo, a functional test for your Engine data,
@@ -89,7 +91,7 @@ npm install netlify-cli -g
8991
netlify deploy # enter ./build as the deploy path
9092
```
9193

92-
You'll then simply follow the command prompt to log into Netlify and deploy your site. This can be
94+
You'll then simply follow the command prompt to log into Netlify and deploy your site. This can be completed in just a few minutes.
9395

9496
## Understanding the Reference UI Code
9597

bin/ensure_engine_config.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
project_root=$( cd "$(dirname "${BASH_SOURCE[0]}")/.." ; pwd -P )
2+
3+
if [[ ! -e ${project_root}/src/config/engine.json ]]; then
4+
echo "{}" > src/config/engine.json
5+
echo "wrote empty engine.json"
6+
else
7+
echo "engine.json already exists"
8+
fi

src/search-lib/AppSearchAPIConnector.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as SwiftypeAppSearch from "swiftype-app-search-javascript";
1111
function toAPIFacetSyntax(facetConfig = {}) {
1212
return Object.entries(facetConfig).reduce((acc, [key, value]) => {
1313
acc[key] = Object.entries(value).reduce((propAcc, [propKey, propValue]) => {
14-
if (["type", "size", "ranges"].includes(propKey)) {
14+
if (!["disjunctive", "conditional"].includes(propKey)) {
1515
propAcc[propKey] = propValue;
1616
}
1717
return propAcc;

src/search-lib/URLManager.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ export default class URLManager {
177177
// want to notify that the URL changed.
178178
if (`?${this.lastPushSearchString}` === location.search) return;
179179

180+
// Once we've decided to return based on lastPushSearchString, reset
181+
// it so that we don't break back / forward button.
182+
this.lastPushSearchString = "";
183+
180184
callback(
181185
paramsToState(
182186
queryString.parse(location.search, {

src/search-lib/URLManager.test.js

Lines changed: 129 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,50 @@ function createManager() {
1212
return manager;
1313
}
1414

15+
const basicParameterState = {
16+
filters: [
17+
{
18+
dependencies: ["underscore", "another"]
19+
},
20+
{
21+
keywords: ["node"]
22+
}
23+
],
24+
resultsPerPage: 20,
25+
searchTerm: "node",
26+
sortDirection: "asc",
27+
sortField: "name"
28+
};
29+
30+
const basicParameterStateAsUrl =
31+
"?fv-dependencies=underscore&fv-dependencies=another&fv-keywords=node&q=node&size=20&sort-direction=asc&sort-field=name";
32+
33+
const parameterStateWithRangeFilters = {
34+
filters: [
35+
{
36+
date: [
37+
{
38+
from: 12,
39+
to: 4000
40+
},
41+
{
42+
to: 4000
43+
}
44+
]
45+
},
46+
{
47+
cost: [
48+
{
49+
from: 50
50+
}
51+
]
52+
},
53+
{
54+
keywords: "node"
55+
}
56+
]
57+
};
58+
1559
it("can be initialized", () => {
1660
const manager = new URLManager();
1761
expect(manager).toBeInstanceOf(URLManager);
@@ -58,20 +102,7 @@ describe("#getStateFromURL", () => {
58102
describe("#pushStateToURL", () => {
59103
it("will update the url with the url corresponding to the provided state", () => {
60104
const manager = createManager();
61-
manager.pushStateToURL({
62-
filters: [
63-
{
64-
dependencies: ["underscore", "another"]
65-
},
66-
{
67-
keywords: ["node"]
68-
}
69-
],
70-
resultsPerPage: 20,
71-
searchTerm: "node",
72-
sortDirection: "asc",
73-
sortField: "name"
74-
});
105+
manager.pushStateToURL(basicParameterState);
75106
const queryString = manager.history.push.mock.calls[0][0].search;
76107
expect(queryString).toEqual(
77108
"?fv-dependencies=underscore&fv-dependencies=another&fv-keywords=node&q=node&size=20&sort-field=name&sort-direction=asc"
@@ -80,31 +111,7 @@ describe("#pushStateToURL", () => {
80111

81112
it("will update the url with range filter types", () => {
82113
const manager = createManager();
83-
manager.pushStateToURL({
84-
filters: [
85-
{
86-
date: [
87-
{
88-
from: 12,
89-
to: 4000
90-
},
91-
{
92-
to: 4000
93-
}
94-
]
95-
},
96-
{
97-
cost: [
98-
{
99-
from: 50
100-
}
101-
]
102-
},
103-
{
104-
keywords: "node"
105-
}
106-
]
107-
});
114+
manager.pushStateToURL(parameterStateWithRangeFilters);
108115
const queryString = manager.history.push.mock.calls[0][0].search;
109116
expect(queryString).toEqual(
110117
"?fr-date=12_4000&fr-date=_4000&fr-cost=50_&fv-keywords=node"
@@ -113,20 +120,94 @@ describe("#pushStateToURL", () => {
113120
});
114121

115122
describe("#onURLStateChange", () => {
123+
let manager;
124+
125+
function setup() {
126+
manager = createManager();
127+
}
128+
129+
function pushStateToURL(state) {
130+
manager.pushStateToURL(state);
131+
return manager.history.push.mock.calls[0][0].search;
132+
}
133+
134+
function simulateBrowserHistoryEvent(newUrl) {
135+
// Since we're not in a real browser, we simulate the change event that
136+
// would have ocurred.
137+
manager.history.listen.mock.calls[0][0]({
138+
search: newUrl
139+
});
140+
}
141+
142+
function subject(onURLStateChangeHandler, newUrl) {
143+
// Set our handler, which we be called any time url state changes
144+
manager.onURLStateChange(onURLStateChangeHandler);
145+
146+
simulateBrowserHistoryEvent(newUrl);
147+
}
148+
116149
it("will call provided callback with updated state when url changes", () => {
117-
const manager = createManager();
150+
setup();
151+
let newState;
152+
153+
// Provide url state change handler
154+
subject(state => {
155+
newState = state;
156+
}, basicParameterStateAsUrl);
157+
158+
// Verify it is called when url state changes
159+
expect(newState).toEqual(basicParameterState);
160+
});
161+
162+
/*
163+
If we triggered the callback every time we pushed state to the url,
164+
we would have an infinite loop.
165+
166+
pushStateToURL -> UpdateUrl -> HistoryEvent -> Handler -> pushstateToURL
167+
168+
Instead, we just want:
169+
pushStateToURL -> UpdateUrl -> HistoryEvent -> Handler
118170
171+
*/
172+
it("will not trigger callback as a result of pushStateToURL", () => {
173+
setup();
174+
175+
// Push state to the url
176+
// newUrl is the url that the browser changed to
177+
const newUrl = pushStateToURL(basicParameterState);
119178
let newState;
120-
manager.onURLStateChange(state => {
179+
180+
// Provide url state change handler
181+
subject(state => {
121182
newState = state;
122-
});
183+
}, newUrl);
123184

124-
// Simulate state change
125-
manager.history.listen.mock.calls[0][0]({
126-
search:
127-
"?fv-dependencies=underscore&fv-keywords=node&q=node&size=20&sort-direction=asc&sort-field=name"
128-
});
185+
// Verify it is called when url state changes
186+
expect(newState).toBe(undefined);
187+
});
188+
189+
it("will trigger callback if navigating back to a url with browser buttons", () => {
190+
setup();
191+
192+
// Push state to the url
193+
// newUrl is the url that the browser changed to
194+
const newUrl = pushStateToURL(basicParameterState);
195+
let newState;
196+
197+
// Provide url state change handler
198+
subject(state => {
199+
newState = state;
200+
}, newUrl);
201+
202+
// Verify it is called when url state changes
203+
expect(newState).toBe(undefined);
204+
205+
// Back button to empty state
206+
simulateBrowserHistoryEvent("");
207+
expect(newState).toEqual({}); // Was called, but empty state
129208

130-
expect(newState).toMatchSnapshot();
209+
// Forward button to double back to the original "newUrl"
210+
simulateBrowserHistoryEvent(newUrl);
211+
expect(newState).toEqual(basicParameterState);
131212
});
132213
});

src/search-lib/__snapshots__/URLManager.test.js.snap

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,3 @@ Object {
7878
"sortField": "name",
7979
}
8080
`;
81-
82-
exports[`#onURLStateChange will call provided callback with updated state when url changes 1`] = `
83-
Object {
84-
"filters": Array [
85-
Object {
86-
"dependencies": Array [
87-
"underscore",
88-
],
89-
},
90-
Object {
91-
"keywords": Array [
92-
"node",
93-
],
94-
},
95-
],
96-
"resultsPerPage": 20,
97-
"searchTerm": "node",
98-
"sortDirection": "asc",
99-
"sortField": "name",
100-
}
101-
`;

src/search-lib/withSearch.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ import SearchConsumer from "./SearchConsumer";
66
* This is a Higher Order Component that is used to expose (as `props`) all
77
* state and Actions provided by the SearchProvider to "container"
88
* components.
9+
*
10+
* `mapContextToProps` can be used to manipulate actions and state from context
11+
* before they are passed on to the container.
912
*/
1013
export default function withSearch(Component) {
1114
return function(props) {
15+
const { mapContextToProps = context => context, ...rest } = props;
16+
1217
return (
1318
<SearchConsumer>
14-
{context => <Component {...context} {...props} />}
19+
{context => <Component {...mapContextToProps(context)} {...rest} />}
1520
</SearchConsumer>
1621
);
1722
};

0 commit comments

Comments
 (0)