-
Notifications
You must be signed in to change notification settings - Fork 166
/
Copy pathchangeURL.js
317 lines (309 loc) · 13.7 KB
/
changeURL.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
import queryString from "query-string";
import * as types from "../actions/types";
import { numericToCalendar } from "../util/dateHelpers";
import { shouldDisplayTemporalConfidence } from "../reducers/controls";
import { genotypeSymbol, nucleotide_gene, strainSymbol } from "../util/globals";
import { encodeGenotypeFilters, decodeColorByGenotype, isColorByGenotype } from "../util/getGenotype";
import { removeInvalidMeasurementsFilterQuery } from "../actions/measurements";
export const strainSymbolUrlString = "__strain__";
/**
* This middleware acts to keep the app state and the URL query state in sync by
* intercepting actions and updating the URL accordingly. Thus, in theory, this
* middleware can be disabled and the app will still work as expected.
*
* The only modification of redux state by this app is (potentially) an action
* of type types.UPDATE_PATHNAME which is used to "save" the current pathname
* so we can diff against a new one.
*
* This is the way by which the URL updates (e.g. when the server auto-completes
* a URL from /flu -> /flu/seasonal/h3n2/ha/3y, when you change the color-by,
* or when you change dataset via the dropdowns)
*
* @param {store} store: a Redux store
*/
export const changeURLMiddleware = (store) => (next) => (action) => {
const state = store.getState(); // this is "old" state, i.e. before the reducers have updated by this action
const result = next(action); // send action to other middleware / reducers
// if (action.dontModifyURL !== undefined) {
// console.log("changeURL middleware skipped")
// return result;
// }
/* starting URL values & flags */
let query = queryString.parse(window.location.search);
let pathname = window.location.pathname;
/* first switch: query change */
switch (action.type) {
case types.CLEAN_START: // fallthrough
case types.URL_QUERY_CHANGE_WITH_COMPUTED_STATE:
/* don't use queries when debugging a narrative as those URLs aren't intended to be restorable (yet) */
if (state.general.displayComponent==="debugNarrative") break;
query = action.query;
if (query.n === 0) delete query.n;
if (query.tt) delete query.tt;
break;
case types.CHANGE_BRANCH_LABEL:
query.branchLabel = state.controls.defaults.selectedBranchLabel === action.value ?
undefined :
action.value;
break;
case types.TOGGLE_SHOW_ALL_BRANCH_LABELS:
/* This is not yet settable in display_defaults, and thus we want a URL query
that will still make sense when the default for the given branch labelling is "show all" */
query.showBranchLabels = action.value ? 'all' : undefined;
break;
case types.CHANGE_ZOOM: {
/* entropy panel genome zoom coordinates. On a page load these queries
will be combined with the selected CDS from the color-by genotype, or be
applied to the nuc view. As such, if the entropy panel's selected CDS is
_not_ the same as the colorBy (if applicable) then we don't set gmin/gmax
*/
const entropyCdsName = state.entropy.selectedCds===nucleotide_gene ?
nucleotide_gene :
state.entropy.selectedCds.name;
const colorByCdsName = isColorByGenotype(state.controls.colorBy) &&
decodeColorByGenotype(state.controls.colorBy, state.entropy.genomeMap).gene;
const bounds = state.entropy.genomeMap[0].range; // guaranteed to exist, as the action comes from <entropy>
if (
((!colorByCdsName || colorByCdsName===nucleotide_gene) && entropyCdsName===nucleotide_gene) ||
(colorByCdsName===entropyCdsName)
) {
query.gmin = action.zoomc[0] <= bounds[0] ? undefined : action.zoomc[0];
query.gmax = action.zoomc[1] >= bounds[1] ? undefined : action.zoomc[1];
} else {
[query.gmin, query.gmax] = [undefined, undefined];
}
break;
}
case types.NEW_COLORS:
query.c = action.colorBy === state.controls.defaults.colorBy ? undefined : action.colorBy;
break;
case types.TOGGLE_TEMPORAL_CONF:
if ("ci" in query) {
query.ci = undefined;
} else {
// We have to use null here to put "ci" in the query without an "=" after it and a value, i.e. to treat it as a boolean without having "=true"
query.ci = null;
}
break;
case types.APPLY_FILTER: {
if (action.trait === genotypeSymbol) {
query.gt = encodeGenotypeFilters(action.values);
break;
}
const queryKey = action.trait === strainSymbol ? 's' : // for historical reasons, strains get stored under the `s` query key
`f_${action.trait}`;
query[queryKey] = action.values
.filter((item) => item.active) // only active filters in the URL
.map((item) => item.value)
.join(',');
break;
}
case types.CHANGE_LAYOUT: {
const sv = action.scatterVariables;
query.scatterX = action.layout==="scatter" && state.controls.distanceMeasure!==sv.x ? sv.x : undefined;
query.scatterY = action.layout==="scatter" && state.controls.colorBy!==sv.y ? sv.y : undefined;
query.branches = (action.layout==="scatter" || action.layout==="clock") && sv.showBranches===false ? "hide" : undefined;
query.regression = action.layout==="scatter" && sv.showRegression===true ? "show" :
action.layout==="clock" && sv.showRegression===false ? "hide" :
undefined;
query.l = action.layout === state.controls.defaults.layout ? undefined : action.layout;
if (!shouldDisplayTemporalConfidence(state.controls.temporalConfidence.exists, state.controls.distanceMeasure, query.l)) {
query.ci = undefined;
}
break;
}
case types.CHANGE_GEO_RESOLUTION: {
query.r = action.data === state.controls.defaults.geoResolution ? undefined : action.data;
break;
}
case types.TOGGLE_TRANSMISSION_LINES: {
if (action.data === state.controls.defaults.showTransmissionLines) query.transmissions = undefined;
else query.transmissions = action.data ? 'show' : 'hide';
break;
}
case types.CHANGE_LANGUAGE: {
query.lang = action.data === state.general.defaults.language ? undefined : action.data;
break;
}
case types.CHANGE_DISTANCE_MEASURE: {
query.m = action.data === state.controls.defaults.distanceMeasure ? undefined : action.data;
if (!shouldDisplayTemporalConfidence(state.controls.temporalConfidence.exists, query.m, state.controls.layout)) {
query.ci = undefined;
}
break;
}
case types.CHANGE_PANEL_LAYOUT: {
query.p = action.notInURLState === true ? undefined : action.data;
break;
}
case types.TOGGLE_SIDEBAR: {
// we never add this to the URL on purpose -- it should be manually set as it specifies a world
// where resizes can not open / close the sidebar. The exception is if it's toggled, we
// remove it from the URL query, as it's no longer valid to call it open if it's closed!
if ("sidebar" in query) {
query.sidebar = undefined;
}
break;
}
case types.TOGGLE_LEGEND: {
// we treat this the same as sidebar above -- it can only be added to the URL manually
if ("legend" in query) {
query.legend = undefined;
}
break;
}
case types.TOGGLE_PANEL_DISPLAY: {
/* check this against the defaults set by the dataset (and this default is all available panels if not specifically set) */
if (
state.controls.defaults.panels.length===action.panelsToDisplay.length &&
state.controls.defaults.panels.filter((p) => !action.panelsToDisplay.includes(p)).length===0
) {
query.d = undefined;
} else {
query.d = action.panelsToDisplay.join(",");
}
query.p = action.panelLayout;
break;
}
case types.CHANGE_TIP_LABEL_KEY: {
query.tl = action.key===state.controls.defaults.tipLabelKey ? undefined :
action.key===strainSymbol ? strainSymbolUrlString :
action.key;
break;
}
case types.CHANGE_DATES_VISIBILITY_THICKNESS: {
if (state.controls.animationPlayPauseButton === "Pause") { // animation in progress - no dates in URL
query.dmin = undefined;
query.dmax = undefined;
} else {
query.dmin = action.dateMin === state.controls.absoluteDateMin ? undefined : action.dateMin;
query.dmax = action.dateMax === state.controls.absoluteDateMax ? undefined : action.dateMax;
}
break;
}
case types.UPDATE_VISIBILITY_AND_BRANCH_THICKNESS: {
query.label = action.cladeName ? action.cladeName : undefined;
break;
}
case types.MAP_ANIMATION_PLAY_PAUSE_BUTTON:
if (action.data === "Play") { // animation stopping - restore dates in URL
query.animate = undefined;
query.dmin = state.controls.dateMin === state.controls.absoluteDateMin ? undefined : state.controls.dateMin;
query.dmax = state.controls.dateMax === state.controls.absoluteDateMax ? undefined : state.controls.dateMax;
}
break;
case types.MIDDLEWARE_ONLY_ANIMATION_STARTED: {
/* animation started - format: start bound, end bound, loop 0|1, cumulative 0|1, speed in ms */
const a = numericToCalendar(window.NEXTSTRAIN.animationStartPoint);
const b = numericToCalendar(window.NEXTSTRAIN.animationEndPoint);
const c = state.controls.mapAnimationShouldLoop ? "1" : "0";
const d = state.controls.mapAnimationCumulative ? "1" : "0";
const e = state.controls.mapAnimationDurationInMilliseconds;
query.animate = `${a},${b},${c},${d},${e}`;
break;
}
case types.PAGE_CHANGE:
if (action.query) {
query = action.query;
} else if (action.displayComponent !== state.general.displayComponent) {
query = {};
}
break;
case types.TOGGLE_NARRATIVE: {
/* don't use queries when debugging a narrative as those URLs aren't intended to be restorable (yet) */
if (state.general.displayComponent==="debugNarrative") break;
if (action.narrativeOn === true) {
query = {n: state.narrative.blockIdx};
} else if (action.narrativeOn === false) {
query = queryString.parse(state.narrative.blocks[state.narrative.blockIdx].query);
}
break;
}
case types.CHANGE_MEASUREMENTS_COLLECTION: // fallthrough
case types.APPLY_MEASUREMENTS_FILTER:
query = removeInvalidMeasurementsFilterQuery(query, action.queryParams)
query = {...query, ...action.queryParams};
break;
case types.CHANGE_MEASUREMENTS_DISPLAY: // fallthrough
case types.CHANGE_MEASUREMENTS_GROUP_BY: // fallthrough
case types.TOGGLE_MEASUREMENTS_OVERALL_MEAN: // fallthrough
case types.TOGGLE_MEASUREMENTS_THRESHOLD: // fallthrough
query = {...query, ...action.queryParams};
break;
default:
break;
}
/* second switch: path change */
switch (action.type) {
case types.CLEAN_START:
if (typeof action.pathnameShouldBe === "string" && !action.narrative) {
pathname = action.pathnameShouldBe;
break;
}
/* we also double check that if there are 2 trees both are represented
in the URL */
if (action.tree.name && action.treeToo && action.treeToo.name && !action.narrative) {
const treeUrlShouldBe = `${action.tree.name}:${action.treeToo.name}`;
if (!window.location.pathname.includes(treeUrlShouldBe)) {
pathname = treeUrlShouldBe;
}
}
break;
case types.TOGGLE_NARRATIVE: {
/* when toggling between the narrative view & the underlying dataset view the intention is to
have the pathname represent the narrative and the dataset, respectively. However when we are
editing a narrative we _do not_ want to change the pathname */
if (state.general.displayComponent==="debugNarrative") break;
if (action.narrativeOn === true) {
pathname = state.narrative.pathname;
} else if (action.narrativeOn === false) {
pathname = state.narrative.blocks[state.narrative.blockIdx].dataset;
}
break;
}
case types.PAGE_CHANGE:
/* desired behaviour depends on the displayComponent selected... */
if (action.displayComponent === "main" || action.displayComponent === "datasetLoader" || action.displayComponent === "splash") {
pathname = action.path || pathname;
} else if (pathname.startsWith(`/${action.displayComponent}`)) {
// leave the pathname alone!
} else {
// fallthrough
pathname = action.displayComponent;
}
break;
case types.REMOVE_TREE_TOO: {
pathname = pathname.split(":")[0];
break;
}
case types.TREE_TOO_DATA: {
const treeUrl = action.tree.name;
const secondTreeUrl = action.treeToo.name;
pathname = treeUrl.concat(":", secondTreeUrl);
break;
}
default:
break;
}
Object.keys(query).filter((q) => query[q] === "").forEach((k) => delete query[k]);
if (state.narrative.display) {
Object.keys(query).filter((q) => q!=='n').forEach((k) => delete query[k]);
}
let search = queryString.stringify(query).replace(/%2C/g, ',').replace(/%2F/g, '/').replace(/%3A/g, ':');
if (search) {search = "?" + search;}
if (!pathname.startsWith("/")) {pathname = "/" + pathname;}
/* now that we have determined our desired pathname & query we modify the URL */
if (pathname !== window.location.pathname || search !== window.location.search) {
let newURLString = pathname;
if (search) {newURLString += search;}
if (action.pushState) {
window.history.pushState({}, "", newURLString);
} else {
window.history.replaceState({}, "", newURLString);
}
next({type: types.UPDATE_PATHNAME, pathname: pathname});
} else if (pathname !== state.general.pathname && action.type === types.PAGE_CHANGE) {
next({type: types.UPDATE_PATHNAME, pathname: pathname});
}
return result;
};