@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 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.211",
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.82",
55
- "@stoker-platform/utils": "0.5.73",
56
- "@stoker-platform/web-client": "0.5.86",
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
- <div className={cn(className, "rounded-lg", "space-y-4")}>
1048
- {isPreloadCacheEnabled || isServerReadOnly ? (
1049
- <FixedSizeList
1050
- height={height}
1051
- width="100%"
1052
- itemSize={itemSize}
1053
- itemCount={records.length}
1054
- itemKey={itemKey}
1055
- overscanCount={5}
1056
- itemData={itemData}
1057
- >
1058
- {renderRow}
1059
- </FixedSizeList>
1060
- ) : (
1061
- // eslint-disable-next-line security/detect-object-injection
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
- itemCount={100000}
1066
- loadMoreItems={() => loadMoreItems(statusValue)}
1067
- minimumBatchSize={itemsPerPage || 10}
1068
- threshold={itemsPerPage || 40}
1069
- >
1070
- {({ onItemsRendered, ref }) => (
1071
- <FixedSizeList
1072
- height={height}
1073
- width="100%"
1074
- itemSize={itemSize}
1075
- itemCount={records.length}
1076
- overscanCount={10}
1077
- itemKey={itemKey}
1078
- ref={ref}
1079
- onItemsRendered={onItemsRendered}
1080
- itemData={itemData}
1081
- >
1082
- {renderRow}
1083
- </FixedSizeList>
1084
- )}
1085
- </InfiniteLoader>
1086
- )}
1087
- </div>
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>
@@ -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 = customization.admin?.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
- let hitsPerPage = 0
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={isRouteLoading.has(location.pathname)}
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
- <Check className="absolute h-3.5 w-3.5 mr-1" />
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
- }, [list, removedList, columns, orderByField, orderByDirection, search])
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 isServerFullTextSearch = !!(search && fullTextSearch && !isPreloadCacheEnabled && !isServerReadOnly)
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 = isServerFullTextSearch ? Math.max(searchList.length, 1) : pageSize
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 searchOptions = tryFunction(customization.admin?.searchOptions) || {
710
- fuzzy: false,
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 (isSearchRelevanceOrder && !exactPhrase) return
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: !isSearchRelevanceOrder || exactPhrase,
767
+ enableSorting: !disableSortingForSearch,
763
768
  state: {
764
- sorting: isSearchRelevanceOrder && !exactPhrase ? [] : sorting,
769
+ sorting: disableSortingForSearch ? [] : sorting,
765
770
  columnFilters,
766
771
  rowSelection,
767
772
  pagination: {
768
773
  pageSize: tablePageSize,
769
- pageIndex: isServerFullTextSearch ? 0 : 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 || isServerFullTextSearch)) {
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 (isServerFullTextSearch) {
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, isServerFullTextSearch])
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 (isServerFullTextSearch) {
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, isServerFullTextSearch])
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
- {isServerFullTextSearch && searchList.length > 0 && (
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 && !isServerFullTextSearch && (
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
- {!isServerFullTextSearch && !isPreloadCacheEnabled && !isServerReadOnly && (
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
- {!isServerFullTextSearch &&
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
- {!isServerFullTextSearch && (
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 customization = getCollectionConfigModule(collection.labels.collection)
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 = searchOptions.fuzzy === false && searchOptions.prefix === false
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(() => {