@stoker-platform/web-app 0.5.198 → 0.5.200

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,20 @@
1
1
  # @stoker-platform/web-app
2
2
 
3
+ ## 0.5.200
4
+
5
+ ### Patch Changes
6
+
7
+ - fix: improve relation list assign mode
8
+ - @stoker-platform/node-client@0.5.78
9
+ - @stoker-platform/utils@0.5.69
10
+ - @stoker-platform/web-client@0.5.81
11
+
12
+ ## 0.5.199
13
+
14
+ ### Patch Changes
15
+
16
+ - chore: restore Dashboard reminder sorting improvements
17
+
3
18
  ## 0.5.198
4
19
 
5
20
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stoker-platform/web-app",
3
- "version": "0.5.198",
3
+ "version": "0.5.200",
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.81",
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,6 +479,15 @@ 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
  }
@@ -517,6 +539,7 @@ function Collection({
517
539
  } else if (search) {
518
540
  setServerList((prev) => ({ ...prev, [key]: [] }))
519
541
  setOptimisticList([], key)
542
+ syncDisplayIsAssigning()
520
543
  setIsRouteLoading("-", location.pathname)
521
544
  setCursor({})
522
545
  setPages({})
@@ -591,6 +614,7 @@ function Collection({
591
614
  setServerList((prev) => ({ ...prev, [key]: loadedDocs }))
592
615
  setOptimisticList(loadedDocs, key)
593
616
  }
617
+ syncDisplayIsAssigning()
594
618
  if (!query.infinite || firstLoad) {
595
619
  setCursor((prev) => ({ ...prev, [key]: newCursor }))
596
620
  }
@@ -641,12 +665,12 @@ function Collection({
641
665
  },
642
666
  {
643
667
  ...currentQuery.options,
644
- constraints: [
668
+ constraints: combineQueryConstraints([
645
669
  ...(currentQuery.constraints as QueryConstraint[]),
646
670
  ...(additionalConstraintsRef.current?.map((constraint) =>
647
671
  where(constraint[0], constraint[1] as WhereFilterOp, constraint[2]),
648
672
  ) || []),
649
- ],
673
+ ]),
650
674
  tempCache:
651
675
  isPreloadCacheEnabled && relationList?.loadAll
652
676
  ? {
@@ -684,6 +708,7 @@ function Collection({
684
708
  })
685
709
  setServerList((prev) => ({ ...prev, [key]: data.records }))
686
710
  setOptimisticList(data.records, key)
711
+ syncDisplayIsAssigning()
687
712
  setIsRouteLoading("-", location.pathname)
688
713
  resolve()
689
714
  }
@@ -1444,12 +1469,12 @@ function Collection({
1444
1469
  ...(additionalConstraints || []),
1445
1470
  ]
1446
1471
  } else {
1447
- finalConstraints = [
1472
+ finalConstraints = combineQueryConstraints([
1448
1473
  ...(constraints as QueryConstraint[]),
1449
1474
  ...(additionalConstraints?.map((constraint) =>
1450
1475
  where(constraint[0], constraint[1], constraint[2]),
1451
1476
  ) || []),
1452
- ]
1477
+ ])
1453
1478
  }
1454
1479
  // TODO: subcollection support
1455
1480
  const serverData = await getSome(
@@ -2233,6 +2258,8 @@ function Collection({
2233
2258
  collection={collection}
2234
2259
  excluded={excludedFilters}
2235
2260
  relationList={relationList}
2261
+ assignable={assignable}
2262
+ isAssigning={displayIsAssigning}
2236
2263
  />
2237
2264
  </SheetContent>
2238
2265
  </Sheet>
@@ -2602,7 +2629,7 @@ function Collection({
2602
2629
  relationCollection={relationCollection}
2603
2630
  relationParent={relationParent}
2604
2631
  formList={!!formList}
2605
- isAssigning={isAssigning}
2632
+ isAssigning={displayIsAssigning}
2606
2633
  assignable={assignable}
2607
2634
  hasBreadcrumbs={hasBreadcrumbs}
2608
2635
  />
@@ -17,6 +17,7 @@ import { Card, CardContent } from "./components/ui/card"
17
17
  import { useConnection } from "./providers/ConnectionProvider"
18
18
  import { getField, getFieldCustomization, isRelationField, tryFunction } from "@stoker-platform/utils"
19
19
  import { getFormattedFieldValue } from "./utils/getFormattedFieldValue"
20
+ import { getSortingValue } from "./utils/getSortingValue"
20
21
 
21
22
  interface DashboardReminderProps {
22
23
  reminder: Reminder
@@ -212,21 +213,55 @@ export const DashboardReminder = ({ reminder, title, collection }: DashboardRemi
212
213
  .sort((a, b) => {
213
214
  if (!sorting) return 0
214
215
  const sortingField = getField(fields, sorting.field)
215
- const fieldCustomization = getFieldCustomization(
216
- sortingField,
216
+ const valueA = getSortingValue(
217
+ collectionSchema,
217
218
  customization,
219
+ sorting.field,
220
+ a,
218
221
  )
219
- let sortA = a[sorting.field]
220
- let sortB = b[sorting.field]
221
- if (fieldCustomization.admin?.sort) {
222
- sortA = tryFunction(fieldCustomization.admin?.sort, [a])
223
- sortB = tryFunction(fieldCustomization.admin?.sort, [b])
224
- }
225
- if (sorting.direction === "asc") {
226
- return sortA - sortB
222
+ const valueB = getSortingValue(
223
+ collectionSchema,
224
+ customization,
225
+ sorting.field,
226
+ b,
227
+ )
228
+ let result: number
229
+ if (sortingField.type === "String") {
230
+ const rawA = valueA?.toString().toLowerCase() ?? ""
231
+ const rawB = valueB?.toString().toLowerCase() ?? ""
232
+ result = rawA > rawB ? 1 : rawA < rawB ? -1 : 0
233
+ } else if (isRelationField(sortingField)) {
234
+ const titleField = sortingField.titleField
235
+ let rawA: string
236
+ let rawB: string
237
+ if (titleField) {
238
+ const recordA = Object.values(a[sortingField.name] ?? {})[0] as
239
+ | StokerRecord
240
+ | undefined
241
+ const recordB = Object.values(b[sortingField.name] ?? {})[0] as
242
+ | StokerRecord
243
+ | undefined
244
+ // eslint-disable-next-line security/detect-object-injection
245
+ rawA = recordA?.[titleField]?.toString().toLowerCase() ?? ""
246
+ // eslint-disable-next-line security/detect-object-injection
247
+ rawB = recordB?.[titleField]?.toString().toLowerCase() ?? ""
248
+ } else {
249
+ rawA = (
250
+ Object.keys(a[sortingField.name] ?? {})[0] ?? ""
251
+ ).toLowerCase()
252
+ rawB = (
253
+ Object.keys(b[sortingField.name] ?? {})[0] ?? ""
254
+ ).toLowerCase()
255
+ }
256
+ result = rawA > rawB ? 1 : rawA < rawB ? -1 : 0
257
+ } else if (sortingField.type === "Timestamp") {
258
+ const rawA = Number(valueA?.valueOf() || 0)
259
+ const rawB = Number(valueB?.valueOf() || 0)
260
+ result = rawA > rawB ? 1 : rawA < rawB ? -1 : 0
227
261
  } else {
228
- return sortB - sortA
262
+ result = valueA > valueB ? 1 : valueA < valueB ? -1 : 0
229
263
  }
264
+ return sorting.direction === "asc" ? result : -result
230
265
  })
231
266
  .slice(page * pages, (page + 1) * pages)
232
267
  .map((result: StokerRecord) => (
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/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
+ }