@tellescope/react-components 1.183.0 → 1.184.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.
- package/lib/cjs/Forms/hooks.d.ts.map +1 -1
- package/lib/cjs/Forms/hooks.js +2 -1
- package/lib/cjs/Forms/hooks.js.map +1 -1
- package/lib/cjs/inputs_shared.d.ts +43 -0
- package/lib/cjs/inputs_shared.d.ts.map +1 -1
- package/lib/cjs/inputs_shared.js +433 -2
- package/lib/cjs/inputs_shared.js.map +1 -1
- package/lib/cjs/layout.d.ts.map +1 -1
- package/lib/cjs/layout.js +4 -2
- package/lib/cjs/layout.js.map +1 -1
- package/lib/cjs/state.d.ts +6 -1
- package/lib/cjs/state.d.ts.map +1 -1
- package/lib/cjs/state.js +38 -57
- package/lib/cjs/state.js.map +1 -1
- package/lib/cjs/table.d.ts +8 -3
- package/lib/cjs/table.d.ts.map +1 -1
- package/lib/cjs/table.js +16 -13
- package/lib/cjs/table.js.map +1 -1
- package/lib/esm/CMS/components.d.ts +1 -0
- package/lib/esm/CMS/components.d.ts.map +1 -1
- package/lib/esm/Forms/form_responses.d.ts +1 -0
- package/lib/esm/Forms/form_responses.d.ts.map +1 -1
- package/lib/esm/Forms/forms.d.ts +3 -3
- package/lib/esm/Forms/hooks.d.ts.map +1 -1
- package/lib/esm/Forms/hooks.js +2 -1
- package/lib/esm/Forms/hooks.js.map +1 -1
- package/lib/esm/Forms/inputs.d.ts +1 -1
- package/lib/esm/Forms/inputs.native.d.ts +1 -0
- package/lib/esm/Forms/inputs.native.d.ts.map +1 -1
- package/lib/esm/controls.d.ts +2 -2
- package/lib/esm/inputs.d.ts +1 -1
- package/lib/esm/inputs.native.d.ts +1 -0
- package/lib/esm/inputs.native.d.ts.map +1 -1
- package/lib/esm/inputs_shared.d.ts +43 -0
- package/lib/esm/inputs_shared.d.ts.map +1 -1
- package/lib/esm/inputs_shared.js +427 -3
- package/lib/esm/inputs_shared.js.map +1 -1
- package/lib/esm/layout.d.ts.map +1 -1
- package/lib/esm/layout.js +4 -2
- package/lib/esm/layout.js.map +1 -1
- package/lib/esm/state.d.ts +292 -287
- package/lib/esm/state.d.ts.map +1 -1
- package/lib/esm/state.js +38 -57
- package/lib/esm/state.js.map +1 -1
- package/lib/esm/table.d.ts +8 -3
- package/lib/esm/table.d.ts.map +1 -1
- package/lib/esm/table.js +16 -13
- package/lib/esm/table.js.map +1 -1
- package/lib/esm/theme.native.d.ts +1 -0
- package/lib/esm/theme.native.d.ts.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/Forms/hooks.tsx +2 -1
- package/src/inputs_shared.tsx +444 -5
- package/src/layout.tsx +10 -1
- package/src/state.tsx +38 -46
- package/src/table.tsx +31 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tellescope/react-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.184.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.
|
|
51
|
-
"@tellescope/sdk": "^1.
|
|
52
|
-
"@tellescope/types-client": "^1.
|
|
53
|
-
"@tellescope/types-models": "^1.
|
|
54
|
-
"@tellescope/types-utilities": "^1.
|
|
55
|
-
"@tellescope/utilities": "^1.
|
|
56
|
-
"@tellescope/validation": "^1.
|
|
50
|
+
"@tellescope/constants": "^1.184.0",
|
|
51
|
+
"@tellescope/sdk": "^1.184.0",
|
|
52
|
+
"@tellescope/types-client": "^1.184.0",
|
|
53
|
+
"@tellescope/types-models": "^1.184.0",
|
|
54
|
+
"@tellescope/types-utilities": "^1.184.0",
|
|
55
|
+
"@tellescope/utilities": "^1.184.0",
|
|
56
|
+
"@tellescope/validation": "^1.184.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": "
|
|
87
|
+
"gitHead": "ad996fbec7a2840e68b90a0df0b2ff182e348ee3",
|
|
88
88
|
"publishConfig": {
|
|
89
89
|
"access": "public"
|
|
90
90
|
}
|
package/src/Forms/hooks.tsx
CHANGED
|
@@ -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,
|
|
566
|
+
...children.map(c => getNumberOfRemainingPages(c, explored))
|
|
566
567
|
)
|
|
567
568
|
)
|
|
568
569
|
}, [activeField, fields])
|
package/src/inputs_shared.tsx
CHANGED
|
@@ -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,
|
|
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,
|
|
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 &&
|
|
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>
|