@tellescope/react-components 1.167.1 → 1.169.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 (70) hide show
  1. package/lib/cjs/Forms/form_responses.js +1 -1
  2. package/lib/cjs/Forms/form_responses.js.map +1 -1
  3. package/lib/cjs/Forms/forms.d.ts +9 -2
  4. package/lib/cjs/Forms/forms.d.ts.map +1 -1
  5. package/lib/cjs/Forms/forms.js +50 -42
  6. package/lib/cjs/Forms/forms.js.map +1 -1
  7. package/lib/cjs/Forms/hooks.d.ts +7 -0
  8. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  9. package/lib/cjs/Forms/hooks.js +96 -68
  10. package/lib/cjs/Forms/hooks.js.map +1 -1
  11. package/lib/cjs/Forms/inputs.d.ts +3 -2
  12. package/lib/cjs/Forms/inputs.d.ts.map +1 -1
  13. package/lib/cjs/Forms/inputs.js +70 -24
  14. package/lib/cjs/Forms/inputs.js.map +1 -1
  15. package/lib/cjs/Forms/types.d.ts +7 -0
  16. package/lib/cjs/Forms/types.d.ts.map +1 -1
  17. package/lib/cjs/Forms/wysiwyg.d.ts +12 -0
  18. package/lib/cjs/Forms/wysiwyg.d.ts.map +1 -0
  19. package/lib/cjs/Forms/wysiwyg.js +225 -0
  20. package/lib/cjs/Forms/wysiwyg.js.map +1 -0
  21. package/lib/cjs/inputs.d.ts +1 -1
  22. package/lib/cjs/inputs.d.ts.map +1 -1
  23. package/lib/cjs/state.d.ts.map +1 -1
  24. package/lib/cjs/state.js +4 -0
  25. package/lib/cjs/state.js.map +1 -1
  26. package/lib/cjs/table.js +1 -1
  27. package/lib/cjs/table.js.map +1 -1
  28. package/lib/esm/Forms/form_responses.js +1 -1
  29. package/lib/esm/Forms/form_responses.js.map +1 -1
  30. package/lib/esm/Forms/forms.d.ts +10 -3
  31. package/lib/esm/Forms/forms.d.ts.map +1 -1
  32. package/lib/esm/Forms/forms.js +51 -43
  33. package/lib/esm/Forms/forms.js.map +1 -1
  34. package/lib/esm/Forms/hooks.d.ts +7 -0
  35. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  36. package/lib/esm/Forms/hooks.js +96 -68
  37. package/lib/esm/Forms/hooks.js.map +1 -1
  38. package/lib/esm/Forms/inputs.d.ts +4 -3
  39. package/lib/esm/Forms/inputs.d.ts.map +1 -1
  40. package/lib/esm/Forms/inputs.js +68 -23
  41. package/lib/esm/Forms/inputs.js.map +1 -1
  42. package/lib/esm/Forms/inputs.native.d.ts +0 -1
  43. package/lib/esm/Forms/inputs.native.d.ts.map +1 -1
  44. package/lib/esm/Forms/types.d.ts +7 -0
  45. package/lib/esm/Forms/types.d.ts.map +1 -1
  46. package/lib/esm/Forms/wysiwyg.d.ts +12 -0
  47. package/lib/esm/Forms/wysiwyg.d.ts.map +1 -0
  48. package/lib/esm/Forms/wysiwyg.js +218 -0
  49. package/lib/esm/Forms/wysiwyg.js.map +1 -0
  50. package/lib/esm/controls.d.ts +2 -2
  51. package/lib/esm/inputs.d.ts +1 -1
  52. package/lib/esm/inputs.d.ts.map +1 -1
  53. package/lib/esm/state.d.ts.map +1 -1
  54. package/lib/esm/state.js +4 -0
  55. package/lib/esm/state.js.map +1 -1
  56. package/lib/esm/table.js +1 -1
  57. package/lib/esm/table.js.map +1 -1
  58. package/lib/esm/theme.native.d.ts +0 -1
  59. package/lib/esm/theme.native.d.ts.map +1 -1
  60. package/lib/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +13 -9
  62. package/src/Forms/form_responses.tsx +1 -1
  63. package/src/Forms/forms.tsx +20 -2
  64. package/src/Forms/hooks.tsx +43 -26
  65. package/src/Forms/inputs.tsx +32 -7
  66. package/src/Forms/types.ts +4 -0
  67. package/src/Forms/wysiwyg.tsx +234 -0
  68. package/src/inputs.tsx +1 -1
  69. package/src/state.tsx +1 -0
  70. package/src/table.tsx +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/react-components",
3
- "version": "1.167.1",
3
+ "version": "1.169.0",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -47,23 +47,27 @@
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.167.1",
51
- "@tellescope/sdk": "^1.167.1",
52
- "@tellescope/types-client": "^1.167.1",
53
- "@tellescope/types-models": "^1.167.1",
54
- "@tellescope/types-utilities": "^1.167.1",
55
- "@tellescope/utilities": "^1.167.1",
56
- "@tellescope/validation": "^1.167.1",
50
+ "@tellescope/constants": "^1.169.0",
51
+ "@tellescope/sdk": "^1.169.0",
52
+ "@tellescope/types-client": "^1.169.0",
53
+ "@tellescope/types-models": "^1.169.0",
54
+ "@tellescope/types-utilities": "^1.169.0",
55
+ "@tellescope/utilities": "^1.169.0",
56
+ "@tellescope/validation": "^1.169.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",
60
+ "draft-js": "^0.11.7",
61
+ "draftjs-to-html": "^0.9.1",
60
62
  "eslint": "^7.32.0",
61
63
  "eslint-plugin-react": "^7.26.1",
62
64
  "formik": "^2.2.9",
63
65
  "heic2any": "^0.0.4",
66
+ "html-to-draftjs": "^1.5.0",
64
67
  "nodemon": "^2.0.13",
65
68
  "react-beautiful-dnd": "^13.1.1",
66
69
  "react-datepicker": "^3.4.1",
70
+ "react-draft-wysiwyg": "^1.15.0",
67
71
  "react-draggable": "^4.4.6",
68
72
  "react-dropzone": "^11.4.2",
69
73
  "react-ga4": "^1.4.1",
@@ -80,7 +84,7 @@
80
84
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
81
85
  "react-native": "^0.65.0 || ^0.66.0 || ^0.67.0 || ^0.68.0 || ^0.71.0"
82
86
  },
83
- "gitHead": "3d83827d046d02c02f29f947abf4e447da024553",
87
+ "gitHead": "038ccd6e24a110a290ce7b0b6333ec2593817204",
84
88
  "publishConfig": {
85
89
  "access": "public"
86
90
  }
@@ -31,7 +31,7 @@ export const ResponseAnswer = ({ formResponse, fieldId, isHTML, answer: a, print
31
31
  onImageClick?: (args: { src: string }) => void,
32
32
  isHTML?: boolean,
33
33
  }) => (
34
- (isHTML && typeof a.value === 'string')
34
+ ((isHTML || a.type === 'Rich Text') && typeof a.value === 'string')
35
35
  ? <div dangerouslySetInnerHTML={{ __html: remove_script_tags(a.value) }} />
36
36
  : a.value
37
37
  ? (
@@ -1,8 +1,8 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
- import { Button, CircularProgress, Flex, LinearProgress, LoadingButton, Modal, Paper, Styled, Typography, form_display_text_for_language, useFileUpload, useFormResponses, useSession } from "../index"
2
+ import { Button, CircularProgress, FileBlob, FileUploadHandler, Flex, LinearProgress, LoadingButton, Modal, Paper, Styled, Typography, form_display_text_for_language, useFileUpload, useFormResponses, useSession } from "../index"
3
3
  import { useListForFormFields, useOrganizationTheme, useTellescopeForm, WithOrganizationTheme, Response, FileResponse, NextFieldLogicOptions } from "./hooks"
4
4
  import { ChangeHandler, FormInputs } from "./types"
5
- import { AddressInput, AllergiesInput, AppointmentBookingInput, ConditionsInput, DatabaseSelectInput, DateInput, DateStringInput, DropdownInput, EmailInput, EmotiiInput, FileInput, FilesInput, HeightInput, HiddenValueInput, InsuranceInput, LanguageSelect, MedicationsInput, MultipleChoiceInput, NumberInput, PhoneInput, Progress, RankingInput, RatingInput, RedirectInput, RelatedContactsInput, SignatureInput, StringInput, StringLongInput, StripeInput, TableInput, TimeInput, defaultButtonStyles } from "./inputs"
5
+ import { AddressInput, AllergiesInput, AppointmentBookingInput, ConditionsInput, DatabaseSelectInput, DateInput, DateStringInput, DropdownInput, EmailInput, EmotiiInput, FileInput, FilesInput, HeightInput, HiddenValueInput, InsuranceInput, LanguageSelect, MedicationsInput, MultipleChoiceInput, NumberInput, PhoneInput, Progress, RankingInput, RatingInput, RedirectInput, RelatedContactsInput, RichTextInput, SignatureInput, StringInput, StringLongInput, StripeInput, TableInput, TimeInput, defaultButtonStyles } from "./inputs"
6
6
  import { PRIMARY_HEX } from "@tellescope/constants"
7
7
  import { FormResponse, FormField, Form, Enduser } from "@tellescope/types-client"
8
8
  import { FormResponseAnswerFileValue, OrganizationTheme } from "@tellescope/types-models"
@@ -135,6 +135,7 @@ export const QuestionForField = ({
135
135
  rootResponseId,
136
136
  isInQuestionGroup,
137
137
  logicOptions,
138
+ uploadingFiles, setUploadingFiles, handleFileUpload,
138
139
  } : {
139
140
  spacing?: number,
140
141
  form?: Form,
@@ -147,6 +148,9 @@ export const QuestionForField = ({
147
148
  isSinglePage?: boolean,
148
149
  isInQuestionGroup?: boolean,
149
150
  logicOptions?: NextFieldLogicOptions,
151
+ handleFileUpload: (blob: FileBlob, fieldId: string) => Promise<any>,
152
+ uploadingFiles: { fieldId: string }[]
153
+ setUploadingFiles: React.Dispatch<React.SetStateAction<{ fieldId: string }[]>>,
150
154
  } & Pick<TellescopeFormProps, "rootResponseId" | "goToNextField" | "groupId" | "groupInstance" | "submit" | "formResponseId" | 'enduserId' | 'isPreviousDisabled' | 'goToPreviousField' | 'enduser' | 'handleDatabaseSelect' | 'onAddFile' | 'onFieldChange' | 'fields' | 'customInputs' | 'responses' | 'selectedFiles' | 'validateField'>) => {
151
155
  const String = customInputs?.['string'] ?? StringInput
152
156
  const StringLong = customInputs?.['stringLong'] ?? StringLongInput
@@ -175,6 +179,7 @@ export const QuestionForField = ({
175
179
  const Emotii = customInputs?.['Emotii'] ?? EmotiiInput
176
180
  const Allergies = customInputs?.['Allergies'] ?? AllergiesInput
177
181
  const Conditions = customInputs?.['Conditions'] ?? ConditionsInput
182
+ const RichText = customInputs?.['Rich Text'] ?? RichTextInput
178
183
 
179
184
  const validationMessage = validateField(field)
180
185
 
@@ -233,6 +238,7 @@ export const QuestionForField = ({
233
238
  ? value.answer.value?.name
234
239
  : ''
235
240
  }
241
+ handleFileUpload={handleFileUpload} uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
236
242
  />
237
243
  )
238
244
  : field.type === 'files' ? (
@@ -242,6 +248,7 @@ export const QuestionForField = ({
242
248
  // ? value.answer.value?.name
243
249
  // : ''
244
250
  // }
251
+ handleFileUpload={handleFileUpload} uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
245
252
  />
246
253
  )
247
254
  : field.type === 'dateString' ? (
@@ -283,6 +290,9 @@ export const QuestionForField = ({
283
290
  : field.type === 'stringLong' ? (
284
291
  <StringLong field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'string' | 'stringLong'>} form={form} />
285
292
  )
293
+ : field.type === 'Rich Text' ? (
294
+ <RichText field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Rich Text'>} form={form} />
295
+ )
286
296
  : field.type === 'email' ? (
287
297
  <Email field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'email'>} form={form} />
288
298
  )
@@ -358,6 +368,8 @@ export const QuestionForField = ({
358
368
  spacing={field.options?.groupPadding}
359
369
  logicOptions={logicOptions}
360
370
  isInQuestionGroup
371
+ uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
372
+ handleFileUpload={handleFileUpload}
361
373
  />
362
374
  </Flex>
363
375
  )
@@ -443,6 +455,7 @@ export const TellescopeSingleQuestionFlow: typeof TellescopeForm = ({
443
455
  groupId,
444
456
  groupInstance,
445
457
  logicOptions,
458
+ uploadingFiles, setUploadingFiles, handleFileUpload,
446
459
  }) => {
447
460
  const beforeunloadHandler = React.useCallback((e: BeforeUnloadEvent) => {
448
461
  try {
@@ -541,6 +554,8 @@ export const TellescopeSingleQuestionFlow: typeof TellescopeForm = ({
541
554
  validateField={validateField}
542
555
  groupId={groupId} groupInstance={groupInstance}
543
556
  logicOptions={logicOptions}
557
+ uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
558
+ handleFileUpload={handleFileUpload}
544
559
  />
545
560
  </Flex>
546
561
  </Flex>
@@ -942,6 +957,7 @@ export const TellescopeSinglePageForm: React.JSXElementConstructor<TellescopeFor
942
957
  enduser,
943
958
  groupId,
944
959
  groupInstance,
960
+ uploadingFiles, setUploadingFiles, handleFileUpload,
945
961
  ...props
946
962
  }) => {
947
963
  const list = useListForFormFields(fields, responses, { form: props.form, gender: enduser?.gender })
@@ -1019,6 +1035,8 @@ export const TellescopeSinglePageForm: React.JSXElementConstructor<TellescopeFor
1019
1035
  responses={responses} selectedFiles={selectedFiles}
1020
1036
  validateField={validateField}
1021
1037
  groupId={groupId} groupInstance={groupInstance}
1038
+ uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
1039
+ handleFileUpload={handleFileUpload}
1022
1040
  />
1023
1041
  </Flex>
1024
1042
  </Flex>
@@ -531,6 +531,7 @@ export const useTellescopeForm = ({ isPublicForm, form, urlLogicValue, customiza
531
531
  const [submittingStatus, setSubmittingStatus] = useState<SubmitStatus>(undefined)
532
532
  const [submitErrorMessage, setSubmitErrorMessage] = useState('')
533
533
  const [currentPageIndex, setCurrentPageIndex] = useState(0)
534
+ const [uploadingFiles, setUploadingFiles] = useState<{ fieldId: string }[]>([])
534
535
  const prevFieldStackRef = useRef<typeof root[]>([])
535
536
 
536
537
  const [repeats, setRepeats] = useState({} as Record<string, string | number>)
@@ -815,6 +816,10 @@ export const useTellescopeForm = ({ isPublicForm, form, urlLogicValue, customiza
815
816
  return null
816
817
  }
817
818
 
819
+ if (value.answer?.type === 'Rich Text' && value.answer?.value?.trim() === '<p></p>' && !field.isOptional) {
820
+ return "Answer is required"
821
+ }
822
+
818
823
  if (value.answer.type === 'Insurance') {
819
824
  if (value.answer.value?.relationshipDetails?.dateOfBirth && !isDateString(value.answer.value.relationshipDetails.dateOfBirth)) {
820
825
  return "Enter date of birth in MM-DD-YYYY format"
@@ -1102,6 +1107,31 @@ export const useTellescopeForm = ({ isPublicForm, form, urlLogicValue, customiza
1102
1107
  return responsesToSubmit
1103
1108
  }, [responses])
1104
1109
 
1110
+ const handleFileUpload = useCallback(async (blob: FileBlob, fieldId: string) => {
1111
+ const responseIndex = responses.findIndex(f => f.fieldId === fieldId)
1112
+ const result: FormResponseAnswerFileValue = { name: blob.name, secureName: '' }
1113
+ const { secureName } = await handleUpload(
1114
+ {
1115
+ name: blob.name,
1116
+ size: blob.size,
1117
+ type: blob.type,
1118
+ enduserId,
1119
+ },
1120
+ blob
1121
+ )
1122
+
1123
+ if (responses[responseIndex].answer.type === 'files') {
1124
+ if (!responses[responseIndex].answer.value) {
1125
+ responses[responseIndex].answer.value = []
1126
+ }
1127
+ (responses[responseIndex].answer.value as any[]).push({
1128
+ ...result, type: blob.type, secureName, name: result.name ?? ''
1129
+ })
1130
+ } else {
1131
+ responses[responseIndex].answer.value = { ...result, type: blob.type, secureName, name: result.name ?? '' }
1132
+ }
1133
+ }, [responses, handleUpload])
1134
+
1105
1135
  const submit = useCallback(async (options?: { onPreRedirect?: () => void, onFileUploadsDone?: () => void, onSuccess?: (r: FormResponse) => void, includedFieldIds?: string[], otherEnduserIds?: string[], onBulkErrors?: (errors: { enduserId: string, message: string }[]) => void }) => {
1106
1136
  setSubmitErrorMessage('')
1107
1137
  const hasFile = selectedFiles.find(f => !!f.blobs?.length) !== undefined
@@ -1113,31 +1143,14 @@ export const useTellescopeForm = ({ isPublicForm, form, urlLogicValue, customiza
1113
1143
  for (const blobInfo of selectedFiles) {
1114
1144
  const { blobs, fieldId } = blobInfo
1115
1145
  if (!blobs) continue
1116
-
1146
+
1147
+ const responseIndex = responses.findIndex(f => f.fieldId === fieldId)
1148
+
1149
+ const response = responses[responseIndex]
1150
+ if (response.field?.options?.autoUploadFiles) { continue } // must have uploaded prior to submission
1151
+
1117
1152
  for (const blob of blobs) {
1118
- const result: FormResponseAnswerFileValue = { name: blob.name, secureName: '' }
1119
- const { secureName } = await handleUpload(
1120
- {
1121
- name: blob.name,
1122
- size: blob.size,
1123
- type: blob.type,
1124
- enduserId,
1125
- },
1126
- blob
1127
- )
1128
-
1129
- const responseIndex = responses.findIndex(f => f.fieldId === fieldId)
1130
-
1131
- if (responses[responseIndex].answer.type === 'files') {
1132
- if (!responses[responseIndex].answer.value) {
1133
- responses[responseIndex].answer.value = []
1134
- }
1135
- (responses[responseIndex].answer.value as any[]).push({
1136
- ...result, type: blob.type, secureName, name: result.name ?? ''
1137
- })
1138
- } else {
1139
- responses[responseIndex].answer.value = { ...result, type: blob.type, secureName, name: result.name ?? '' }
1140
- }
1153
+ await handleFileUpload(blob, fieldId)
1141
1154
  }
1142
1155
  }
1143
1156
 
@@ -1260,9 +1273,11 @@ export const useTellescopeForm = ({ isPublicForm, form, urlLogicValue, customiza
1260
1273
  } finally {
1261
1274
  setSubmittingStatus(undefined)
1262
1275
  }
1263
- }, [accessCode, automationStepId, enduserId, responses, selectedFiles, session, handleUpload, existingResponses, ga4measurementId, rootResponseId, parentResponseId, calendarEventId, goBackURL, logicOptions])
1276
+ }, [accessCode, automationStepId, enduserId, responses, selectedFiles, session, handleUpload, existingResponses, ga4measurementId, rootResponseId, parentResponseId, calendarEventId, goBackURL, logicOptions, handleFileUpload])
1264
1277
 
1265
1278
  const isNextDisabled = useCallback(() => {
1279
+ if (uploadingFiles.length) { return true }
1280
+
1266
1281
  if (activeField.children.length === 0) {
1267
1282
  return true
1268
1283
  }
@@ -1272,7 +1287,7 @@ export const useTellescopeForm = ({ isPublicForm, form, urlLogicValue, customiza
1272
1287
  }
1273
1288
 
1274
1289
  return false
1275
- }, [activeField, validateField])
1290
+ }, [activeField, validateField, uploadingFiles])
1276
1291
 
1277
1292
  const autoAdvanceRef = useRef(false)
1278
1293
  const goToNextField = useCallback(() => {
@@ -1445,5 +1460,7 @@ export const useTellescopeForm = ({ isPublicForm, form, urlLogicValue, customiza
1445
1460
  customization,
1446
1461
  handleDatabaseSelect,
1447
1462
  logicOptions,
1463
+ uploadingFiles, setUploadingFiles,
1464
+ handleFileUpload,
1448
1465
  }
1449
1466
  }
@@ -24,6 +24,7 @@ import LanguageIcon from '@mui/icons-material/Language';
24
24
  import { Elements, PaymentElement, useStripe, useElements, EmbeddedCheckout, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js';
25
25
  import { loadStripe } from '@stripe/stripe-js';
26
26
  import { CheckCircleOutline, Delete, Edit } from "@mui/icons-material"
27
+ import { WYSIWYG } from "./wysiwyg"
27
28
 
28
29
  export const LanguageSelect = ({ value, ...props }: { value: string, onChange: (s: string) => void}) => (
29
30
  <Grid container alignItems="center" justifyContent={"center"} wrap="nowrap" spacing={1}>
@@ -1185,7 +1186,7 @@ export async function convertHEIC (file: FileBlob | string){
1185
1186
  };
1186
1187
 
1187
1188
  const value_is_image = (f?: { type?: string })=> f?.type?.includes('image')
1188
- export const FileInput = ({ value, onChange, field, existingFileName }: FormInputProps<'file'> & { existingFileName?: string }) => {
1189
+ export const FileInput = ({ value, onChange, field, existingFileName, uploadingFiles, handleFileUpload, setUploadingFiles }: FormInputProps<'file'> & { existingFileName?: string }) => {
1189
1190
  const [error, setError] = useState('')
1190
1191
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
1191
1192
  onDrop: useCallback(
@@ -1202,7 +1203,16 @@ export const FileInput = ({ value, onChange, field, existingFileName }: FormInpu
1202
1203
 
1203
1204
  setError('')
1204
1205
  onChange(file, field.id)
1205
- }, [onChange, field.options?.validFileTypes]
1206
+
1207
+ if (field.options?.autoUploadFiles && handleFileUpload) {
1208
+ setUploadingFiles?.(fs => [...fs, { fieldId: field.id }])
1209
+
1210
+ handleFileUpload(file, field.id)
1211
+ .finally(
1212
+ () => setUploadingFiles?.(fs => fs.filter(f => f.fieldId !== field.id))
1213
+ )
1214
+ }
1215
+ }, [onChange, field.options?.validFileTypes, handleFileUpload, setUploadingFiles]
1206
1216
  ),
1207
1217
  })
1208
1218
 
@@ -1221,7 +1231,9 @@ export const FileInput = ({ value, onChange, field, existingFileName }: FormInpu
1221
1231
  }
1222
1232
  }, [value])
1223
1233
 
1224
- // console.log(document.createElement('input').capture )
1234
+ if (uploadingFiles?.find(f => f.fieldId === field.id)) {
1235
+ return <LinearProgress />
1236
+ }
1225
1237
  return (
1226
1238
  <Grid container direction="column">
1227
1239
  <Grid container {...getRootProps()} sx={{
@@ -1287,11 +1299,12 @@ export const safe_create_url = (file: any) => {
1287
1299
  }
1288
1300
  }
1289
1301
 
1290
- export const FilesInput = ({ value, onChange, field, existingFileName }: FormInputProps<'files'> & { existingFileName?: string }) => {
1302
+ export const FilesInput = ({ value, onChange, field, existingFileName, uploadingFiles, handleFileUpload, setUploadingFiles }: FormInputProps<'files'> & { existingFileName?: string }) => {
1291
1303
  const [error, setError] = useState('')
1292
1304
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
1293
1305
  onDrop: useCallback(
1294
- acceptedFiles => {
1306
+ async acceptedFiles => {
1307
+ setUploadingFiles?.(fs => [...fs, { fieldId: field.id }])
1295
1308
  for (const file of acceptedFiles) {
1296
1309
  if (field.options?.validFileTypes?.length) {
1297
1310
  const match = field.options.validFileTypes.find(t => file.type.includes(t.toLowerCase()))
@@ -1299,11 +1312,16 @@ export const FilesInput = ({ value, onChange, field, existingFileName }: FormInp
1299
1312
  return setError(`File must have type: ${field.options.validFileTypes.join(', ')}`)
1300
1313
  }
1301
1314
  }
1315
+
1316
+ if (field.options?.autoUploadFiles && handleFileUpload) {
1317
+ await handleFileUpload(file, field.id).catch(console.error)
1318
+ }
1302
1319
  }
1320
+ setUploadingFiles?.(fs => fs.filter(f => f.fieldId !== field.id))
1303
1321
 
1304
1322
  setError('')
1305
1323
  onChange([...(value ?? []), ...acceptedFiles], field.id)
1306
- }, [onChange, value, field.options?.validFileTypes]
1324
+ }, [onChange, value, field.options?.validFileTypes, handleFileUpload, setUploadingFiles]
1307
1325
  ),
1308
1326
  })
1309
1327
 
@@ -1313,6 +1331,9 @@ export const FilesInput = ({ value, onChange, field, existingFileName }: FormInp
1313
1331
  })
1314
1332
  ), [value])
1315
1333
 
1334
+ if (uploadingFiles?.find(f => f.fieldId === field.id)) {
1335
+ return <LinearProgress />
1336
+ }
1316
1337
  return (
1317
1338
  <Grid container direction="column">
1318
1339
  <Grid container {...getRootProps()} sx={{
@@ -3277,4 +3298,8 @@ export const ConditionsInput = ({ goToNextField, goToPreviousField, field, value
3277
3298
  }
3278
3299
  />
3279
3300
  )
3280
- }
3301
+ }
3302
+
3303
+ export const RichTextInput = ({ field, value, onChange }: FormInputProps<'Rich Text'>) => (
3304
+ <WYSIWYG initialHTML={value} onChange={v => onChange(v, field.id)} style={{ width: '100%' }} editorStyle={{ width: '100%' }} />
3305
+ )
@@ -3,6 +3,7 @@ import { DatabaseRecord, Enduser, Form, FormField } from "@tellescope/types-clie
3
3
  import { FileBlob, TreeNode } from "@tellescope/types-utilities";
4
4
  import { JSXElementConstructor } from "react";
5
5
  import { Response } from "./hooks";
6
+ import { FileUploadHandler } from "../inputs";
6
7
 
7
8
  export type FormFieldNode = TreeNode<FormField>
8
9
 
@@ -32,6 +33,9 @@ export interface FormInputProps<K extends keyof AnswerForType> {
32
33
  groupInsance?: string,
33
34
  disabled?: boolean,
34
35
  isSinglePage?: boolean,
36
+ handleFileUpload?: (blob: FileBlob, fieldId: string) => Promise<any>,
37
+ uploadingFiles?: { fieldId: string }[]
38
+ setUploadingFiles?: React.Dispatch<React.SetStateAction<{ fieldId: string }[]>>,
35
39
  }
36
40
 
37
41
  export type FormInputs = {
@@ -0,0 +1,234 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { Editor } from 'react-draft-wysiwyg';
3
+ import draftToHtml from 'draftjs-to-html';
4
+ import htmlToDraft from 'html-to-draftjs';
5
+ import { EditorState, ContentState, convertToRaw } from 'draft-js';
6
+ import { Paper } from '@mui/material';
7
+ import { Styled } from '../mui';
8
+ import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css";
9
+
10
+ const getToolbar = ({ hideEmoji } : { hideEmoji?: boolean }) => ({
11
+ // hide image and embedded in favor of custom sections
12
+ options: [
13
+ // 'inline',
14
+ // 'blockType',
15
+ 'fontSize',
16
+ // 'fontFamily',
17
+ 'list',
18
+ 'textAlign',
19
+ // 'colorPicker',
20
+ 'link',
21
+ ...(hideEmoji ? [] : ['emoji']),
22
+ // 'remove',
23
+ // 'history',
24
+ // 'image',
25
+ // 'embedded',
26
+ ],
27
+ inline: {
28
+ inDropdown: false,
29
+ className: undefined,
30
+ component: undefined,
31
+ dropdownClassName: undefined,
32
+ options: ['bold', 'italic', 'underline', 'strikethrough', 'monospace', 'superscript', 'subscript'],
33
+ // bold: { icon: bold, className: undefined },
34
+ // italic: { icon: italic, className: undefined },
35
+ // underline: { icon: underline, className: undefined },
36
+ // strikethrough: { icon: strikethrough, className: undefined },
37
+ // monospace: { icon: monospace, className: undefined },
38
+ // superscript: { icon: superscript, className: undefined },
39
+ // subscript: { icon: subscript, className: undefined },
40
+ },
41
+ blockType: {
42
+ inDropdown: true,
43
+ options: ['Normal', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Blockquote', 'Code'],
44
+ className: undefined,
45
+ component: undefined,
46
+ dropdownClassName: undefined,
47
+ },
48
+ fontSize: {
49
+ // icon: fontSize,
50
+ options: [8, 9, 10, 11, 12, 14, 16, 18, 24, 30, 36, 48, 60, 72, 96],
51
+ className: undefined,
52
+ component: undefined,
53
+ dropdownClassName: undefined,
54
+ },
55
+ fontFamily: {
56
+ options: ['Arial', 'Georgia', 'Impact', 'Tahoma', 'Times New Roman', 'Verdana'],
57
+ className: undefined,
58
+ component: undefined,
59
+ dropdownClassName: undefined,
60
+ },
61
+ list: {
62
+ inDropdown: false,
63
+ className: undefined,
64
+ component: undefined,
65
+ dropdownClassName: undefined,
66
+ options: ['unordered', 'ordered', 'indent', 'outdent'],
67
+ // unordered: { icon: unordered, className: undefined },
68
+ // ordered: { icon: ordered, className: undefined },
69
+ // indent: { icon: indent, className: undefined },
70
+ // outdent: { icon: outdent, className: undefined },
71
+ },
72
+ textAlign: {
73
+ inDropdown: false,
74
+ className: undefined,
75
+ component: undefined,
76
+ dropdownClassName: undefined,
77
+ options: ['left', 'center', 'right', 'justify'],
78
+ // left: { icon: left, className: undefined },
79
+ // center: { icon: center, className: undefined },
80
+ // right: { icon: right, className: undefined },
81
+ // justify: { icon: justify, className: undefined },
82
+ },
83
+ colorPicker: {
84
+ // icon: color,
85
+ className: undefined,
86
+ component: undefined,
87
+ popupClassName: undefined,
88
+ colors: ['rgb(97,189,109)', 'rgb(26,188,156)', 'rgb(84,172,210)', 'rgb(44,130,201)',
89
+ 'rgb(147,101,184)', 'rgb(71,85,119)', 'rgb(204,204,204)', 'rgb(65,168,95)', 'rgb(0,168,133)',
90
+ 'rgb(61,142,185)', 'rgb(41,105,176)', 'rgb(85,57,130)', 'rgb(40,50,78)', 'rgb(0,0,0)',
91
+ 'rgb(247,218,100)', 'rgb(251,160,38)', 'rgb(235,107,86)', 'rgb(226,80,65)', 'rgb(163,143,132)',
92
+ 'rgb(239,239,239)', 'rgb(255,255,255)', 'rgb(250,197,28)', 'rgb(243,121,52)', 'rgb(209,72,65)',
93
+ 'rgb(184,49,47)', 'rgb(124,112,107)', 'rgb(209,213,216)'],
94
+ },
95
+ link: {
96
+ inDropdown: false,
97
+ className: undefined,
98
+ component: undefined,
99
+ popupClassName: undefined,
100
+ dropdownClassName: undefined,
101
+ showOpenOptionOnHover: true,
102
+ defaultTargetOption: '_blank',
103
+ options: ['link', 'unlink'],
104
+ // link: { icon: link, className: undefined },
105
+ // unlink: { icon: unlink, className: undefined },
106
+ linkCallback: undefined
107
+ },
108
+ emoji: {
109
+ // icon: emoji,
110
+ className: undefined,
111
+ component: undefined,
112
+ popupClassName: undefined,
113
+ emojis: [
114
+ '😀', '😁', '😂', '😃', '😉', '😋', '😎', '😍', '😗', '🤗', '🤔', '😣', '😫', '😴', '😌', '🤓',
115
+ '😛', '😜', '😠', '😇', '😷', '😈', '👻', '😺', '😸', '😹', '😻', '😼', '😽', '🙀', '🙈',
116
+ '🙉', '🙊', '👼', '👮', '🕵', '💂', '👳', '🎅', '👸', '👰', '👲', '🙍', '🙇', '🚶', '🏃', '💃',
117
+ '⛷', '🏂', '🏌', '🏄', '🚣', '🏊', '⛹', '🏋', '🚴', '👫', '💪', '👈', '👉', '👉', '👆', '🖕',
118
+ '👇', '🖖', '🤘', '🖐', '👌', '👍', '👎', '✊', '👊', '👏', '🙌', '🙏', '🐵', '🐶', '🐇', '🐥',
119
+ '🐸', '🐌', '🐛', '🐜', '🐝', '🍉', '🍄', '🍔', '🍤', '🍨', '🍪', '🎂', '🍰', '🍾', '🍷', '🍸',
120
+ '🍺', '🌍', '🚑', '⏰', '🌙', '🌝', '🌞', '⭐', '🌟', '🌠', '🌨', '🌩', '⛄', '🔥', '🎄', '🎈',
121
+ '🎉', '🎊', '🎁', '🎗', '🏀', '🏈', '🎲', '🔇', '🔈', '📣', '🔔', '🎵', '🎷', '💰', '🖊', '📅',
122
+ '✅', '❎', '💯',
123
+ ],
124
+ },
125
+ embedded: {
126
+ // icon: embedded,
127
+ className: undefined,
128
+ component: undefined,
129
+ popupClassName: undefined,
130
+ embedCallback: undefined,
131
+ defaultSize: {
132
+ height: 'auto',
133
+ width: 'auto',
134
+ },
135
+ },
136
+ image: {
137
+ // icon: image,
138
+ className: undefined,
139
+ component: undefined,
140
+ popupClassName: undefined,
141
+ urlEnabled: true,
142
+ uploadEnabled: true,
143
+ alignmentEnabled: true,
144
+ // uploadCallback,
145
+ previewImage: true,
146
+ inputAccept: 'image/gif,image/jpeg,image/jpg,image/png,image/svg',
147
+ alt: { present: false, mandatory: false },
148
+ defaultSize: {
149
+ height: 'auto',
150
+ width: 'auto',
151
+ },
152
+ },
153
+ remove: {
154
+ // icon: eraser,
155
+ className: undefined, component: undefined
156
+ },
157
+ history: {
158
+ inDropdown: false,
159
+ className: undefined,
160
+ component: undefined,
161
+ dropdownClassName: undefined,
162
+ options: ['undo', 'redo'],
163
+ // undo: { icon: undo, className: undefined },
164
+ // redo: { icon: redo, className: undefined },
165
+ },
166
+ })
167
+
168
+
169
+
170
+ export const WYSIWYG = ({ updateHtml, initialHTML: _initialHTML='', autoFocus, onChange, style, editorStyle, hideEmoji } : {
171
+ initialHTML?: string, onChange: (html: string) => void, autoFocus?: boolean,
172
+ editorStyle?: React.CSSProperties,
173
+ hideEmoji?: boolean,
174
+ updateHtml?: string,
175
+ } & Styled) => {
176
+ const trimmed = _initialHTML.trim()
177
+ const initialHTML = (
178
+ trimmed.startsWith('<p>') && trimmed.endsWith('</p>')
179
+ ? trimmed
180
+ : `<p>${trimmed}</p>`
181
+ )
182
+
183
+ const [editorState, setEditorState] = useState(EditorState.createWithContent(
184
+ ContentState.createFromBlockArray(htmlToDraft(initialHTML).contentBlocks)
185
+ ))
186
+ const editorStateRef = useRef(editorState)
187
+
188
+ const editorRef: React.MutableRefObject<Editor | null> = useRef(null)
189
+
190
+ useEffect(() => {
191
+ if (!updateHtml) return
192
+
193
+ setEditorState(
194
+ EditorState.createWithContent(
195
+ ContentState.createFromBlockArray(htmlToDraft(updateHtml).contentBlocks)
196
+ )
197
+ )
198
+ }, [updateHtml])
199
+
200
+ useEffect(() => {
201
+ if (!autoFocus) return
202
+
203
+ if (editorRef.current) {
204
+ editorRef.current?.focusEditor?.()
205
+ setEditorState(es => EditorState.moveFocusToEnd(es))
206
+ }
207
+ }, [editorRef, autoFocus])
208
+
209
+ useEffect(() => {
210
+ if (editorStateRef.current === editorState) return
211
+ editorStateRef.current = editorState
212
+
213
+ // unbounce a bit for perf
214
+ const t = setTimeout(() => (
215
+ onChange(draftToHtml(convertToRaw(editorState.getCurrentContent())))
216
+ ), 99)
217
+
218
+ return () => { clearTimeout(t) }
219
+ }, [onChange, editorState])
220
+
221
+ const toolbar = useMemo(() => getToolbar({ hideEmoji }), [hideEmoji])
222
+
223
+ return (
224
+ <Paper sx={{ padding: 1 }} style={style}>
225
+ <Editor spellCheck ref={editorRef} editorStyle={editorStyle}
226
+ editorState={editorState}
227
+ wrapperClassName="demo-wrapper"
228
+ editorClassName="demo-editor"
229
+ onEditorStateChange={setEditorState}
230
+ toolbar={toolbar}
231
+ />
232
+ </Paper>
233
+ )
234
+ }
package/src/inputs.tsx CHANGED
@@ -95,7 +95,7 @@ export const useFileDropzone = ({ DropzoneComponent=DefaultDropzoneContent, styl
95
95
  }
96
96
  }
97
97
 
98
- type FileUploadHandler = (details: FileDetails & { externalId?: string, }, file: Blob | Buffer | ReactNativeFile, options?: {}) => Promise<FileClientType>
98
+ export type FileUploadHandler = (details: FileDetails & { externalId?: string, }, file: Blob | Buffer | ReactNativeFile, options?: {}) => Promise<FileClientType>
99
99
  interface UseFileUploaderOptions {
100
100
  enduserId?: string
101
101
  publicRead?: boolean,
package/src/state.tsx CHANGED
@@ -544,6 +544,7 @@ export const useDataSync____internal = () => {
544
544
  try {
545
545
  // not compatible with React Native
546
546
  if (typeof window === 'undefined') { return }
547
+ if (global?.navigator?.product === 'ReactNative') { return }
547
548
 
548
549
  const onMouseMove = () => { lastActiveForSync.at = new Date() }
549
550
  window.addEventListener('mousemove', onMouseMove)
package/src/table.tsx CHANGED
@@ -1038,7 +1038,7 @@ export const Table = <T extends Item>({
1038
1038
  onExport ? () => {
1039
1039
  onExport({
1040
1040
  // use items, not sorted, as sorted only includes first page when paginated
1041
- data: (paginated ? items : sorted).map(s => fields.map(f => f.getExportData?.(s) || '')),
1041
+ data: (paginated ? items : filtered).map(s => fields.map(f => f.getExportData?.(s) || '')),
1042
1042
  labels: fields.map(f => f.label)
1043
1043
  })
1044
1044
  }