@stoker-platform/web-app 0.5.199 → 0.5.202

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @stoker-platform/web-app
2
2
 
3
+ ## 0.5.202
4
+
5
+ ### Patch Changes
6
+
7
+ - fix: fix incorrect multipleQueries option
8
+
9
+ ## 0.5.201
10
+
11
+ ### Patch Changes
12
+
13
+ - feat: improve server side full text search
14
+ - Updated dependencies
15
+ - @stoker-platform/web-client@0.5.82
16
+
17
+ ## 0.5.200
18
+
19
+ ### Patch Changes
20
+
21
+ - fix: improve relation list assign mode
22
+ - @stoker-platform/node-client@0.5.78
23
+ - @stoker-platform/utils@0.5.69
24
+ - @stoker-platform/web-client@0.5.81
25
+
3
26
  ## 0.5.199
4
27
 
5
28
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stoker-platform/web-app",
3
- "version": "0.5.199",
3
+ "version": "0.5.202",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "scripts": {
@@ -51,9 +51,9 @@
51
51
  "@radix-ui/react-tooltip": "^1.2.8",
52
52
  "@react-google-maps/api": "^2.20.8",
53
53
  "@sentry/react": "^10.56.0",
54
- "@stoker-platform/node-client": "0.5.77",
55
- "@stoker-platform/utils": "0.5.68",
56
- "@stoker-platform/web-client": "0.5.80",
54
+ "@stoker-platform/node-client": "0.5.78",
55
+ "@stoker-platform/utils": "0.5.69",
56
+ "@stoker-platform/web-client": "0.5.82",
57
57
  "@tanstack/react-table": "^8.21.3",
58
58
  "@types/react": "18.3.13",
59
59
  "@types/react-dom": "18.3.1",
@@ -82,6 +82,7 @@ import { ScrollArea } from "./components/ui/scroll-area"
82
82
  import { DateRangeSelector } from "./DateRange"
83
83
  import { useCache } from "./providers/CacheProvider"
84
84
  import { getOrderBy } from "./utils/getOrderBy"
85
+ import { combineQueryConstraints } from "./utils/combineQueryConstraints"
85
86
  import { preloadCacheEnabled } from "./utils/preloadCacheEnabled"
86
87
  import { localFullTextSearch } from "./utils/localFullTextSearch"
87
88
  import { TooltipProvider } from "./components/ui/tooltip"
@@ -252,6 +253,11 @@ function Collection({
252
253
  const currentField = currentFieldAll[labels.collection]
253
254
  const [backToStartKey, setBackToStartKey] = useState(0)
254
255
 
256
+ const [displayIsAssigning, setDisplayIsAssigning] = useState(isAssigning)
257
+ const filtersIsAssigningRef = useRef(isAssigning)
258
+ const assignQueryGenerationRef = useRef(0)
259
+ const displayedAssignGenerationRef = useRef(0)
260
+
255
261
  const preventChange = isRouteLoadingImmediate.has(location.pathname)
256
262
 
257
263
  useEffect(() => {
@@ -376,6 +382,7 @@ function Collection({
376
382
 
377
383
  useEffect(() => {
378
384
  if (!relationList || !isInitialized) return
385
+ filtersIsAssigningRef.current = isAssigning
379
386
  setFilters((prev) => {
380
387
  let next = prev
381
388
  .filter((filter) => !(filter.type === "relation" && filter.field === relationList.field))
@@ -396,6 +403,12 @@ function Collection({
396
403
  })
397
404
  }, [isAssigning, isInitialized])
398
405
 
406
+ useEffect(() => {
407
+ if (!relationList) {
408
+ setDisplayIsAssigning(isAssigning)
409
+ }
410
+ }, [relationList, isAssigning])
411
+
399
412
  // This is to ensure that the optimistic list is set in cases where cached documents exactly match the downloaded server documents
400
413
  // In this case, the cache-only snapshot listener does not fire a second time when the cache has loaded because there is no change to the list
401
414
  useEffect(() => {
@@ -466,10 +479,21 @@ function Collection({
466
479
  const startingTab = tabRef.current
467
480
  key ||= "default"
468
481
 
482
+ const queryGeneration = ++assignQueryGenerationRef.current
483
+ const queryIsAssigning = filtersIsAssigningRef.current
484
+ const syncDisplayIsAssigning = () => {
485
+ if (queryGeneration >= displayedAssignGenerationRef.current) {
486
+ displayedAssignGenerationRef.current = queryGeneration
487
+ setDisplayIsAssigning(queryIsAssigning)
488
+ }
489
+ }
490
+
469
491
  if (!isPreloadCacheEnabled || relationList?.loadAll) {
470
492
  setIsRouteLoading("+", location.pathname)
471
493
  }
472
494
 
495
+ const multipleQueries: QueryConstraint[][] = []
496
+
473
497
  if (
474
498
  fullTextSearch &&
475
499
  !isPreloadCacheEnabled &&
@@ -478,11 +502,6 @@ function Collection({
478
502
  tab !== "map" &&
479
503
  tab !== "calendar"
480
504
  ) {
481
- const disjunctions = getFilterDisjunctions(collection)
482
- const hitsPerPage =
483
- disjunctions === 0
484
- ? Math.min(30, itemsPerPage || 10)
485
- : Math.min(itemsPerPage || 10, Math.max(1, Math.floor(30 / disjunctions)))
486
505
  let latestFilters = filters
487
506
  if (tab === "cards" && prevTabRef.current !== "cards") {
488
507
  latestFilters = [...filters]
@@ -495,10 +514,32 @@ function Collection({
495
514
  }
496
515
  }
497
516
  }
517
+ const disjunctions = getFilterDisjunctions(collection, {
518
+ assignable,
519
+ isAssigning: queryIsAssigning,
520
+ filters: latestFilters,
521
+ relationList,
522
+ relationParent,
523
+ })
524
+ const searchOptions = customization.admin?.searchOptions
525
+ const exactPhrase = searchOptions?.fuzzy === false && searchOptions?.prefix === false
526
+ const batchSize =
527
+ disjunctions === 0
528
+ ? Math.min(30, itemsPerPage || 10)
529
+ : Math.min(itemsPerPage || 10, Math.max(1, Math.floor(30 / disjunctions)))
530
+ let hitsPerPage = 0
531
+ if (exactPhrase && !(hasEntityRestrictions.length > 0 || hasEntityParentFilters.length > 0)) {
532
+ hitsPerPage = 1000
533
+ } else {
534
+ hitsPerPage = batchSize
535
+ }
498
536
  const constraints = getFilterConstraints(latestFilters, false, true) as [string, "==" | "in", unknown][]
499
537
  const objectIDs = await performFullTextSearch(collection, search, hitsPerPage, constraints)
500
538
  searchResults.current = { ...searchResults.current, [key]: objectIDs }
501
- if (objectIDs.length > 0) {
539
+ if (
540
+ objectIDs.length > 0 &&
541
+ (!exactPhrase || hasEntityRestrictions.length > 0 || hasEntityParentFilters.length > 0)
542
+ ) {
502
543
  if (isServerReadOnly) {
503
544
  query.queries = query.queries.map((q) => ({
504
545
  ...q,
@@ -514,9 +555,14 @@ function Collection({
514
555
  constraints: [...q.constraints, where("id", "in", objectIDs)] as QueryConstraint[],
515
556
  }))
516
557
  }
558
+ } else if (objectIDs.length > 0 && exactPhrase) {
559
+ for (let i = 0; i < objectIDs.length; i += batchSize) {
560
+ multipleQueries.push([where("id", "in", objectIDs.slice(i, i + batchSize))])
561
+ }
517
562
  } else if (search) {
518
563
  setServerList((prev) => ({ ...prev, [key]: [] }))
519
564
  setOptimisticList([], key)
565
+ syncDisplayIsAssigning()
520
566
  setIsRouteLoading("-", location.pathname)
521
567
  setCursor({})
522
568
  setPages({})
@@ -591,6 +637,7 @@ function Collection({
591
637
  setServerList((prev) => ({ ...prev, [key]: loadedDocs }))
592
638
  setOptimisticList(loadedDocs, key)
593
639
  }
640
+ syncDisplayIsAssigning()
594
641
  if (!query.infinite || firstLoad) {
595
642
  setCursor((prev) => ({ ...prev, [key]: newCursor }))
596
643
  }
@@ -615,6 +662,27 @@ function Collection({
615
662
  }
616
663
  }
617
664
 
665
+ const subscribeOptions = {
666
+ ...currentQuery.options,
667
+ constraints: combineQueryConstraints([
668
+ ...(currentQuery.constraints as QueryConstraint[]),
669
+ ...(additionalConstraintsRef.current?.map((constraint) =>
670
+ where(constraint[0], constraint[1] as WhereFilterOp, constraint[2]),
671
+ ) || []),
672
+ ]),
673
+ tempCache:
674
+ isPreloadCacheEnabled && relationList?.loadAll
675
+ ? {
676
+ label: `${labels.collection}-${relationList?.field}`,
677
+ constraints: [[`${relationList?.field}_Single.id`, "==", relationParent?.id]],
678
+ }
679
+ : undefined,
680
+ multipleQueries: multipleQueries.length > 0 ? multipleQueries : undefined,
681
+ } as SubscribeManyOptions
682
+ if (multipleQueries.length > 0) {
683
+ delete subscribeOptions.pagination
684
+ }
685
+
618
686
  // TODO: subcollection support
619
687
  const result = await subscribeMany(
620
688
  [labels.collection],
@@ -639,24 +707,7 @@ function Collection({
639
707
  window.location.reload()
640
708
  }
641
709
  },
642
- {
643
- ...currentQuery.options,
644
- constraints: [
645
- ...(currentQuery.constraints as QueryConstraint[]),
646
- ...(additionalConstraintsRef.current?.map((constraint) =>
647
- where(constraint[0], constraint[1] as WhereFilterOp, constraint[2]),
648
- ) || []),
649
- ],
650
- tempCache:
651
- isPreloadCacheEnabled && relationList?.loadAll
652
- ? {
653
- label: `${labels.collection}-${relationList?.field}`,
654
- constraints: [
655
- [`${relationList?.field}_Single.id`, "==", relationParent?.id],
656
- ],
657
- }
658
- : undefined,
659
- } as SubscribeManyOptions,
710
+ subscribeOptions,
660
711
  )
661
712
  const { unsubscribe: newUnsubscribe, count: newCount, pages: newPages } = result
662
713
  promiseLoaded = true
@@ -684,6 +735,7 @@ function Collection({
684
735
  })
685
736
  setServerList((prev) => ({ ...prev, [key]: data.records }))
686
737
  setOptimisticList(data.records, key)
738
+ syncDisplayIsAssigning()
687
739
  setIsRouteLoading("-", location.pathname)
688
740
  resolve()
689
741
  }
@@ -1444,12 +1496,12 @@ function Collection({
1444
1496
  ...(additionalConstraints || []),
1445
1497
  ]
1446
1498
  } else {
1447
- finalConstraints = [
1499
+ finalConstraints = combineQueryConstraints([
1448
1500
  ...(constraints as QueryConstraint[]),
1449
1501
  ...(additionalConstraints?.map((constraint) =>
1450
1502
  where(constraint[0], constraint[1], constraint[2]),
1451
1503
  ) || []),
1452
- ]
1504
+ ])
1453
1505
  }
1454
1506
  // TODO: subcollection support
1455
1507
  const serverData = await getSome(
@@ -2233,6 +2285,8 @@ function Collection({
2233
2285
  collection={collection}
2234
2286
  excluded={excludedFilters}
2235
2287
  relationList={relationList}
2288
+ assignable={assignable}
2289
+ isAssigning={displayIsAssigning}
2236
2290
  />
2237
2291
  </SheetContent>
2238
2292
  </Sheet>
@@ -2602,7 +2656,7 @@ function Collection({
2602
2656
  relationCollection={relationCollection}
2603
2657
  relationParent={relationParent}
2604
2658
  formList={!!formList}
2605
- isAssigning={isAssigning}
2659
+ isAssigning={displayIsAssigning}
2606
2660
  assignable={assignable}
2607
2661
  hasBreadcrumbs={hasBreadcrumbs}
2608
2662
  />
package/src/Filters.tsx CHANGED
@@ -1,8 +1,11 @@
1
1
  import {
2
+ Assignable,
2
3
  CollectionField,
3
4
  CollectionSchema,
4
5
  Filter,
6
+ RelationFilter,
5
7
  RelationList,
8
+ SelectFilter,
6
9
  StokerCollection,
7
10
  StokerRecord,
8
11
  } from "@stoker-platform/types"
@@ -51,9 +54,11 @@ interface FiltersProps {
51
54
  collection: CollectionSchema
52
55
  excluded: string[]
53
56
  relationList?: RelationList
57
+ assignable?: Assignable
58
+ isAssigning?: boolean
54
59
  }
55
60
 
56
- export function Filters({ collection, excluded, relationList }: FiltersProps) {
61
+ export function Filters({ collection, excluded, relationList, assignable, isAssigning }: FiltersProps) {
57
62
  const { labels, fields } = collection
58
63
  const location = useLocation()
59
64
  const schema = getSchema()
@@ -83,6 +88,25 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
83
88
 
84
89
  const preventChange = isRouteLoadingImmediate.has(location.pathname)
85
90
 
91
+ const isArrayOrRelationFilter = useCallback(
92
+ (filter: Filter): filter is SelectFilter | RelationFilter => {
93
+ if (filter.type === "relation") return true
94
+ if (filter.type === "select") {
95
+ const fieldSchema = getField(fields, filter.field)
96
+ return fieldSchema.type === "Array"
97
+ }
98
+ return false
99
+ },
100
+ [fields],
101
+ )
102
+
103
+ const isIncludeAssignedActive = useCallback(() => {
104
+ if (!isAssigning || !assignable?.includeAssignedInFilters?.length) return false
105
+ return assignable.includeAssignedInFilters.some((field) =>
106
+ filters.some((filter) => filter.type === "select" && filter.field === field && filter.value),
107
+ )
108
+ }, [isAssigning, assignable, filters])
109
+
86
110
  const isMobile = useIsMobile()
87
111
  const someFilterOpen = Object.values(open).some(Boolean)
88
112
  const { offset: keyboardOffset, viewportHeight } = useKeyboardOffset(isMobile && someFilterOpen)
@@ -112,10 +136,6 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
112
136
  const relationCollection = schema.collections[field.collection]
113
137
  const relationCustomization = getCollectionConfigModule(relationCollection.labels.collection)
114
138
 
115
- if (!isPreloadCacheEnabled && filter.value) {
116
- setArrayContainsFilterSet(filter.field)
117
- }
118
-
119
139
  const collectionAdminPath: ["collections", StokerCollection, "admin"] = [
120
140
  "collections",
121
141
  field.collection,
@@ -151,11 +171,6 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
151
171
  })
152
172
  }
153
173
  } else if (filter.type === "select") {
154
- const field = getField(fields, filter.field)
155
- if (!isPreloadCacheEnabled && filter.value && field.type === "Array") {
156
- setArrayContainsFilterSet(filter.field)
157
- }
158
-
159
174
  setValue((prev) => ({
160
175
  ...prev,
161
176
  [filter.field]: filter.value?.toString() || "no_selection",
@@ -166,11 +181,33 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
166
181
  initialize()
167
182
  }, [])
168
183
 
184
+ useEffect(() => {
185
+ if (isPreloadCacheEnabled) return
186
+
187
+ let arrayOrRelationField: string | undefined
188
+
189
+ if (isIncludeAssignedActive()) {
190
+ const activeField = assignable?.includeAssignedInFilters?.find((field) =>
191
+ filters.some((filter) => filter.type === "select" && filter.field === field && filter.value),
192
+ )
193
+ if (activeField) {
194
+ arrayOrRelationField = activeField
195
+ }
196
+ }
197
+
198
+ const arrayOrRelationFilter = filters.find(
199
+ (filter): filter is SelectFilter | RelationFilter => !!filter.value && isArrayOrRelationFilter(filter),
200
+ )
201
+ if (arrayOrRelationFilter) {
202
+ arrayOrRelationField = arrayOrRelationFilter.field
203
+ }
204
+ setArrayContainsFilterSet(arrayOrRelationField)
205
+ }, [filters, isAssigning, assignable, isPreloadCacheEnabled, isIncludeAssignedActive, isArrayOrRelationFilter])
206
+
169
207
  const handleChange = useCallback(
170
208
  (filter: Filter, value: string, type: CollectionField["type"]) => {
171
209
  if (preventChange) return
172
210
  if (filter.type === "range" || filter.type === "status") return
173
- const fieldSchema = getField(fields, filter.field)
174
211
  const index = filters
175
212
  .filter((filterItem) => filterItem.type !== "status" && filterItem.type !== "range")
176
213
  .findIndex((filterItem) => filter.field === filterItem.field)
@@ -187,9 +224,6 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
187
224
  }
188
225
  return newFilters
189
226
  })
190
- if (!isPreloadCacheEnabled && (filter.type === "relation" || fieldSchema.type === "Array")) {
191
- setArrayContainsFilterSet(filter.field)
192
- }
193
227
  } else {
194
228
  setFilters((filters) => {
195
229
  newFilters = [...filters]
@@ -197,9 +231,6 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
197
231
  delete newFilters[index].value
198
232
  return newFilters
199
233
  })
200
- if (!isPreloadCacheEnabled && (filter.type === "relation" || fieldSchema.type === "Array")) {
201
- setArrayContainsFilterSet(undefined)
202
- }
203
234
  }
204
235
  const filterParam = newFilters
205
236
  .filter((filter: Filter) => filter.type !== "status" && filter.type !== "range" && filter.value)
@@ -386,15 +417,24 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
386
417
  }, [searchValue])
387
418
 
388
419
  const isFilterDisabled = useCallback(
420
+ (filter: Filter) => {
421
+ if (filter.type === "range" || filter.type === "status") return false
422
+ return isRouteLoading.has(location.pathname)
423
+ },
424
+ [isRouteLoading, location.pathname],
425
+ )
426
+
427
+ const isFilterInactive = useCallback(
389
428
  (filter: Filter) => {
390
429
  if (filter.type === "range" || filter.type === "status") return false
391
430
  const fieldSchema = getField(fields, filter.field)
392
431
  return !!(
393
- isRouteLoading.has(location.pathname) ||
394
432
  (!isPreloadCacheEnabled &&
395
433
  arrayContainsFilterSet &&
396
434
  arrayContainsFilterSet !== filter.field &&
397
- (filter.type === "relation" || fieldSchema.type === "Array")) ||
435
+ (filter.type === "relation" ||
436
+ fieldSchema.type === "Array" ||
437
+ (isAssigning && assignable?.includeAssignedInFilters?.includes(filter.field)))) ||
398
438
  (isRelationField(fieldSchema) &&
399
439
  connectionStatus === "offline" &&
400
440
  !preloadCacheEnabled(schema.collections[fieldSchema.collection]))
@@ -412,7 +452,7 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
412
452
  if (!field) return null
413
453
  const fieldCustomization = getFieldCustomization(field, customization)
414
454
  const label = tryFunction(fieldCustomization.admin?.label)
415
- const disabled = isFilterDisabled(filter)
455
+ const disabled = isFilterDisabled(filter) || isFilterInactive(filter)
416
456
  if (excluded.includes(filter.field)) return null
417
457
  if (filter.type === "select") {
418
458
  const title = tryFunction(filter.title) || label || field.name
@@ -501,7 +541,10 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
501
541
  handleChange(filter, value, field.type)
502
542
  })
503
543
  }}
504
- className="disabled:opacity-100"
544
+ className={cn(
545
+ "disabled:opacity-100",
546
+ isFilterInactive(filter) && "disabled:opacity-50",
547
+ )}
505
548
  >
506
549
  {value}
507
550
  </Button>
@@ -520,7 +563,10 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
520
563
  handleChange(filter, "no_selection", field.type)
521
564
  })
522
565
  }}
523
- className="disabled:opacity-100"
566
+ className={cn(
567
+ "disabled:opacity-100",
568
+ isFilterInactive(filter) && "disabled:opacity-50",
569
+ )}
524
570
  >
525
571
  All
526
572
  </Button>
@@ -765,9 +811,6 @@ export function Filters({ collection, excluded, relationList }: FiltersProps) {
765
811
  variant="outline"
766
812
  disabled={isRouteLoading.has(location.pathname)}
767
813
  onClick={() => {
768
- if (!isPreloadCacheEnabled) {
769
- setArrayContainsFilterSet(undefined)
770
- }
771
814
  filters.forEach((filter) => {
772
815
  if (filter.type === "status" || filter.type === "range") return
773
816
  if (relationList && relationList.field === filter.field) return
package/src/List.tsx CHANGED
@@ -677,6 +677,8 @@ export function List({
677
677
  return allColumns
678
678
  }, [fields, isPreloadCacheEnabled, isServerReadOnly, recordTitleField, connectionStatus])
679
679
 
680
+ const isServerFullTextSearch = !!(search && fullTextSearch && !isPreloadCacheEnabled && !isServerReadOnly)
681
+
680
682
  const isSearchRelevanceOrder = !!(search && isPreloadCacheEnabled)
681
683
 
682
684
  const searchList = useMemo(() => {
@@ -694,6 +696,8 @@ export function List({
694
696
  return list || []
695
697
  }, [isPreloadCacheEnabled, isServerReadOnly, list, search])
696
698
 
699
+ const tablePageSize = isServerFullTextSearch ? Math.max(searchList.length, 1) : pageSize
700
+
697
701
  const selectedRecords = useMemo(() => {
698
702
  const selectedIds = Object.keys(rowSelection)
699
703
  if (selectedIds.length === 0) return []
@@ -761,12 +765,13 @@ export function List({
761
765
  columnFilters,
762
766
  rowSelection,
763
767
  pagination: {
764
- pageSize,
765
- pageIndex,
768
+ pageSize: tablePageSize,
769
+ pageIndex: isServerFullTextSearch ? 0 : pageIndex,
766
770
  },
767
771
  },
768
772
  onPaginationChange: (updater) => {
769
- const newPagination = typeof updater === "function" ? updater({ pageIndex, pageSize }) : updater
773
+ const newPagination =
774
+ typeof updater === "function" ? updater({ pageIndex, pageSize: tablePageSize }) : updater
770
775
  if (pageCount && newPagination.pageIndex < pageCount) {
771
776
  setPageIndex(newPagination.pageIndex)
772
777
  }
@@ -868,7 +873,7 @@ export function List({
868
873
  }
869
874
  }, 750)
870
875
 
871
- if (isInitialized && (isPreloadCacheEnabled || isServerReadOnly)) {
876
+ if (isInitialized && (isPreloadCacheEnabled || isServerReadOnly || isServerFullTextSearch)) {
872
877
  setPageIndex(0)
873
878
  setState(`collection-page-number-${labels.collection.toLowerCase()}`, "page", 1)
874
879
  }
@@ -1070,12 +1075,15 @@ export function List({
1070
1075
  }, [table, list, pageSize, isLoading, cursor, pageNumber, pageCount, constraints, orderByField, orderByDirection])
1071
1076
 
1072
1077
  const canGetNextPage = useCallback(() => {
1078
+ if (isServerFullTextSearch) {
1079
+ return false
1080
+ }
1073
1081
  if (isPreloadCacheEnabled || isServerReadOnly) {
1074
1082
  return table.getCanNextPage()
1075
1083
  } else {
1076
1084
  return !isLoadingDebounced && pageCount && pageNumber < pageCount && list?.length === pageSize
1077
1085
  }
1078
- }, [table, isLoadingDebounced, pageNumber, pageCount, list, pageSize])
1086
+ }, [table, isLoadingDebounced, pageNumber, pageCount, list, pageSize, isServerFullTextSearch])
1079
1087
 
1080
1088
  const prevPage = useCallback(() => {
1081
1089
  if (isLoading) return
@@ -1161,12 +1169,15 @@ export function List({
1161
1169
  }, [table, list, pageSize, isLoading, cursor, prevCursor, pageNumber, constraints, orderByField, orderByDirection])
1162
1170
 
1163
1171
  const canGetPrevPage = useCallback(() => {
1172
+ if (isServerFullTextSearch) {
1173
+ return false
1174
+ }
1164
1175
  if (isPreloadCacheEnabled || isServerReadOnly) {
1165
1176
  return table.getCanPreviousPage()
1166
1177
  } else {
1167
1178
  return !isLoadingDebounced && pageNumber > 1
1168
1179
  }
1169
- }, [table, isLoadingDebounced, pageNumber])
1180
+ }, [table, isLoadingDebounced, pageNumber, isServerFullTextSearch])
1170
1181
 
1171
1182
  const onChangePageNumber = useCallback(
1172
1183
  (event: React.ChangeEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>) => {
@@ -2019,7 +2030,12 @@ export function List({
2019
2030
  </ScrollArea>
2020
2031
  </Card>
2021
2032
  <div className="flex items-center justify-end space-x-2 py-4 print:hidden">
2022
- {pagesLoaded && (
2033
+ {isServerFullTextSearch && searchList.length > 0 && (
2034
+ <Badge variant="secondary" className="hidden sm:block">
2035
+ {searchList.length} {searchList.length === 1 ? "result" : "results"}
2036
+ </Badge>
2037
+ )}
2038
+ {pagesLoaded && !isServerFullTextSearch && (
2023
2039
  <Badge variant="secondary" className="hidden sm:block">
2024
2040
  Page{" "}
2025
2041
  {isPreloadCacheEnabled || isServerReadOnly
@@ -2046,7 +2062,7 @@ export function List({
2046
2062
  />
2047
2063
  </div>
2048
2064
  )}
2049
- {!isPreloadCacheEnabled && !isServerReadOnly && (
2065
+ {!isServerFullTextSearch && !isPreloadCacheEnabled && !isServerReadOnly && (
2050
2066
  <Button
2051
2067
  type="button"
2052
2068
  variant="outline"
@@ -2059,19 +2075,28 @@ export function List({
2059
2075
  Back to start
2060
2076
  </Button>
2061
2077
  )}
2062
- {!(
2063
- !isPreloadCacheEnabled &&
2064
- !isServerReadOnly &&
2065
- typeof sortingField?.sorting === "object" &&
2066
- sortingField.sorting.direction
2067
- ) && (
2068
- <Button type="button" variant="outline" size="sm" onClick={prevPage} disabled={!canGetPrevPage()}>
2069
- Previous
2078
+ {!isServerFullTextSearch &&
2079
+ !(
2080
+ !isPreloadCacheEnabled &&
2081
+ !isServerReadOnly &&
2082
+ typeof sortingField?.sorting === "object" &&
2083
+ sortingField.sorting.direction
2084
+ ) && (
2085
+ <Button
2086
+ type="button"
2087
+ variant="outline"
2088
+ size="sm"
2089
+ onClick={prevPage}
2090
+ disabled={!canGetPrevPage()}
2091
+ >
2092
+ Previous
2093
+ </Button>
2094
+ )}
2095
+ {!isServerFullTextSearch && (
2096
+ <Button type="button" variant="outline" size="sm" onClick={nextPage} disabled={!canGetNextPage()}>
2097
+ Next
2070
2098
  </Button>
2071
2099
  )}
2072
- <Button type="button" variant="outline" size="sm" onClick={nextPage} disabled={!canGetNextPage()}>
2073
- Next
2074
- </Button>
2075
2100
  </div>
2076
2101
  </>
2077
2102
  )
package/src/Record.tsx CHANGED
@@ -253,6 +253,15 @@ export const Record = ({ collection }: { collection: CollectionSchema }) => {
253
253
  <FiltersProvider
254
254
  key={`${relationList.collection}-filters`}
255
255
  collection={relationCollection}
256
+ relationList={relationList}
257
+ relationParent={record}
258
+ assignable={assignable?.find(
259
+ (item: Assignable) =>
260
+ item.collection === relationList.collection,
261
+ )}
262
+ isAssigning={
263
+ isAssigning?.[relationList.collection.toLowerCase()]
264
+ }
256
265
  >
257
266
  {hasBreadcrumbs && record && (
258
267
  <>
@@ -1,12 +1,33 @@
1
1
  import { getMaxDate, getMinDate } from "@/utils/getMaxDateRange"
2
2
  import { preloadCacheEnabled } from "@/utils/preloadCacheEnabled"
3
3
  import { serverReadOnly } from "@/utils/serverReadOnly"
4
- import { CalendarConfig, CollectionSchema, Filter, StokerCollection, StokerRecord } from "@stoker-platform/types"
4
+ import {
5
+ Assignable,
6
+ CalendarConfig,
7
+ CollectionSchema,
8
+ Filter,
9
+ RelationList,
10
+ StokerCollection,
11
+ StokerRecord,
12
+ } from "@stoker-platform/types"
5
13
  import { getCachedConfigValue, getField, getFieldCustomization, tryFunction } from "@stoker-platform/utils"
6
14
  import { getCollectionConfigModule } from "@stoker-platform/web-client"
7
- import { QueryConstraint, where, WhereFilterOp } from "firebase/firestore"
15
+ import { or, QueryConstraint, where, WhereFilterOp } from "firebase/firestore"
8
16
  import { createContext, useCallback, useContext, useEffect, useState } from "react"
9
17
 
18
+ const includesAssigned = (
19
+ relationList: RelationList | undefined,
20
+ relationParent: StokerRecord | undefined,
21
+ assignable: Assignable | undefined,
22
+ isAssigning: boolean | undefined,
23
+ filter: Filter,
24
+ ) => {
25
+ if (!isAssigning || !relationList || !relationParent?.id) return false
26
+ if (filter.type !== "select" || !filter.value) return false
27
+ const includeAssignedInFilters = assignable?.includeAssignedInFilters || []
28
+ return !!includeAssignedInFilters.includes(filter.field)
29
+ }
30
+
10
31
  export const FiltersContext = createContext<
11
32
  | {
12
33
  filters: Filter[]
@@ -25,11 +46,22 @@ export const FiltersContext = createContext<
25
46
 
26
47
  interface FiltersProviderProps {
27
48
  collection: CollectionSchema
49
+ relationList?: RelationList
50
+ relationParent?: StokerRecord
51
+ assignable?: Assignable
52
+ isAssigning?: boolean
28
53
  children: React.ReactNode
29
54
  }
30
55
 
31
56
  /* eslint-disable react/prop-types */
32
- export const FiltersProvider: React.FC<FiltersProviderProps> = ({ collection, children }) => {
57
+ export const FiltersProvider: React.FC<FiltersProviderProps> = ({
58
+ collection,
59
+ relationList,
60
+ relationParent,
61
+ assignable,
62
+ isAssigning,
63
+ children,
64
+ }) => {
33
65
  const { labels, fields, softDelete } = collection
34
66
  const [filters, setFilters] = useState<Filter[]>([])
35
67
  const [order, setOrder] = useState<{ field: string; direction: "asc" | "desc" }>()
@@ -112,6 +144,14 @@ export const FiltersProvider: React.FC<FiltersProviderProps> = ({ collection, ch
112
144
  constraints.push(where(filter.field, "array-contains", filter.value))
113
145
  } else if (field.type === "Number") {
114
146
  constraints.push(where(filter.field, "==", Number(filter.value)))
147
+ } else if (includesAssigned(relationList, relationParent, assignable, isAssigning, filter)) {
148
+ constraints.push(
149
+ or(
150
+ where(filter.field, "==", filter.value),
151
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
152
+ where(`${relationList!.field}_Array`, "array-contains", relationParent!.id),
153
+ ) as unknown as QueryConstraint,
154
+ )
115
155
  } else {
116
156
  constraints.push(where(filter.field, "==", filter.value))
117
157
  }
@@ -205,7 +245,20 @@ export const FiltersProvider: React.FC<FiltersProviderProps> = ({ collection, ch
205
245
  return constraints
206
246
  }
207
247
  },
208
- [filters, fields, isPreloadCacheEnabled, isServerReadOnly, statusField, softDeleteField],
248
+ [
249
+ filters,
250
+ fields,
251
+ relationList,
252
+ relationParent,
253
+ assignable,
254
+ isAssigning,
255
+ isPreloadCacheEnabled,
256
+ isServerReadOnly,
257
+ statusField,
258
+ softDeleteField,
259
+ calendarConfig,
260
+ customization,
261
+ ],
209
262
  )
210
263
 
211
264
  const filterRecord = useCallback(
@@ -268,6 +321,12 @@ export const FiltersProvider: React.FC<FiltersProviderProps> = ({ collection, ch
268
321
  show = show && record[filter.field]?.includes(filter.value)
269
322
  } else if (field.type === "Number") {
270
323
  show = show && record[filter.field] === Number(filter.value)
324
+ } else if (includesAssigned(relationList, relationParent, assignable, isAssigning, filter)) {
325
+ show =
326
+ show &&
327
+ (record[filter.field] === filter.value ||
328
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
329
+ record[`${relationList!.field}_Array`]?.includes(relationParent!.id))
271
330
  } else {
272
331
  show = show && record[filter.field] === filter.value
273
332
  }
@@ -279,7 +338,17 @@ export const FiltersProvider: React.FC<FiltersProviderProps> = ({ collection, ch
279
338
  })
280
339
  return show
281
340
  },
282
- [filters, statusField, softDeleteField],
341
+ [
342
+ filters,
343
+ fields,
344
+ relationList,
345
+ relationParent,
346
+ assignable,
347
+ isAssigning,
348
+ statusField,
349
+ softDeleteField,
350
+ customization,
351
+ ],
283
352
  )
284
353
 
285
354
  return (
@@ -0,0 +1,10 @@
1
+ import { and, QueryConstraint, QueryFilterConstraint } from "firebase/firestore"
2
+
3
+ export const combineQueryConstraints = (constraints: QueryConstraint[]): QueryConstraint[] => {
4
+ const hasCompositeFilter = constraints.some((constraint) => {
5
+ const type = (constraint as { type?: string }).type
6
+ return type === "or" || type === "and"
7
+ })
8
+ if (!hasCompositeFilter || constraints.length <= 1) return constraints
9
+ return [and(...(constraints as unknown as QueryFilterConstraint[])) as unknown as QueryConstraint]
10
+ }
@@ -1,8 +1,16 @@
1
- import { CollectionSchema } from "@stoker-platform/types"
1
+ import { Assignable, CollectionSchema, Filter, RelationList, StokerRecord } from "@stoker-platform/types"
2
2
  import { getAttributeRestrictions } from "@stoker-platform/utils"
3
3
  import { getCurrentUserPermissions } from "@stoker-platform/web-client"
4
4
 
5
- export const getFilterDisjunctions = (collection: CollectionSchema) => {
5
+ export interface FilterDisjunctionsOptions {
6
+ assignable?: Assignable
7
+ isAssigning?: boolean
8
+ filters?: Filter[]
9
+ relationList?: RelationList
10
+ relationParent?: StokerRecord
11
+ }
12
+
13
+ export const getFilterDisjunctions = (collection: CollectionSchema, options?: FilterDisjunctionsOptions) => {
6
14
  const permissions = getCurrentUserPermissions()
7
15
  if (!permissions?.Role) throw new Error("PERMISSION_DENIED")
8
16
  let disjunctions = 0
@@ -25,5 +33,23 @@ export const getFilterDisjunctions = (collection: CollectionSchema) => {
25
33
  }
26
34
  }
27
35
  }
36
+ if (
37
+ options?.isAssigning &&
38
+ options.relationList &&
39
+ options.relationParent?.id &&
40
+ options.assignable?.includeAssignedInFilters?.length &&
41
+ options.filters
42
+ ) {
43
+ const activeOrCount = options.filters.filter(
44
+ (filter) =>
45
+ filter.type === "select" &&
46
+ filter.value &&
47
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
48
+ options.assignable!.includeAssignedInFilters!.includes(filter.field),
49
+ ).length
50
+ for (let i = 0; i < activeOrCount; i++) {
51
+ incrementDisjunctions(2)
52
+ }
53
+ }
28
54
  return disjunctions
29
55
  }