Skip to content

Commit b7890e8

Browse files
authored
Fix table filtering issues on create and manage page (#2766)
* Refactor filter criteria management and enhance task analysis table * fix bug in work on multiple tasks table * Remove unused filter criteria hook and clean up TaskAnalysisTable component
1 parent 3784049 commit b7890e8

11 files changed

Lines changed: 1183 additions & 816 deletions

File tree

src/components/HOCs/WithFilterCriteria/WithFilterCriteria.jsx

Lines changed: 167 additions & 86 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

@@ -138,16 +201,17 @@ export default function WithFilterCriteria(
138201
};
139202

140203
updateIncludedFilters(props, criteria = {}) {
204+
const includedFilters = buildIncludedFilters(props);
205+
206+
this.setState((prevState) => {
207+
const typedCriteria = _merge({}, criteria, _cloneDeep(prevState.criteria));
208+
typedCriteria.filters = { ...typedCriteria.filters, ...includedFilters };
209+
typedCriteria.page = 0;
210+
return { criteria: typedCriteria };
211+
});
212+
141213
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 });
214+
typedCriteria.filters = { ...typedCriteria.filters, ...includedFilters };
151215
return typedCriteria;
152216
}
153217

@@ -188,13 +252,49 @@ export default function WithFilterCriteria(
188252

189253
const criteria = typedCriteria || _cloneDeep(this.state.criteria);
190254

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

193295
this.debouncedTasksFetch(challengeId, criteria, this.state.criteria.pageSize);
194296
};
195297

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

206306
updateCriteriaFromURL(props) {
207307
const criteria = props.history.location.search
208-
? buildSearchCriteriafromURL(props.history.location.search)
308+
? parseCriteriaString(props.history.location.search)
209309
: _cloneDeep(props.history.location.state);
210310

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-
232311
if (!criteria?.filters?.status) {
233312
this.updateIncludedFilters(props);
234313
} else {
@@ -243,36 +322,14 @@ export default function WithFilterCriteria(
243322
: "";
244323
const criteria =
245324
savedFilters && savedFilters.length > 0
246-
? buildSearchCriteriafromURL(savedFilters)
325+
? parseCriteriaString(savedFilters)
247326
: _cloneDeep(props.history.location.state);
248327

249-
//Use default filter values if no saved values are present
250328
if (!criteria) {
251329
this.updateIncludedFilters(props);
252330
return;
253331
}
254332

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-
276333
if (!criteria?.filters?.status) {
277334
this.updateIncludedFilters(props);
278335
} else {
@@ -281,6 +338,8 @@ export default function WithFilterCriteria(
281338
}
282339

283340
componentDidMount() {
341+
this.initialLoadTriggered = false;
342+
284343
if (
285344
!ignoreURL &&
286345
(!_isEmpty(this.props.history.location.search) ||
@@ -296,10 +355,14 @@ export default function WithFilterCriteria(
296355

297356
componentDidUpdate(prevProps, prevState) {
298357
const challengeId = this.props.challenge?.id || this.props.challengeId;
358+
const prevChallengeId = prevProps?.challenge?.id || prevProps?.challengeId;
359+
299360
if (!challengeId) {
300361
return;
301362
}
302363

364+
const challengeIdJustBecameAvailable = !prevChallengeId && challengeId;
365+
303366
if (!ignoreURL && this.props.history.location?.state?.refresh) {
304367
this.props.history.push({
305368
pathname: this.props.history.location.pathname,
@@ -316,22 +379,23 @@ export default function WithFilterCriteria(
316379

317380
let typedCriteria = _cloneDeep(this.state.criteria);
318381

319-
if (
382+
const filterPropsChanged =
320383
prevProps.includeTaskStatuses !== this.props.includeTaskStatuses ||
321384
prevProps.includeTaskReviewStatuses !== this.props.includeTaskReviewStatuses ||
322385
prevProps.includeMetaReviewStatuses !== this.props.includeMetaReviewStatuses ||
323-
prevProps.includeTaskPriorities !== this.props.includeTaskPriorities
324-
) {
325-
typedCriteria = this.updateIncludedFilters(this.props);
386+
prevProps.includeTaskPriorities !== this.props.includeTaskPriorities;
387+
388+
if (filterPropsChanged) {
389+
this.updateIncludedFilters(this.props);
326390
return;
327391
}
328392

329393
if (!_isEqual(prevState.criteria, this.state.criteria)) {
330394
this.refreshTasks(typedCriteria);
331-
} else if (
332-
prevProps?.challenge?.id !== this.props.challenge?.id ||
333-
this.props.challengeId !== prevProps.challengeId
334-
) {
395+
} else if (challengeIdJustBecameAvailable || challengeId !== prevChallengeId) {
396+
this.refreshTasks(typedCriteria);
397+
} else if (!this.initialLoadTriggered) {
398+
this.initialLoadTriggered = true;
335399
this.refreshTasks(typedCriteria);
336400
} else if (this.props.history.location?.state?.refreshAfterSave) {
337401
this.refreshTasks(typedCriteria);
@@ -345,6 +409,23 @@ export default function WithFilterCriteria(
345409
render() {
346410
const criteria = _cloneDeep(this.state.criteria) || DEFAULT_CRITERIA;
347411

412+
if (this.props.includeTaskStatuses) {
413+
criteria.filters.status = _keys(_pickBy(this.props.includeTaskStatuses, (s) => s));
414+
}
415+
if (this.props.includeTaskPriorities) {
416+
criteria.filters.priorities = _keys(_pickBy(this.props.includeTaskPriorities, (p) => p));
417+
}
418+
if (this.props.includeTaskReviewStatuses) {
419+
criteria.filters.reviewStatus = _keys(
420+
_pickBy(this.props.includeTaskReviewStatuses, (r) => r),
421+
);
422+
}
423+
if (this.props.includeMetaReviewStatuses) {
424+
criteria.filters.metaReviewStatus = _keys(
425+
_pickBy(this.props.includeMetaReviewStatuses, (r) => r),
426+
);
427+
}
428+
348429
return (
349430
<WrappedComponent
350431
defaultPageSize={DEFAULT_PAGE_SIZE}

src/components/TableShared/EnhancedTable.jsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,25 @@ const useDebounce = (value, delay = 300) => {
3030
export const SearchFilter = ({ value, onChange, placeholder, inputClassName = "" }) => {
3131
const [localValue, setLocalValue] = useState(value || "");
3232
const debouncedValue = useDebounce(localValue, 1000);
33+
const isExternalUpdateRef = React.useRef(false);
3334

3435
useEffect(() => {
35-
if (localValue !== value) {
36-
setLocalValue(value || "");
36+
const normalizedValue = value || "";
37+
if (localValue !== normalizedValue) {
38+
isExternalUpdateRef.current = true;
39+
setLocalValue(normalizedValue);
3740
}
3841
}, [value]);
3942

4043
useEffect(() => {
41-
if (debouncedValue !== value) {
42-
onChange(debouncedValue || undefined);
44+
if (isExternalUpdateRef.current) {
45+
isExternalUpdateRef.current = false;
46+
return;
47+
}
48+
const normalizedDebouncedValue = debouncedValue || undefined;
49+
const normalizedValue = value || undefined;
50+
if (normalizedDebouncedValue !== normalizedValue) {
51+
onChange(normalizedDebouncedValue);
4352
}
4453
}, [debouncedValue]);
4554

0 commit comments

Comments
 (0)