@@ -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