@snapdragonsnursery/react-components 1.23.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapdragonsnursery/react-components",
3
- "version": "1.23.0",
3
+ "version": "1.26.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -1,6 +1,7 @@
1
1
  import React, {
2
2
  useState,
3
3
  useEffect,
4
+ useLayoutEffect,
4
5
  useRef,
5
6
  useCallback,
6
7
  useMemo,
@@ -90,6 +91,30 @@ const ChildSearchModal = ({
90
91
  const { instance, accounts } = useMsal();
91
92
  const modalRef = useRef();
92
93
  const searchInputRef = useRef();
94
+ const prevIsOpenRef = useRef(false);
95
+ const statusPropRef = useRef(status);
96
+ const activeOnlyPropRef = useRef(activeOnly);
97
+ statusPropRef.current = status;
98
+ activeOnlyPropRef.current = activeOnly;
99
+
100
+ // When the modal opens (closed → open), reset status from props so a previous session
101
+ // does not stick and cause searches with no status param (API defaults to active-only).
102
+ useLayoutEffect(() => {
103
+ if (!isOpen) {
104
+ prevIsOpenRef.current = false;
105
+ return;
106
+ }
107
+ const wasOpen = prevIsOpenRef.current;
108
+ prevIsOpenRef.current = true;
109
+ if (wasOpen) return;
110
+ setAdvancedFilters((prev) => ({
111
+ ...prev,
112
+ // Match useState initialiser: treat "" like missing so we do not omit status on the API.
113
+ status:
114
+ statusPropRef.current ||
115
+ (activeOnlyPropRef.current ? "active" : "all"),
116
+ }));
117
+ }, [isOpen]);
93
118
 
94
119
  // Debounce search term
95
120
  useEffect(() => {
@@ -101,6 +126,42 @@ const ChildSearchModal = ({
101
126
  return () => clearTimeout(timer);
102
127
  }, [searchTerm]);
103
128
 
129
+ // Value-stable key for siteIds so the sync effect only runs when the set of site IDs actually
130
+ // changes, not on every parent re-render (avoids resetting user's site/room selection and pagination).
131
+ const siteIdsKey = useMemo(
132
+ () =>
133
+ Array.isArray(siteIds) && siteIds.length > 0
134
+ ? [...siteIds].sort((a, b) => a - b).join(",")
135
+ : "",
136
+ [siteIds]
137
+ );
138
+
139
+ // Keep the internal site filter in sync with externally controlled site changes
140
+ // (for example when the app site is switched from a sidebar while the page stays mounted).
141
+ useEffect(() => {
142
+ const nextSelectedSiteId =
143
+ siteIdsKey !== "" ? "" : siteId || "";
144
+
145
+ setAdvancedFilters((prev) => {
146
+ if (
147
+ prev.selectedSiteId === nextSelectedSiteId &&
148
+ prev.selectedRoomId === ""
149
+ ) {
150
+ return prev;
151
+ }
152
+
153
+ return {
154
+ ...prev,
155
+ selectedSiteId: nextSelectedSiteId,
156
+ selectedRoomId: "",
157
+ };
158
+ });
159
+
160
+ setPagination((prev) =>
161
+ prev.page === 1 ? prev : { ...prev, page: 1 }
162
+ );
163
+ }, [siteId, siteIdsKey]);
164
+
104
165
  // Close modal when clicking outside
105
166
  useEffect(() => {
106
167
  const handleClickOutside = (event) => {
@@ -196,9 +257,8 @@ const ChildSearchModal = ({
196
257
  );
197
258
  }
198
259
 
199
- // Build query parameters
260
+ // Build query parameters (caller identity: Bearer token — Common API extracts oid from JWT)
200
261
  const params = new URLSearchParams({
201
- entra_id: accounts[0].localAccountId,
202
262
  search_term: debouncedSearchTerm,
203
263
  page: pagination.page,
204
264
  page_size: pagination.pageSize,
@@ -215,11 +275,20 @@ const ChildSearchModal = ({
215
275
  params.append("site_id", siteId.toString());
216
276
  }
217
277
 
218
- // Handle status filtering
219
- // Important: when status is "all", do not send active_only=true.
220
- // The status dropdown is the source of truth for active/inactive/all.
221
- if (advancedFilters.status && advancedFilters.status !== "all") {
222
- params.append("status", advancedFilters.status);
278
+ // Resolve status: prefer non-empty UI state, else props (handles empty/stale state before open sync).
279
+ const resolvedStatus =
280
+ advancedFilters.status != null &&
281
+ String(advancedFilters.status).trim() !== ""
282
+ ? String(advancedFilters.status).trim()
283
+ : status || (activeOnly ? "active" : "all");
284
+
285
+ // Backend (search-children) defaults active_only to true when status is omitted, so "all"
286
+ // must be sent explicitly as status=all — otherwise only active children are returned.
287
+ if (resolvedStatus === "all") {
288
+ params.append("status", "all");
289
+ params.append("active_only", "false");
290
+ } else if (resolvedStatus) {
291
+ params.append("status", resolvedStatus);
223
292
  }
224
293
 
225
294
  // Add room filter
@@ -296,6 +365,8 @@ const ChildSearchModal = ({
296
365
  advancedFilters,
297
366
  applicationContext,
298
367
  bypassPermissions,
368
+ status,
369
+ activeOnly,
299
370
  ]);
300
371
 
301
372
  // Search when debounced term changes
@@ -329,8 +329,8 @@ const ChildSearchPage = ({
329
329
  );
330
330
  }
331
331
 
332
+ // Caller identity: Bearer token — Common API extracts oid from JWT (do not send entra_id in query)
332
333
  const params = new URLSearchParams({
333
- entra_id: accounts[0].localAccountId,
334
334
  search_term: debouncedSearchTerm,
335
335
  page: pagination.page,
336
336
  page_size: pagination.pageSize,
@@ -347,14 +347,19 @@ const ChildSearchPage = ({
347
347
  params.append("site_id", siteId.toString());
348
348
  }
349
349
 
350
- // Handle status filtering
351
- // Important: when status is "all", do not send active_only=true.
352
- // The status dropdown is the source of truth for active/inactive/all.
353
- if (
354
- debouncedAdvancedFilters.status &&
355
- debouncedAdvancedFilters.status !== "all"
356
- ) {
357
- params.append("status", debouncedAdvancedFilters.status);
350
+ const resolvedStatus =
351
+ debouncedAdvancedFilters.status != null &&
352
+ String(debouncedAdvancedFilters.status).trim() !== ""
353
+ ? String(debouncedAdvancedFilters.status).trim()
354
+ : status || (activeOnly ? "active" : "all");
355
+
356
+ // Backend (search-children) defaults active_only to true when status is omitted, so "all"
357
+ // must be sent explicitly as status=all — otherwise only active children are returned.
358
+ if (resolvedStatus === "all") {
359
+ params.append("status", "all");
360
+ params.append("active_only", "false");
361
+ } else if (resolvedStatus) {
362
+ params.append("status", resolvedStatus);
358
363
  }
359
364
 
360
365
  // Add room filter
@@ -437,6 +442,8 @@ const ChildSearchPage = ({
437
442
  debouncedAdvancedFilters,
438
443
  applicationContext,
439
444
  bypassPermissions,
445
+ status,
446
+ activeOnly,
440
447
  ]);
441
448
 
442
449
  // Search when debounced term changes
@@ -613,8 +613,8 @@ const EmployeeSearchModal = ({
613
613
  );
614
614
  }
615
615
 
616
+ // Caller identity: Bearer token — Common API extracts oid from JWT (do not send entra_id in query)
616
617
  const params = new URLSearchParams({
617
- entra_id: accounts[0].localAccountId,
618
618
  search_term: debouncedSearchTerm,
619
619
  page: pagination.page,
620
620
  page_size: pagination.pageSize,
@@ -426,8 +426,8 @@ const EmployeeSearchPage = ({
426
426
  );
427
427
  }
428
428
 
429
+ // Caller identity: Bearer token — Common API extracts oid from JWT (do not send entra_id in query)
429
430
  const params = new URLSearchParams({
430
- entra_id: accounts[0].localAccountId,
431
431
  search_term: debouncedSearchTerm,
432
432
  page: loadAllResults ? 1 : pagination.page,
433
433
  page_size: loadAllResults ? 10000 : pagination.pageSize,