@tellescope/react-components 1.247.0 → 1.248.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/form_responses.d.ts +11 -0
  2. package/lib/cjs/Forms/form_responses.d.ts.map +1 -1
  3. package/lib/cjs/Forms/form_responses.js +38 -2
  4. package/lib/cjs/Forms/form_responses.js.map +1 -1
  5. package/lib/cjs/Forms/forms.d.ts +2 -1
  6. package/lib/cjs/Forms/forms.d.ts.map +1 -1
  7. package/lib/cjs/Forms/forms.js +34 -8
  8. package/lib/cjs/Forms/forms.js.map +1 -1
  9. package/lib/cjs/Forms/forms.v2.js +1 -1
  10. package/lib/cjs/Forms/forms.v2.js.map +1 -1
  11. package/lib/cjs/Forms/hooks.d.ts +1 -0
  12. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  13. package/lib/cjs/Forms/hooks.js +5 -4
  14. package/lib/cjs/Forms/hooks.js.map +1 -1
  15. package/lib/cjs/state.d.ts +7 -1
  16. package/lib/cjs/state.d.ts.map +1 -1
  17. package/lib/cjs/state.js +43 -1
  18. package/lib/cjs/state.js.map +1 -1
  19. package/lib/esm/Forms/form_responses.d.ts +11 -0
  20. package/lib/esm/Forms/form_responses.d.ts.map +1 -1
  21. package/lib/esm/Forms/form_responses.js +37 -2
  22. package/lib/esm/Forms/form_responses.js.map +1 -1
  23. package/lib/esm/Forms/forms.d.ts +2 -1
  24. package/lib/esm/Forms/forms.d.ts.map +1 -1
  25. package/lib/esm/Forms/forms.js +34 -8
  26. package/lib/esm/Forms/forms.js.map +1 -1
  27. package/lib/esm/Forms/forms.v2.js +1 -1
  28. package/lib/esm/Forms/forms.v2.js.map +1 -1
  29. package/lib/esm/Forms/hooks.d.ts +1 -0
  30. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  31. package/lib/esm/Forms/hooks.js +6 -5
  32. package/lib/esm/Forms/hooks.js.map +1 -1
  33. package/lib/esm/state.d.ts +7 -1
  34. package/lib/esm/state.d.ts.map +1 -1
  35. package/lib/esm/state.js +42 -1
  36. package/lib/esm/state.js.map +1 -1
  37. package/lib/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +9 -9
  39. package/src/Forms/form_responses.tsx +133 -2
  40. package/src/Forms/forms.tsx +32 -6
  41. package/src/Forms/forms.v2.tsx +1 -1
  42. package/src/Forms/hooks.tsx +9 -5
  43. package/src/state.tsx +37 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/react-components",
3
- "version": "1.247.0",
3
+ "version": "1.248.0",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -51,13 +51,13 @@
51
51
  "@reduxjs/toolkit": "1.9.0",
52
52
  "@stripe/react-stripe-js": "2.9.0",
53
53
  "@stripe/stripe-js": "1.52.1",
54
- "@tellescope/constants": "1.247.0",
55
- "@tellescope/sdk": "1.247.0",
56
- "@tellescope/types-client": "1.247.0",
57
- "@tellescope/types-models": "1.247.0",
58
- "@tellescope/types-utilities": "1.247.0",
59
- "@tellescope/utilities": "1.247.0",
60
- "@tellescope/validation": "1.247.0",
54
+ "@tellescope/constants": "1.248.0",
55
+ "@tellescope/sdk": "1.248.0",
56
+ "@tellescope/types-client": "1.248.0",
57
+ "@tellescope/types-models": "1.248.0",
58
+ "@tellescope/types-utilities": "1.248.0",
59
+ "@tellescope/utilities": "1.248.0",
60
+ "@tellescope/validation": "1.248.0",
61
61
  "css-to-react-native": "3.0.0",
62
62
  "draft-js": "0.11.7",
63
63
  "draftjs-to-html": "0.9.1",
@@ -84,7 +84,7 @@
84
84
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.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": "408421d578d9c63493ff94464da6e4317aa3af20",
87
+ "gitHead": "c10ecca2464920b76cda6af60fafbd1c9fde5174",
88
88
  "publishConfig": {
89
89
  "access": "public"
90
90
  }
@@ -2,7 +2,7 @@ import React, { useEffect } from "react"
2
2
  import { Divider, Grid, Typography } from "@mui/material"
3
3
  import { Enduser, FormResponse } from "@tellescope/types-client"
4
4
  import { form_response_value_to_string, formatted_date, getOrgnizationLogoURL, remove_script_tags, user_display_name } from "@tellescope/utilities"
5
- import { DownloadFileIconButton, ImageProps, LabeledIconButton, SecureImage, useEndusers, useOrganization, useResolvedSession, useSession, useUsers, value_is_loaded } from "../index"
5
+ import { DownloadFileIconButton, ImageProps, LabeledIconButton, SecureImage, useEndusers, useEnduserMedications, useEnduserObservations, useOrganization, useResolvedSession, useSession, useUsers, value_is_loaded } from "../index"
6
6
  import CloseIcon from '@mui/icons-material/Close';
7
7
  import { DatabaseSelectResponse, FormResponseAnswerAddress, FormResponseValueAnswer } from "@tellescope/types-models"
8
8
  import { Image } from "../layout"
@@ -23,6 +23,135 @@ export const AddressDisplay = ({ value } : { value: Required<FormResponseAnswerA
23
23
  </Grid>
24
24
  )
25
25
 
26
+ type SnapshotRef = { id: string, label: string }
27
+
28
+ export const HistoricalDataSnapshotDisplay = ({ snapshot } : { snapshot: { observations?: SnapshotRef[], medications?: SnapshotRef[], snapshotAt?: string } }) => {
29
+ const { observations: obsRefs = [], medications: medRefs = [], snapshotAt } = snapshot
30
+ const [, { findById: findObservation }] = useEnduserObservations({ dontFetch: true })
31
+ const [, { findById: findMedication }] = useEnduserMedications({ dontFetch: true })
32
+
33
+ const tdStyle = { padding: '6px 8px' } as const
34
+ const deletedStyle = { padding: '6px 8px', color: '#999', fontStyle: 'italic' } as const
35
+
36
+ return (
37
+ <div style={{ marginTop: 10 }}>
38
+ {snapshotAt && (
39
+ <Typography style={{ fontSize: 12, color: '#888', marginBottom: 8 }}>
40
+ Snapshot taken at {formatted_date(new Date(snapshotAt))}
41
+ </Typography>
42
+ )}
43
+
44
+ {obsRefs.length > 0 && (
45
+ <div style={{ marginBottom: 15 }}>
46
+ <Typography style={{ fontWeight: 'bold', marginBottom: 5 }}>Observations</Typography>
47
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
48
+ <thead>
49
+ <tr style={{ borderBottom: '2px solid #ccc', textAlign: 'left' }}>
50
+ <th style={tdStyle}>Date</th>
51
+ <th style={tdStyle}>Type</th>
52
+ <th style={tdStyle}>Value</th>
53
+ <th style={tdStyle}>Category</th>
54
+ <th style={tdStyle}>Status</th>
55
+ </tr>
56
+ </thead>
57
+ <tbody>
58
+ {obsRefs.map((ref, i) => {
59
+ const obs = findObservation(ref.id, { batch: true })
60
+ if (obs === undefined) return (
61
+ <tr key={ref.id || i} style={{ borderBottom: '1px solid #eee' }}>
62
+ <td colSpan={5} style={tdStyle}>Loading...</td>
63
+ </tr>
64
+ )
65
+ if (obs === null) return (
66
+ <tr key={ref.id || i} style={{ borderBottom: '1px solid #eee' }}>
67
+ <td colSpan={5} style={deletedStyle}>{ref.label} — Record no longer available</td>
68
+ </tr>
69
+ )
70
+ return (
71
+ <tr key={obs.id || i} style={{ borderBottom: '1px solid #eee' }}>
72
+ <td style={tdStyle}>{obs.timestamp ? formatted_date(new Date(obs.timestamp)) : '-'}</td>
73
+ <td style={tdStyle}>{obs.type || obs.code || '-'}</td>
74
+ <td style={tdStyle}>
75
+ {obs.measurement ? `${obs.measurement.value} ${obs.measurement.unit}` : obs.qualitativeResult || '-'}
76
+ </td>
77
+ <td style={tdStyle}>{obs.category || '-'}</td>
78
+ <td style={tdStyle}>{obs.status || '-'}</td>
79
+ </tr>
80
+ )
81
+ })}
82
+ </tbody>
83
+ </table>
84
+ </div>
85
+ )}
86
+
87
+ {medRefs.length > 0 && (
88
+ <div style={{ marginBottom: 15 }}>
89
+ <Typography style={{ fontWeight: 'bold', marginBottom: 5 }}>Medications</Typography>
90
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
91
+ <thead>
92
+ <tr style={{ borderBottom: '2px solid #ccc', textAlign: 'left' }}>
93
+ <th style={tdStyle}>Medication</th>
94
+ <th style={tdStyle}>Dosage</th>
95
+ <th style={tdStyle}>Dispensing</th>
96
+ <th style={tdStyle}>Pharmacy</th>
97
+ <th style={tdStyle}>Prescriber</th>
98
+ <th style={tdStyle}>Date</th>
99
+ </tr>
100
+ </thead>
101
+ <tbody>
102
+ {medRefs.map((ref, i) => {
103
+ const med = findMedication(ref.id, { batch: true })
104
+ if (med === undefined) return (
105
+ <tr key={ref.id || i} style={{ borderBottom: '1px solid #eee' }}>
106
+ <td colSpan={6} style={tdStyle}>Loading...</td>
107
+ </tr>
108
+ )
109
+ if (med === null) return (
110
+ <tr key={ref.id || i} style={{ borderBottom: '1px solid #eee' }}>
111
+ <td colSpan={6} style={deletedStyle}>{ref.label} — Record no longer available</td>
112
+ </tr>
113
+ )
114
+ return (
115
+ <tr key={med.id || i} style={{ borderBottom: '1px solid #eee' }}>
116
+ <td style={tdStyle}>
117
+ {med.title || '-'}
118
+ {med.allergyNote ? <div style={{ color: 'red', fontSize: 12 }}>Allergies: {med.allergyNote}</div> : null}
119
+ {med.directions ? <div style={{ color: '#888', fontSize: 12 }}>Directions: {med.directions}</div> : null}
120
+ </td>
121
+ <td style={tdStyle}>
122
+ {med.dosage
123
+ ? med.dosage.description
124
+ ? med.dosage.description
125
+ : `${med.dosage.value || ''}${med.dosage.unit ? ` ${med.dosage.unit}` : ''}${med.dosage.quantity ? ` ${med.dosage.quantity} units` : ''}${med.dosage.frequency ? ` ${!isNaN(parseInt(med.dosage.frequency)) ? `${med.dosage.frequency}x ${med.dosage?.frequencyDescriptor ? `Per ${med.dosage.frequencyDescriptor}` : 'daily'}` : med.dosage.frequency}` : ''}`
126
+ : '-'}
127
+ </td>
128
+ <td style={tdStyle}>
129
+ {med.dispensing ? `${med.dispensing.quantity || ''} ${med.dispensing.unit || ''}`.trim() || '-' : '-'}
130
+ </td>
131
+ <td style={tdStyle}>{med.pharmacyName || med.pharmacyId || '-'}</td>
132
+ <td style={tdStyle}>
133
+ {med.prescriberName || '-'}
134
+ {med.source ? <div style={{ fontStyle: 'italic', fontSize: 12 }}>{med.source}</div> : null}
135
+ {med.notes ? <div style={{ fontSize: 12 }}>{med.notes}</div> : null}
136
+ </td>
137
+ <td style={tdStyle}>
138
+ {formatted_date(new Date(med.startedTakingAt || med.prescribedAt || med.createdAt))}
139
+ </td>
140
+ </tr>
141
+ )
142
+ })}
143
+ </tbody>
144
+ </table>
145
+ </div>
146
+ )}
147
+
148
+ {obsRefs.length === 0 && medRefs.length === 0 && (
149
+ <Typography style={{ fontStyle: 'italic', color: '#888' }}>No historical data recorded</Typography>
150
+ )}
151
+ </div>
152
+ )
153
+ }
154
+
26
155
  export const ResponseAnswer = ({ formResponse, fieldId, isHTML, answer: a, printing, onImageClick } : {
27
156
  answer: FormResponseValueAnswer,
28
157
  formResponse: FormResponse,
@@ -177,7 +306,9 @@ export const ResponseAnswer = ({ formResponse, fieldId, isHTML, answer: a, print
177
306
  </Typography>
178
307
  ) : (
179
308
  a.type === 'description'
180
- ? <></>
309
+ ? (a.value && typeof a.value === 'string' && a.value.startsWith('{'))
310
+ ? (() => { try { return <HistoricalDataSnapshotDisplay snapshot={JSON.parse(a.value)} /> } catch { return <></> } })()
311
+ : <></>
181
312
  : <Typography>No value provided</Typography>
182
313
  )
183
314
  )
@@ -236,7 +236,7 @@ export const QuestionForField = ({
236
236
  <div style={{ marginTop: 15 }}></div>
237
237
  }
238
238
 
239
- <Description field={field} style={{ fontSize: 16 }} enduserId={enduserId} />
239
+ <Description field={field} style={{ fontSize: 16 }} enduserId={enduserId} onFieldChange={onFieldChange} />
240
240
 
241
241
  {feedback.length > 0 &&
242
242
  <Flex column style={{ marginBottom: 11, marginTop: 3, }}>
@@ -320,7 +320,7 @@ export const QuestionForField = ({
320
320
  <String field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'string'>} form={form} />
321
321
  )
322
322
  : field.type === 'Appointment Booking' ? (
323
- <AppointmentBooking formResponseId={formResponseId} enduserId={enduserId} goToPreviousField={goToPreviousField} isPreviousDisabled={isPreviousDisabled} responses={responses} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Appointment Booking'>} form={form} />
323
+ <AppointmentBooking key={field.id} formResponseId={formResponseId} enduserId={enduserId} goToPreviousField={goToPreviousField} isPreviousDisabled={isPreviousDisabled} responses={responses} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Appointment Booking'>} form={form} />
324
324
  )
325
325
  : field.type === 'Stripe' ? (
326
326
  <Stripe enduserId={enduserId} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<any>} setCustomerId={setCustomerId} form={form} responses={responses} enduser={enduser} />
@@ -1017,7 +1017,7 @@ export const UpdateResponse = ({
1017
1017
  )
1018
1018
  }
1019
1019
 
1020
- const HistoricalDataSection = ({ sources, enduserId } : { sources: HistoricalDataSource[], enduserId: string }) => {
1020
+ const HistoricalDataSection = ({ sources, enduserId, onDataLoaded } : { sources: HistoricalDataSource[], enduserId: string, onDataLoaded?: (json: string) => void }) => {
1021
1021
  const session = useSession({ throwIfMissingContext: false })
1022
1022
  const [observations, setObservations] = useState<any[]>([])
1023
1023
  const [medications, setMedications] = useState<any[]>([])
@@ -1039,6 +1039,8 @@ const HistoricalDataSection = ({ sources, enduserId } : { sources: HistoricalDat
1039
1039
  setError('')
1040
1040
  try {
1041
1041
  const promises: Promise<void>[] = []
1042
+ let loadedObservations: any[] = []
1043
+ let loadedMedications: any[] = []
1042
1044
 
1043
1045
  for (const source of sources) {
1044
1046
  if (source.type === 'Observations') {
@@ -1047,7 +1049,7 @@ const HistoricalDataSection = ({ sources, enduserId } : { sources: HistoricalDat
1047
1049
  filter: { enduserId, ...source.filter },
1048
1050
  limit: source.limit,
1049
1051
  })
1050
- .then((obs: any[]) => setObservations(obs))
1052
+ .then((obs: any[]) => { loadedObservations = obs; setObservations(obs) })
1051
1053
  )
1052
1054
  } else if (source.type === 'Medications') {
1053
1055
  promises.push(
@@ -1055,12 +1057,35 @@ const HistoricalDataSection = ({ sources, enduserId } : { sources: HistoricalDat
1055
1057
  filter: { enduserId, status: { _ne: 'draft' }, ...source.filter },
1056
1058
  limit: source.limit,
1057
1059
  })
1058
- .then((meds: any[]) => setMedications(meds))
1060
+ .then((meds: any[]) => { loadedMedications = meds; setMedications(meds) })
1059
1061
  )
1060
1062
  }
1061
1063
  }
1062
1064
 
1063
1065
  await Promise.all(promises)
1066
+
1067
+ const obsLabel = (o: any) => {
1068
+ const name = o.type || o.code || ''
1069
+ const val = o.measurement ? `${o.measurement.value} ${o.measurement.unit}` : o.qualitativeResult || ''
1070
+ return `${name}${val ? ' ' + val : ''}`.slice(0, 50)
1071
+ }
1072
+ const medLabel = (m: any) => {
1073
+ const dose = m.dosage?.quantity ? ` ${m.dosage.quantity}${m.dosage.unit ? m.dosage.unit : ''}` : ''
1074
+ return `${m.title || ''}${dose}`.slice(0, 50)
1075
+ }
1076
+
1077
+ const MAX_SNAPSHOT_LENGTH = 24000
1078
+ let obsRefs = loadedObservations.map((o: any) => ({ id: o.id, label: obsLabel(o) }))
1079
+ let medRefs = loadedMedications.map((m: any) => ({ id: m.id, label: medLabel(m) }))
1080
+
1081
+ let json = JSON.stringify({ observations: obsRefs, medications: medRefs, snapshotAt: new Date().toISOString() })
1082
+ while (json.length > MAX_SNAPSHOT_LENGTH && (obsRefs.length + medRefs.length) > 0) {
1083
+ if (obsRefs.length >= medRefs.length) { obsRefs.pop() }
1084
+ else { medRefs.pop() }
1085
+ json = JSON.stringify({ observations: obsRefs, medications: medRefs, snapshotAt: new Date().toISOString() })
1086
+ }
1087
+
1088
+ onDataLoaded?.(json)
1064
1089
  } catch (err: any) {
1065
1090
  setError(err?.message || 'Failed to load historical data')
1066
1091
  } finally {
@@ -1183,7 +1208,7 @@ const HistoricalDataSection = ({ sources, enduserId } : { sources: HistoricalDat
1183
1208
  )
1184
1209
  }
1185
1210
 
1186
- export const Description = ({ field, color="primary", style, enduserId } : { field: FormField, color?: string, enduserId?: string } & Styled) => {
1211
+ export const Description = ({ field, color="primary", style, enduserId, onFieldChange } : { field: FormField, color?: string, enduserId?: string, onFieldChange?: (value: any, fieldId: string) => void } & Styled) => {
1187
1212
  const existingContent = (
1188
1213
  !field.htmlDescription && field.description ? (
1189
1214
  <Typography color={color as any} style={style}>
@@ -1203,6 +1228,7 @@ export const Description = ({ field, color="primary", style, enduserId } : { fie
1203
1228
  <HistoricalDataSection
1204
1229
  sources={field.options.historicalDataSources}
1205
1230
  enduserId={enduserId}
1231
+ onDataLoaded={onFieldChange ? (jsonString) => onFieldChange(jsonString, field.id) : undefined}
1206
1232
  />
1207
1233
  ) : null}
1208
1234
  </>
@@ -330,7 +330,7 @@ export const QuestionForField = ({
330
330
  <String field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'string'>} form={form} error={!!validationMessage && (!['A response is required', 'A value must be checked', 'A file is required', 'Enter a valid phone number', 'Insurer is required'].includes(validationMessage) || value.touched)} />
331
331
  )
332
332
  : field.type === 'Appointment Booking' ? (
333
- <AppointmentBooking formResponseId={formResponseId} enduserId={enduserId} goToPreviousField={goToPreviousField} isPreviousDisabled={isPreviousDisabled} responses={responses} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Appointment Booking'>} form={form} />
333
+ <AppointmentBooking key={field.id} formResponseId={formResponseId} enduserId={enduserId} goToPreviousField={goToPreviousField} isPreviousDisabled={isPreviousDisabled} responses={responses} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Appointment Booking'>} form={form} />
334
334
  )
335
335
  : field.type === 'Stripe' ? (
336
336
  <Stripe enduserId={enduserId} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<any>} setCustomerId={setCustomerId} form={form} responses={responses} enduser={enduser} />
@@ -2,7 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
2
2
  import { Session } from "@tellescope/sdk"
3
3
  import { ChangeHandler, FormFieldNode } from "./types"
4
4
  import { DatabaseRecord, Enduser, Form, FormField, FormResponse } from "@tellescope/types-client"
5
- import { phoneValidator } from "@tellescope/validation"
5
+ import { phoneValidator, is_valid_mm_dd_yyyy } from "@tellescope/validation"
6
6
  import { FileBlob, Indexable } from "@tellescope/types-utilities"
7
7
  import { CompoundFilter, EnduserRelationship, FormCustomization, FormFieldOptionDetails, FormResponseAnswerAddress, FormResponseAnswerFileValue, FormResponseValue, FormResponseValueAnswer, OrganizationTheme, PreviousFormCompoundLogic, PreviousFormFieldType, Timezone, TIMEZONES } from "@tellescope/types-models"
8
8
  import { WithTheme, contact_is_valid, useAddGTMTag, useFileUpload, useFormFields, useFormResponses, useResolvedSession, value_is_loaded } from "../index"
@@ -425,6 +425,7 @@ export const useOrganizationTheme = () => {
425
425
  return context?.theme ?? theme
426
426
  }
427
427
 
428
+ /** @deprecated Use is_valid_mm_dd_yyyy from @tellescope/validation instead — it validates days-in-month and leap years */
428
429
  export const isDateString = (_s='') => {
429
430
  const s = _s.trim()
430
431
 
@@ -442,7 +443,7 @@ export const isDateString = (_s='') => {
442
443
  // const [mm,dd,yyyy] = s.split('-').map(v => parseInt(v)) // don't shorthand, for radix argument of parseInt gets messed up
443
444
  // const d = Date.parse(`${yyyy}-${mm}-${dd}`) // this format should be explicitly supported by all implementations
444
445
  // if (isNaN(d)) return false
445
-
446
+
446
447
  return true
447
448
  }
448
449
  const isZIPString = (s='') => /^\d{5}$/.test(s) || /^\d{5}-\d{4}$/.test(s)
@@ -993,7 +994,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
993
994
  }
994
995
 
995
996
  if (value.answer.type === 'Insurance') {
996
- if (value.answer.value?.relationshipDetails?.dateOfBirth && !isDateString(value.answer.value.relationshipDetails.dateOfBirth)) {
997
+ if (value.answer.value?.relationshipDetails?.dateOfBirth && !is_valid_mm_dd_yyyy(value.answer.value.relationshipDetails.dateOfBirth)) {
997
998
  return "Enter date of birth in MM-DD-YYYY format"
998
999
  }
999
1000
  if (field.isOptional) return null
@@ -1240,7 +1241,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1240
1241
  }
1241
1242
  }
1242
1243
  } else if (value.answer.type === 'dateString') {
1243
- if (!isDateString(value.answer.value)) {
1244
+ if (!is_valid_mm_dd_yyyy(value.answer.value)) {
1244
1245
  return "Enter a date in MM-DD-YYYY format"
1245
1246
  }
1246
1247
  } else if (value.answer.type === 'multiple_choice' || value.answer.type === 'Dropdown') {
@@ -1291,7 +1292,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1291
1292
  for (const row of value.answer.value ?? []) {
1292
1293
  for (const cell of row) {
1293
1294
  const type = field.options?.tableChoices?.find(t => t.label === cell.label)?.type
1294
- if (type === 'Date' && !isDateString(cell.entry)) {
1295
+ if (type === 'Date' && !is_valid_mm_dd_yyyy(cell.entry)) {
1295
1296
  return `Enter a date in MM-DD-YYYY format for ${cell.label} in row ${(value.answer.value?.indexOf(row) ?? 0) + 1}`
1296
1297
  }
1297
1298
  }
@@ -1634,6 +1635,9 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1634
1635
  touched,
1635
1636
  isCalledOut: shouldCallout(fields?.find(f => f?.id === fieldId), value),
1636
1637
  isHighlightedOnTimeline: fields?.find(f => f?.id === fieldId)?.highlightOnTimeline,
1638
+ // description fields are never "active", so the normal updateInclusion effect won't fire for them
1639
+ // explicitly mark as included when they receive a non-empty string value (historical data snapshot)
1640
+ ...(field?.type === 'description' && typeof value === 'string' && value ? { includeInSubmit: true } : {}),
1637
1641
  answer: {
1638
1642
  ...r.answer,
1639
1643
  value: value as any,
package/src/state.tsx CHANGED
@@ -2637,6 +2637,43 @@ export const useIntegrations = (options={} as HookOptions<Integration>) => {
2637
2637
  )
2638
2638
  }
2639
2639
 
2640
+ export const useRedactedIntegrations = () => {
2641
+ const session = useSession()
2642
+ const [loadingState, setLoadingState] = useState<LoadedData<Integration[]>>({ status: LoadingStatus.Fetching, value: [] as any })
2643
+ const fetchedRef = useRef(false)
2644
+
2645
+ useEffect(() => {
2646
+ if (fetchedRef.current) return
2647
+ fetchedRef.current = true
2648
+
2649
+ let cancelled = false
2650
+ session.api.integrations.load_redacted({})
2651
+ .then((result: any) => {
2652
+ if (!cancelled) setLoadingState({ status: LoadingStatus.Loaded, value: result.integrations })
2653
+ })
2654
+ .catch((err: any) => {
2655
+ if (!cancelled) setLoadingState({ status: LoadingStatus.Error, value: err })
2656
+ })
2657
+ return () => { cancelled = true }
2658
+ }, [session])
2659
+
2660
+ const updateIntegration = useCallback(async (id: string, updates: Partial<Integration>) => {
2661
+ const result: any = await session.api.integrations.update_settings({ id, updates })
2662
+ setLoadingState((prev: LoadedData<Integration[]>) => {
2663
+ if (prev.status !== LoadingStatus.Loaded) return prev
2664
+ return {
2665
+ ...prev,
2666
+ value: prev.value.map((i: Integration) => i.id === id ? { ...i, ...result.integration } : i),
2667
+ }
2668
+ })
2669
+ }, [session])
2670
+
2671
+ return [
2672
+ loadingState,
2673
+ { updateIntegration },
2674
+ ] as const
2675
+ }
2676
+
2640
2677
  export const usePortalCustomizations = (options={} as HookOptions<PortalCustomization>) => {
2641
2678
  const session = useResolvedSession()
2642
2679
  return useListStateHook(