@stoker-platform/web-app 0.5.211 → 0.5.213
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/Cards.tsx +56 -42
- package/src/Collection.tsx +60 -34
- package/src/Images.tsx +25 -2
- package/src/List.tsx +27 -22
- package/src/utils/fullTextSearch.ts +24 -0
- package/src/utils/localFullTextSearch.ts +3 -7
- package/src/utils/runViewTransition.ts +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @stoker-platform/web-app
|
|
2
2
|
|
|
3
|
+
## 0.5.213
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fix: fix collection change UI flicker
|
|
8
|
+
|
|
9
|
+
## 0.5.212
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- feat: improve Algolia search
|
|
14
|
+
- @stoker-platform/node-client@0.5.83
|
|
15
|
+
- @stoker-platform/utils@0.5.74
|
|
16
|
+
- @stoker-platform/web-client@0.5.87
|
|
17
|
+
|
|
3
18
|
## 0.5.211
|
|
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.213",
|
|
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.83",
|
|
55
|
+
"@stoker-platform/utils": "0.5.74",
|
|
56
|
+
"@stoker-platform/web-client": "0.5.87",
|
|
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/Cards.tsx
CHANGED
|
@@ -51,6 +51,7 @@ import { useLocation } from "react-router"
|
|
|
51
51
|
import { FirestoreError, QueryConstraint, Timestamp, where } from "firebase/firestore"
|
|
52
52
|
import { preloadCacheEnabled } from "./utils/preloadCacheEnabled"
|
|
53
53
|
import { localFullTextSearch } from "./utils/localFullTextSearch"
|
|
54
|
+
import { isServerFullTextSearch } from "./utils/fullTextSearch"
|
|
54
55
|
import { Helmet } from "react-helmet"
|
|
55
56
|
import { useConnection } from "./providers/ConnectionProvider"
|
|
56
57
|
|
|
@@ -525,6 +526,7 @@ interface DropZoneProps {
|
|
|
525
526
|
backToStart: () => void
|
|
526
527
|
setOptimisticList: (serverList?: StokerRecord[], key?: string | number) => void
|
|
527
528
|
search: string | undefined
|
|
529
|
+
searchClearing: boolean
|
|
528
530
|
}
|
|
529
531
|
|
|
530
532
|
function DropZone({
|
|
@@ -544,8 +546,9 @@ function DropZone({
|
|
|
544
546
|
backToStart,
|
|
545
547
|
setOptimisticList,
|
|
546
548
|
search,
|
|
549
|
+
searchClearing,
|
|
547
550
|
}: DropZoneProps) {
|
|
548
|
-
const { labels } = collection
|
|
551
|
+
const { labels, fullTextSearch } = collection
|
|
549
552
|
const { toast } = useToast()
|
|
550
553
|
const { setGlobalLoading } = useGlobalLoading()
|
|
551
554
|
const permissions = getCurrentUserPermissions()
|
|
@@ -810,7 +813,6 @@ function DropZone({
|
|
|
810
813
|
|
|
811
814
|
const records = useMemo(() => {
|
|
812
815
|
if (!statusList) return []
|
|
813
|
-
if (typeof orderByField !== "string") return []
|
|
814
816
|
const removeEmptyRecords: StokerRecord[] = []
|
|
815
817
|
let latestFiltered =
|
|
816
818
|
latestList?.filter(
|
|
@@ -838,6 +840,10 @@ function DropZone({
|
|
|
838
840
|
}
|
|
839
841
|
})
|
|
840
842
|
const dedupedRecords = Array.from(new Map(removeEmptyRecords.map((record) => [record.id, record])).values())
|
|
843
|
+
if (isServerFullTextSearch(search, collection, isPreloadCacheEnabled, isServerReadOnly)) {
|
|
844
|
+
return dedupedRecords
|
|
845
|
+
}
|
|
846
|
+
if (typeof orderByField !== "string") return []
|
|
841
847
|
let sortedList = sortList(collection, dedupedRecords, orderByField, orderByDirection)
|
|
842
848
|
if (search && (isPreloadCacheEnabled || isServerReadOnly)) {
|
|
843
849
|
const searchResults = localFullTextSearch(collection, search, sortedList).map((result) => result.id)
|
|
@@ -854,6 +860,9 @@ function DropZone({
|
|
|
854
860
|
orderByField,
|
|
855
861
|
orderByDirection,
|
|
856
862
|
search,
|
|
863
|
+
fullTextSearch,
|
|
864
|
+
isPreloadCacheEnabled,
|
|
865
|
+
isServerReadOnly,
|
|
857
866
|
])
|
|
858
867
|
|
|
859
868
|
const [isFirstLoad, setIsFirstLoad] = useState(true)
|
|
@@ -1044,47 +1053,49 @@ function DropZone({
|
|
|
1044
1053
|
</CardHeader>
|
|
1045
1054
|
</Card>
|
|
1046
1055
|
{isOverDebounced && <div className="bg-primary/50 rounded-lg h-full"></div>}
|
|
1047
|
-
|
|
1048
|
-
{
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
<InfiniteLoader
|
|
1063
|
-
isItemLoaded={(index) => index < records.length}
|
|
1056
|
+
{!searchClearing && (
|
|
1057
|
+
<div className={cn(className, "rounded-lg", "space-y-4")}>
|
|
1058
|
+
{isPreloadCacheEnabled || isServerReadOnly ? (
|
|
1059
|
+
<FixedSizeList
|
|
1060
|
+
height={height}
|
|
1061
|
+
width="100%"
|
|
1062
|
+
itemSize={itemSize}
|
|
1063
|
+
itemCount={records.length}
|
|
1064
|
+
itemKey={itemKey}
|
|
1065
|
+
overscanCount={5}
|
|
1066
|
+
itemData={itemData}
|
|
1067
|
+
>
|
|
1068
|
+
{renderRow}
|
|
1069
|
+
</FixedSizeList>
|
|
1070
|
+
) : (
|
|
1064
1071
|
// eslint-disable-next-line security/detect-object-injection
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1072
|
+
<InfiniteLoader
|
|
1073
|
+
isItemLoaded={(index) => index < records.length}
|
|
1074
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
1075
|
+
itemCount={100000}
|
|
1076
|
+
loadMoreItems={() => loadMoreItems(statusValue)}
|
|
1077
|
+
minimumBatchSize={itemsPerPage || 10}
|
|
1078
|
+
threshold={itemsPerPage || 40}
|
|
1079
|
+
>
|
|
1080
|
+
{({ onItemsRendered, ref }) => (
|
|
1081
|
+
<FixedSizeList
|
|
1082
|
+
height={height}
|
|
1083
|
+
width="100%"
|
|
1084
|
+
itemSize={itemSize}
|
|
1085
|
+
itemCount={records.length}
|
|
1086
|
+
overscanCount={10}
|
|
1087
|
+
itemKey={itemKey}
|
|
1088
|
+
ref={ref}
|
|
1089
|
+
onItemsRendered={onItemsRendered}
|
|
1090
|
+
itemData={itemData}
|
|
1091
|
+
>
|
|
1092
|
+
{renderRow}
|
|
1093
|
+
</FixedSizeList>
|
|
1094
|
+
)}
|
|
1095
|
+
</InfiniteLoader>
|
|
1096
|
+
)}
|
|
1097
|
+
</div>
|
|
1098
|
+
)}
|
|
1088
1099
|
</div>
|
|
1089
1100
|
)
|
|
1090
1101
|
}
|
|
@@ -1108,6 +1119,7 @@ interface CardsProps {
|
|
|
1108
1119
|
setOptimisticList: () => void
|
|
1109
1120
|
autoUpdateStatusFilter: boolean
|
|
1110
1121
|
search: string | undefined
|
|
1122
|
+
searchClearing: boolean
|
|
1111
1123
|
relationList?: boolean
|
|
1112
1124
|
formList?: boolean
|
|
1113
1125
|
hasBreadcrumbs?: boolean
|
|
@@ -1129,6 +1141,7 @@ export function Cards({
|
|
|
1129
1141
|
setOptimisticList,
|
|
1130
1142
|
autoUpdateStatusFilter,
|
|
1131
1143
|
search,
|
|
1144
|
+
searchClearing,
|
|
1132
1145
|
relationList,
|
|
1133
1146
|
formList,
|
|
1134
1147
|
hasBreadcrumbs,
|
|
@@ -1563,6 +1576,7 @@ export function Cards({
|
|
|
1563
1576
|
backToStart={backToStart}
|
|
1564
1577
|
setOptimisticList={setOptimisticList}
|
|
1565
1578
|
search={search}
|
|
1579
|
+
searchClearing={searchClearing}
|
|
1566
1580
|
/>
|
|
1567
1581
|
))}
|
|
1568
1582
|
</div>
|
package/src/Collection.tsx
CHANGED
|
@@ -90,6 +90,7 @@ import { TooltipProvider } from "./components/ui/tooltip"
|
|
|
90
90
|
import { Thread } from "./components/assistant-ui/thread"
|
|
91
91
|
import { MyRuntimeProvider } from "./providers/RuntimeProvider"
|
|
92
92
|
import { getFilterDisjunctions } from "./utils/getFilterDisjunctions"
|
|
93
|
+
import { getSearchOptions, isServerFullTextSearch } from "./utils/fullTextSearch"
|
|
93
94
|
import { performFullTextSearch } from "./utils/performFullTextSearch"
|
|
94
95
|
import { CSVLink } from "react-csv"
|
|
95
96
|
import { prepareCSVData } from "./utils/prepareCSVData"
|
|
@@ -236,6 +237,12 @@ function Collection({
|
|
|
236
237
|
const [showCalendar, setShowCalendar] = useState(false)
|
|
237
238
|
|
|
238
239
|
const [search, setSearch] = useState("")
|
|
240
|
+
const isServerFullTextSearchActive = isServerFullTextSearch(
|
|
241
|
+
search,
|
|
242
|
+
collection,
|
|
243
|
+
isPreloadCacheEnabled,
|
|
244
|
+
isServerReadOnly,
|
|
245
|
+
)
|
|
239
246
|
const [tab, setTab] = useState<string | undefined>("list")
|
|
240
247
|
const tabRef = useRef<string | undefined>(undefined)
|
|
241
248
|
const prevTabRef = useRef<string | undefined>(undefined)
|
|
@@ -250,6 +257,19 @@ function Collection({
|
|
|
250
257
|
const { filters, setFilters, order, setOrder, getFilterConstraints } = useFilters()
|
|
251
258
|
const { orderByField, orderByDirection } = useMemo(() => getOrderBy(collection, order), [order])
|
|
252
259
|
const searchResults = useRef<{ [key: string | number]: string[] | undefined }>({})
|
|
260
|
+
const [searchClearing, setSearchClearing] = useState(false)
|
|
261
|
+
const searchClearingRef = useRef(false)
|
|
262
|
+
const pendingSearchClearingKeys = useRef<Set<string | number>>(new Set())
|
|
263
|
+
const getSearchClearingKeys = useCallback(
|
|
264
|
+
(currentTab: string | undefined) =>
|
|
265
|
+
currentTab === "cards"
|
|
266
|
+
? (statusValues.current || []).filter(
|
|
267
|
+
(statusValue) =>
|
|
268
|
+
!cardsConfig?.excludeValues?.some((excludedValue) => excludedValue === statusValue),
|
|
269
|
+
)
|
|
270
|
+
: ["default"],
|
|
271
|
+
[cardsConfig],
|
|
272
|
+
)
|
|
253
273
|
const additionalConstraintsRef = useRef(additionalConstraints)
|
|
254
274
|
additionalConstraintsRef.current = additionalConstraints
|
|
255
275
|
const { currentField: currentFieldAll } = useCache()
|
|
@@ -448,6 +468,14 @@ function Collection({
|
|
|
448
468
|
|
|
449
469
|
const loadedKeys = useRef<Set<string | number>>(new Set())
|
|
450
470
|
const keysLength = useRef(0)
|
|
471
|
+
const finishSearchClearingKey = useCallback((key: string | number) => {
|
|
472
|
+
if (!searchClearingRef.current || !pendingSearchClearingKeys.current.has(key)) return
|
|
473
|
+
pendingSearchClearingKeys.current.delete(key)
|
|
474
|
+
if (pendingSearchClearingKeys.current.size === 0) {
|
|
475
|
+
searchClearingRef.current = false
|
|
476
|
+
setSearchClearing(false)
|
|
477
|
+
}
|
|
478
|
+
}, [])
|
|
451
479
|
const getKeysLength = useCallback(() => {
|
|
452
480
|
let length = 1
|
|
453
481
|
if (tab === "cards") {
|
|
@@ -555,18 +583,12 @@ function Collection({
|
|
|
555
583
|
relationList,
|
|
556
584
|
relationParent,
|
|
557
585
|
})
|
|
558
|
-
const searchOptions =
|
|
559
|
-
const exactPhrase = searchOptions?.fuzzy === false && searchOptions?.prefix === false
|
|
586
|
+
const searchOptions = getSearchOptions(collection)
|
|
560
587
|
const batchSize =
|
|
561
588
|
disjunctions === 0
|
|
562
589
|
? Math.min(30, itemsPerPage || 10)
|
|
563
590
|
: Math.min(itemsPerPage || 10, Math.max(1, Math.floor(30 / disjunctions)))
|
|
564
|
-
|
|
565
|
-
if (exactPhrase && !(hasEntityRestrictions.length > 0 || hasEntityParentFilters.length > 0)) {
|
|
566
|
-
hitsPerPage = 500
|
|
567
|
-
} else {
|
|
568
|
-
hitsPerPage = batchSize
|
|
569
|
-
}
|
|
591
|
+
const hitsPerPage = searchOptions?.hitsPerPage || itemsPerPage || 10
|
|
570
592
|
const constraints = getFilterConstraints(latestFilters, false, true) as [string, "==" | "in", unknown][]
|
|
571
593
|
const assigning =
|
|
572
594
|
queryIsAssigning && assignable && relationList && relationCollection && relationParent?.id
|
|
@@ -577,26 +599,7 @@ function Collection({
|
|
|
577
599
|
return
|
|
578
600
|
}
|
|
579
601
|
searchResults.current = { ...searchResults.current, [key]: objectIDs }
|
|
580
|
-
if (
|
|
581
|
-
objectIDs.length > 0 &&
|
|
582
|
-
(!exactPhrase || hasEntityRestrictions.length > 0 || hasEntityParentFilters.length > 0)
|
|
583
|
-
) {
|
|
584
|
-
if (isServerReadOnly) {
|
|
585
|
-
query.queries = query.queries.map((q) => ({
|
|
586
|
-
...q,
|
|
587
|
-
constraints: [...q.constraints, ["id", "in", objectIDs]] as [
|
|
588
|
-
string,
|
|
589
|
-
WhereFilterOp,
|
|
590
|
-
unknown,
|
|
591
|
-
][],
|
|
592
|
-
}))
|
|
593
|
-
} else {
|
|
594
|
-
query.queries = query.queries.map((q) => ({
|
|
595
|
-
...q,
|
|
596
|
-
constraints: [...q.constraints, where("id", "in", objectIDs)] as QueryConstraint[],
|
|
597
|
-
}))
|
|
598
|
-
}
|
|
599
|
-
} else if (objectIDs.length > 0 && exactPhrase) {
|
|
602
|
+
if (objectIDs.length > 0) {
|
|
600
603
|
for (let i = 0; i < objectIDs.length; i += batchSize) {
|
|
601
604
|
multipleQueries.push([where("id", "in", objectIDs.slice(i, i + batchSize))])
|
|
602
605
|
}
|
|
@@ -608,6 +611,7 @@ function Collection({
|
|
|
608
611
|
setCursor({})
|
|
609
612
|
setPages({})
|
|
610
613
|
setCount({})
|
|
614
|
+
finishSearchClearingKey(key)
|
|
611
615
|
return
|
|
612
616
|
}
|
|
613
617
|
}
|
|
@@ -713,6 +717,7 @@ function Collection({
|
|
|
713
717
|
setPages((prev) => ({ ...prev, [key]: newPages || 1 }))
|
|
714
718
|
setCount((prev) => ({ ...prev, [key]: newCount }))
|
|
715
719
|
}
|
|
720
|
+
finishSearchClearingKey(key)
|
|
716
721
|
resolve()
|
|
717
722
|
}
|
|
718
723
|
}
|
|
@@ -772,6 +777,7 @@ function Collection({
|
|
|
772
777
|
console.error(error)
|
|
773
778
|
if (isCurrentData()) {
|
|
774
779
|
releaseRouteLoading()
|
|
780
|
+
finishSearchClearingKey(key)
|
|
775
781
|
}
|
|
776
782
|
resolve()
|
|
777
783
|
if (error instanceof FirestoreError && error.code === "not-found") {
|
|
@@ -793,6 +799,7 @@ function Collection({
|
|
|
793
799
|
} catch (error) {
|
|
794
800
|
if (isCurrentData()) {
|
|
795
801
|
releaseRouteLoading()
|
|
802
|
+
finishSearchClearingKey(key)
|
|
796
803
|
}
|
|
797
804
|
reject(error)
|
|
798
805
|
}
|
|
@@ -1421,8 +1428,11 @@ function Collection({
|
|
|
1421
1428
|
if (isInitialized) {
|
|
1422
1429
|
loadedKeys.current = new Set()
|
|
1423
1430
|
getKeysLength()
|
|
1431
|
+
if (searchClearingRef.current) {
|
|
1432
|
+
pendingSearchClearingKeys.current = new Set(getSearchClearingKeys(tab))
|
|
1433
|
+
}
|
|
1424
1434
|
}
|
|
1425
|
-
}, [tab, isInitialized])
|
|
1435
|
+
}, [tab, isInitialized, getSearchClearingKeys])
|
|
1426
1436
|
|
|
1427
1437
|
const onChangeSearch = useCallback(
|
|
1428
1438
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -1431,9 +1441,15 @@ function Collection({
|
|
|
1431
1441
|
setState(`collection-search-${labels.collection.toLowerCase()}`, "search", event.target.value)
|
|
1432
1442
|
} else {
|
|
1433
1443
|
setState(`collection-search-${labels.collection.toLowerCase()}`, "search", "DELETE_STATE")
|
|
1444
|
+
if (fullTextSearch && !isServerReadOnly && !isPreloadCacheEnabled) {
|
|
1445
|
+
const pendingKeys = getSearchClearingKeys(tabRef.current)
|
|
1446
|
+
pendingSearchClearingKeys.current = new Set(pendingKeys)
|
|
1447
|
+
searchClearingRef.current = pendingKeys.length > 0
|
|
1448
|
+
setSearchClearing(pendingKeys.length > 0)
|
|
1449
|
+
}
|
|
1434
1450
|
}
|
|
1435
1451
|
},
|
|
1436
|
-
[table, recordTitleField, isPreloadCacheEnabled, isServerReadOnly],
|
|
1452
|
+
[table, recordTitleField, fullTextSearch, isPreloadCacheEnabled, isServerReadOnly, getSearchClearingKeys],
|
|
1437
1453
|
)
|
|
1438
1454
|
|
|
1439
1455
|
const excludedFilters = useMemo(() => {
|
|
@@ -2035,6 +2051,9 @@ function Collection({
|
|
|
2035
2051
|
relationList ? "xl:flex-row" : "lg:flex-row",
|
|
2036
2052
|
)}
|
|
2037
2053
|
>
|
|
2054
|
+
{!isInitialized && !relationList && (
|
|
2055
|
+
<div className="hidden lg:block h-9 shrink-0" aria-hidden="true" />
|
|
2056
|
+
)}
|
|
2038
2057
|
{isInitialized && (
|
|
2039
2058
|
<>
|
|
2040
2059
|
{formList && (
|
|
@@ -2287,7 +2306,10 @@ function Collection({
|
|
|
2287
2306
|
size="sm"
|
|
2288
2307
|
variant="outline"
|
|
2289
2308
|
className="h-7 gap-1"
|
|
2290
|
-
disabled={
|
|
2309
|
+
disabled={
|
|
2310
|
+
isRouteLoading.has(location.pathname) ||
|
|
2311
|
+
isServerFullTextSearchActive
|
|
2312
|
+
}
|
|
2291
2313
|
>
|
|
2292
2314
|
<ChevronsUpDown className="h-3.5 w-3.5" />
|
|
2293
2315
|
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">
|
|
@@ -2350,9 +2372,10 @@ function Collection({
|
|
|
2350
2372
|
)
|
|
2351
2373
|
}}
|
|
2352
2374
|
>
|
|
2353
|
-
{order?.field === field.name &&
|
|
2354
|
-
|
|
2355
|
-
|
|
2375
|
+
{order?.field === field.name &&
|
|
2376
|
+
!isServerFullTextSearchActive && (
|
|
2377
|
+
<Check className="absolute h-3.5 w-3.5 mr-1" />
|
|
2378
|
+
)}
|
|
2356
2379
|
<span className="ml-5">{label}</span>
|
|
2357
2380
|
</DropdownMenuItem>
|
|
2358
2381
|
)
|
|
@@ -2711,6 +2734,7 @@ function Collection({
|
|
|
2711
2734
|
backToStartKey={backToStartKey}
|
|
2712
2735
|
setBackToStartKey={setBackToStartKey}
|
|
2713
2736
|
search={search}
|
|
2737
|
+
searchClearing={searchClearing}
|
|
2714
2738
|
defaultSort={defaultSort}
|
|
2715
2739
|
secondarySort={secondarySort}
|
|
2716
2740
|
setOptimisticList={setOptimisticList}
|
|
@@ -2741,6 +2765,7 @@ function Collection({
|
|
|
2741
2765
|
setOptimisticList={setOptimisticList}
|
|
2742
2766
|
autoUpdateStatusFilter={autoUpdateStatusFilter}
|
|
2743
2767
|
search={search}
|
|
2768
|
+
searchClearing={searchClearing}
|
|
2744
2769
|
relationList={!!relationList}
|
|
2745
2770
|
formList={!!formList}
|
|
2746
2771
|
hasBreadcrumbs={hasBreadcrumbs}
|
|
@@ -2759,6 +2784,7 @@ function Collection({
|
|
|
2759
2784
|
getData={getData}
|
|
2760
2785
|
unsubscribe={unsubscribe}
|
|
2761
2786
|
search={search}
|
|
2787
|
+
searchClearing={searchClearing}
|
|
2762
2788
|
backToStartKey={backToStartKey}
|
|
2763
2789
|
relationList={relationList}
|
|
2764
2790
|
relationCollection={relationCollection}
|
package/src/Images.tsx
CHANGED
|
@@ -36,6 +36,7 @@ import { preloadCacheEnabled } from "./utils/preloadCacheEnabled"
|
|
|
36
36
|
import cloneDeep from "lodash/cloneDeep.js"
|
|
37
37
|
import isEqual from "lodash/isEqual.js"
|
|
38
38
|
import { localFullTextSearch } from "./utils/localFullTextSearch"
|
|
39
|
+
import { isServerFullTextSearch } from "./utils/fullTextSearch"
|
|
39
40
|
import { Helmet } from "react-helmet"
|
|
40
41
|
import { useConnection } from "./providers/ConnectionProvider"
|
|
41
42
|
import { getSafeUrl } from "./utils/isSafeUrl"
|
|
@@ -433,6 +434,7 @@ interface ImagesProps {
|
|
|
433
434
|
isAssigning?: boolean
|
|
434
435
|
assignable?: Assignable
|
|
435
436
|
hasBreadcrumbs?: boolean
|
|
437
|
+
searchClearing?: boolean
|
|
436
438
|
}
|
|
437
439
|
|
|
438
440
|
export const Images = memo(
|
|
@@ -454,6 +456,7 @@ export const Images = memo(
|
|
|
454
456
|
isAssigning,
|
|
455
457
|
assignable,
|
|
456
458
|
hasBreadcrumbs,
|
|
459
|
+
searchClearing,
|
|
457
460
|
}: ImagesProps) => {
|
|
458
461
|
const { labels, recordTitleField, fullTextSearch } = collection
|
|
459
462
|
const customization = getCollectionConfigModule(labels.collection)
|
|
@@ -750,7 +753,6 @@ export const Images = memo(
|
|
|
750
753
|
|
|
751
754
|
const groupedRecords = useMemo(() => {
|
|
752
755
|
if (!list) return []
|
|
753
|
-
if (typeof orderByField !== "string") return []
|
|
754
756
|
const removeEmptyRecords: StokerRecord[] = []
|
|
755
757
|
let latestFiltered = latestList?.filter((record) => filterRecord(record)) || []
|
|
756
758
|
let removedFiltered = removedList
|
|
@@ -772,6 +774,15 @@ export const Images = memo(
|
|
|
772
774
|
}
|
|
773
775
|
})
|
|
774
776
|
const dedupedRecords = Array.from(new Map(removeEmptyRecords.map((record) => [record.id, record])).values())
|
|
777
|
+
if (isServerFullTextSearch(search, collection, isPreloadCacheEnabled, isServerReadOnly)) {
|
|
778
|
+
const groups: StokerRecord[][] = []
|
|
779
|
+
if (!columns) return []
|
|
780
|
+
for (let i = 0; i < dedupedRecords.length; i += columns) {
|
|
781
|
+
groups.push(dedupedRecords.slice(i, i + columns))
|
|
782
|
+
}
|
|
783
|
+
return groups
|
|
784
|
+
}
|
|
785
|
+
if (typeof orderByField !== "string") return []
|
|
775
786
|
const groups: StokerRecord[][] = []
|
|
776
787
|
let sortedList = sortList(collection, dedupedRecords, orderByField, orderByDirection)
|
|
777
788
|
if (search && (isPreloadCacheEnabled || isServerReadOnly)) {
|
|
@@ -783,7 +794,17 @@ export const Images = memo(
|
|
|
783
794
|
groups.push(sortedList.slice(i, i + columns))
|
|
784
795
|
}
|
|
785
796
|
return groups
|
|
786
|
-
}, [
|
|
797
|
+
}, [
|
|
798
|
+
list,
|
|
799
|
+
removedList,
|
|
800
|
+
columns,
|
|
801
|
+
orderByField,
|
|
802
|
+
orderByDirection,
|
|
803
|
+
search,
|
|
804
|
+
fullTextSearch,
|
|
805
|
+
isPreloadCacheEnabled,
|
|
806
|
+
isServerReadOnly,
|
|
807
|
+
])
|
|
787
808
|
|
|
788
809
|
const [isFirstLoad, setIsFirstLoad] = useState(true)
|
|
789
810
|
const [prevIds, setPrevIds] = useState<Set<string>>(new Set())
|
|
@@ -932,6 +953,8 @@ export const Images = memo(
|
|
|
932
953
|
)
|
|
933
954
|
}
|
|
934
955
|
|
|
956
|
+
if (searchClearing) return null
|
|
957
|
+
|
|
935
958
|
if (isPreloadCacheEnabled || isServerReadOnly) {
|
|
936
959
|
return (
|
|
937
960
|
<div
|
package/src/List.tsx
CHANGED
|
@@ -83,6 +83,7 @@ import { getOrderBy } from "./utils/getOrderBy"
|
|
|
83
83
|
import { useLocation } from "react-router"
|
|
84
84
|
import { preloadCacheEnabled } from "./utils/preloadCacheEnabled"
|
|
85
85
|
import { localFullTextSearch } from "./utils/localFullTextSearch"
|
|
86
|
+
import { isExactPhraseSearch, isServerFullTextSearch } from "./utils/fullTextSearch"
|
|
86
87
|
import {
|
|
87
88
|
ChartConfig,
|
|
88
89
|
ChartContainer,
|
|
@@ -216,6 +217,7 @@ interface ListProps {
|
|
|
216
217
|
backToStartKey: number
|
|
217
218
|
setBackToStartKey: React.Dispatch<React.SetStateAction<number>>
|
|
218
219
|
search: string | undefined
|
|
220
|
+
searchClearing: boolean
|
|
219
221
|
defaultSort:
|
|
220
222
|
| {
|
|
221
223
|
field: string
|
|
@@ -252,6 +254,7 @@ export function List({
|
|
|
252
254
|
backToStartKey,
|
|
253
255
|
setBackToStartKey,
|
|
254
256
|
search,
|
|
257
|
+
searchClearing,
|
|
255
258
|
defaultSort,
|
|
256
259
|
secondarySort,
|
|
257
260
|
setOptimisticList,
|
|
@@ -677,7 +680,12 @@ export function List({
|
|
|
677
680
|
return allColumns
|
|
678
681
|
}, [fields, isPreloadCacheEnabled, isServerReadOnly, recordTitleField, connectionStatus])
|
|
679
682
|
|
|
680
|
-
const
|
|
683
|
+
const isServerFullTextSearchActive = isServerFullTextSearch(
|
|
684
|
+
search,
|
|
685
|
+
collection,
|
|
686
|
+
isPreloadCacheEnabled,
|
|
687
|
+
isServerReadOnly,
|
|
688
|
+
)
|
|
681
689
|
|
|
682
690
|
const isSearchRelevanceOrder = !!(search && isPreloadCacheEnabled)
|
|
683
691
|
|
|
@@ -696,7 +704,7 @@ export function List({
|
|
|
696
704
|
return list || []
|
|
697
705
|
}, [isPreloadCacheEnabled, isServerReadOnly, list, search])
|
|
698
706
|
|
|
699
|
-
const tablePageSize =
|
|
707
|
+
const tablePageSize = isServerFullTextSearchActive ? Math.max(searchList.length, 1) : pageSize
|
|
700
708
|
|
|
701
709
|
const selectedRecords = useMemo(() => {
|
|
702
710
|
const selectedIds = Object.keys(rowSelection)
|
|
@@ -706,11 +714,8 @@ export function List({
|
|
|
706
714
|
.filter((record): record is StokerRecord => record !== undefined)
|
|
707
715
|
}, [rowSelection, searchList])
|
|
708
716
|
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
prefix: false,
|
|
712
|
-
}
|
|
713
|
-
const exactPhrase = searchOptions.fuzzy === false && searchOptions.prefix === false
|
|
717
|
+
const exactPhrase = isExactPhraseSearch(collection)
|
|
718
|
+
const disableSortingForSearch = (isSearchRelevanceOrder && !exactPhrase) || isServerFullTextSearchActive
|
|
714
719
|
|
|
715
720
|
const table = useReactTable<StokerRecord>({
|
|
716
721
|
data: searchList,
|
|
@@ -719,7 +724,7 @@ export function List({
|
|
|
719
724
|
getCoreRowModel: getCoreRowModel(),
|
|
720
725
|
getPaginationRowModel: getPaginationRowModel(),
|
|
721
726
|
onSortingChange: (sortingUpdater) => {
|
|
722
|
-
if (
|
|
727
|
+
if (disableSortingForSearch) return
|
|
723
728
|
if (typeof sortingUpdater === "function") {
|
|
724
729
|
const newSorting = sortingUpdater(sorting)
|
|
725
730
|
const field = getField(fields, newSorting[0].id)
|
|
@@ -759,14 +764,14 @@ export function List({
|
|
|
759
764
|
onRowSelectionChange: setRowSelection,
|
|
760
765
|
pageCount,
|
|
761
766
|
autoResetPageIndex: false,
|
|
762
|
-
enableSorting: !
|
|
767
|
+
enableSorting: !disableSortingForSearch,
|
|
763
768
|
state: {
|
|
764
|
-
sorting:
|
|
769
|
+
sorting: disableSortingForSearch ? [] : sorting,
|
|
765
770
|
columnFilters,
|
|
766
771
|
rowSelection,
|
|
767
772
|
pagination: {
|
|
768
773
|
pageSize: tablePageSize,
|
|
769
|
-
pageIndex:
|
|
774
|
+
pageIndex: isServerFullTextSearchActive ? 0 : pageIndex,
|
|
770
775
|
},
|
|
771
776
|
},
|
|
772
777
|
onPaginationChange: (updater) => {
|
|
@@ -876,7 +881,7 @@ export function List({
|
|
|
876
881
|
search ? 750 : 250,
|
|
877
882
|
)
|
|
878
883
|
|
|
879
|
-
if (isInitialized && (isPreloadCacheEnabled || isServerReadOnly ||
|
|
884
|
+
if (isInitialized && (isPreloadCacheEnabled || isServerReadOnly || isServerFullTextSearchActive)) {
|
|
880
885
|
setPageIndex(0)
|
|
881
886
|
setState(`collection-page-number-${labels.collection.toLowerCase()}`, "page", 1)
|
|
882
887
|
}
|
|
@@ -1078,7 +1083,7 @@ export function List({
|
|
|
1078
1083
|
}, [table, list, pageSize, isLoading, cursor, pageNumber, pageCount, constraints, orderByField, orderByDirection])
|
|
1079
1084
|
|
|
1080
1085
|
const canGetNextPage = useCallback(() => {
|
|
1081
|
-
if (
|
|
1086
|
+
if (isServerFullTextSearchActive) {
|
|
1082
1087
|
return false
|
|
1083
1088
|
}
|
|
1084
1089
|
if (isPreloadCacheEnabled || isServerReadOnly) {
|
|
@@ -1086,7 +1091,7 @@ export function List({
|
|
|
1086
1091
|
} else {
|
|
1087
1092
|
return !isLoadingDebounced && pageCount && pageNumber < pageCount && list?.length === pageSize
|
|
1088
1093
|
}
|
|
1089
|
-
}, [table, isLoadingDebounced, pageNumber, pageCount, list, pageSize,
|
|
1094
|
+
}, [table, isLoadingDebounced, pageNumber, pageCount, list, pageSize, isServerFullTextSearchActive])
|
|
1090
1095
|
|
|
1091
1096
|
const prevPage = useCallback(() => {
|
|
1092
1097
|
if (isLoading) return
|
|
@@ -1172,7 +1177,7 @@ export function List({
|
|
|
1172
1177
|
}, [table, list, pageSize, isLoading, cursor, prevCursor, pageNumber, constraints, orderByField, orderByDirection])
|
|
1173
1178
|
|
|
1174
1179
|
const canGetPrevPage = useCallback(() => {
|
|
1175
|
-
if (
|
|
1180
|
+
if (isServerFullTextSearchActive) {
|
|
1176
1181
|
return false
|
|
1177
1182
|
}
|
|
1178
1183
|
if (isPreloadCacheEnabled || isServerReadOnly) {
|
|
@@ -1180,7 +1185,7 @@ export function List({
|
|
|
1180
1185
|
} else {
|
|
1181
1186
|
return !isLoadingDebounced && pageNumber > 1
|
|
1182
1187
|
}
|
|
1183
|
-
}, [table, isLoadingDebounced, pageNumber,
|
|
1188
|
+
}, [table, isLoadingDebounced, pageNumber, isServerFullTextSearchActive])
|
|
1184
1189
|
|
|
1185
1190
|
const onChangePageNumber = useCallback(
|
|
1186
1191
|
(event: React.ChangeEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>) => {
|
|
@@ -1826,7 +1831,7 @@ export function List({
|
|
|
1826
1831
|
)}
|
|
1827
1832
|
</div>
|
|
1828
1833
|
)}
|
|
1829
|
-
{pagesLoaded && list && (
|
|
1834
|
+
{!searchClearing && pagesLoaded && list && (
|
|
1830
1835
|
<Table className="list-table">
|
|
1831
1836
|
<TableHeader>
|
|
1832
1837
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
@@ -2033,12 +2038,12 @@ export function List({
|
|
|
2033
2038
|
</ScrollArea>
|
|
2034
2039
|
</Card>
|
|
2035
2040
|
<div className="flex items-center justify-end space-x-2 py-4 print:hidden">
|
|
2036
|
-
{
|
|
2041
|
+
{isServerFullTextSearchActive && searchList.length > 0 && (
|
|
2037
2042
|
<Badge variant="secondary" className="hidden sm:block">
|
|
2038
2043
|
{searchList.length} {searchList.length === 1 ? "result" : "results"}
|
|
2039
2044
|
</Badge>
|
|
2040
2045
|
)}
|
|
2041
|
-
{pagesLoaded && !
|
|
2046
|
+
{pagesLoaded && !isServerFullTextSearchActive && (
|
|
2042
2047
|
<Badge variant="secondary" className="hidden sm:block">
|
|
2043
2048
|
Page{" "}
|
|
2044
2049
|
{isPreloadCacheEnabled || isServerReadOnly
|
|
@@ -2065,7 +2070,7 @@ export function List({
|
|
|
2065
2070
|
/>
|
|
2066
2071
|
</div>
|
|
2067
2072
|
)}
|
|
2068
|
-
{!
|
|
2073
|
+
{!isServerFullTextSearchActive && !isPreloadCacheEnabled && !isServerReadOnly && (
|
|
2069
2074
|
<Button
|
|
2070
2075
|
type="button"
|
|
2071
2076
|
variant="outline"
|
|
@@ -2078,7 +2083,7 @@ export function List({
|
|
|
2078
2083
|
Back to start
|
|
2079
2084
|
</Button>
|
|
2080
2085
|
)}
|
|
2081
|
-
{!
|
|
2086
|
+
{!isServerFullTextSearchActive &&
|
|
2082
2087
|
!(
|
|
2083
2088
|
!isPreloadCacheEnabled &&
|
|
2084
2089
|
!isServerReadOnly &&
|
|
@@ -2095,7 +2100,7 @@ export function List({
|
|
|
2095
2100
|
Previous
|
|
2096
2101
|
</Button>
|
|
2097
2102
|
)}
|
|
2098
|
-
{!
|
|
2103
|
+
{!isServerFullTextSearchActive && (
|
|
2099
2104
|
<Button type="button" variant="outline" size="sm" onClick={nextPage} disabled={!canGetNextPage()}>
|
|
2100
2105
|
Next
|
|
2101
2106
|
</Button>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CollectionSchema } from "@stoker-platform/types"
|
|
2
|
+
import { getCollectionConfigModule } from "@stoker-platform/web-client"
|
|
3
|
+
|
|
4
|
+
export const getSearchOptions = (collection: CollectionSchema) => {
|
|
5
|
+
const customization = getCollectionConfigModule(collection.labels.collection)
|
|
6
|
+
return (
|
|
7
|
+
customization.admin?.searchOptions || {
|
|
8
|
+
fuzzy: false,
|
|
9
|
+
prefix: false,
|
|
10
|
+
}
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const isExactPhraseSearch = (collection: CollectionSchema) => {
|
|
15
|
+
const searchOptions = getSearchOptions(collection)
|
|
16
|
+
return searchOptions.fuzzy === false && searchOptions.prefix === false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const isServerFullTextSearch = (
|
|
20
|
+
search: string | undefined,
|
|
21
|
+
collection: CollectionSchema,
|
|
22
|
+
isPreloadCacheEnabled: boolean | undefined,
|
|
23
|
+
isServerReadOnly: boolean | undefined,
|
|
24
|
+
) => !!(search && collection.fullTextSearch && !isPreloadCacheEnabled && !isServerReadOnly)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CollectionSchema, StokerRecord } from "@stoker-platform/types"
|
|
2
|
-
import { getCollectionConfigModule } from "@stoker-platform/web-client"
|
|
3
2
|
import MiniSearch, { Options } from "minisearch"
|
|
3
|
+
import { getSearchOptions, isExactPhraseSearch } from "./fullTextSearch"
|
|
4
4
|
|
|
5
5
|
const flattenToSearchText = (value: unknown): string => {
|
|
6
6
|
if (value == null) return ""
|
|
@@ -42,18 +42,14 @@ export const localFullTextSearch = (
|
|
|
42
42
|
) => {
|
|
43
43
|
const { recordTitleField, fullTextSearch } = collection
|
|
44
44
|
const fields = fullTextSearch || [recordTitleField]
|
|
45
|
-
const
|
|
46
|
-
const searchOptions = customization.admin?.searchOptions || {
|
|
47
|
-
fuzzy: false,
|
|
48
|
-
prefix: false,
|
|
49
|
-
}
|
|
45
|
+
const searchOptions = getSearchOptions(collection)
|
|
50
46
|
|
|
51
47
|
if (filter) {
|
|
52
48
|
list = list.filter((record) => filter(record))
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
const phrase = query.trim().toLowerCase()
|
|
56
|
-
const exactPhrase =
|
|
52
|
+
const exactPhrase = isExactPhraseSearch(collection)
|
|
57
53
|
|
|
58
54
|
if (exactPhrase) {
|
|
59
55
|
if (!phrase) return []
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { flushSync } from "react-dom"
|
|
2
2
|
|
|
3
|
+
const isWebKit =
|
|
4
|
+
typeof navigator !== "undefined" &&
|
|
5
|
+
/AppleWebKit/.test(navigator.userAgent) &&
|
|
6
|
+
!/Chrom(e|ium)|Edg|OPR|Android/.test(navigator.userAgent)
|
|
7
|
+
|
|
3
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
9
|
export const runViewTransition = (callback: () => any) => {
|
|
5
|
-
if (document.startViewTransition && document.visibilityState === "visible") {
|
|
10
|
+
if (!isWebKit && document.startViewTransition && document.visibilityState === "visible") {
|
|
6
11
|
try {
|
|
7
12
|
const transition = document.startViewTransition(() => {
|
|
8
13
|
flushSync(() => {
|