Skip to content

Commit 02c1ce7

Browse files
committed
Refactor filter criteria management and enhance task analysis table
1 parent 3784049 commit 02c1ce7

File tree

11 files changed

+1663
-816
lines changed

11 files changed

+1663
-816
lines changed

src/components/HOCs/WithFilterCriteria/WithFilterCriteria.jsx

Lines changed: 167 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
buildSearchURL,
1616
} from "../../../services/SearchCriteria/SearchCriteria";
1717

18-
const DEFAULT_PAGE_SIZE = 20;
19-
const DEFAULT_CRITERIA = {
18+
export const DEFAULT_PAGE_SIZE = 20;
19+
20+
export const DEFAULT_CRITERIA = {
2021
sortCriteria: { sortBy: "name", direction: "DESC" },
2122
pageSize: DEFAULT_PAGE_SIZE,
2223
filters: {
@@ -28,6 +29,61 @@ const DEFAULT_CRITERIA = {
2829
invertFields: {},
2930
};
3031

32+
/**
33+
* Parse a criteria string (URL search params) into a criteria object.
34+
* Shared utility for both HOC and hook implementations.
35+
*
36+
* @param {string} criteriaString - The URL search string to parse
37+
* @returns {Object|null} - The parsed criteria object or null
38+
*/
39+
export const parseCriteriaString = (criteriaString) => {
40+
if (!criteriaString) return null;
41+
42+
const criteria = buildSearchCriteriafromURL(criteriaString);
43+
if (!criteria) return null;
44+
45+
const keysToSplit = ["status", "reviewStatus", "metaReviewStatus", "priorities", "boundingBox"];
46+
47+
for (const key of keysToSplit) {
48+
if (criteria[key] !== undefined && key === "boundingBox") {
49+
if (typeof criteria[key] === "string") {
50+
criteria[key] = criteria[key].split(",").map((x) => parseFloat(x));
51+
}
52+
} else if (criteria?.filters?.[key] !== undefined) {
53+
if (typeof criteria.filters[key] === "string") {
54+
criteria.filters[key] = criteria.filters[key].split(",").map((x) => _toInteger(x));
55+
}
56+
}
57+
}
58+
59+
return criteria;
60+
};
61+
62+
/**
63+
* Build included filters from props.
64+
* Shared utility for both HOC and hook implementations.
65+
* Only includes filter values if the corresponding props exist,
66+
* otherwise returns empty object to preserve defaults.
67+
*/
68+
export const buildIncludedFilters = (props) => {
69+
const filters = {};
70+
71+
if (props.includeTaskStatuses) {
72+
filters.status = _keys(_pickBy(props.includeTaskStatuses, (s) => s));
73+
}
74+
if (props.includeTaskReviewStatuses) {
75+
filters.reviewStatus = _keys(_pickBy(props.includeTaskReviewStatuses, (r) => r));
76+
}
77+
if (props.includeMetaReviewStatuses) {
78+
filters.metaReviewStatus = _keys(_pickBy(props.includeMetaReviewStatuses, (r) => r));
79+
}
80+
if (props.includeTaskPriorities) {
81+
filters.priorities = _keys(_pickBy(props.includeTaskPriorities, (p) => p));
82+
}
83+
84+
return filters;
85+
};
86+
3187
/**
3288
* WithFilterCriteria keeps track of the current criteria being used
3389
* to filter, sort and page the tasks. If a use case requires user app settings for
@@ -52,16 +108,32 @@ export default function WithFilterCriteria(
52108
};
53109

54110
updateCriteria = (newCriteria) => {
55-
const criteria = _cloneDeep(this.state.criteria);
56-
criteria.sortCriteria = newCriteria.sortCriteria;
57-
criteria.page = newCriteria.page;
58-
criteria.filters = newCriteria.filters;
59-
criteria.includeTags = newCriteria.includeTags;
111+
this.setState(
112+
(prevState) => {
113+
const criteria = _cloneDeep(prevState.criteria);
60114

61-
this.setState({ criteria });
62-
if (this.props.setSearchFilters) {
63-
this.props.setSearchFilters(criteria);
64-
}
115+
if (newCriteria.sortCriteria !== undefined) {
116+
criteria.sortCriteria = newCriteria.sortCriteria;
117+
}
118+
if (newCriteria.page !== undefined) {
119+
criteria.page = newCriteria.page;
120+
}
121+
if (newCriteria.includeTags !== undefined) {
122+
criteria.includeTags = newCriteria.includeTags;
123+
}
124+
125+
if (newCriteria.filters && typeof newCriteria.filters === "object") {
126+
criteria.filters = { ...criteria.filters, ...newCriteria.filters };
127+
}
128+
129+
return { criteria };
130+
},
131+
() => {
132+
if (this.props.setSearchFilters) {
133+
this.props.setSearchFilters(this.state.criteria);
134+
}
135+
},
136+
);
65137
};
66138

67139
updateTaskFilterBounds = (bounds, zoom) => {
@@ -101,21 +173,12 @@ export default function WithFilterCriteria(
101173
const newCriteria = _cloneDeep(DEFAULT_CRITERIA);
102174
newCriteria.boundingBox = usePersistedFilters ? this.state.criteria.boundingBox : null;
103175
newCriteria.zoom = this.state.zoom;
104-
newCriteria.filters["status"] = _keys(_pickBy(this.props.includeTaskStatuses, (s) => s));
105-
newCriteria.filters["reviewStatus"] = _keys(
106-
_pickBy(this.props.includeReviewStatuses, (r) => r),
107-
);
108-
newCriteria.filters["metaReviewStatus"] = _keys(
109-
_pickBy(this.props.includeMetaReviewStatuses, (r) => r),
110-
);
111-
newCriteria.filters["priorities"] = _keys(
112-
_pickBy(this.props.includeTaskPriorities, (p) => p),
113-
);
114176

115177
if (!ignoreURL) {
116178
this.props.history.push({
117179
pathname: this.props.history.location.pathname,
118-
state: { refresh: true },
180+
search: "",
181+
state: {},
119182
});
120183
}
121184

@@ -131,23 +194,22 @@ export default function WithFilterCriteria(
131194
setFiltered = (column, value) => {
132195
const typedCriteria = _cloneDeep(this.state.criteria);
133196
typedCriteria.filters[column] = value;
134-
135-
//Reset Page so it goes back to 0
136197
typedCriteria.page = 0;
137198
this.setState({ criteria: typedCriteria });
138199
};
139200

140201
updateIncludedFilters(props, criteria = {}) {
202+
const includedFilters = buildIncludedFilters(props);
203+
204+
this.setState((prevState) => {
205+
const typedCriteria = _merge({}, criteria, _cloneDeep(prevState.criteria));
206+
typedCriteria.filters = { ...typedCriteria.filters, ...includedFilters };
207+
typedCriteria.page = 0;
208+
return { criteria: typedCriteria };
209+
});
210+
141211
const typedCriteria = _merge({}, criteria, _cloneDeep(this.state.criteria));
142-
typedCriteria.filters["status"] = _keys(_pickBy(props.includeTaskStatuses, (s) => s));
143-
typedCriteria.filters["reviewStatus"] = _keys(
144-
_pickBy(props.includeTaskReviewStatuses, (r) => r),
145-
);
146-
typedCriteria.filters["metaReviewStatus"] = _keys(
147-
_pickBy(props.includeMetaReviewStatuses, (r) => r),
148-
);
149-
typedCriteria.filters["priorities"] = _keys(_pickBy(props.includeTaskPriorities, (p) => p));
150-
this.setState({ criteria: typedCriteria });
212+
typedCriteria.filters = { ...typedCriteria.filters, ...includedFilters };
151213
return typedCriteria;
152214
}
153215

@@ -173,8 +235,6 @@ export default function WithFilterCriteria(
173235

174236
if (!ignoreURL) {
175237
const searchURL = this.updateURL(this.props, typedCriteria);
176-
// If our search on the URL hasn't changed then don't do another
177-
// update as we could receive a second update when we change the URL.
178238
if (_isEqual(this.props.history.location.search, searchURL) && this.state.loading) {
179239
return;
180240
}
@@ -188,13 +248,49 @@ export default function WithFilterCriteria(
188248

189249
const criteria = typedCriteria || _cloneDeep(this.state.criteria);
190250

251+
if (!criteria.boundingBox && this.props.challenge?.bounding) {
252+
try {
253+
const bounding = this.props.challenge.bounding;
254+
if (bounding.bbox) {
255+
criteria.boundingBox = bounding.bbox;
256+
} else if (bounding.coordinates) {
257+
const coords = bounding.coordinates.flat(3);
258+
const lngs = coords.filter((_, i) => i % 2 === 0);
259+
const lats = coords.filter((_, i) => i % 2 === 1);
260+
criteria.boundingBox = [
261+
Math.min(...lngs),
262+
Math.min(...lats),
263+
Math.max(...lngs),
264+
Math.max(...lats),
265+
];
266+
}
267+
} catch (e) {
268+
console.warn("Could not extract bounding box from challenge:", e);
269+
}
270+
}
271+
272+
if (this.props.includeTaskStatuses) {
273+
criteria.filters.status = _keys(_pickBy(this.props.includeTaskStatuses, (s) => s));
274+
}
275+
if (this.props.includeTaskPriorities) {
276+
criteria.filters.priorities = _keys(_pickBy(this.props.includeTaskPriorities, (p) => p));
277+
}
278+
if (this.props.includeTaskReviewStatuses) {
279+
criteria.filters.reviewStatus = _keys(
280+
_pickBy(this.props.includeTaskReviewStatuses, (r) => r),
281+
);
282+
}
283+
if (this.props.includeMetaReviewStatuses) {
284+
criteria.filters.metaReviewStatus = _keys(
285+
_pickBy(this.props.includeMetaReviewStatuses, (r) => r),
286+
);
287+
}
288+
191289
criteria.filters.archived = true;
192290

193291
this.debouncedTasksFetch(challengeId, criteria, this.state.criteria.pageSize);
194292
};
195293

196-
// Debouncing to give a chance for filters and bounds to all be applied before
197-
// making the server call.
198294
debouncedTasksFetch = _debounce((challengeId, criteria, pageSize) => {
199295
this.props
200296
.augmentClusteredTasks(challengeId, false, criteria, pageSize, false, ignoreLocked)
@@ -205,30 +301,9 @@ export default function WithFilterCriteria(
205301

206302
updateCriteriaFromURL(props) {
207303
const criteria = props.history.location.search
208-
? buildSearchCriteriafromURL(props.history.location.search)
304+
? parseCriteriaString(props.history.location.search)
209305
: _cloneDeep(props.history.location.state);
210306

211-
// These values will come in as comma-separated strings and need to be turned
212-
// into number arrays
213-
const keysToSplit = [
214-
"status",
215-
"reviewStatus",
216-
"metaReviewStatus",
217-
"priorities",
218-
"boundingBox",
219-
];
220-
for (const key of keysToSplit) {
221-
if (criteria[key] !== undefined && key === "boundingBox") {
222-
if (typeof criteria[key] === "string") {
223-
criteria[key] = criteria[key].split(",").map((x) => parseFloat(x));
224-
}
225-
} else if (criteria?.filters?.[key] !== undefined) {
226-
if (typeof criteria.filters[key] === "string") {
227-
criteria.filters[key] = criteria.filters[key].split(",").map((x) => _toInteger(x));
228-
}
229-
}
230-
}
231-
232307
if (!criteria?.filters?.status) {
233308
this.updateIncludedFilters(props);
234309
} else {
@@ -243,36 +318,14 @@ export default function WithFilterCriteria(
243318
: "";
244319
const criteria =
245320
savedFilters && savedFilters.length > 0
246-
? buildSearchCriteriafromURL(savedFilters)
321+
? parseCriteriaString(savedFilters)
247322
: _cloneDeep(props.history.location.state);
248323

249-
//Use default filter values if no saved values are present
250324
if (!criteria) {
251325
this.updateIncludedFilters(props);
252326
return;
253327
}
254328

255-
// These values will come in as comma-separated strings and need to be turned
256-
// into number arrays
257-
const keysToSplit = [
258-
"status",
259-
"reviewStatus",
260-
"metaReviewStatus",
261-
"priorities",
262-
"boundingBox",
263-
];
264-
for (const key of keysToSplit) {
265-
if (criteria[key] !== undefined && key === "boundingBox") {
266-
if (typeof criteria[key] === "string") {
267-
criteria[key] = criteria[key].split(",").map((x) => parseFloat(x));
268-
}
269-
} else if (criteria?.filters?.[key] !== undefined) {
270-
if (typeof criteria.filters[key] === "string") {
271-
criteria.filters[key] = criteria.filters[key].split(",").map((x) => _toInteger(x));
272-
}
273-
}
274-
}
275-
276329
if (!criteria?.filters?.status) {
277330
this.updateIncludedFilters(props);
278331
} else {
@@ -281,6 +334,8 @@ export default function WithFilterCriteria(
281334
}
282335

283336
componentDidMount() {
337+
this.initialLoadTriggered = false;
338+
284339
if (
285340
!ignoreURL &&
286341
(!_isEmpty(this.props.history.location.search) ||
@@ -296,10 +351,14 @@ export default function WithFilterCriteria(
296351

297352
componentDidUpdate(prevProps, prevState) {
298353
const challengeId = this.props.challenge?.id || this.props.challengeId;
354+
const prevChallengeId = prevProps?.challenge?.id || prevProps?.challengeId;
355+
299356
if (!challengeId) {
300357
return;
301358
}
302359

360+
const challengeIdJustBecameAvailable = !prevChallengeId && challengeId;
361+
303362
if (!ignoreURL && this.props.history.location?.state?.refresh) {
304363
this.props.history.push({
305364
pathname: this.props.history.location.pathname,
@@ -316,22 +375,23 @@ export default function WithFilterCriteria(
316375

317376
let typedCriteria = _cloneDeep(this.state.criteria);
318377

319-
if (
378+
const filterPropsChanged =
320379
prevProps.includeTaskStatuses !== this.props.includeTaskStatuses ||
321380
prevProps.includeTaskReviewStatuses !== this.props.includeTaskReviewStatuses ||
322381
prevProps.includeMetaReviewStatuses !== this.props.includeMetaReviewStatuses ||
323-
prevProps.includeTaskPriorities !== this.props.includeTaskPriorities
324-
) {
325-
typedCriteria = this.updateIncludedFilters(this.props);
382+
prevProps.includeTaskPriorities !== this.props.includeTaskPriorities;
383+
384+
if (filterPropsChanged) {
385+
this.updateIncludedFilters(this.props);
326386
return;
327387
}
328388

329389
if (!_isEqual(prevState.criteria, this.state.criteria)) {
330390
this.refreshTasks(typedCriteria);
331-
} else if (
332-
prevProps?.challenge?.id !== this.props.challenge?.id ||
333-
this.props.challengeId !== prevProps.challengeId
334-
) {
391+
} else if (challengeIdJustBecameAvailable || challengeId !== prevChallengeId) {
392+
this.refreshTasks(typedCriteria);
393+
} else if (!this.initialLoadTriggered) {
394+
this.initialLoadTriggered = true;
335395
this.refreshTasks(typedCriteria);
336396
} else if (this.props.history.location?.state?.refreshAfterSave) {
337397
this.refreshTasks(typedCriteria);
@@ -345,6 +405,23 @@ export default function WithFilterCriteria(
345405
render() {
346406
const criteria = _cloneDeep(this.state.criteria) || DEFAULT_CRITERIA;
347407

408+
if (this.props.includeTaskStatuses) {
409+
criteria.filters.status = _keys(_pickBy(this.props.includeTaskStatuses, (s) => s));
410+
}
411+
if (this.props.includeTaskPriorities) {
412+
criteria.filters.priorities = _keys(_pickBy(this.props.includeTaskPriorities, (p) => p));
413+
}
414+
if (this.props.includeTaskReviewStatuses) {
415+
criteria.filters.reviewStatus = _keys(
416+
_pickBy(this.props.includeTaskReviewStatuses, (r) => r),
417+
);
418+
}
419+
if (this.props.includeMetaReviewStatuses) {
420+
criteria.filters.metaReviewStatus = _keys(
421+
_pickBy(this.props.includeMetaReviewStatuses, (r) => r),
422+
);
423+
}
424+
348425
return (
349426
<WrappedComponent
350427
defaultPageSize={DEFAULT_PAGE_SIZE}

0 commit comments

Comments
 (0)