@tellescope/react-components 1.183.0 → 1.185.0

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.
Files changed (43) hide show
  1. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  2. package/lib/cjs/Forms/hooks.js +6 -5
  3. package/lib/cjs/Forms/hooks.js.map +1 -1
  4. package/lib/cjs/inputs_shared.d.ts +43 -0
  5. package/lib/cjs/inputs_shared.d.ts.map +1 -1
  6. package/lib/cjs/inputs_shared.js +433 -2
  7. package/lib/cjs/inputs_shared.js.map +1 -1
  8. package/lib/cjs/layout.d.ts.map +1 -1
  9. package/lib/cjs/layout.js +4 -2
  10. package/lib/cjs/layout.js.map +1 -1
  11. package/lib/cjs/state.d.ts +9 -2
  12. package/lib/cjs/state.d.ts.map +1 -1
  13. package/lib/cjs/state.js +117 -68
  14. package/lib/cjs/state.js.map +1 -1
  15. package/lib/cjs/table.d.ts +8 -3
  16. package/lib/cjs/table.d.ts.map +1 -1
  17. package/lib/cjs/table.js +17 -13
  18. package/lib/cjs/table.js.map +1 -1
  19. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  20. package/lib/esm/Forms/hooks.js +6 -5
  21. package/lib/esm/Forms/hooks.js.map +1 -1
  22. package/lib/esm/inputs_shared.d.ts +43 -0
  23. package/lib/esm/inputs_shared.d.ts.map +1 -1
  24. package/lib/esm/inputs_shared.js +427 -3
  25. package/lib/esm/inputs_shared.js.map +1 -1
  26. package/lib/esm/layout.d.ts.map +1 -1
  27. package/lib/esm/layout.js +4 -2
  28. package/lib/esm/layout.js.map +1 -1
  29. package/lib/esm/state.d.ts +9 -2
  30. package/lib/esm/state.d.ts.map +1 -1
  31. package/lib/esm/state.js +117 -68
  32. package/lib/esm/state.js.map +1 -1
  33. package/lib/esm/table.d.ts +8 -3
  34. package/lib/esm/table.d.ts.map +1 -1
  35. package/lib/esm/table.js +17 -13
  36. package/lib/esm/table.js.map +1 -1
  37. package/lib/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +9 -9
  39. package/src/Forms/hooks.tsx +3 -2
  40. package/src/inputs_shared.tsx +444 -5
  41. package/src/layout.tsx +10 -1
  42. package/src/state.tsx +89 -51
  43. package/src/table.tsx +32 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/react-components",
3
- "version": "1.183.0",
3
+ "version": "1.185.0",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -47,13 +47,13 @@
47
47
  "@reduxjs/toolkit": "^1.6.2",
48
48
  "@stripe/react-stripe-js": "^2.9.0",
49
49
  "@stripe/stripe-js": "^1.52.1",
50
- "@tellescope/constants": "^1.183.0",
51
- "@tellescope/sdk": "^1.183.0",
52
- "@tellescope/types-client": "^1.183.0",
53
- "@tellescope/types-models": "^1.183.0",
54
- "@tellescope/types-utilities": "^1.183.0",
55
- "@tellescope/utilities": "^1.183.0",
56
- "@tellescope/validation": "^1.183.0",
50
+ "@tellescope/constants": "^1.185.0",
51
+ "@tellescope/sdk": "^1.185.0",
52
+ "@tellescope/types-client": "^1.185.0",
53
+ "@tellescope/types-models": "^1.185.0",
54
+ "@tellescope/types-utilities": "^1.185.0",
55
+ "@tellescope/utilities": "^1.185.0",
56
+ "@tellescope/validation": "^1.185.0",
57
57
  "@typescript-eslint/eslint-plugin": "^4.33.0",
58
58
  "@typescript-eslint/parser": "^4.33.0",
59
59
  "css-to-react-native": "^3.0.0",
@@ -84,7 +84,7 @@
84
84
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
85
85
  "react-native": "^0.65.0 || ^0.66.0 || ^0.67.0 || ^0.68.0 || ^0.71.0"
86
86
  },
87
- "gitHead": "1b65f936169b011cfd20179c25625af7a6f34cba",
87
+ "gitHead": "cfec3c3b78a63582657d3fbf942cdbe2a0375f22",
88
88
  "publishConfig": {
89
89
  "access": "public"
90
90
  }
@@ -552,6 +552,7 @@ export const useTellescopeForm = ({ isPublicForm, form, urlLogicValue, customiza
552
552
  const getNumberOfRemainingPages: (field?: FormField, explored?: string[]) => number = useCallback((field=activeField.value, explored=[]) => {
553
553
  // prevents recursing on an already explored node (there shouldn't be loops anyway, but just in case)
554
554
  if (explored.includes(field.id)) return 0
555
+ explored.push(field.id) // make sure to push this node to prevent future exploration
555
556
 
556
557
  const children = fields.filter(
557
558
  f => (
@@ -562,7 +563,7 @@ export const useTellescopeForm = ({ isPublicForm, form, urlLogicValue, customiza
562
563
 
563
564
  return (
564
565
  1 + Math.max(
565
- ...children.map(c => getNumberOfRemainingPages(c, [...explored, field.id]))
566
+ ...children.map(c => getNumberOfRemainingPages(c, explored))
566
567
  )
567
568
  )
568
569
  }, [activeField, fields])
@@ -1099,7 +1100,7 @@ export const useTellescopeForm = ({ isPublicForm, form, urlLogicValue, customiza
1099
1100
  // ensure Question Group responses are included
1100
1101
 
1101
1102
  for (const r of responsesToSubmit) {
1102
- if (r.answer.type !== 'Question Group') continue
1103
+ if (r?.answer?.type !== 'Question Group') continue
1103
1104
 
1104
1105
  for (const f of r.answer.value ?? []) {
1105
1106
  const match = responses.find(r => r.fieldId === f?.id)
@@ -1,14 +1,454 @@
1
1
  import React, { useEffect, useCallback, useMemo, useState, useRef } from "react"
2
2
  import { Indexable, ScoreFilter } from "@tellescope/types-utilities"
3
- import { objects_equivalent, read_local_storage, safeJSONParse, to_human_readable_phone_number, update_local_storage, user_display_name } from "@tellescope/utilities"
3
+ import { is_full_iso_string_heuristic, object_is_empty, objects_equivalent, read_local_storage, replace_keys_and_values_in_object, safeJSONParse, update_local_storage, user_display_name, value_for_dotted_key } from "@tellescope/utilities"
4
4
  import { LoadFunction, LoadFunctionArguments } from "@tellescope/sdk"
5
- import { ALL_ACCESS, UNSEARCHABLE_FIELDS } from "@tellescope/constants"
5
+ import { ALL_ACCESS, HEALTHIE_TITLE, UNSEARCHABLE_FIELDS } from "@tellescope/constants"
6
6
  import { SearchAPIProps, useSearchAPI } from "./hooks"
7
7
  import { TextFieldProps } from "./mui"
8
8
  import { AgentRecord, AllergyCode, AppointmentBookingPage, AppointmentLocation, AutomationTrigger, CalendarEventTemplate, CallHoldQueue, ChatRoom, Database, DatabaseRecord, DiagnosisCode, Enduser, EnduserOrder, FaxLog, File, Form, FormGroup, Forum, Journey, ManagedContentRecord, MessageTemplateSnippet, Organization, PrescriptionRoute, SuggestedContact, Template, Ticket, TicketQueue, User, UserNotification, Waitlist } from "@tellescope/types-client"
9
- import { Button, Checkbox, Flex, HoverPaper, LoadingButton, LoadingData, LoadingLinear, ScrollingList, SearchTextInput, Typography, useAgentRecords, useAllergyCodes, useAppointmentBookingPages, useAppointmentLocations, useAutomationTriggers, useCalendarEventTemplates, useCallHoldQueues, useChatRooms, useDatabaseRecords, useDatabases, useDiagnosisCodes, useEnduserOrders, useEndusers, useFaxLogs, useFiles, useFormGroups, useForms, useForums, useJourneys, useManagedContentRecords, useMessageTemplateSnippets, useNotifications, useOrganization, useOrganizations, usePrescriptionRoutes, useResolvedSession, useSession, useSuggestedContacts, useTemplates, useTicketQueues, useTickets, useUsers, useWaitlists, value_is_loaded } from "."
9
+ import { Button, Checkbox, Flex, HoverPaper, LoadingButton, LoadingData, ScrollingList, SearchTextInput, Typography, useAgentRecords, useAllergyCodes, useAppointmentBookingPages, useAppointmentLocations, useAutomationTriggers, useCalendarEventTemplates, useCallHoldQueues, useChatRooms, useDatabaseRecords, useDatabases, useDiagnosisCodes, useEnduserOrders, useEndusers, useFaxLogs, useFiles, useFormGroups, useForms, useForums, useJourneys, useManagedContentRecords, useMessageTemplateSnippets, useNotifications, useOrganization, useOrganizations, usePrescriptionRoutes, useResolvedSession, useSession, useSuggestedContacts, useTemplates, useTicketQueues, useTickets, useUsers, useWaitlists, value_is_loaded } from "."
10
10
  import { SxProps } from "@mui/material"
11
- import { AccessPermissions } from "@tellescope/types-models"
11
+ import { AccessPermissions, ListOfStringsWithQualifier } from "@tellescope/types-models"
12
+
13
+ export type FilterV2 = Record<string, any>
14
+ export type FiltersV2 = Record<string, FilterV2>
15
+ export type FilterV2Options = { showArchived: boolean }
16
+
17
+ export const enduser_condition_to_mongodb_filter = (condition: Record<string, any> | undefined, customFields: string[]): Record<string, any> | undefined => {
18
+ if (!condition) { return condition }
19
+
20
+ if (condition.$and) {
21
+ return { $and: condition.$and.map((v: any) => enduser_condition_to_mongodb_filter(v, customFields)) }
22
+ }
23
+ if (condition.$or) {
24
+ return { $or: condition.$or.map((v: any) => enduser_condition_to_mongodb_filter(v, customFields)) }
25
+ }
26
+ if (condition.$nor) {
27
+ return { $nor: condition.$nor.map((v: any) => enduser_condition_to_mongodb_filter(v, customFields)) }
28
+ }
29
+ if (condition.$not) {
30
+ return { $not: enduser_condition_to_mongodb_filter(condition.$not, customFields) }
31
+ }
32
+
33
+ if (condition && typeof condition === 'object' && condition.constructor === Object) {
34
+ const updated = { } as Record<string, any>
35
+ for (const [_key, _value] of Object.entries(condition)) {
36
+ const key = customFields.includes(_key) ? `fields.${_key}` : _key
37
+ const value = enduser_condition_to_mongodb_filter(_value, customFields)
38
+ // console.log(key, _value, value)
39
+
40
+ if (key === 'condition') {
41
+ const toReturn = enduser_condition_to_mongodb_filter(value, customFields)
42
+ delete updated.condition
43
+ return toReturn
44
+ } // base case is comparison to value, so just return it
45
+
46
+ // journeys currently have a special ui/syntax in filter
47
+ if (key === 'Journeys') {
48
+ if ((typeof (value as any)?.$in) === 'string') {
49
+ return { [`journeys.${(value as any).$in}`]: { $exists: true } }
50
+ }
51
+ else if ((typeof (value as any)?.$nin) === 'string') {
52
+ return { [`journeys.${(value as any).$nin}`]: { $exists: false } }
53
+ }
54
+ }
55
+
56
+ if (key === 'Healthie ID') {
57
+ // $setSet ($nin null, '')
58
+ if ((value as any)?.$nin) return { $or: [{ source: HEALTHIE_TITLE, externalId: { $nin: [null, ''] } }, { 'references.type': HEALTHIE_TITLE }] }
59
+ // $isNotSet ($in null, '')
60
+ if ((value as any)?.$in) return { $and: [{ source: { $ne: HEALTHIE_TITLE } }, { 'references.type': { $ne: HEALTHIE_TITLE } }] }
61
+ }
62
+
63
+ // ensure to parse string to number values
64
+ if (key === 'height') {
65
+ return {
66
+ 'height.value': replace_keys_and_values_in_object(value, v => typeof v === 'string' && !v.startsWith('$') ? parseFloat(v) : v)
67
+ }
68
+ }
69
+ if (key === 'weight') {
70
+ return {
71
+ 'weight.value': replace_keys_and_values_in_object(value, v => typeof v === 'string' && !v.startsWith('$') ? parseFloat(v) : v)
72
+ }
73
+ }
74
+ if (key === "relationships") {
75
+ return {
76
+ 'relationships.type': value
77
+ }
78
+ }
79
+
80
+ // case for isSet and isNotSet includes empty string as unset value
81
+ if (key === '$isSet') {
82
+ updated.$nin = [null, '', []]
83
+ }
84
+ else if (key === '$isNotSet') {
85
+ updated.$in = [null, '', []]
86
+ }
87
+ else if (key === '$contains') {
88
+ updated.$regex = value
89
+ }
90
+ else if (key === '$doesNotContain') {
91
+ updated.$not = { $regex: value }
92
+ }
93
+ else if (key === '$before') {
94
+ updated.$lt = value
95
+ }
96
+ else if (key === '$after') {
97
+ updated.$gt = value
98
+ }
99
+ else {
100
+ updated[key] = value
101
+ }
102
+ }
103
+
104
+ return updated
105
+ }
106
+
107
+ return condition
108
+ }
109
+
110
+ export const mongo_db_filter_to_enduser_condition = (filter?: FilterV2): Record<string, any> | undefined => {
111
+ if (!filter) { return filter }
112
+
113
+ if (objects_equivalent(filter, {
114
+ "$and": [
115
+ {
116
+ "source": {
117
+ "$ne": "Healthie"
118
+ }
119
+ },
120
+ {
121
+ "references.type": {
122
+ "$ne": "Healthie"
123
+ }
124
+ }
125
+ ]
126
+ })) {
127
+ return { condition: { 'Healthie ID': { $isNotSet: 'Value' } } }
128
+ }
129
+ if (objects_equivalent(filter, {
130
+ "$or": [
131
+ {
132
+ "source": "Healthie",
133
+ "externalId": {
134
+ "$nin": [
135
+ null,
136
+ ""
137
+ ]
138
+ }
139
+ },
140
+ {
141
+ "references.type": "Healthie"
142
+ }
143
+ ]
144
+ })) {
145
+ return { condition: { 'Healthie ID': { $isSet: 'Value' } } }
146
+ }
147
+
148
+ if (filter.$and) {
149
+ return { $and: filter.$and.map((v: any) => mongo_db_filter_to_enduser_condition(v)) }
150
+ }
151
+ if (filter.$or) {
152
+ return { $or: filter.$or.map((v: any) => mongo_db_filter_to_enduser_condition(v)) }
153
+ }
154
+ if (filter.$nor) {
155
+ return { $nor: filter.$nor.map((v: any) => mongo_db_filter_to_enduser_condition(v)) }
156
+ }
157
+ if (filter.$not) {
158
+ return { $not: mongo_db_filter_to_enduser_condition(filter.$not) }
159
+ }
160
+
161
+ if (filter && typeof filter === 'object' && filter.constructor === Object) {
162
+ if (object_is_empty(filter)) return filter
163
+
164
+ const updated = { condition: { } as Record<string, any> }
165
+
166
+ for (const [_key, value] of Object.entries(filter)) {
167
+ const key = (
168
+ (typeof _key === 'string' && _key.startsWith('fields.')) ? _key.replace('fields.', '')
169
+ : _key === 'height.value' ? 'height'
170
+ : _key === 'weight.value' ? 'weight'
171
+ : _key === 'relationships.type' ? 'relationships'
172
+ : _key
173
+ )
174
+
175
+ if (value && typeof value === 'object' && value.constructor === Object) {
176
+ updated.condition[key] = {} as Record<string, any>
177
+ if (Object.keys(value)[0] === '$exists') {
178
+ if (key.startsWith('journeys.')) {
179
+ // journeys currently have a special ui/syntax in filter
180
+ updated.condition['Journeys'] = { [value.$exists ? '$in' : '$nin']: key.split('.')[1] }
181
+ }
182
+ else if (value.$exists) { updated.condition[key]['$isSet'] = "Value" }
183
+ else { updated.condition[key]['$isNotSet'] = "Value" }
184
+ }
185
+
186
+ // case for isSet and isNotSet includes empty string as unset value
187
+ else if (Array.isArray(value.$in) && value.$in.length === 3 && value.$in.includes(null) && value.$in.includes('') && value.$in.find((v: any) => Array.isArray(v) && v.length === 0)) {
188
+ updated.condition[key]['$isNotSet'] = "Value"
189
+ }
190
+ else if (Array.isArray(value.$nin) && value.$nin.length === 3 && value.$nin.includes(null) && value.$nin.includes('') && value.$nin.find((v: any) => Array.isArray(v) && v.length === 0)) {
191
+ updated.condition[key]['$isSet'] = "Value"
192
+ }
193
+
194
+ else if (Object.keys(value)[0] === '$gt' && (value.$gt instanceof Date || is_full_iso_string_heuristic(value.$gt) || value.$gt === '$now')) {
195
+ updated.condition[key]['$after'] = value.$gt
196
+ }
197
+ else if (Object.keys(value)[0] === '$lt' && (value.$lt instanceof Date || is_full_iso_string_heuristic(value.$lt) || value.$lt === '$now')) {
198
+ updated.condition[key]['$before'] = value.$lt
199
+ }
200
+ else if (Object.keys(value)[0] === '$regex') {
201
+ updated.condition[key]['$contains'] = value.$regex
202
+ }
203
+ else if (Object.keys(value)[0] === '$not' && Object.keys(value.$not ?? {})?.[0] === '$regex') {
204
+ updated.condition[key]['$doesNotContain'] = value.$not.$regex
205
+ } else {
206
+ updated.condition[key] = value
207
+ }
208
+ }
209
+ else {
210
+ updated.condition[key] = value
211
+ }
212
+ }
213
+
214
+ return updated
215
+ }
216
+
217
+ return filter
218
+ }
219
+
220
+ export const list_of_strings_with_qualifier_to_mongodb_filter = (tags?: ListOfStringsWithQualifier) => {
221
+ if (tags?.qualifier === 'All Of') {
222
+ return { $all: tags.values || [] }
223
+ }
224
+ return { $in: tags?.values || [] }
225
+ }
226
+ export const mongo_db_filter_to_list_of_strings_with_qualifier = (filter?: FilterV2): ListOfStringsWithQualifier => {
227
+ const defaultValue: ListOfStringsWithQualifier = { qualifier: 'One Of', values: [] }
228
+
229
+ if (!filter) { return defaultValue }
230
+ if (filter.$all) {
231
+ return { values: filter.$all, qualifier: 'All Of' }
232
+ }
233
+ if (filter.$in) {
234
+ return { values: filter.$in, qualifier: 'One Of' }
235
+ }
236
+
237
+ return defaultValue
238
+ }
239
+
240
+ export interface FilterComponentWithDefaultKeyV2 {
241
+ filters: FiltersV2,
242
+ setFilters: React.Dispatch<React.SetStateAction<FiltersV2>>,
243
+ onKeyDown?: (e: { code: string }) => void,
244
+ }
245
+ // can include a version with an optional key, but make sure to use it in all cases when it's possibly passed as a prop (don't just use a string literal as default)
246
+ export interface FilterComponentV2 extends FilterComponentWithDefaultKeyV2 {
247
+ filterKey: string,
248
+ }
249
+
250
+ export const apply_mongodb_style_filter = <T,>(data: T[], filter: FilterV2, options: FilterV2Options): T[] => {
251
+ const matchesFilter = (item: any, filter: FilterV2): boolean => {
252
+ if (!options.showArchived && item?.archivedAt) return false
253
+
254
+ for (const [key, condition] of Object.entries(filter)) {
255
+ if (key === "$and") {
256
+ if (!Array.isArray(condition) || !condition.every((subFilter) => matchesFilter(item, subFilter))) {
257
+ return false;
258
+ }
259
+ } else if (key === "$or") {
260
+ if (!Array.isArray(condition) || !condition.some((subFilter) => matchesFilter(item, subFilter))) {
261
+ return false;
262
+ }
263
+ } else if (key === "$nor") {
264
+ if (!Array.isArray(condition) || condition.some((subFilter) => matchesFilter(item, subFilter))) {
265
+ return false;
266
+ }
267
+ } else if (key === "$not") {
268
+ if (typeof condition !== "object" || matchesFilter(item, condition)) {
269
+ return false;
270
+ }
271
+ } else {
272
+ const value = value_for_dotted_key(item, key, { handleArray: true });
273
+ // console.log('checking value', key, value, condition)
274
+
275
+ // to be consistent with mongodb, $in/$nin should match null and undefined
276
+ const modifyArrayOperandForNull = (v: any[]) => ([
277
+ ...v,
278
+ ...(v.includes(null) ? [undefined] : []),
279
+ ])
280
+
281
+ if (Array.isArray(value)) {
282
+ if (typeof condition === "object" && condition !== null) {
283
+ for (const [operator, operand] of Object.entries(condition)) {
284
+ // handle empty array
285
+ if (Array.isArray(value) && value.length === 0) {
286
+ if (operator === '$eq' && Array.isArray(operand) && operand.length === 0) { continue }
287
+ if (operator === '$ne' && Array.isArray(operand) && operand.length === 0) { return false }
288
+ if (Array.isArray(operand) && operand.find(a => Array.isArray(a) && a.length === 0)) {
289
+ if (operator === '$in') { continue }
290
+ if (operator === '$nin') { return false }
291
+ }
292
+ }
293
+
294
+ if (operator === '$eq' && !value.includes(operand)) return false
295
+ if (operator === '$ne' && value.includes(operand)) return false
296
+
297
+ if (operator === "$in" && Array.isArray(operand) && !modifyArrayOperandForNull(operand).some((o: any) => value.includes(o))) return false;
298
+ if (operator === "$nin" && Array.isArray(operand) && modifyArrayOperandForNull(operand).some((o: any) => value.includes(o))) return false;
299
+
300
+ if (operator === "$all" && Array.isArray(operand) && !operand.every((o: any) => value.includes(o))) return false;
301
+ if (operator === '$all' && condition.length === 0) return false
302
+ if (operator === '$all' && Array.isArray(operand) && operand.length === 0) return false
303
+
304
+ if (operator === "$size" && value.length !== operand) return false;
305
+
306
+ if (operator === '$exists' && operand === false) return false
307
+ }
308
+ } else {
309
+ if (!value.includes(condition)) return false;
310
+ }
311
+ } else {
312
+ if (typeof condition === "object" && condition !== null) {
313
+ for (const [operator, operand] of Object.entries(condition)) {
314
+ const numberValue = (
315
+ is_full_iso_string_heuristic((value as any)?.toString())
316
+ ? new Date(value).getTime()
317
+ : parseFloat(value)
318
+ )
319
+ const parsedOperandForNumber = (
320
+ is_full_iso_string_heuristic((operand as any)?.toString())
321
+ ? new Date(operand as any).getTime()
322
+ : operand === '$now'
323
+ ? new Date().getTime()
324
+ : parseFloat(operand as any)
325
+ )
326
+
327
+ if (operator === "$eq" && value !== operand) return false;
328
+ if (operator === "$ne" && value === operand) return false;
329
+ if (operator === "$in" && (!Array.isArray(operand) || !(modifyArrayOperandForNull(operand).includes(value)))) return false;
330
+ if (operator === "$nin" && !(Array.isArray(operand) && !modifyArrayOperandForNull(operand).includes(value))) return false;
331
+ if (operator === "$exists" && ((operand && value === undefined) || (!operand && value !== undefined))) return false;
332
+ if (operator === "$regex" && !(typeof value === "string" && new RegExp(operand as string).test(value))) return false;
333
+ if (operator === "$gt" && (numberValue <= parseFloat(parsedOperandForNumber as any) || isNaN(numberValue) || numberValue === null || numberValue === undefined)) return false;
334
+ if (operator === "$gte" && (numberValue < parseFloat(parsedOperandForNumber as any) || isNaN(numberValue) || numberValue === null || numberValue === undefined)) return false;
335
+ if (operator === "$lt" && (numberValue >= parseFloat(parsedOperandForNumber as any) || isNaN(numberValue) || numberValue === null || numberValue === undefined)) return false;
336
+ if (operator === "$lte" && (numberValue > parseFloat(parsedOperandForNumber as any) || isNaN(numberValue) || numberValue === null || numberValue === undefined)) return false;
337
+
338
+ // only valid for lists, shoujld return false by default
339
+ if (operator === '$all') return false
340
+ }
341
+ } else {
342
+ if (value !== condition) return false;
343
+ }
344
+ }
345
+ }
346
+ }
347
+
348
+ return true;
349
+ };
350
+
351
+ try {
352
+ return data.filter(item => matchesFilter(item, filter))
353
+ } catch(err) {
354
+ console.error("Filter error:", err)
355
+ }
356
+
357
+ return data
358
+ }
359
+
360
+ export const remove_inactive_filters = (filters: Record<string, any>[]) => (
361
+ filters.map(f => {
362
+ // gpt4o
363
+ const cleanedFilter = Object.entries(f).reduce((acc, [key, value]) => {
364
+ if (key === "$and" || key === "$or") {
365
+ const subFilters = Array.isArray(value) ? value.filter(sub => Object.keys(sub).length > 0) : [];
366
+ if (subFilters.length > 0) {
367
+ acc[key] = subFilters;
368
+ }
369
+ } else if (key === "$in" || key === "$all") {
370
+ if (Array.isArray(value) && value.length > 0) {
371
+ acc[key] = value;
372
+ }
373
+ } else if (key === "$exists" || key === "$not") {
374
+ acc[key] = value;
375
+ } else if (typeof value === "object" && value !== null) {
376
+ const nestedFilter = remove_inactive_filters([value])[0];
377
+ if (nestedFilter && Object.keys(nestedFilter).length > 0) {
378
+ acc[key] = nestedFilter;
379
+ }
380
+ } else {
381
+ acc[key] = value;
382
+ }
383
+ return acc;
384
+ }, {} as Record<string, any>);
385
+
386
+ return Object.keys(cleanedFilter).length > 0 ? cleanedFilter : null;
387
+
388
+ })
389
+ .filter(v => v && !object_is_empty(v))
390
+ )
391
+
392
+ export const useFiltersV2 = <T,>(
393
+ args?: {
394
+ memoryId?: string,
395
+ initialFilters?: FiltersV2,
396
+ reload?: boolean,
397
+ onFilterChange?: (fs: FiltersV2) => void,
398
+ showArchived?: boolean,
399
+ }
400
+ ) => {
401
+ const { onFilterChange, reload, memoryId, initialFilters, showArchived } = args ?? {}
402
+
403
+ const loadFilters = useCallback(() => (
404
+ initialFilters || (
405
+ memoryId
406
+ ? (safeJSONParse(read_local_storage(memoryId)) || {})
407
+ : {}
408
+ )
409
+ ), [initialFilters, memoryId])
410
+
411
+ const [filters, setFilters] = React.useState<FiltersV2>(loadFilters())
412
+
413
+ const didReloadRef = useRef(false)
414
+ useEffect(() => {
415
+ if (!reload) return
416
+ if (didReloadRef.current) return
417
+ didReloadRef.current = true
418
+
419
+ setFilters(loadFilters)
420
+ }, [reload, loadFilters])
421
+
422
+ useEffect(() => {
423
+ if (!memoryId) return
424
+ update_local_storage(memoryId, JSON.stringify(filters))
425
+ }, [filters, memoryId])
426
+
427
+ const prevFilterRef = React.useRef(filters)
428
+ useEffect(() => {
429
+ if (!onFilterChange) return
430
+ if (objects_equivalent(prevFilterRef.current, filters)) return
431
+
432
+ prevFilterRef.current = filters
433
+ onFilterChange(filters)
434
+ }, [filters, onFilterChange])
435
+
436
+ const mdbFilter = useMemo(() => ({
437
+ $and: remove_inactive_filters(Object.values(filters))
438
+ }), [filters])
439
+
440
+ const applyFilters = useCallback((
441
+ (data: T[]) => apply_mongodb_style_filter(data, mdbFilter, { showArchived: !!showArchived })
442
+ ), [mdbFilter, showArchived])
443
+
444
+ return {
445
+ mdbFilter,
446
+ filters,
447
+ setFilters,
448
+ applyFilters,
449
+ activeFilterCount: Object.values(filters).filter(f => !!f.filter).length
450
+ }
451
+ }
12
452
 
13
453
  /* FILTER / SEARCH */
14
454
  export const filter_setter_for_key = <T,>(key: string, setFilters: React.Dispatch<React.SetStateAction<Filters<T>>>) => (
@@ -215,7 +655,6 @@ export const filter_for_query = <T,>(query: string, getAdditionalFields?: (v: T)
215
655
 
216
656
  const toAdd = getAdditionalFields?.(record as T)
217
657
  const joined = { ...toAdd, ...record }
218
- // console.log(JSON.stringify(joined, null, 2))
219
658
 
220
659
  for (const field in joined) {
221
660
  const value = joined[field as keyof typeof record]
package/src/layout.tsx CHANGED
@@ -14,6 +14,7 @@ import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
14
14
  import { FixedSizeList } from 'react-window';
15
15
  import { usePageWidth } from "./CMS";
16
16
  import { LoadMoreOptions } from "./state";
17
+ import { LoadingButton } from ".";
17
18
 
18
19
  export const IN_REACT_WEB = true
19
20
 
@@ -400,12 +401,20 @@ export const ScrollingList = <T extends { id: string | number }>({
400
401
  if (doneLoading?.() || !loadMore) return
401
402
 
402
403
  setLoading(true)
403
- loadMore(loadMoreOptions).finally(() => setLoading(false))
404
+ loadMore(loadMoreOptions).catch(console.error).finally(() => setLoading(false))
404
405
  }}
405
406
  >
406
407
  {({ data, index, style }) => (
407
408
  <div style={style}>
408
409
  <Item key={data[index].id} item={data[index]} index={index} />
410
+ {index === items.length -1 && loadMore && !doneLoading?.() &&
411
+ <div style={{ textAlign: 'center' }}>
412
+ <LoadingButton submitText="Load Older Data" submittingText="Loading..."
413
+ disabled={doneLoading?.()} onClick={loadMore}
414
+ variant="outlined" style={{ width: 200, textAlign: 'center', marginTop: 10 }}
415
+ />
416
+ </div>
417
+ }
409
418
  </div>
410
419
  )}
411
420
  </FixedSizeList>