@@ -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 }
0 commit comments