@stoker-platform/web-app 0.5.200 → 0.5.203

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,28 @@
1
1
  # @stoker-platform/web-app
2
2
 
3
+ ## 0.5.203
4
+
5
+ ### Patch Changes
6
+
7
+ - feat: add filter condition method
8
+ - @stoker-platform/node-client@0.5.79
9
+ - @stoker-platform/utils@0.5.70
10
+ - @stoker-platform/web-client@0.5.83
11
+
12
+ ## 0.5.202
13
+
14
+ ### Patch Changes
15
+
16
+ - fix: fix incorrect multipleQueries option
17
+
18
+ ## 0.5.201
19
+
20
+ ### Patch Changes
21
+
22
+ - feat: improve server side full text search
23
+ - Updated dependencies
24
+ - @stoker-platform/web-client@0.5.82
25
+
3
26
  ## 0.5.200
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.200",
3
+ "version": "0.5.203",
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.78",
55
- "@stoker-platform/utils": "0.5.69",
56
- "@stoker-platform/web-client": "0.5.81",
54
+ "@stoker-platform/node-client": "0.5.79",
55
+ "@stoker-platform/utils": "0.5.70",
56
+ "@stoker-platform/web-client": "0.5.83",
57
57
  "@tanstack/react-table": "^8.21.3",
58
58
  "@types/react": "18.3.13",
59
59
  "@types/react-dom": "18.3.1",
@@ -492,6 +492,8 @@ function Collection({
492
492
  setIsRouteLoading("+", location.pathname)
493
493
  }
494
494
 
495
+ const multipleQueries: QueryConstraint[][] = []
496
+
495
497
  if (
496
498
  fullTextSearch &&
497
499
  !isPreloadCacheEnabled &&
@@ -500,11 +502,6 @@ function Collection({
500
502
  tab !== "map" &&
501
503
  tab !== "calendar"
502
504
  ) {
503
- const disjunctions = getFilterDisjunctions(collection)
504
- const hitsPerPage =
505
- disjunctions === 0
506
- ? Math.min(30, itemsPerPage || 10)
507
- : Math.min(itemsPerPage || 10, Math.max(1, Math.floor(30 / disjunctions)))
508
505
  let latestFilters = filters
509
506
  if (tab === "cards" && prevTabRef.current !== "cards") {
510
507
  latestFilters = [...filters]
@@ -517,10 +514,32 @@ function Collection({
517
514
  }
518
515
  }
519
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 = 500
533
+ } else {
534
+ hitsPerPage = batchSize
535
+ }
520
536
  const constraints = getFilterConstraints(latestFilters, false, true) as [string, "==" | "in", unknown][]
521
537
  const objectIDs = await performFullTextSearch(collection, search, hitsPerPage, constraints)
522
538
  searchResults.current = { ...searchResults.current, [key]: objectIDs }
523
- if (objectIDs.length > 0) {
539
+ if (
540
+ objectIDs.length > 0 &&
541
+ (!exactPhrase || hasEntityRestrictions.length > 0 || hasEntityParentFilters.length > 0)
542
+ ) {
524
543
  if (isServerReadOnly) {
525
544
  query.queries = query.queries.map((q) => ({
526
545
  ...q,
@@ -536,6 +555,10 @@ function Collection({
536
555
  constraints: [...q.constraints, where("id", "in", objectIDs)] as QueryConstraint[],
537
556
  }))
538
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
+ }
539
562
  } else if (search) {
540
563
  setServerList((prev) => ({ ...prev, [key]: [] }))
541
564
  setOptimisticList([], key)
@@ -639,6 +662,27 @@ function Collection({
639
662
  }
640
663
  }
641
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
+
642
686
  // TODO: subcollection support
643
687
  const result = await subscribeMany(
644
688
  [labels.collection],
@@ -663,24 +707,7 @@ function Collection({
663
707
  window.location.reload()
664
708
  }
665
709
  },
666
- {
667
- ...currentQuery.options,
668
- constraints: combineQueryConstraints([
669
- ...(currentQuery.constraints as QueryConstraint[]),
670
- ...(additionalConstraintsRef.current?.map((constraint) =>
671
- where(constraint[0], constraint[1] as WhereFilterOp, constraint[2]),
672
- ) || []),
673
- ]),
674
- tempCache:
675
- isPreloadCacheEnabled && relationList?.loadAll
676
- ? {
677
- label: `${labels.collection}-${relationList?.field}`,
678
- constraints: [
679
- [`${relationList?.field}_Single.id`, "==", relationParent?.id],
680
- ],
681
- }
682
- : undefined,
683
- } as SubscribeManyOptions,
710
+ subscribeOptions,
684
711
  )
685
712
  const { unsubscribe: newUnsubscribe, count: newCount, pages: newPages } = result
686
713
  promiseLoaded = true
@@ -2258,6 +2285,8 @@ function Collection({
2258
2285
  collection={collection}
2259
2286
  excluded={excludedFilters}
2260
2287
  relationList={relationList}
2288
+ relationCollection={relationCollection}
2289
+ relationParent={relationParent}
2261
2290
  assignable={assignable}
2262
2291
  isAssigning={displayIsAssigning}
2263
2292
  />
package/src/Filters.tsx CHANGED
@@ -54,11 +54,21 @@ interface FiltersProps {
54
54
  collection: CollectionSchema
55
55
  excluded: string[]
56
56
  relationList?: RelationList
57
+ relationCollection?: CollectionSchema
58
+ relationParent?: StokerRecord
57
59
  assignable?: Assignable
58
60
  isAssigning?: boolean
59
61
  }
60
62
 
61
- export function Filters({ collection, excluded, relationList, assignable, isAssigning }: FiltersProps) {
63
+ export function Filters({
64
+ collection,
65
+ excluded,
66
+ relationList,
67
+ relationCollection,
68
+ relationParent,
69
+ assignable,
70
+ isAssigning,
71
+ }: FiltersProps) {
62
72
  const { labels, fields } = collection
63
73
  const location = useLocation()
64
74
  const schema = getSchema()
@@ -446,6 +456,12 @@ export function Filters({ collection, excluded, relationList, assignable, isAssi
446
456
  return (
447
457
  <div className="flex flex-col gap-6">
448
458
  {filters.map((filter: Filter) => {
459
+ if (
460
+ "condition" in filter &&
461
+ filter.condition &&
462
+ filter.condition(relationCollection, relationParent, isAssigning) === false
463
+ )
464
+ return null
449
465
  if (filter.type === "relation" && !hasRelationFilterAccess(filter)) return
450
466
  if (filter.type === "status" || filter.type === "range") return
451
467
  const field = getField(fields, filter.field)
@@ -474,7 +490,7 @@ export function Filters({ collection, excluded, relationList, assignable, isAssi
474
490
  <Label htmlFor={title}>{title}:</Label>
475
491
  <RadioGroup defaultValue="no_selection" className="mt-2">
476
492
  {values.map((value: string) => {
477
- if (filter.condition && filter.condition(value) === false) return null
493
+ if (filter.filterValues && filter.filterValues(value) === false) return null
478
494
  return (
479
495
  <div key={value} className="flex items-center space-x-2">
480
496
  <RadioGroupItem
@@ -525,7 +541,7 @@ export function Filters({ collection, excluded, relationList, assignable, isAssi
525
541
  <Label htmlFor={title}>{title}:</Label>
526
542
  <div className="mt-2 flex flex-col gap-2">
527
543
  {values.map((value: string) => {
528
- if (filter.condition && filter.condition(value) === false) return null
544
+ if (filter.filterValues && filter.filterValues(value) === false) return null
529
545
  return (
530
546
  <Button
531
547
  key={value}
@@ -600,7 +616,7 @@ export function Filters({ collection, excluded, relationList, assignable, isAssi
600
616
  <SelectContent>
601
617
  <SelectItem value="no_selection">----</SelectItem>
602
618
  {values.map((value: string) => {
603
- if (filter.condition && filter.condition(value) === false) return
619
+ if (filter.filterValues && filter.filterValues(value) === false) return
604
620
  return (
605
621
  <SelectItem key={value} value={value}>
606
622
  {value}
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 = typeof updater === "function" ? updater({ pageIndex, pageSize }) : updater
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
- {pagesLoaded && (
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
- !isPreloadCacheEnabled &&
2064
- !isServerReadOnly &&
2065
- typeof sortingField?.sorting === "object" &&
2066
- sortingField.sorting.direction
2067
- ) && (
2068
- <Button type="button" variant="outline" size="sm" onClick={prevPage} disabled={!canGetPrevPage()}>
2069
- Previous
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
  )
@@ -135,6 +135,9 @@ export const FiltersProvider: React.FC<FiltersProviderProps> = ({
135
135
  }
136
136
  if (filter.type === "select") {
137
137
  const field = getField(fields, filter.field)
138
+ const includeValueInFilters = assignable?.includeValueInFilters?.find(
139
+ (include) => include.field === filter.field && include.values.includes(filter.value),
140
+ )
138
141
  if (field.type === "Boolean") {
139
142
  const fieldCustomization = getFieldCustomization(field, customization)
140
143
  const label = tryFunction(fieldCustomization.admin?.label) || field.name
@@ -145,13 +148,21 @@ export const FiltersProvider: React.FC<FiltersProviderProps> = ({
145
148
  } else if (field.type === "Number") {
146
149
  constraints.push(where(filter.field, "==", Number(filter.value)))
147
150
  } else if (includesAssigned(relationList, relationParent, assignable, isAssigning, filter)) {
151
+ const filterValues = [filter.value]
152
+ if (includeValueInFilters) {
153
+ filterValues.push(includeValueInFilters.includeValue as string | number)
154
+ }
148
155
  constraints.push(
149
156
  or(
150
- where(filter.field, "==", filter.value),
157
+ where(filter.field, "in", filterValues),
151
158
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
152
159
  where(`${relationList!.field}_Array`, "array-contains", relationParent!.id),
153
160
  ) as unknown as QueryConstraint,
154
161
  )
162
+ } else if (isAssigning && includeValueInFilters) {
163
+ constraints.push(
164
+ where(filter.field, "in", [filter.value, includeValueInFilters.includeValue]),
165
+ )
155
166
  } else {
156
167
  constraints.push(where(filter.field, "==", filter.value))
157
168
  }
@@ -312,6 +323,9 @@ export const FiltersProvider: React.FC<FiltersProviderProps> = ({
312
323
  }
313
324
  if (filter.type === "select") {
314
325
  const field = getField(fields, filter.field)
326
+ const includeValueInFilters = assignable?.includeValueInFilters?.find(
327
+ (include) => include.field === filter.field && include.values.includes(filter.value),
328
+ )
315
329
  if (field.type === "Boolean") {
316
330
  const fieldCustomization = getFieldCustomization(field, customization)
317
331
  const label = tryFunction(fieldCustomization.admin?.label) || field.name
@@ -327,6 +341,8 @@ export const FiltersProvider: React.FC<FiltersProviderProps> = ({
327
341
  (record[filter.field] === filter.value ||
328
342
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
329
343
  record[`${relationList!.field}_Array`]?.includes(relationParent!.id))
344
+ } else if (isAssigning && includeValueInFilters) {
345
+ show = show && [filter.value, includeValueInFilters.includeValue].includes(record[filter.field])
330
346
  } else {
331
347
  show = show && record[filter.field] === filter.value
332
348
  }
@@ -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 const getFilterDisjunctions = (collection: CollectionSchema) => {
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
  }