@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 +15 -0
- package/package.json +4 -4
- package/src/Collection.tsx +32 -5
- package/src/DashboardReminder.tsx +46 -11
- package/src/Filters.tsx +68 -25
- package/src/Record.tsx +9 -0
- package/src/providers/FiltersProvider.tsx +74 -5
- package/src/utils/combineQueryConstraints.ts +10 -0
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.
|
|
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.
|
|
55
|
-
"@stoker-platform/utils": "0.5.
|
|
56
|
-
"@stoker-platform/web-client": "0.5.
|
|
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",
|
package/src/Collection.tsx
CHANGED
|
@@ -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={
|
|
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
|
|
216
|
-
|
|
216
|
+
const valueA = getSortingValue(
|
|
217
|
+
collectionSchema,
|
|
217
218
|
customization,
|
|
219
|
+
sorting.field,
|
|
220
|
+
a,
|
|
218
221
|
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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" ||
|
|
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=
|
|
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=
|
|
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 {
|
|
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> = ({
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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
|
+
}
|