@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 +23 -0
- package/package.json +4 -4
- package/src/Collection.tsx +81 -27
- package/src/Filters.tsx +68 -25
- package/src/List.tsx +44 -19
- package/src/Record.tsx +9 -0
- package/src/providers/FiltersProvider.tsx +74 -5
- package/src/utils/combineQueryConstraints.ts +10 -0
- package/src/utils/getFilterDisjunctions.ts +28 -2
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.
|
|
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.
|
|
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.82",
|
|
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,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 (
|
|
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={
|
|
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" ||
|
|
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/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 =
|
|
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
|
-
{
|
|
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
|
-
!
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
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 {
|
|
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
|
+
}
|
|
@@ -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
|
|
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
|
}
|