Merge pull request #2102 from OpenSignLabs/staging

v2.36.0
This commit is contained in:
prafull-opensignlabs
2026-02-06 18:21:57 +05:30
committed by GitHub
61 changed files with 4548 additions and 33099 deletions
-16871
View File
File diff suppressed because it is too large Load Diff
+26 -26
View File
@@ -5,44 +5,44 @@
"dependencies": {
"@formkit/auto-animate": "^0.9.0",
"@imgly/background-removal": "^1.7.0",
"@lottiefiles/dotlottie-react": "^0.17.12",
"@lottiefiles/dotlottie-react": "^0.17.13",
"@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.2.1",
"@radix-ui/themes": "^3.3.0",
"@reactour/tour": "^3.8.0",
"@reduxjs/toolkit": "^2.11.2",
"axios": "^1.13.2",
"axios": "^1.13.4",
"body-scroll-lock": "^4.0.0-beta.0",
"date-fns-tz": "^3.2.0",
"file-saver": "^2.0.5",
"i18next": "^25.7.3",
"i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"parse": "^8.0.0",
"parse": "^8.0.3",
"pdf-lib": "^1.17.1",
"pkijs": "^3.3.3",
"print-js": "^1.6.0",
"prismjs": "^1.30.0",
"quill-html-edit-button": "^3.0.0",
"radix-ui": "^1.4.3",
"react": "^19.2.3",
"react": "^19.2.4",
"react-bootstrap": "^2.10.10",
"react-confetti": "^6.4.0",
"react-datepicker": "^9.1.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dnd-multi-backend": "^9.0.0",
"react-dom": "^19.2.3",
"react-dom": "^19.2.4",
"react-gtm-module": "^2.0.11",
"react-i18next": "^16.5.0",
"react-i18next": "^16.5.4",
"react-konva": "^19.2.1",
"react-pdf": "^10.2.0",
"react-quill-new": "^3.7.0",
"react-quill-new": "^3.8.3",
"react-redux": "^9.2.0",
"react-rnd": "^10.5.2",
"react-router": "^7.11.0",
"react-router": "^7.13.0",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.2",
"react-signature-canvas": "^1.1.0-alpha.2",
@@ -93,37 +93,37 @@
}
},
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/preset-env": "^7.28.3",
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/runtime-corejs2": "^7.28.3",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.1",
"@babel/runtime-corejs2": "^7.28.6",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.7",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/ui": "^4.0.16",
"autoprefixer": "^10.4.23",
"@types/react": "^19.2.10",
"@vitejs/plugin-react": "^5.1.3",
"@vitest/ui": "^4.0.18",
"autoprefixer": "^10.4.24",
"babel-loader": "^10.0.0",
"commitizen": "^4.3.1",
"concurrently": "^9.2.1",
"css-loader": "^7.1.2",
"css-loader": "^7.1.3",
"daisyui": "^4.12.24",
"eslint": "^9.39.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^27.4.0",
"jsdom": "^28.0.0",
"lint-staged": "^16.2.7",
"postcss": "^8.5.6",
"prettier": "^3.7.4",
"prettier": "^3.8.1",
"pretty-quick": "^4.2.2",
"rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.19",
"tmp": "^0.2.5",
"vite": "^7.3.0",
"vite": "^7.3.1",
"vite-plugin-svgr": "^4.5.0",
"vite-tsconfig-paths": "^6.0.3",
"vitest": "^4.0.16"
"vite-tsconfig-paths": "^6.0.5",
"vitest": "^4.0.18"
},
"overrides": {
"tmp": "$tmp"
@@ -224,7 +224,7 @@
"New Template": "Neue Vorlage",
"file-type": "pdf, png, jpg, jpeg",
"docx": "docx",
"file-selected": "Datei ausgewählt",
"files-selected": "Datei(en) ausgewählt",
"template-title": "Vorlagentitel",
"document-title": "Dokumenttitel",
"title": "Titel",
@@ -337,7 +337,7 @@
"quick-send-alert-1": "Alle Rollen in diesem Dokument sind derzeit mit Kontakten verknüpft. Um Kopien dieser Vorlage schnell an mehrere Unterzeichner zu senden, stellen Sie sicher, dass mindestens eine Rolle keinem Kontakt zugeordnet ist.",
"quick-send-alert-2": "Bitte stellen Sie sicher, dass für alle Empfänger mindestens ein Signatur-Widget hinzugefügt wurde.",
"quick-send-alert-3": "Bitte fügen Sie diesem template mindestens eine Rolle hinzu.",
"quick-send-alert-4": "Das Limit für Schnellsendungen wurde erreicht.",
"quick-send-alert-4": "Limit für {{flow}} erreicht.",
"copy-link": "Link kopieren",
"copy": "Kopieren",
"revoke-document": "Dokument widerrufen",
@@ -1340,5 +1340,25 @@
"date-pref-help-format": "Wählen Sie das standardmäßige Datumsformat für Datums-Widgets.",
"date-pref-help-default-date": "Legen Sie einen Standard-Datumswert für Datums-Widgets fest.",
"date-pref-help-signing-date": "Wenn aktiviert, verwendet das Datums-Widget automatisch das Datum, an dem der Unterzeichner das Dokument signiert.",
"date-pref-help-read-only": "Wenn aktiviert, ist das Datums-Widget für den Unterzeichner schreibgeschützt."
"date-pref-help-read-only": "Wenn aktiviert, ist das Datums-Widget für den Unterzeichner schreibgeschützt.",
"upload-csv": "CSV hochladen",
"file-selected": "Datei ausgewählt",
"bulk-send-csv-helper": "Verwenden Sie Header für Unterzeichnerrollen (Rollen-E-Mail/Name/Telefon) und Vorabfüllwerte (Vorabfüllung: Feldname). Erforderliche Vorabfüllfelder müssen ausgefüllt sein.",
"bulk-send-csv-helper-note": "Hinweis: Für Vorabfüllfelder vom Typ Datum in der CSV muss der Wert in folgendem Format angegeben werden: {{value}} (Beispiel: {{example}}).",
"bulk-send-csv-summary": "{{total}} Zeilen importiert. {{invalid}} ungültige Zeilen wurden übersprungen.",
"bulk-send-csv-missing-headers": "Erforderliche CSV-Header fehlen. Rollen: {{roles}}. Vorabfüllung: {{prefill}}.",
"bulk-send-no-records": "Bitte fügen Sie vor dem Senden mindestens einen Empfänger hinzu.",
"csv-only-support": "Es werden nur CSV-Dateien unterstützt.",
"csv-no-records": "Keine Datensätze in der CSV gefunden.",
"select-at-least-option": "Bitte wählen Sie mindestens eine Option aus.",
"draw-required": "Bitte zeichnen Sie in das Feld.",
"enter-smtp-username": "SMTP-Benutzernamen eingeben",
"enter-webhook-url": "Webhook-URL eingeben",
"enter-user-name": "Benutzernamen eingeben",
"enter-tagline": "Slogan eingeben",
"enter-password-or-click-autogenerate": "Passwort eingeben oder auf „Automatisch generieren“ klicken",
"enter-daily-email-quota": "Tägliches E-Mail-Kontingent eingeben",
"enter-custom-regular-expression": "Benutzerdefinierten regulären Ausdruck eingeben",
"enter-value": "{{value}} eingeben",
"are-you-sure-want-to-send-document": "Sind Sie sicher, dass Sie das Dokument senden möchten?"
}
@@ -224,7 +224,7 @@
"New Template": "New template",
"file-type": "pdf, png, jpg, jpeg",
"docx": "docx",
"file-selected": "file(s) selected",
"files-selected": "file(s) selected",
"template-title": "Template title",
"document-title": "Document title",
"title": "Title",
@@ -337,7 +337,7 @@
"quick-send-alert-1": "All roles in this document are currently linked to contacts. To quick send copies of this template to multiple signers, please ensure that at least one role is not linked to any contact.",
"quick-send-alert-2": "Please ensure there's at least one signature widget added for all recipients.",
"quick-send-alert-3": "Please add at least one role to this template.",
"quick-send-alert-4": "Quick send reached limit.",
"quick-send-alert-4": "Limit reached for {{flow}}.",
"copy-link": "Copy link",
"copy": "Copy",
"revoke-document": "Revoke document",
@@ -1341,5 +1341,25 @@
"date-pref-help-format": "Choose the default date format used for Date widgets.",
"date-pref-help-default-date": "Set a default date value for Date widgets.",
"date-pref-help-signing-date": "When enabled, the Date widget automatically uses the date the signer signs the document.",
"date-pref-help-read-only": "When enabled, the Date widget is read-only for the signer."
"date-pref-help-read-only": "When enabled, the Date widget is read-only for the signer.",
"upload-csv": "Upload CSV",
"file-selected": "file selected",
"bulk-send-csv-helper": "Use headers for signer roles (Role Email/Name/Phone) and prefill values (Prefill: Field Name). Required prefill fields must be completed.",
"bulk-send-csv-helper-note": "Note: For Date-type prefill fields in the CSV, the value must be provided in this format: {{value}} (example: {{example}}).",
"bulk-send-csv-summary": "Imported {{total}} rows. {{invalid}} invalid rows were skipped.",
"bulk-send-csv-missing-headers": "Missing required CSV headers. Roles: {{roles}}. Prefill: {{prefill}}.",
"bulk-send-no-records": "Please add at least one recipient before sending.",
"csv-only-support": "Only CSV files are supported.",
"csv-no-records": "No records found in the CSV.",
"select-at-least-option": "Please select at least one option.",
"draw-required": "Please draw in the box.",
"enter-smtp-username": "Enter your SMTP username",
"enter-webhook-url": "Enter webhook url",
"enter-user-name": "Enter user name",
"enter-tagline": "Enter tagline",
"enter-password-or-click-autogenerate": "Enter password or click Autogenerate",
"enter-daily-email-quota": "Enter daily email quota",
"enter-custom-regular-expression": "Enter custom regular expression",
"enter-value": "Enter {{value}}",
"are-you-sure-want-to-send-document": "Are you sure want to send document?"
}
@@ -224,7 +224,7 @@
"New Template": "Nueva plantilla",
"file-type": "pdf, png, jpg, jpeg",
"docx": "docx",
"file-selected": "archivo seleccionado",
"files-selected": "archivo(s) seleccionado(s)",
"template-title": "Título de la plantilla",
"document-title": "Título del documento",
"title": "Título",
@@ -337,7 +337,7 @@
"quick-send-alert-1": "Todos los roles de este documento están actualmente vinculados a contactos. Para enviar rápidamente copias de esta plantilla a varios firmantes, por favor, asegúrate de que al menos un rol no esté vinculado a ningún contacto.",
"quick-send-alert-2": "Por favor, asegúrate de que hay al menos un widget de firma añadido para cada destinatario.",
"quick-send-alert-3": "Veuillez ajouter au moins un rôle à ce template.",
"quick-send-alert-4": "El envío rápido ha alcanzado el límite.",
"quick-send-alert-4": "Se alcanzó el límite para {{flow}}.",
"copy-link": "Copiar enlace",
"copy": "Copiar",
"revoke-document": "Revocar documento",
@@ -1341,5 +1341,25 @@
"date-pref-help-format": "Elija el formato de fecha predeterminado utilizado para los widgets de fecha.",
"date-pref-help-default-date": "Establezca un valor de fecha predeterminado para los widgets de fecha.",
"date-pref-help-signing-date": "Cuando está habilitado, el widget de fecha utiliza automáticamente la fecha en que el firmante firma el documento.",
"date-pref-help-read-only": "Cuando está habilitado, el widget de fecha es de solo lectura para el firmante."
"date-pref-help-read-only": "Cuando está habilitado, el widget de fecha es de solo lectura para el firmante.",
"upload-csv": "Subir CSV",
"file-selected": "archivo seleccionado",
"bulk-send-csv-helper": "Use encabezados para los roles de firmantes (Correo/Nombre/Teléfono del rol) y valores de prellenado (Prellenado: Nombre del campo). Los campos de prellenado obligatorios deben completarse.",
"bulk-send-csv-helper-note": "Nota: Para los campos de prellenado de tipo fecha en el CSV, el valor debe proporcionarse en el siguiente formato: {{value}} (ejemplo: {{example}}).",
"bulk-send-csv-summary": "{{total}} filas importadas. {{invalid}} filas no válidas fueron omitidas.",
"bulk-send-csv-missing-headers": "Faltan encabezados obligatorios del CSV. Roles: {{roles}}. Prellenado: {{prefill}}.",
"bulk-send-no-records": "Agregue al menos un destinatario antes de enviar.",
"csv-only-support": "Solo se admiten archivos CSV.",
"csv-no-records": "No se encontraron registros en el CSV.",
"select-at-least-option": "Por favor, seleccione al menos una opción.",
"draw-required": "Por favor, dibuje en el campo.",
"enter-smtp-username": "Ingrese su nombre de usuario SMTP",
"enter-webhook-url": "Ingrese la URL del webhook",
"enter-user-name": "Ingrese el nombre de usuario",
"enter-tagline": "Ingrese el eslogan",
"enter-password-or-click-autogenerate": "Ingrese la contraseña o haga clic en Autogenerar",
"enter-daily-email-quota": "Ingrese la cuota diaria de correo electrónico",
"enter-custom-regular-expression": "Ingrese una expresión regular personalizada",
"enter-value": "Ingrese {{value}}",
"are-you-sure-want-to-send-document": "¿Está seguro de que desea enviar el documento?"
}
@@ -224,7 +224,7 @@
"New Template": "Nouveau modèle",
"file-type": "pdf, png, jpg, jpeg",
"docx": "docx",
"file-selected": "fichier sélectionné",
"files-selected": "fichier(s) sélectionné(s)",
"template-title": "Titre du modèle",
"document-title": "Titre du document",
"title": "Titre",
@@ -337,7 +337,7 @@
"quick-send-alert-1": "Tous les rôles dans ce document sont actuellement liés à des contacts. Pour envoyer rapidement des copies de ce modèle à plusieurs signataires, veuillez vous assurer qu'au moins un rôle n'est lié à aucun contact.",
"quick-send-alert-2": "Veuillez vous assurer qu'au moins un widget de signature est ajouté pour tous les destinataires.",
"quick-send-alert-3": "Veuillez ajouter au moins un rôle à ce template.",
"quick-send-alert-4": "L'envoi rapide a atteint la limite.",
"quick-send-alert-4": "Limite atteint pour {{flow}}.",
"copy-link": "Copier le lien",
"copy": "Copier",
"revoke-document": "Révoquer le document",
@@ -1340,5 +1340,25 @@
"date-pref-help-format": "Choisissez le format de date par défaut utilisé pour les widgets de date.",
"date-pref-help-default-date": "Définissez une valeur de date par défaut pour les widgets de date.",
"date-pref-help-signing-date": "Lorsqu'elle est activée, le widget de date utilise automatiquement la date à laquelle le signataire signe le document.",
"date-pref-help-read-only": "Lorsqu'elle est activée, le widget de date est en lecture seule pour le signataire."
"date-pref-help-read-only": "Lorsqu'elle est activée, le widget de date est en lecture seule pour le signataire.",
"upload-csv": "Téléverser un CSV",
"file-selected": "fichier sélectionné",
"bulk-send-csv-helper": "Utilisez des en-têtes pour les rôles des signataires (E-mail/Nom/Téléphone du rôle) et les valeurs de préremplissage (Préremplissage : Nom du champ). Les champs de préremplissage requis doivent être complétés.",
"bulk-send-csv-helper-note": "Remarque : Pour les champs de préremplissage de type date dans le CSV, la valeur doit être fournie dans le format suivant : {{value}} (exemple : {{example}}).",
"bulk-send-csv-summary": "{{total}} lignes importées. {{invalid}} lignes non valides ont été ignorées.",
"bulk-send-csv-missing-headers": "En-têtes CSV requis manquants. Rôles : {{roles}}. Préremplissage : {{prefill}}.",
"bulk-send-no-records": "Veuillez ajouter au moins un destinataire avant l'envoi.",
"csv-only-support": "Seuls les fichiers CSV sont pris en charge.",
"csv-no-records": "Aucun enregistrement trouvé dans le CSV.",
"select-at-least-option": "Veuillez sélectionner au moins une option.",
"draw-required": "Veuillez dessiner dans le champ.",
"enter-smtp-username": "Saisissez votre nom d'utilisateur SMTP",
"enter-webhook-url": "Saisissez l'URL du webhook",
"enter-user-name": "Saisissez le nom d'utilisateur",
"enter-tagline": "Saisissez le slogan",
"enter-password-or-click-autogenerate": "Saisissez le mot de passe ou cliquez sur Génération automatique",
"enter-daily-email-quota": "Saisissez le quota quotidien d'e-mails",
"enter-custom-regular-expression": "Saisissez une expression régulière personnalisée",
"enter-value": "Saisissez {{value}}",
"are-you-sure-want-to-send-document": "Êtes-vous sûr de vouloir envoyer le document ?"
}
@@ -224,7 +224,7 @@
"New Template": "नया टेम्पलेट",
"file-type": "pdf, png, jpg, jpeg",
"docx": "docx",
"file-selected": "फ़ाइल चयनित",
"files-selected": "फ़ाइलें चयनित",
"template-title": "टेम्पलेट शीर्षक",
"document-title": "दस्तावेज़ शीर्षक",
"title": "शीर्षक",
@@ -337,7 +337,7 @@
"quick-send-alert-1": "इस दस्तावेज़ में सभी भूमिकाएँ वर्तमान में संपर्कों से जुड़ी हुई हैं। इस टेम्पलेट की प्रतियां कई हस्ताक्षरकर्ताओं को त्वरित रूप से भेजने के लिए, कृपया सुनिश्चित करें कि कम से कम एक भूमिका किसी भी संपर्क से जुड़ी नहीं है।",
"quick-send-alert-2": "कृपया सुनिश्चित करें कि सभी प्राप्तकर्ताओं के लिए कम से कम एक हस्ताक्षर विजेट जोड़ा गया है।",
"quick-send-alert-3": "कृपया इस टेम्पलेट में कम से कम एक भूमिका जोड़ें।",
"quick-send-alert-4": "त्वरित भेजें सीमा तक पहुँच गया।",
"quick-send-alert-4": "{{flow}} के लिए सीमा पूरी हो गई है।",
"copy-link": "लिंक कॉपी करें",
"copy": "कॉपी करें",
"revoke-document": "दस्तावेज़ रद्द करें",
@@ -1340,5 +1340,25 @@
"date-pref-help-format": "डेट विजेट के लिए उपयोग किए जाने वाले डिफ़ॉल्ट तिथि प्रारूप का चयन करें।",
"date-pref-help-default-date": "डेट विजेट के लिए एक डिफ़ॉल्ट तिथि मान सेट करें।",
"date-pref-help-signing-date": "सक्षम होने पर, डेट विजेट स्वतः उस तिथि का उपयोग करता है जब साइनर दस्तावेज़ पर हस्ताक्षर करता है।",
"date-pref-help-read-only": "सक्षम होने पर, डेट विजेट साइनर के लिए केवल-पढ़ने योग्य होता है।"
"date-pref-help-read-only": "सक्षम होने पर, डेट विजेट साइनर के लिए केवल-पढ़ने योग्य होता है।",
"upload-csv": "CSV अपलोड करें",
"file-selected": "फ़ाइल चयनित",
"bulk-send-csv-helper": "साइनर भूमिकाओं (भूमिका ईमेल/नाम/फ़ोन) और प्रीफ़िल मानों (प्रीफ़िल: फ़ील्ड नाम) के लिए हेडर का उपयोग करें। आवश्यक प्रीफ़िल फ़ील्ड भरना अनिवार्य है।",
"bulk-send-csv-helper-note": "नोट: CSV में तिथि-प्रकार के प्रीफ़िल फ़ील्ड्स के लिए मान इस प्रारूप में प्रदान किया जाना चाहिए: {{value}} (उदाहरण: {{example}})।",
"bulk-send-csv-summary": "{{total}} पंक्तियाँ आयात की गईं। {{invalid}} अमान्य पंक्तियाँ छोड़ दी गईं।",
"bulk-send-csv-missing-headers": "आवश्यक CSV हेडर अनुपलब्ध हैं। भूमिकाएँ: {{roles}}। प्रीफ़िल: {{prefill}}।",
"bulk-send-no-records": "भेजने से पहले कृपया कम से कम एक प्राप्तकर्ता जोड़ें।",
"csv-only-support": "केवल CSV फ़ाइलें समर्थित हैं।",
"csv-no-records": "CSV में कोई रिकॉर्ड नहीं मिला।",
"select-at-least-option": "कृपया कम से कम एक विकल्प चुनें।",
"draw-required": "कृपया बॉक्स में ड्रॉ करें।",
"enter-smtp-username": "अपना SMTP उपयोगकर्ता नाम दर्ज करें",
"enter-webhook-url": "Webhook URL दर्ज करें",
"enter-user-name": "उपयोगकर्ता नाम दर्ज करें",
"enter-tagline": "टैगलाइन दर्ज करें",
"enter-password-or-click-autogenerate": "पासवर्ड दर्ज करें या ऑटो-जनरेट पर क्लिक करें",
"enter-daily-email-quota": "दैनिक ईमेल कोटा दर्ज करें",
"enter-custom-regular-expression": "कस्टम रेगुलर एक्सप्रेशन दर्ज करें",
"enter-value": "{{value}} दर्ज करें",
"are-you-sure-want-to-send-document": "क्या आप वाकई दस्तावेज़ भेजना चाहते हैं?"
}
@@ -224,7 +224,7 @@
"New Template": "Nuovo modello",
"file-type": "pdf, png, jpg, jpeg",
"docx": "docx",
"file-selected": "file selezionato",
"files-selected": "file selezionati",
"template-title": "Titolo del modello",
"document-title": "Titolo del documento",
"title": "Titolo",
@@ -337,7 +337,7 @@
"quick-send-alert-1": "Tutti i ruoli in questo documento sono attualmente collegati a contatti. Per inviare rapidamente copie di questo modello a più firmatari, assicurati che almeno un ruolo non sia collegato a nessun contatto.",
"quick-send-alert-2": "Assicurati che ci sia almeno un widget firma aggiunto per tutti i destinatari.",
"quick-send-alert-3": "Si prega di aggiungere almeno un ruolo a questo template.",
"quick-send-alert-4": "Limite di invio rapido raggiunto.",
"quick-send-alert-4": "Limite raggiunto per {{flow}}.",
"copy-link": "Copia link",
"copy": "Copia",
"revoke-document": "Revoca documento",
@@ -1340,5 +1340,25 @@
"date-pref-help-format": "Scegli il formato di data predefinito utilizzato per i widget Data.",
"date-pref-help-default-date": "Imposta un valore di data predefinito per i widget Data.",
"date-pref-help-signing-date": "Quando abilitato, il widget Data utilizza automaticamente la data in cui il firmatario firma il documento.",
"date-pref-help-read-only": "Quando abilitato, il widget Data è in sola lettura per il firmatario."
"date-pref-help-read-only": "Quando abilitato, il widget Data è in sola lettura per il firmatario.",
"upload-csv": "Carica CSV",
"file-selected": "file selezionato",
"bulk-send-csv-helper": "Utilizza le intestazioni per i ruoli dei firmatari (Email/Nome/Telefono del ruolo) e i valori di precompilazione (Precompilazione: Nome campo). I campi di precompilazione obbligatori devono essere compilati.",
"bulk-send-csv-helper-note": "Nota: Per i campi di precompilazione di tipo data nel CSV, il valore deve essere fornito in questo formato: {{value}} (esempio: {{example}}).",
"bulk-send-csv-summary": "{{total}} righe importate. {{invalid}} righe non valide sono state ignorate.",
"bulk-send-csv-missing-headers": "Intestazioni CSV obbligatorie mancanti. Ruoli: {{roles}}. Precompilazione: {{prefill}}.",
"bulk-send-no-records": "Aggiungi almeno un destinatario prima dell'invio.",
"csv-only-support": "Sono supportati solo file CSV.",
"csv-no-records": "Nessun record trovato nel CSV.",
"select-at-least-option": "Seleziona almeno un'opzione.",
"draw-required": "Per favore, disegna nel riquadro.",
"enter-smtp-username": "Inserisci il nome utente SMTP",
"enter-webhook-url": "Inserisci l'URL del webhook",
"enter-user-name": "Inserisci il nome utente",
"enter-tagline": "Inserisci lo slogan",
"enter-password-or-click-autogenerate": "Inserisci la password o fai clic su Genera automaticamente",
"enter-daily-email-quota": "Inserisci la quota giornaliera di e-mail",
"enter-custom-regular-expression": "Inserisci un'espressione regolare personalizzata",
"enter-value": "Inserisci {{value}}",
"are-you-sure-want-to-send-document": "Sei sicuro di voler inviare il documento?"
}
File diff suppressed because it is too large Load Diff
-318
View File
@@ -1,318 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
import SuggestionInput from "./shared/fields/SuggestionInput";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
import {
emailRegex,
} from "../constant/const";
const BulkSendUi = (props) => {
const { t } = useTranslation();
const [forms, setForms] = useState([]);
const formRef = useRef(null);
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
const [isSubmit, setIsSubmit] = useState(false);
const [isSignatureExist, setIsSignatureExist] = useState();
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
const [isLoader, setIsLoader] = useState(false);
const [signers, setSigners] = useState([]);
const [emails, setEmails] = useState([]);
const [isPrefillExist, setIsPrefillExist] = useState(false);
useEffect(() => {
signatureExist();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//function to check at least one signature field exist
const signatureExist = async () => {
const isPrefill =
props?.Placeholders?.some((x) => x?.Role === "prefill") || false;
if (isPrefill) {
setIsPrefillExist(isPrefill);
}
setIsDisableBulkSend(false);
const getPlaceholder = props?.Placeholders;
const removePrefill = getPlaceholder?.filter(
(x) => x?.Role !== "prefill"
);
const checkIsSignatureExistt = removePrefill?.every((placeholderObj) =>
placeholderObj?.placeHolder?.some((holder) =>
holder?.pos?.some((posItem) => posItem?.type === "signature")
)
);
setIsSignatureExist(checkIsSignatureExistt);
setIsLoader(false);
};
useEffect(() => {
if (scrollOnNextUpdate && formRef.current) {
formRef.current.scrollIntoView({
behavior: "smooth",
block: "end",
inline: "nearest"
});
setScrollOnNextUpdate(false);
}
}, [forms, scrollOnNextUpdate]);
useEffect(() => {
(() => {
if (props?.Placeholders?.length > 0) {
let users = [];
let emails = [];
props?.Placeholders?.forEach((element) => {
const signerEmail = element?.email || element?.signerPtr?.Email;
// only add when there's a non-empty signerEmail
if (signerEmail) {
emails = [...emails, signerEmail];
}
if (!element.signerObjId) {
users = [
...users,
{
fieldId: element.Id,
email: "",
label: element.Role,
signer: {}
}
];
}
});
setEmails(emails);
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
const signer = props.item?.Signers?.filter((x) => x?.objectId);
setSigners(signer);
}
})();
// eslint-disable-next-line
}, []);
const handleInputChange = (index, signer, fieldIndex) => {
const newForms = [...forms];
newForms[index].fields[fieldIndex].email = signer?.Email
? signer?.Email
: signer || "";
newForms[index].fields[fieldIndex].signer = signer?.objectId ? signer : "";
setForms(newForms);
};
const handleRemoveForm = (index) => {
const updatedForms = forms.filter((_, i) => i !== index);
setForms(updatedForms);
};
function validateEmails(data) {
for (const item of data) {
let email = "";
for (const field of item.fields) {
if (!emailRegex.test(field.email)) {
alert(t("invalid-email-found", { email: field.email }));
return false;
} else if (email === field.email || emails?.includes(field.email)) {
alert(t("duplicate-email-found", { email: field.email }));
return false;
} else {
email = field.email;
}
}
}
return true;
}
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
setIsSubmit(true);
if (validateEmails(forms)) {
// Create a copy of Placeholders array from props.item
let Placeholders = [...props.Placeholders];
// Initialize an empty array to store updated documents
let Documents = [];
// Loop through each form
forms.forEach((form) => {
//checking if user enter email which already exist as a signer then add user in a signers array
let existSigner = [];
form.fields.map((data) => {
if (data.signer) {
existSigner.push(data.signer);
}
});
// Map through the copied Placeholders array to update email values
const updatedPlaceholders = Placeholders.map((placeholder) => {
// Find the field in the current form that matches the placeholder Id
const field = form.fields.find(
(element) => parseInt(element.fieldId) === placeholder.Id
);
// If a matching field is found, update the email value in the placeholder
const signer = field?.signer?.objectId ? field.signer : "";
if (field) {
if (signer) {
return {
...placeholder,
signerObjId: field?.signer?.objectId || "",
signerPtr: signer
};
} else {
return {
...placeholder,
email: field.email,
signerObjId: field?.signer?.objectId || "",
signerPtr: signer
};
}
}
// If no matching field is found, keep the placeholder as is
return placeholder;
});
// Push a new document object with updated Placeholders into the Documents array
if (existSigner?.length > 0) {
Documents.push({
...props.item,
Placeholders: updatedPlaceholders,
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
});
} else {
Documents.push({
...props.item,
Placeholders: updatedPlaceholders,
SignatureType: props.signatureType,
Signers: signers
});
}
});
await batchQuery(Documents);
} else {
setIsSubmit(false);
}
};
const batchQuery = async (Documents) => {
const token =
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
const functionsUrl = `${localStorage.getItem("baseUrl")}functions/batchdocuments`;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
...token,
};
const params = { Documents: JSON.stringify(Documents) };
try {
const res = await axios.post(functionsUrl, params, { headers: headers });
if (res.data && res.data.result) {
props.handleClose("success", Documents?.length);
}
} catch (err) {
const message =
err?.response?.data?.error || err?.message || "something went wrong.";
console.error("Error sending documents:", message);
props.handleClose("error", 0, message);
} finally {
setIsSubmit(false);
}
};
return (
<>
{isLoader ? (
<div className="w-full h-[100px] flex justify-center items-center z-[999]">
<Loader />
</div>
) : (
<>
{isSubmit && (
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
<Loader />
</div>
)}
{isPrefillExist ? (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("prefill-bulk-error")}
</div>
) : !isDisableBulkSend ? (
<>
{props.Placeholders?.length > 0 ? (
isSignatureExist ? (
<>
{props.Placeholders?.some((x) => !x.signerObjId) ? (
<>
<form onSubmit={handleSubmit}>
<div className="min-h-max max-h-[250px] overflow-y-auto">
{forms?.map((form, index) => (
<div
key={form.Id}
className="p-3 op-card border-[1px] border-gray-400 mt-3 mx-4 mb-4 bg-base-200 text-base-content grid grid-cols-1 md:grid-cols-2 gap-2 relative"
>
{form?.fields?.map((field, fieldIndex) => (
<div
className="flex flex-col"
key={field.fieldId}
>
<label className="block text-xs font-semibold">
{field.label}
</label>
<SuggestionInput
required
type="email"
value={field.value}
index={fieldIndex}
onChange={(signer) =>
handleInputChange(
index,
signer,
fieldIndex
)
}
/>
</div>
))}
{forms?.length > 1 && (
<button
onClick={() => handleRemoveForm(index)}
className="absolute right-3 top-1 text-[red] border-[1px] border-[red] rounded-lg w-[1.7rem] h-[1.7rem]"
>
<i className="fa-light fa-trash"></i>
</button>
)}
<div ref={formRef}></div>
</div>
))}
</div>
<div className="flex flex-col mx-4 mb-4 gap-3">
<button
type="submit"
className="op-btn op-btn-accent focus:outline-none"
>
<i className="fa-light fa-paper-plane"></i>{" "}
<span>{t("send")}</span>
</button>
</div>
</form>
</>
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-1")}
</div>
)}
</>
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-2")}
</div>
)
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-3")}
</div>
)}
</>
) : (
<>
</>
)}
</>
)}
</>
);
};
export default BulkSendUi;
+7 -2
View File
@@ -1,6 +1,9 @@
import { useTranslation } from "react-i18next";
const DateFormat = ({ selectDate, dateFormatList, handleChangeFormat }) => {
const selectedFormatIndex = dateFormatList?.findIndex(
(item) => item.format === selectDate?.format
);
const { t } = useTranslation();
return (
@@ -8,7 +11,7 @@ const DateFormat = ({ selectDate, dateFormatList, handleChangeFormat }) => {
<span className="capitalize">{t("format")}: </span>
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-2"
defaultValue={""}
value={selectedFormatIndex >= 0 ? selectedFormatIndex : ""}
onChange={(e) => handleChangeFormat(e)}
>
<option value="" disabled>
@@ -22,7 +25,9 @@ const DateFormat = ({ selectDate, dateFormatList, handleChangeFormat }) => {
);
})}
</select>
<span className="text-xs text-gray-400 ml-1 uppercase">{selectDate.format}</span>
<span className="text-xs text-gray-400 ml-1 uppercase">
{selectDate.format}
</span>
</div>
);
};
+3 -1
View File
@@ -26,7 +26,9 @@ const DatePicker = ({
ref={ref}
>
{value}
<i className={`${value ? "ml-[5px]" : ""} fa-light fa-calendar `}></i>
<i
className={`${value ? "ml-[15px]" : "w-20"} fa-light fa-calendar `}
></i>
</div>
));
CustomInput.displayName = "CustomInput";
@@ -0,0 +1,605 @@
import {
useState,
useEffect,
useMemo
} from "react";
import axios from "axios";
import SuggestionInput from "../shared/fields/SuggestionInput";
import Loader from "../../primitives/Loader";
import { useTranslation } from "react-i18next";
import {
emailRegex,
} from "../../constant/const";
import {
convertPdfArrayBuffer,
getSignedUrl,
isBase64,
getBase64FromUrl,
} from "../../constant/Utils";
import {
formatCSVDate,
handleEmbedPrefillToDoc,
normalizeKey
} from "../../utils";
import RenderWidgets from "./components/RenderWidgets";
import { useDispatch, useSelector } from "react-redux";
import { setBulkLoader } from "../../redux/reducers/widgetSlice";
const EXCLUDED_PREFILL_TYPES = new Set(["image", "draw"]);
const EXCLUDED_WIDGET_TYPES = new Set([
"image",
"signature",
"stamp",
"initials"
]);
const ALL_EXCLUDED_TYPES = new Set([
...EXCLUDED_PREFILL_TYPES,
...EXCLUDED_WIDGET_TYPES
]);
const requiredAsteriskCls = (isRequired = false) => {
return isRequired ? "after:content-['_*'] after:text-red-500" : "";
};
const BulkSendUi = (props) => {
const { t } = useTranslation();
const appName =
"OpenSign™";
const dispatch = useDispatch();
const { isBulkLoader } = useSelector((state) => state.widget);
const [forms, setForms] = useState([]);
const [isSignatureExist, setIsSignatureExist] = useState();
const [isLoader, setIsLoader] = useState(false);
const [signers, setSigners] = useState([]);
const [emails, setEmails] = useState([]);
const [isVacantRoles, setIsVacantRoles] = useState(false);
const [fields, setFields] = useState([]);
const [headers, setHeaders] = useState([]);
const tokenHeader = useMemo(() => {
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId")
};
return {
...headers,
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
}, [props?.jwttoken]);
// Initialize form state (fields, widgets/responses, emails, headers, forms, signers, allowedForm) from template placeholders with existing values.
const initializeWidgetsWithValues = async () => {
if (props?.Placeholders?.length > 0) {
const roles = props?.Placeholders?.filter((p) => !p.signerObjId) || [];
const emails = [];
const users = await Promise.all(
roles.map(async (role) => {
const isPrefill = role?.Role?.toLowerCase() === "prefill";
const uniqueNames = new Set();
const pages = role?.placeHolder ?? [];
const widgets = await Promise.all(
pages.flatMap((page) =>
page.pos
.filter((widget) => {
const widgetName = widget?.options?.name;
if (!widgetName) return false; //|| widget?.options?.isReadOnly
// exclude certain widget types for non-prefill
if (EXCLUDED_WIDGET_TYPES.has(widget.type) && !isPrefill)
return false;
// unique by widget name
if (uniqueNames.has(widgetName)) return false;
uniqueNames.add(widgetName);
return true;
})
.map(async (widget) => {
let response = "";
if (ALL_EXCLUDED_TYPES.has(widget.type)) {
const url =
widget.options?.response ||
widget.options?.defaultValue ||
"";
if (url && !isBase64(url)) {
const signedUrl = await getSignedUrl(
url,
"", // docId
props.item.objectId, // templateId
);
response = await getBase64FromUrl(signedUrl, true);
} else {
response = "";
}
} else {
if (widget.type === "date") {
const value =
widget.options?.response ||
widget.options?.defaultValue ||
"";
response = value ? formatCSVDate(widget, value) : "";
} else {
response =
widget.options?.response ||
widget.options?.defaultValue ||
"";
}
}
const label = `${role.Role}::${widget.options?.name}`;
return {
...widget,
label: label,
response,
pageNumber: page.pageNumber
};
})
)
);
const signerEmail =
normalizeKey(role?.email) || role?.signerPtr?.Email;
if (signerEmail) emails.push(signerEmail);
return {
fieldId: role.Id,
email: "",
label: role.Role,
signer: {},
widgets: widgets
};
})
);
setFields(users);
setEmails(emails);
// Build headers with required ordering:
// 1) all role emails first
// 2) then all widgets
const emailHeaders = [];
const widgetHeaders = [];
for (const role of users) {
const isPrefill = role?.label?.toLowerCase() === "prefill";
if (!isPrefill) {
emailHeaders.push({
label: `${role.label}::Email`,
role: role.label,
type: "role",
isRequired: true
});
}
for (const widget of role?.widgets ?? []) {
const isRequired =
widget?.options?.status === "required" && isPrefill;
widgetHeaders.push({
label: widget?.label,
role: role?.label,
type: widget?.type,
isRequired
});
}
}
setHeaders([...emailHeaders, ...widgetHeaders]);
setForms((prev) => [...prev, { Id: 1, fields: users }]);
const signer = props.item?.Signers?.filter((x) => x?.objectId);
setSigners(signer);
}
};
useEffect(() => {
signatureExist();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const checkSignatureAndRoles = (placeholders = []) => {
const filtered = placeholders.filter((x) => x?.Role !== "prefill");
const isSignExist = filtered.every((p) =>
p?.placeHolder?.some((h) => h?.pos?.some((p) => p?.type === "signature"))
);
setIsSignatureExist(isSignExist);
setIsVacantRoles(placeholders.some((x) => !x.signerObjId));
};
//function to check at least one signature field exist
const signatureExist = async () => {
setIsLoader(true);
await initializeWidgetsWithValues();
checkSignatureAndRoles(props?.Placeholders);
setIsLoader(false);
};
const handleInputChange = (formIndex, signer, role) => {
const newForms = [...forms];
const email = signer?.Email ? signer?.Email : signer || "";
const fieldId = newForms[formIndex].fields.findIndex(
(f) => f.label === role
);
newForms[formIndex].fields[fieldId].email = normalizeKey(email);
newForms[formIndex].fields[fieldId].signer = signer?.objectId ? signer : "";
setForms(newForms);
};
const handleWidgetDetails = (
value = "",
formIndex,
RoleIndex = 0,
widgetIndex
) => {
const newForms = [...forms];
newForms[formIndex].fields[RoleIndex].widgets[widgetIndex].response = value;
setForms(newForms);
};
function validateEmails(data) {
for (const item of data) {
let email = "";
const filterFields = item.fields.filter((p) => p.label !== "prefill");
for (const field of filterFields) {
if (!emailRegex.test(field.email)) {
alert(t("invalid-email-found", { email: field.email }));
return false;
} else if (email === field.email || emails?.includes(field.email)) {
alert(t("duplicate-email-found", { email: field.email }));
return false;
} else {
email = field.email;
}
}
}
return true;
}
// `updateWidgetValues` will update responses for all widget types
const updateWidgetValues = (widgetValues = [], placeholders = []) => {
if (!widgetValues?.length) return placeholders;
const responseByName = new Map(
widgetValues
.map((w) => [w?.options?.name, w?.response])
.filter(([name]) => !!name)
);
return placeholders.map((ph) => {
const updatedPages = (ph?.placeHolder ?? []).map((page) => {
const updatedPos = (page?.pos ?? []).map((widgetDetails) => {
const name = widgetDetails?.options?.name;
if (!name || !responseByName.has(name)) return widgetDetails;
const newResponse = responseByName.get(name);
return {
...widgetDetails,
options: {
...widgetDetails.options,
// update response; if you DON'T want undefined to overwrite, keep the ?? fallback
response:
newResponse ??
widgetDetails.options?.response ??
widgetDetails.options?.defaultValue
}
};
});
return { ...page, pos: updatedPos };
});
return { ...ph, placeHolder: updatedPages };
});
};
// `removeWidgetValues` will clear out responses for draw/image/signature/stamp/initials widgets
const removeWidgetValues = (widgetValues = [], placeholders = []) => {
if (!widgetValues?.length) return placeholders;
return placeholders.map((ph) => {
const updatedPages = (ph?.placeHolder ?? []).map((page) => {
const updatedPos = (page?.pos ?? []).map((widgetDetails) => {
const isImageType = ALL_EXCLUDED_TYPES.has(widgetDetails?.type);
return {
...widgetDetails,
options: {
...widgetDetails.options,
// update response; if you DON'T want undefined to overwrite, keep the ?? fallback
response: isImageType ? "" : widgetDetails.options?.response,
defaultValue: isImageType
? ""
: widgetDetails.options?.defaultValue
}
};
});
return { ...page, pos: updatedPos };
});
return { ...ph, placeHolder: updatedPages };
});
};
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
dispatch(setBulkLoader(true));
if (!forms.length) {
alert(t("bulk-send-no-records"));
dispatch(setBulkLoader(false));
return;
}
if (validateEmails(forms)) {
// Create a copy of Placeholders array from props.item
let Placeholders = [...props.Placeholders];
// Initialize an empty array to store updated documents
let Documents = [];
let error = "";
// Loop through each form
for (const form of forms) {
//checking if user enter email which already exist as a signer then add user in a signers array
let existSigner = [];
form.fields.map((data) => {
if (data.signer) {
existSigner.push(data.signer);
}
});
// Map through the copied Placeholders array to update email values
const updatedPlaceholders = Placeholders.map((placeholder) => {
// Find the field in the current form that matches the placeholder Id
const field = form.fields.find(
(element) => parseInt(element.fieldId) === placeholder.Id
);
// If a matching field is found, update the email value in the placeholder
const signer = field?.signer?.objectId ? field.signer : "";
if (field) {
if (signer) {
return {
...placeholder,
signerObjId: field?.signer?.objectId || "",
signerPtr: signer
};
} else {
return {
...placeholder,
email: normalizeKey(field.email),
signerObjId: field?.signer?.objectId || "",
signerPtr: signer
};
}
}
// If no matching field is found, keep the placeholder as is
return placeholder;
});
const widgetValues = form?.fields?.flatMap((f) => f.widgets);
const updateWidgetPlaceholder = updateWidgetValues(
widgetValues,
updatedPlaceholders
);
const url = props.item?.URL || props.item?.SignedUrl;
let signedUrl = await getSignedUrl(
url,
"", //docId
props.item.objectId, // templateId
);
const prefillField = form?.fields?.find((f) => f.label === "prefill");
const prefills = prefillField?.widgets ?? [];
if (prefills?.length > 0) {
const prefillDetails = updateWidgetPlaceholder?.find(
(x) => x.Role === "prefill"
);
const pdfArrayBuffer = await convertPdfArrayBuffer(signedUrl);
if (pdfArrayBuffer === "Error") {
error = t("something-went-wrong-mssg");
dispatch(setBulkLoader(false));
break;
}
signedUrl = await handleEmbedPrefillToDoc(
prefillDetails,
1, // scale
pdfArrayBuffer,
[], // prefillImg,
props.item?.ExtUserPtr?.UserId?.objectId // userId
);
if (signedUrl?.error) {
error = signedUrl.error.includes("not compatible")
? t("pdf-uncompatible", { appName: appName })
: signedUrl?.error;
dispatch(setBulkLoader(false));
break;
}
}
const placeholders = removeWidgetValues(
widgetValues,
updateWidgetPlaceholder
);
if (!signedUrl) {
error = t("something-went-wrong-mssg");
dispatch(setBulkLoader(false));
break;
}
// Push a new document object with updated Placeholders into the Documents array
if (existSigner?.length > 0) {
Documents.push({
...props.item,
URL: signedUrl,
Placeholders: placeholders,
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
});
} else {
Documents.push({
...props.item,
URL: signedUrl,
Placeholders: placeholders,
SignatureType: props.signatureType,
Signers: signers
});
}
}
if (error) {
alert(error);
} else {
await batchQuery(Documents);
}
} else {
dispatch(setBulkLoader(false));
}
};
const batchQuery = async (Documents) => {
const functionsUrl = `${localStorage.getItem("baseUrl")}functions/batchdocuments`;
const headers = {
...tokenHeader,
};
const params = { Documents: JSON.stringify(Documents) };
try {
const res = await axios.post(functionsUrl, params, { headers: headers });
if (res.data && res.data.result) {
props.handleClose("success", Documents?.length);
}
} catch (err) {
const message =
err?.response?.data?.error || err?.message || "something went wrong.";
console.error("Error sending documents:", message);
props.handleClose("error", 0, message);
} finally {
dispatch(setBulkLoader(false));
}
};
if (isLoader) {
return (
<div className="w-full h-[100px] flex justify-center items-center z-[999]">
<Loader />
</div>
);
}
return (
<div className="relative">
{/* {isBulkLoader && (
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
<Loader />
</div>
)} */}
{props.Placeholders?.length > 0 ? (
isSignatureExist ? (
isVacantRoles ? (
<>
<form onSubmit={handleSubmit}>
<div className="min-h-max max-h-[250px] overflow-auto">
{
forms?.length > 0 && (
<div className="mx-4">
<table className="op-table border-collapse w-full">
<thead className="text-[13px] text-center">
<tr className="border-y-[1px]">
{/* Roles + widgets */}
{headers?.map((header) => (
<th
key={header.label}
className={`${requiredAsteriskCls(header?.isRequired)} p-2`}
>
{header.label}
</th>
))}
{forms?.length > 1 && (
<th className="p-2">{t("action")}</th>
)}
</tr>
</thead>
<tbody className="text-[12px] text-base-content">
{forms.map((form, formIndex) => {
const fields = form?.fields ?? [];
const emailFields = fields.filter(
(f) => f.label !== "prefill"
);
const widgets = fields.flatMap(
(f, fieldIndex) =>
(f.widgets ?? []).map(
(widget, widgetIndex) => ({
widget,
fieldIndex, // keep the fieldIndex that produced this widget
widgetIndex
})
)
);
return (
<tr key={form.Id}>
{/* Email cell (label + SuggestionInput) */}
{emailFields?.map((field, fieldIndex) => (
<td key={field.fieldId} className="p-2">
<div className="flex flex-col min-w-max">
<SuggestionInput
required
type="email"
value={field.email ?? ""}
index={fieldIndex}
onChange={(signer) =>
handleInputChange(
formIndex,
signer,
field.label
)
}
/>
</div>
</td>
))}
{widgets.map(
({ widget, fieldIndex, widgetIndex }) => (
<td key={widget.key} className="p-2">
<RenderWidgets
widget={widget}
formIndex={formIndex}
prefillIndex={widgetIndex}
handleWidgetDetails={(value) =>
handleWidgetDetails(
value,
formIndex,
fieldIndex,
widgetIndex
)
}
/>
</td>
)
)}
</tr>
);
})}
</tbody>
</table>
</div>
)
}
</div>
<div className="flex flex-row flex-wrap pb-3 pt-2 px-3 gap-3 justify-center">
<button
type="submit"
className="op-btn op-btn-accent w-[150px] focus:outline-none"
>
<i className="fa-light fa-paper-plane"></i>
<span>{t("send")}</span>
</button>
</div>
</form>
</>
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-1")}
</div>
)
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-2")}
</div>
)
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-3")}
</div>
)}
</div>
);
};
export default BulkSendUi;
@@ -0,0 +1,582 @@
import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import DatePicker from "react-datepicker";
import SignatureCanvas from "react-signature-canvas";
import { useTranslation } from "react-i18next";
import moment from "moment";
import {
changeDateToMomentFormat,
compressedFileSize,
getMonth,
getYear,
months,
radioButtonWidget,
textInputWidget,
textWidget,
years
} from "../../../constant/Utils";
import PenColorComponent from "../../pdf/tab/PenColorComponent";
import { getDatePickerDate, toHtmlPattern } from "../../../utils";
import CellsInput from "../../shared/fields/CellsInput";
import { emailRegex } from "../../../constant/const";
const inputOpt = new Set(["text", "email", "number"]);
const PREFILL_PREFIX = "prefill::";
const inputTypes = {
email: emailRegex,
number: /^[0-9\s]*$/,
ssn: /^(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}$/
};
const inputValidation = (pattern, type) => {
if (pattern) return pattern;
return inputTypes[type] || "";
};
const canavasTheme = `opensigncss:bg-white opensigndark:bg-[#121212] opensigndark:border-[#f6f3f4]/20 opensigncss:border-gray-300 opensigndark:hover:border-white opensigncss:hover:border-black border-[1px]`;
const DateWidget = ({ widget, isRequired, onChange }) => {
const format = widget?.options?.validation?.format || "MM/dd/yyyy";
const PrefillDateInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{ fontFamily: "Arial, sans-serif" }}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full"
onClick={onClick}
ref={ref}
>
{value ? value : format}
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
PrefillDateInput.displayName = "PrefillDateInput";
const handleDate = (widget) => {
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
try {
return getDatePickerDate(widget?.response, format);
} catch (err) {
console.error("handleDate error ", err);
return;
}
};
//function to set date with required date format onchange date
const handleOnDateChange = (date) => {
let updateDate = date;
let newDate;
const isSpecialDateFormat =
format && ["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(format);
if (isSpecialDateFormat) {
newDate = moment(updateDate).format(changeDateToMomentFormat(format));
} else {
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
newDate = new Date(updateDate);
newDate = moment(newDate.getTime()).format(
changeDateToMomentFormat(format)
);
}
onChange(newDate);
};
return (
<div className="flex flex-col">
<div className="min-w-max">
<DatePicker
portalId="datepicker-portal-root"
id={`widget-${widget?.options?.name}-${widget.key}`}
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start ml-2">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
closeOnScroll={true}
selected={handleDate(widget)}
onChange={(date) => handleOnDateChange(date, widget)}
customInput={<PrefillDateInput />}
dateFormat={widget?.options?.validation?.format || "MM/dd/yyyy"}
required={isRequired}
/>
</div>
</div>
);
};
const TextWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const [value, setValue] = useState([]);
const inputType = widget?.options?.validation?.type || "";
const serverRegex = widget?.options?.validation?.pattern;
const isPredfineType = inputType ? inputOpt?.has(inputType) : "";
const regExpression = inputValidation(serverRegex, inputType);
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
useEffect(() => {
const response =
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
const isNumber = inputType === "number" && response;
setValue(isNumber ? Number(response) : response);
}, []);
const handleInputChange = (e) => {
const text = e.target.value || "";
setValue(text);
onChange(text);
};
return (
<div className="flex flex-col">
<div className="min-w-max">
<input
type={isPredfineType ? inputType : "text"}
id={`widget-${widget?.options?.name}-${widget.key}`}
value={value}
placeholder={
widget?.options?.hint ||
t("enter-value", { value: widget?.options?.name })
}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
onChange={(e) => handleInputChange(e)}
pattern={pattern || undefined} // if no pattern, browser won't do pattern validation
onInvalid={(e) => {
const el = e.currentTarget;
// ✅ Only override message if pattern exists AND the error is patternMismatch
if (pattern && el.validity.patternMismatch) {
el.setCustomValidity(t("validation-alert-1"));
} else {
el.setCustomValidity("");
}
}}
onInput={(e) => {
e.currentTarget.setCustomValidity("");
}}
required={isRequired}
/>
</div>
</div>
);
};
const CheckboxWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
useEffect(() => {
setSelectedCheckbox(
widget?.options?.response ?? widget?.options?.defaultValue ?? []
);
}, []);
const handleCheckboxValue = (isChecked, ind) => {
const prevArr = Array.isArray(selectedCheckbox) ? selectedCheckbox : [];
const updateResponse = isChecked
? prevArr.includes(ind)
? prevArr
: [...prevArr, ind] // add once
: prevArr.filter((v) => v !== ind); // remove
setSelectedCheckbox(updateResponse);
onChange(updateResponse);
};
const noneSelected = selectedCheckbox?.length === 0;
return (
<div className="flex flex-col">
<div className="flex flex-col gap-y-1 min-w-max">
{widget.options?.values?.map((value, ind) => (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`checkbox-${widget.key + ind}`}
className="mt-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
checked={selectedCheckbox.includes(ind)}
required={isRequired && noneSelected && ind === 0} // key line
onInvalid={(e) =>
e.target.setCustomValidity(t("select-at-least-option"))
}
onChange={(e) => {
e.target.setCustomValidity(""); // ✅ clear message as soon as user changes
handleCheckboxValue(e.target.checked, ind);
}}
/>
<label
htmlFor={`checkbox-${widget.key + ind}`}
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
>
{value}
</label>
</div>
))}
</div>
</div>
);
};
const DropdownWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const [selected, setSelected] = useState("");
useEffect(() => {
setSelected(
widget?.options?.response ?? widget?.options?.defaultValue ?? ""
);
}, []);
const handleDropdownChange = (value) => {
setSelected(value);
onChange(value);
};
return (
<div className="flex flex-col">
<div className="min-w-max">
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-base-content w-full"
id={`widget-${widget?.options?.name}-${widget?.key}`}
value={selected}
onChange={(e) => handleDropdownChange(e.target.value)}
required={isRequired}
>
{/* Default/Title option */}
<option value="" disabled hidden>
{t("choose-one")}
</option>
{widget?.options?.values?.map((option, ind) => (
<option key={ind} value={option}>
{option}
</option>
))}
</select>
</div>
</div>
);
};
const RadioButtonWidget = ({ widget, isRequired, onChange, formIndex }) => {
const [selected, setSelected] = useState("");
useEffect(() => {
setSelected(
widget?.options?.response || widget?.options?.defaultValue || ""
);
}, []);
const handleRadioChange = (value) => {
setSelected(value);
onChange(value);
};
return (
<div className="flex flex-col">
<div className="flex flex-col gap-y-1 min-w-max">
{widget.options?.values?.map((data, ind) => (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`radio-${widget.key + ind}`}
className="mt-[2px] op-radio op-radio-xs"
name={`radio-group-${widget.key}-${formIndex}`} // same name => browser treats all radios as ONE group
type="radio"
required={isRequired && ind === 0} // ✅ set required ONCE (on first radio) to make the whole group mandatory
checked={selected === data}
onChange={() => handleRadioChange(data)}
/>
<label
htmlFor={`radio-${widget.key + ind}`}
className="text-xs mb-0 ml-[2px] cursor-pointer"
>
{data}
</label>
</div>
))}
</div>
</div>
);
};
const ImageWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const imageRef = useRef(null);
const [img, setImg] = useState({ src: "", imgType: "" });
const [defaultImg, setDefaultImg] = useState({ src: "", imgType: "" });
useEffect(() => {
if (widget?.response) {
setDefaultImg({ src: widget?.response, imgType: "" });
}
}, []);
const onImageChange = (e) => {
if (e.target.files && e.target.files?.[0]) {
const file = e.target.files?.[0];
compressedFileSize(file, ({ src, imgType }) => {
onChange(src);
setImg({ src, imgType });
});
}
};
const handleClearImage = () => {
setDefaultImg({ src: "", imgType: "" });
setImg({ src: "", imgType: "" });
onChange("");
};
return (
<div className="flex flex-col">
<div className="flex flex-col prefillCanvas">
{defaultImg?.src || img?.src ? (
<>
<div
className={`cursor-pointer rounded-box ${canavasTheme} flex flex-col w-full h-full justify-center items-center`}
>
<img
alt={`image_${widget?.options?.name}`}
src={img.src || defaultImg?.src}
draggable="false"
className="object-contain h-full w-full aspect-[5/2]"
/>
</div>
<span
onClick={handleClearImage}
className="flex justify-start text-blue-500 underline cursor-pointer ml-1"
>
{t("clear")}
</span>
</>
) : (
<div
className={`cursor-pointer rounded-box ${canavasTheme} overflow-hidden w-full h-full aspect-[5/2] flex flex-col justify-center items-center`}
onClick={() => imageRef.current?.click()}
>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
<div className="text-[10px] text-base-content">{t("upload")}</div>
<input
type="file"
onChange={(e) => onImageChange?.(e, widget)}
className="filetype w-[1px] h-[1px] opacity-0"
accept="image/png,image/jpeg"
ref={imageRef} // Assign ref dynamically
required={isRequired}
/>
</div>
)}
</div>
</div>
);
};
const DrawWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const canvasRef = useRef(null);
const sigRequiredRef = useRef(null);
const [hasDraw, setHasDraw] = useState("");
const [penColor, setPenColor] = useState("blue");
const [image, setImage] = useState("");
useEffect(() => {
if (widget?.response) {
setImage(widget?.response);
setHasDraw("signed");
}
}, []);
const handleSignatureChange = () => {
const draw = canvasRef.current?.isEmpty?.() ? "" : "signed";
setHasDraw(draw);
onChange(canvasRef.current.toDataURL());
// clear custom error once user signs
sigRequiredRef.current?.setCustomValidity("");
};
const handleClear = () => {
setImage("");
if (canvasRef?.current) {
canvasRef.current.clear();
onChange("");
setHasDraw("");
// clear custom error once user signs
sigRequiredRef.current?.setCustomValidity("");
}
};
return (
<div className="flex flex-col">
<div className="relative">
{image ? (
<div className={`prefillCanvas ${canavasTheme} rounded-[10px]`}>
<img
alt={`draw_${widget?.options?.name}`}
src={image}
className="prefillCanvas object-contain"
/>
</div>
) : (
<SignatureCanvas
ref={canvasRef}
penColor={penColor}
canvasProps={{
className: `prefillCanvas ${canavasTheme} rounded-[10px]`
}}
onEnd={() => handleSignatureChange()}
dotSize={1}
/>
)}
{/* ✅ Native form validation hook */}
<input
ref={sigRequiredRef}
type="text"
value={hasDraw}
required={isRequired}
tabIndex={-1}
onInvalid={(e) => e.target.setCustomValidity(t("draw-required"))}
onChange={() => {}}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 w-[1px] h-[1px] pointer-events-none"
/>
</div>
<div className="flex flex-row justify-between mt-[8px] w-[200px]">
<PenColorComponent
penColor={penColor}
setPenColor={setPenColor}
hideLabel
penSize="sm"
/>
<span
onClick={handleClear}
className="flex justify-start text-blue-500 underline cursor-pointer"
>
{t("clear")}
</span>
</div>
</div>
);
};
const CellsWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const count = widget?.options?.cellCount || 1;
const [word, setWord] = useState("");
const inputType = widget?.options?.validation?.type || "";
const serverRegex = widget?.options?.validation?.pattern;
const regExpression = inputValidation(serverRegex, inputType);
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
useEffect(() => {
const response =
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
const isNumber = inputType === "number" && response;
setWord(isNumber ? Number(response) : response);
}, []);
const handleChange = (words) => {
const value = words;
setWord(value);
onChange(value);
};
return (
<div className="flex flex-col">
<div className="relative">
<CellsInput
hint={
widget?.options?.hint ||
t("enter-value", { value: widget?.options?.name })
}
count={count}
value={word}
onChange={handleChange}
only="all"
autoFocus
/>
<input
id={`widget-${widget?.options?.name}-${widget.key}`}
value={word}
required={isRequired}
tabIndex={-1}
onChange={() => {}}
pattern={pattern || undefined} // if no pattern, browser won't do pattern validation
onInvalid={(e) => {
const el = e.currentTarget;
// ✅ Only override message if pattern exists AND the error is patternMismatch
if (pattern && el.validity.patternMismatch) {
el.setCustomValidity(t("validation-alert-1"));
} else {
el.setCustomValidity("");
}
}}
onInput={(e) => {
e.currentTarget.setCustomValidity("");
}}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 w-[1px] h-[1px] pointer-events-none"
/>
</div>
</div>
);
};
const RenderWidgets = ({
widget,
formIndex,
prefillIndex,
handleWidgetDetails
}) => {
const label = widget?.label?.toLowerCase() ?? "";
const isPrefill = label.includes(PREFILL_PREFIX);
const commonProps = {
widget,
isRequired: widget.options?.status === "required" && isPrefill,
formIndex
};
const onChange = (value) => {
handleWidgetDetails(value, formIndex, prefillIndex);
};
switch (widget.type) {
case "date":
return <DateWidget {...commonProps} onChange={onChange} />;
case textWidget:
case textInputWidget:
case "name":
case "email":
case "company":
case "job title":
case "number":
return <TextWidget {...commonProps} onChange={onChange} />;
case "checkbox":
return <CheckboxWidget {...commonProps} onChange={onChange} />;
case radioButtonWidget:
return <RadioButtonWidget {...commonProps} onChange={onChange} />;
case "dropdown":
return <DropdownWidget {...commonProps} onChange={onChange} />;
case "image":
case "stamp":
return <ImageWidget {...commonProps} onChange={onChange} />;
case "initials":
case "draw":
return <DrawWidget {...commonProps} onChange={onChange} />;
case "cells":
return <CellsWidget {...commonProps} onChange={onChange} />;
default:
return null;
}
};
export default RenderWidgets;
@@ -5,12 +5,17 @@ import { useEffect, useRef } from "react";
function DragGuideLinesLayer(props) {
const { showGuidelines } = props;
const prevDragState = useRef({ isDragging: false, x: 0, y: 0 });
const { isDraggingWidgets, offset, itemType } = useDragLayer((monitor) => ({
isDraggingWidgets: monitor.isDragging(),
const { isDraggingWidget, offset, itemType } = useDragLayer((monitor) => ({
isDraggingWidget: monitor.isDragging(),
offset: monitor.getClientOffset(),
itemType: monitor.getItem()
}));
useEffect(() => {
// Hide guidelines when dragging stops
if (!isDraggingWidget) {
showGuidelines(false);
return;
}
if (!offset) return;
//getting container by id
const container = document.getElementById("container");
@@ -28,15 +33,14 @@ function DragGuideLinesLayer(props) {
// Check if theres any change in the dragging state or the dragged elements position
// Compare the current drag state with the previously stored one
const hasStateChanged =
prevDragState.current.isDragging !== isDraggingWidgets || // Drag started or stopped
prevDragState.current.isDragging !== isDraggingWidget || // Drag started or stopped
prevDragState.current.x !== x || // X position changed
prevDragState.current.y !== y; // Y position changed
if (hasStateChanged) {
// Update the previous drag state with the latest values
prevDragState.current = { isDragging: isDraggingWidgets, x, y };
if (isDraggingWidgets) {
prevDragState.current = { isDragging: isDraggingWidget, x, y };
if (isDraggingWidget) {
// Adjust the current drag coordinates relative to the initial widgets button position
const getXPosition = props?.signBtnPosition?.[0]
? x - props.signBtnPosition[0].xPos
@@ -64,13 +68,10 @@ function DragGuideLinesLayer(props) {
// Display the alignment guidelines based on the current drag position and widget size
showGuidelines(true, getXPosition, getYPosition, getWidth, getHeight);
} else {
// Hide guidelines when dragging stops
showGuidelines(false);
}
}
}
}, [isDraggingWidgets, offset, itemType, showGuidelines]);
}, [isDraggingWidget, offset, itemType, showGuidelines]);
}
export default DragGuideLinesLayer;
@@ -111,16 +111,15 @@ function DropdownWidgetOption(props) {
return;
}
}
const defaultData =
defaultCheckbox && defaultCheckbox.length > 0
? defaultCheckbox
: defaultValue;
const isDropdownOrRadio =
props?.type === "dropdown" || props?.type === radioButtonWidget;
const isCheckbox = props?.type === "checkbox";
const defaultData = isCheckbox ? defaultCheckbox : defaultValue;
const readOnlyWithoutValue =
isReadOnly && !defaultValue && status !== "optional";
const isCheckbox = props?.type === "checkbox";
const WidgetLayout = ["checkbox", radioButtonWidget].includes(props.type)
? layout
: null;
@@ -157,13 +156,15 @@ function DropdownWidgetOption(props) {
};
const handleDefaultCheck = (index) => {
const getDefaultCheck = defaultCheckbox.includes(index);
if (getDefaultCheck) {
return true;
} else {
return false;
}
const handleSelectDefaultCheckbox = (e, index) => {
const checked = e.target.checked;
setDefaultCheckbox((prev) =>
checked
? prev.includes(index)
? prev
: [...prev, index]
: prev.filter((i) => i !== index)
);
};
return (
<ModalUi isOpen={props.showDropdown} title={props.title} showClose={false}>
@@ -201,21 +202,8 @@ function DropdownWidgetOption(props) {
{props.type === "checkbox" && props.isShowAdvanceFeature && (
<input
type="checkbox"
checked={handleDefaultCheck(index)}
onChange={(e) => {
if (e.target.checked) {
const getDefaultCheck =
defaultCheckbox?.includes(index);
if (!getDefaultCheck) {
setDefaultCheckbox((prev) => [...prev, index]);
}
} else {
const removeOption = defaultCheckbox.filter(
(data) => data !== index
);
setDefaultCheckbox(removeOption);
}
}}
checked={defaultCheckbox?.includes(index)}
onChange={(e) => handleSelectDefaultCheckbox(e, index)}
className="op-checkbox focus:outline-none hover:border-base-content mr-[5px]"
/>
)}
@@ -1,4 +1,3 @@
import React from "react";
import { Quill } from "react-quill-new";
// Use a third-party plugin so editors can toggle raw HTML view.
import htmlEditButton from "quill-html-edit-button";
@@ -18,7 +18,6 @@ export function EmailBody(props) {
onInput={(e) => e.target.setCustomValidity("")}
value={props.requestSubject}
onChange={(e) => {
props?.setRequestSubject(e.target.value);
props?.setCustomizeMail((prev) => ({
...prev,
subject: e.target.value
+297 -237
View File
@@ -32,6 +32,7 @@ import { themeColor } from "../../constant/const";
import { useGuidelinesContext } from "../../context/GuidelinesContext";
import DatePicker from "react-datepicker";
import DateWidgetModal from "../../primitives/DateWidgetModal";
import { dateFormat } from "../../utils";
function Placeholder(props) {
const { t } = useTranslation();
@@ -47,11 +48,18 @@ function Placeholder(props) {
const [clickonWidget, setClickonWidget] = useState({});
const [formdata, setFormdata] = useState({
status: props?.pos?.options?.status || "required",
isReadOnly: props?.pos?.options?.isReadOnly || false
isReadOnly: props?.pos?.options?.isReadOnly || false,
name: props?.pos?.options?.name
});
const [isToday, setIsToday] = useState(
props.pos?.options?.response === "today" ? true : false
);
const selectedFormatIndex = dateFormatList?.findIndex(
(item) =>
item.format === selectDate?.format ||
item?.format === props.pos?.options?.validation?.format
);
const isSelfSign = props.isSignYourself || props.isSelfSign;
const startDate = props?.pos?.options?.response
? getDefaultDate(
props?.pos?.options?.response,
@@ -65,20 +73,6 @@ function Placeholder(props) {
setContainerScale(props.containerWH.width / getPdfPageWidth.width);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.containerWH.width, props.pdfOriginalWH]);
const dateFormatArr = [
"L",
"DD-MM-YYYY",
"YYYY-MM-DD",
"MM.DD.YYYY",
"MM-DD-YYYY",
"MMM DD, YYYY",
"LL",
"DD MMM, YYYY",
"DD MMMM, YYYY",
"DD.MM.YYYY",
"DD/MM/YYYY"
];
useEffect(() => {
if (props?.pos?.type === "date") {
const isDateChange = true;
@@ -91,7 +85,7 @@ function Placeholder(props) {
}, [widgetData]);
//function is used to convert date according to selected format
const handleGetFormatDate = (data, isDateChange) => {
const handleGetFormatDate = (data, isDateChange, isNewDate) => {
const format =
data?.format ||
selectDate?.format ||
@@ -109,6 +103,10 @@ function Placeholder(props) {
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
const newDate = new Date(updateDate);
date = moment(newDate.getTime()).format(changeDateToMomentFormat(format));
} else if (isNewDate) {
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
const newDate = new Date();
date = moment(newDate.getTime()).format(changeDateToMomentFormat(format));
}
setSelectDate({ date: date, format: format });
return date;
@@ -125,7 +123,10 @@ function Placeholder(props) {
};
//function to save date and format on local array onchange date and onclick format
const handleSaveDate = (data, isDateChange) => {
const date = isToday ? "today" : handleGetFormatDate(data, isDateChange);
const date =
props.isPlaceholder && isToday
? "today"
: handleGetFormatDate(data, isDateChange);
//`onChangeInput` is used to save data related to date in a placeholder field
const format =
data?.format ||
@@ -145,7 +146,6 @@ function Placeholder(props) {
formdata,
);
};
//Date-picker- accept date - Thu Dec 30 2025 00:00:00 GMT+0530 (India Standard Time)
//if date have in this format - "30/12/2025"
//then first need to be convert in this format - Tue Dec 30 2025 00:00:00 GMT+0530 (India Standard Time)
@@ -156,7 +156,7 @@ function Placeholder(props) {
//function change format array list with selected date and format
const changeDateFormat = () => {
const updateDate = [];
dateFormatArr.map((data) => {
dateFormat.map((data) => {
let date = selectDate?.date || new Date();
const isString = typeof date === "string";
if (selectDate.format === "dd-MM-yyyy" && isString) {
@@ -180,7 +180,7 @@ function Placeholder(props) {
};
useEffect(() => {
if (props.isPlaceholder || props.isSignYourself || props.isSelfSign) {
if (props.isPlaceholder || isSelfSign) {
selectDate && changeDateFormat();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -250,10 +250,7 @@ function Placeholder(props) {
props?.setIsCheckbox(true);
}
// cells widget settings in sign yourself flow
else if (
props.pos.type === cellsWidget &&
(props.isSignYourself || props.isSelfSign)
) {
else if (props.pos.type === cellsWidget && isSelfSign) {
props.handleCellSettingModal && props.handleCellSettingModal();
}
//condition to handle setting icon for signyour-self flow for all type text widgets
@@ -266,7 +263,7 @@ function Placeholder(props) {
"job title",
"email"
].includes(props.pos.type) &&
(props.isSignYourself || props.isSelfSign)
isSelfSign
) {
props.handleTextSettingModal(true);
}
@@ -379,8 +376,7 @@ function Placeholder(props) {
"job title",
"email",
...isSettingForCells
].includes(props.pos.type) &&
(props.isSignYourself || props.isSelfSign) ? (
].includes(props.pos.type) && isSelfSign ? (
<i
onPointerDown={(e) => {
e.stopPropagation();
@@ -502,6 +498,7 @@ function Placeholder(props) {
className="fa-light fa-circle-xmark icon text-[#188ae2] -right-[8px] -top-[18px] "
onClick={(e) => {
e.stopPropagation();
showGuidelines(false);
//condition for template and placeholder flow
if (props.data) {
props.handleDeleteWidget(props.pos.key, props.data.Id);
@@ -514,6 +511,7 @@ function Placeholder(props) {
//for mobile and tablet touch event
onTouchEnd={(e) => {
e.stopPropagation();
showGuidelines(false);
//condition for template and placeholder flow
if (props.data) {
props.handleDeleteWidget(props.pos.key, props.data?.Id);
@@ -624,6 +622,7 @@ function Placeholder(props) {
return false;
}
};
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
className="border-gray-400 rounded-[50px] border-[1px] px-3 text-xs py-2 focus:outline-none hover:border-base-content "
@@ -631,10 +630,11 @@ function Placeholder(props) {
ref={ref}
>
{value}
<i className={`${value ? "ml-[5px]" : ""} fa-light fa-calendar `}></i>
<i className={`${value ? "ml-[5px]" : "w-20"} fa-light fa-calendar `}></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
const handleChangeFormat = (e) => {
const selectedIndex = e.target.value;
e.stopPropagation();
@@ -648,6 +648,17 @@ function Placeholder(props) {
format: dateFormatList[selectedIndex]?.format
}));
} else {
if (
props?.data?.Role === "prefill" &&
(formdata.status === "optional" || props.pos?.status === "optional")
) {
setSelectDate({
date: "",
format: dateFormatList[selectedIndex]?.format
});
return;
}
setSelectDate(dateFormatList[selectedIndex]);
}
};
@@ -658,6 +669,23 @@ function Placeholder(props) {
const handleCloseDateModal = () => {
setIsDateModal(false);
};
const handleDateStatus = (data) => {
if (data === "Optional" && props?.data?.Role === "prefill") {
setSelectDate((prev) => ({ ...prev, date: "" }));
} else if (
props?.data?.Role === "prefill" &&
data === "Required" &&
props?.pos?.options?.status === "optional"
) {
const isNewDate = true;
handleGetFormatDate(selectDate, null, isNewDate);
}
setFormdata({
...formdata,
status: data.toLowerCase()
});
};
return (
<>
{/* Check if a text widget (prefill type) exists. Once the user enters a value and clicks outside or the widget becomes non-selectable, it should appear as plain text (just like embedded text in a document). When the user clicks on the text again, it should become editable. */}
@@ -848,7 +876,7 @@ function Placeholder(props) {
top={-11}
pos={props.pos}
posHeight={props.posHeight}
isSignYourself={props.isSignYourself || props.isSelfSign}
isSignYourself={isSelfSign}
/>
) : props.data &&
props.isNeedSign &&
@@ -928,220 +956,252 @@ function Placeholder(props) {
)}
<DateWidgetModal isOpen={isDateModal} title={t("widget-info")}>
<div className="text-base-content h-[100%] p-[20px]">
<div className="flex flex-col gap-3">
<div className="flex flex-col md:items-center md:flex-row gap-y-3">
<span className="capitalize">{t("format")} :</span>
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-4"
defaultValue={""}
onChange={(e) => handleChangeFormat(e)}
>
<option value="" disabled>
{t("select-date-format")}
</option>
{dateFormatList.map((data, ind) => {
return (
<option className="text-[13px]" value={ind} key={ind}>
{data?.date ? data?.date : "nodata"}
</option>
);
})}
</select>
<span className="text-xs text-gray-400 ml-1 uppercase">
{selectDate.format}
</span>
</div>
{props?.data?.Role !== "prefill" && props?.isPlaceholder && (
<>
<div className="flex flex-col md:flex-row md:items-center gap-2">
<span>{t("default-date")} :</span>
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start md:ml-2">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) =>
changeYear(value)
}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
closeOnScroll={true}
selected={getDefaultDate(
selectDate?.date,
selectDate?.format
)}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
onChange={(date) => {
setIsToday(false);
const isDateChange = true;
const dateObj = {
date: date,
format: selectDate.format
};
handleGetFormatDate(dateObj, isDateChange);
}}
dateFormat={
selectDate
? selectDate?.format
: props.pos?.options?.validation?.format
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"
}
portalId="root-portal"
/>
<label className="flex items-center gap-1 cursor-pointer mb-0">
<input
checked={isToday}
type="checkbox"
className="op-checkbox op-checkbox-xs"
onClick={() => {
setSelectDate((prev) => ({ ...prev, date: "" }));
setIsToday(!isToday);
}}
/>
<span className="ml-[2px]">{t("set-today")}</span>
<form
onSubmit={(e) => {
e.preventDefault();
saveDateSetting(selectDate);
}}
>
<div className="flex flex-col gap-3">
{!isSelfSign && (
<div className="mb-[0.75rem] text-[13px] md:w-[60%] w-full">
<label htmlFor="name">
{t("name")}
<span className="text-[red]"> *</span>
</label>
<span
onClick={() => handleClearDate()}
className="underline text-blue-500 cursor-pointer ml-2"
>
{t("clear")}
</span>
<input
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
name="name"
value={formdata.name}
onChange={(e) =>
setFormdata({
...formdata,
name: e.target.value
})
}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
required
/>
</div>
</>
)}
<div className="flex flex-row items-center gap-[10px]">
{statusArr.map((data, ind) => {
return (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
name="status"
onChange={() =>
setFormdata({
...formdata,
status: data.toLowerCase()
})
}
checked={
formdata.status.toLowerCase() === data.toLowerCase()
)}
<div className="flex flex-col md:items-center md:flex-row gap-y-3">
<span className="capitalize">{t("format")} :</span>
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-4"
value={selectedFormatIndex >= 0 ? selectedFormatIndex : ""}
onChange={(e) => handleChangeFormat(e)}
>
<option value="" disabled>
{t("select-date-format")}
</option>
{dateFormatList.map((data, ind) => {
return (
<option className="text-[13px]" value={ind} key={ind}>
{data?.date ? data?.date : "nodata"}
</option>
);
})}
</select>
<span className="text-xs text-gray-400 ml-1 uppercase">
{selectDate.format || props.pos?.options?.validation?.format}
</span>
</div>
{props?.data?.Role !== "prefill" && props?.isPlaceholder && (
<>
<div className="flex flex-col md:flex-row md:items-center gap-2">
<span>{t("default-date")} :</span>
<DatePicker
renderCustomHeader={({
date,
changeYear,
changeMonth
}) => (
<div className="flex justify-start md:ml-2">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) =>
changeYear(value)
}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
closeOnScroll={true}
selected={getDefaultDate(
selectDate?.date,
selectDate?.format
)}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
onChange={(date) => {
setIsToday(false);
const isDateChange = true;
const dateObj = {
date: date,
format: selectDate.format
};
handleGetFormatDate(dateObj, isDateChange);
}}
dateFormat={
selectDate
? selectDate?.format
: props.pos?.options?.validation?.format
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"
}
portalId="root-portal"
/>
<div className="text-[13px]">
{t(`widget-status.${data}`)}
</div>
<label className="flex items-center gap-1 cursor-pointer mb-0">
<input
checked={isToday}
type="checkbox"
className="op-checkbox op-checkbox-xs"
onClick={() => {
setSelectDate((prev) => ({ ...prev, date: "" }));
setIsToday(!isToday);
}}
/>
<span className="ml-[2px]">{t("set-today")}</span>
</label>
<span
onClick={() => handleClearDate()}
className="underline text-blue-500 cursor-pointer ml-2"
>
{t("clear")}
</span>
</div>
);
})}
</>
)}
{!isSelfSign && (
<div className="flex flex-row items-center gap-[10px]">
{statusArr.map((data, ind) => {
return (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
name="status"
onChange={() => handleDateStatus(data)}
checked={
formdata.status.toLowerCase() === data.toLowerCase()
}
/>
<div className="text-[13px]">
{t(`widget-status.${data}`)}
</div>
</div>
);
})}
</div>
)}
<div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2">
<div className="flex flex-row items-center">
<span className="capitalize">{t("font-size")} :</span>
<select
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={
props.fontSize || clickonWidget.options?.fontSize || 12
}
onChange={(e) =>
props.setFontSize(parseInt(e.target.value))
}
>
{fontsizeArr.map((size, ind) => (
<option className="text-[13px]" value={size} key={ind}>
{size}
</option>
))}
</select>
</div>
<div className="flex flex-row gap-1 items-center">
<span className="capitalize">{t("color")} :</span>
<select
value={
props.fontColor ||
clickonWidget.options?.fontColor ||
"black"
}
onChange={(e) => props.setFontColor(e.target.value)}
className="ml-[4px] md:ml[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
>
{fontColorArr.map((color, ind) => (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
))}
</select>
<span
style={{
background:
props.fontColor ||
props.pos.options?.fontColor ||
"black"
}}
className="w-5 h-[19px] ml-1"
></span>
</div>
</div>
{props?.isPlaceholder && props?.data?.Role !== "prefill" && (
<div className="flex items-center gap-1 w-max">
<input
name="isReadOnly"
id="isReadOnly"
type="checkbox"
checked={formdata?.isReadOnly}
className="op-checkbox op-checkbox-xs"
onChange={() =>
setFormdata({
...formdata,
isReadOnly: !formdata?.isReadOnly
})
}
/>
<label
htmlFor="isReadOnly"
className="capitalize ml-[2px] mb-0 cursor-pointer"
>
{t("read-only")}
</label>
</div>
)}
</div>
<div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2">
<div className="flex flex-row items-center">
<span className="capitalize">{t("font-size")} :</span>
<select
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={
props.fontSize || clickonWidget.options?.fontSize || 12
}
onChange={(e) => props.setFontSize(parseInt(e.target.value))}
>
{fontsizeArr.map((size, ind) => (
<option className="text-[13px]" value={size} key={ind}>
{size}
</option>
))}
</select>
</div>
<div className="flex flex-row gap-1 items-center">
<span className="capitalize">{t("color")} :</span>
<select
value={
props.fontColor ||
clickonWidget.options?.fontColor ||
"black"
}
onChange={(e) => props.setFontColor(e.target.value)}
className="ml-[4px] md:ml[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
>
{fontColorArr.map((color, ind) => (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
))}
</select>
<span
style={{
background:
props.fontColor || props.pos.options?.fontColor || "black"
}}
className="w-5 h-[19px] ml-1"
></span>
</div>
</div>
{props?.isPlaceholder && props?.data?.Role !== "prefill" && (
<div className="flex items-center gap-1 w-max">
<input
name="isReadOnly"
id="isReadOnly"
type="checkbox"
checked={formdata?.isReadOnly}
className="op-checkbox op-checkbox-xs"
onChange={() =>
setFormdata({
...formdata,
isReadOnly: !formdata?.isReadOnly
})
}
/>
<label
htmlFor="isReadOnly"
className="capitalize ml-[2px] mb-0 cursor-pointer"
>
{t("read-only")}
</label>
</div>
)}
</div>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button
type="button"
className="op-btn op-btn-primary"
onClick={() => saveDateSetting(selectDate)}
>
{t("save")}
</button>
<button
type="button"
className="op-btn op-btn-ghost text-base-content ml-1"
onClick={() => handleCloseDateModal()}
>
{t("cancel")}
</button>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button className="op-btn op-btn-primary" type="submit">
{t("save")}
</button>
<button
type="button"
className="op-btn op-btn-ghost text-base-content ml-1"
onClick={() => handleCloseDateModal()}
>
{t("cancel")}
</button>
</form>
</div>
</DateWidgetModal>
</>
@@ -99,14 +99,8 @@ function PlaceholderType(props) {
ExampleCustomInput.displayName = "ExampleCustomInput";
const handleRadioCheck = (data) => {
const defaultData = props.pos.options?.defaultValue;
if (widgetValue === data) {
return true;
} else if (defaultData === data) {
return true;
} else {
return false;
}
const defaultData = widgetValue || props.pos.options?.defaultValue;
return defaultData === data;
};
//function is used to get prefill image's signedUrl after expired
useEffect(() => {
@@ -33,7 +33,9 @@ import Draw from "./tab/Draw";
import PenColorComponent from "./tab/PenColorComponent";
const widgetTitle = "font-medium";
const widgetLabelCss = (isRequired = false) => {
return `${isRequired ? "after:content-['_*'] after:text-red-500" : ""} block text-xs font-semibold`;
};
const ShowTextWidget = ({ position, handleWidgetDetails }) => {
const inputRef = useRef(null);
const [inputValue, setInputValue] = useState(position.options.response || "");
@@ -51,7 +53,6 @@ const ShowTextWidget = ({ position, handleWidgetDetails }) => {
/>
);
};
const ImageComponent = (props) => {
const { t } = useTranslation();
const prefillImg = useSelector((state) => state.widget.prefillImg);
@@ -70,7 +71,11 @@ const ImageComponent = (props) => {
return (
<>
<span className={widgetTitle}>{props?.position.options?.name}</span>
<span
className={`${widgetLabelCss(props?.position?.options?.status === "required")} ${widgetTitle}`}
>
{props?.position.options?.name}
</span>
{imgUrl ? (
<>
<div className="cursor-pointer op-card border-[1px] border-gray-400 flex flex-col w-full h-full justify-center items-center ">
@@ -168,13 +173,12 @@ function PrefillWidgetModal(props) {
setLoading(false);
}
}, [props?.isPrefillModal]);
useEffect(() => {
setLoading(true);
//function is used to save all image base64 in redux state to display prefill images
const savePrefillImg = async () => {
const prefillImg = await utils?.savePrefillImg(props.xyPosition);
if (Array.isArray(prefillImg)) {
setLoading(true);
prefillImg.forEach((img) => dispatch(setPrefillImg(img)));
}
setLoading(false);
@@ -209,25 +213,7 @@ function PrefillWidgetModal(props) {
}
}
}, [props?.isPrefillModal, uniqueWidget]);
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
const getDatePickerDate = (selectedDate, format = "dd-MM-yyyy") => {
let date;
if (format && format === "dd-MM-yyyy") {
const [day, month, year] = selectedDate?.split("-");
date = new Date(`${year}-${month}-${day}`);
} else if (format && format === "dd.MM.yyyy") {
const [day, month, year] = selectedDate?.split(".");
date = new Date(`${year}.${month}.${day}`);
} else if (format && format === "dd/MM/yyyy") {
const [day, month, year] = selectedDate?.split("/");
date = new Date(`${year}/${month}/${day}`);
} else {
date = new Date(selectedDate);
}
return date;
};
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{ fontFamily: "Arial, sans-serif" }}
@@ -243,8 +229,8 @@ function PrefillWidgetModal(props) {
const handleDate = (position) => {
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
const getDate = getDatePickerDate(
position?.options.response || new Date(),
const getDate = utils.getDatePickerDate(
position?.options.response,
position?.options?.validation?.format
);
return getDate;
@@ -442,7 +428,11 @@ function PrefillWidgetModal(props) {
case "checkbox":
return (
<>
<span className={widgetTitle}>{position.options?.name}</span>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<div className="flex flex-col gap-y-1">
{position.options?.values?.map((data, ind) => (
<div
@@ -472,7 +462,11 @@ function PrefillWidgetModal(props) {
case textWidget:
return (
<>
<span className={widgetTitle}>{position.options?.name}</span>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<ShowTextWidget
position={position}
handleWidgetDetails={handleWidgetDetails}
@@ -482,7 +476,11 @@ function PrefillWidgetModal(props) {
case "dropdown":
return (
<>
<span className={widgetTitle}>{position.options?.name}</span>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-base-content w-full"
id="myDropdown"
@@ -506,7 +504,11 @@ function PrefillWidgetModal(props) {
case "date":
return (
<>
<span className={widgetTitle}>{position.options?.name}</span>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<DatePicker
portalId="datepicker-portal-root"
renderCustomHeader={({ date, changeYear, changeMonth }) => (
@@ -558,7 +560,11 @@ function PrefillWidgetModal(props) {
case radioButtonWidget:
return (
<>
<span className={widgetTitle}>{position.options?.name}</span>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<div className="flex flex-col gap-y-1">
{position.options?.values.map((data, ind) => (
<div
@@ -586,7 +592,11 @@ function PrefillWidgetModal(props) {
case drawWidget:
return (
<div>
<span className={widgetTitle}>{position.options?.name}</span>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<Draw
penColor={penColor}
canvasRef={getCanvasRef(position.key)}
@@ -777,7 +787,7 @@ function PrefillWidgetModal(props) {
{t("recipients")}
</h1>
)}
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400 md:mx-3 text-base-content flex flex-col relative">
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400 text-base-content flex flex-col relative">
{props.forms?.map((field, id) => {
return (
<div className="flex flex-col" key={field?.value}>
@@ -48,7 +48,7 @@ function RenderAllPdfPage(props) {
};
// Use setTimeout to wait for the transition to complete
const timer = setTimeout(updateSize, 100); // match the transition duration
const timer = setTimeout(updateSize, 150); // match the transition duration
return () => clearTimeout(timer);
}, [isSidebar, pageContainer, props?.containerWH]);
@@ -29,12 +29,15 @@ import WidgetsDragPreview from "./WidgetsDragPreview";
function RenderPdf(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
const { guideline, showGuidelines } = useGuidelinesContext();
const [, drop] = useDrop({
accept: "BOX",
drop: (item, monitor) => props?.addPositionOfSignature(item, monitor)
drop: (item, monitor) => {
props?.addPositionOfSignature(item, monitor);
showGuidelines(false);
}
});
const [scaledHeight, setScaledHeight] = useState();
const { guideline, showGuidelines } = useGuidelinesContext();
//check isGuestSigner is present in local if yes than handle login flow header in mobile view
const isGuestSigner = localStorage.getItem("isGuestSigner");
const scrollTriggerId = useSelector((state) => state.widget.scrollTriggerId);
@@ -511,9 +514,10 @@ function RenderPdf(props) {
props.setPdfLoad(true);
props.pageDetails(pdf);
}}
onClick={() =>
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails({})
}
onClick={() => {
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails({});
showGuidelines(false);
}}
file={pdfDataBase64}
>
<Page
@@ -10,7 +10,8 @@ function SelectLanguage(props) {
{ value: "fr", text: "Français" }, //french
{ value: "it", text: "Italiano" }, //italian
{ value: "de", text: "Deutsch" }, //german
{ value: "hi", text: "हिन्दी" } //hindi
{ value: "hi", text: "हिन्दी" }, //hindi
{ value: "kr", text: "한국어" } //korean
];
const defaultLanguage = i18next.language || "en";
const [lang, setLang] = useState(defaultLanguage);
@@ -309,65 +309,64 @@ const WidgetNameModal = (props) => {
</div>
</>
)}
{!props?.isSelfSign &&
!isSignOrInitials && (
<div className={showFontControls ? "mb-[0.5rem]" : "mb-[0.75rem]"}>
<div className="flex flex-row gap-[10px] mb-[0.5rem]">
{statusArr.map((data, ind) => {
return (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
name="status"
onChange={() =>
setFormdata({
...formdata,
status: data.toLowerCase()
})
}
checked={
formdata.status.toLowerCase() === data.toLowerCase()
}
/>
<div className="text-[13px] font-medium">
{t(`widget-status.${data}`)}
</div>
</div>
);
})}
</div>
{[
textInputWidget,
cellsWidget,
].includes(props.defaultdata?.type) && (
<div className="flex items-center">
<input
id="isReadOnly"
name="isReadOnly"
type="checkbox"
checked={formdata.isReadOnly}
className="op-checkbox op-checkbox-xs"
onChange={() =>
setFormdata((prev) => ({
...formdata,
isReadOnly: !prev.isReadOnly
}))
}
/>
<label
className="ml-1.5 mb-0 capitalize text-[13px]"
htmlFor="isreadonly"
{!props?.isSelfSign && !isSignOrInitials && (
<div className={showFontControls ? "mb-[0.5rem]" : "mb-[0.75rem]"}>
<div className="flex flex-row gap-[10px] mb-[0.5rem]">
{statusArr.map((data, ind) => {
return (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
{t("read-only")}
</label>
</div>
)}
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
name="status"
onChange={() =>
setFormdata({
...formdata,
status: data.toLowerCase()
})
}
checked={
formdata.status.toLowerCase() === data.toLowerCase()
}
/>
<div className="text-[13px] font-medium">
{t(`widget-status.${data}`)}
</div>
</div>
);
})}
</div>
)}
{[
textInputWidget,
cellsWidget,
].includes(props.defaultdata?.type) && (
<div className="flex items-center">
<input
id="isReadOnly"
name="isReadOnly"
type="checkbox"
checked={formdata.isReadOnly}
className="op-checkbox op-checkbox-xs"
onChange={() =>
setFormdata((prev) => ({
...formdata,
isReadOnly: !prev.isReadOnly
}))
}
/>
<label
className="ml-1.5 mb-0 capitalize text-[13px]"
htmlFor="isreadonly"
>
{t("read-only")}
</label>
</div>
)}
</div>
)}
{isSignOrInitials && (
<div className="mb-[0.75rem]">
<label htmlFor="signaturetype" className="text-[14px] mb-[0.7rem]">
@@ -26,7 +26,8 @@ import {
getDefaultFormat,
getBase64MimeType,
drawWidget,
getBase64FromUrl
getBase64FromUrl,
clearResponse
} from "../../constant/Utils";
import CellsWidget from "./CellsWidget";
import DatePicker from "react-datepicker";
@@ -41,6 +42,7 @@ import {
setScrollTriggerId,
setPrefillImg,
setPrefillImgLoad,
setSignatureRes,
setTypedSignFont,
setMyStamp
} from "../../redux/reducers/widgetSlice";
@@ -74,6 +76,7 @@ const isTabCls = "bg-[#002864] text-white rounded-[15px] px-[10px] py-[4px]";
function WidgetsValueModal(props) {
const dispatch = useDispatch();
const {
signatureResponse,
prefillImg,
defaultSignImg,
myInitial,
@@ -96,20 +99,26 @@ function WidgetsValueModal(props) {
allowCellResize = true,
penColors
} = props;
const previousWidgetRes = signatureResponse?.find(
(item) => item.type === currWidgetsDetails?.type
);
const [penColor, setPenColor] = useState("");
const [isOptional, setIsOptional] = useState(true);
const [isDefaultSign, setIsDefaultSign] = useState(false);
const [isTab, setIsTab] = useState("");
const [textWidth, setTextWidth] = useState(0);
const [textHeight, setTextHeight] = useState(0);
const [isSignTypes, setIsSignTypes] = useState(true);
const [typedSignature, setTypedSignature] = useState("");
const [typedSignature, setTypedSignature] = useState(
previousWidgetRes?.value || ""
);
const [selectDate, setSelectDate] = useState({});
const [isLastWidget, setIsLastWidget] = useState(false);
const [signature, setSignature] = useState();
const [isImageSelect, setIsImageSelect] = useState(false);
const [isFinish, setIsFinish] = useState(false);
const [fontSelect, setFontSelect] = useState(fontOptions[0].value);
const [fontSelect, setFontSelect] = useState(
previousWidgetRes?.font || fontOptions[0].value
);
const [isSavedSign, setIsSavedSign] = useState(false);
const [isAutoSign, setIsAutoSign] = useState(false);
const [image, setImage] = useState(null);
@@ -330,8 +339,6 @@ function WidgetsValueModal(props) {
//function is used to save image,stamp widgets data
const handleSaveImage = async (signatureType) => {
const widgetsType = currWidgetsDetails?.type;
//`isApplyAll` is used when user edit stamp then updated signature apply all existing drawn signatures
const isApplyAll = true;
let imgUrl;
const isPrefill = xyPosition.some(
(x) => x.Id === uniqueId && x?.Role === "prefill"
@@ -362,7 +369,6 @@ function WidgetsValueModal(props) {
image,
isAutoSign,
widgetsType,
isApplyAll,
isPrefill && imgUrl,
defaultStampImg,
defaultStampType
@@ -375,6 +381,23 @@ function WidgetsValueModal(props) {
})
);
dispatch(setPrefillImgLoad({}));
if (
["image", "myStamp", "uploadStamp"].includes(isTab) &&
["signature", "initials", "stamp"].includes(currWidgetsDetails?.type)
) {
if (defaultStampImg || image?.src) {
dispatch(
setSignatureRes({
tab: isTab,
value: isTab === "myStamp" ? defaultStampImg : image?.src,
imageType:
isTab === "myStamp" ? defaultStampType : image?.imgType,
type: currWidgetsDetails?.type
})
);
}
}
} else {
const index = props?.xyPosition?.findIndex((object) => {
return object.pageNumber === pageNumber;
@@ -388,7 +411,6 @@ function WidgetsValueModal(props) {
false,
widgetsType,
null,
null,
defaultStampImg,
defaultStampType
);
@@ -424,8 +446,6 @@ function WidgetsValueModal(props) {
let imgWH = { width: width ? width : "", height: height ? height : "" };
setIsImageSelect(false);
setImage();
// `isApplyAll` is used when user edit signature/initial then updated signature apply all existing drawn signatures
const isApplyAll = true;
if (uniqueId) {
const isPrefill = xyPosition.some(
(x) => x.Id === uniqueId && x?.Role === "prefill"
@@ -452,7 +472,6 @@ function WidgetsValueModal(props) {
typedSignature,
isAutoSign,
widgetsType,
isApplyAll,
fontSelect,
penColor
);
@@ -463,6 +482,19 @@ function WidgetsValueModal(props) {
})
);
dispatch(setPrefillImgLoad({}));
if (
["signature", "initials"].includes(currWidgetsDetails?.type) &&
(typedSignature || signatureImg)
) {
dispatch(
setSignatureRes({
tab: isTab,
value: isTab === "type" ? typedSignature : signatureImg,
type: currWidgetsDetails?.type,
...(isTab === "type" && fontSelect && { font: fontSelect })
})
);
}
} else {
const index = props?.xyPosition?.findIndex((object) => {
return object.pageNumber === pageNumber;
@@ -478,7 +510,6 @@ function WidgetsValueModal(props) {
typedSignature,
false,
widgetsType,
false,
fontSelect,
penColor
);
@@ -507,35 +538,42 @@ function WidgetsValueModal(props) {
if (getIndex !== -1) {
setIsSignTypes(true);
const tab = signatureTypes?.[getIndex].name;
if (currWidgetsDetails?.signatureType === "type") {
setIsTab("type");
const previousWidgetRes = signatureResponse.find(
(item) => item.type === currWidgetsDetails?.type
);
if (
previousWidgetRes?.tab &&
previousWidgetRes?.type === currWidgetsDetails?.type
) {
setIsTab(previousWidgetRes?.tab || "draw");
} else if (currWidgetsDetails?.signatureType) {
setIsTab(currWidgetsDetails?.signatureType);
} else if (tab === "draw") {
setIsTab("draw");
} else if (tab === "upload") {
setIsImageSelect(true);
setIsTab("uploadImage");
setIsTab("image");
} else if (tab === "typed") {
setIsTab("type");
} else if (tab === "default") {
if (currWidgetsDetails?.type !== "initials" && defaultSignImg) {
setIsDefaultSign(true);
setIsTab("mysignature");
} else if (currWidgetsDetails?.type === "initials" && myInitial) {
setIsDefaultSign(true);
setIsTab("myinitials");
} else {
setIsTab("draw");
}
} else {
setIsTab(true);
}
} else {
setIsSignTypes(false);
}
} else if (currWidgetsDetails?.type === "image") {
setIsTab("uploadImage");
setIsTab("image");
} else if (currWidgetsDetails?.type === "stamp") {
if (currWidgetsDetails?.options?.response) {
const previousWidgetRes = signatureResponse.find(
(item) => item.type === currWidgetsDetails?.type
);
if (previousWidgetRes?.tab === "uploadStamp") {
setIsTab("uploadStamp");
} else if (myStamp) {
setIsTab("myStamp");
@@ -638,7 +676,10 @@ function WidgetsValueModal(props) {
};
const resetToDefault = () => {
props?.setCurrWidgetsDetails({});
if (!image) {
if (
isTab !== "image" &&
!["stamp", "image"].includes(currWidgetsDetails?.type)
) {
if (isTab === "myStamp" && myStamp) {
handleSaveImage();
} else if (isTab === "mysignature") {
@@ -674,17 +715,15 @@ function WidgetsValueModal(props) {
} else {
setSignature("");
canvasRef?.current?.clear();
const tab = isTab === "uploadImage" ? "image" : isTab;
const tab = isTab;
handleSaveSignature(tab);
}
// setPenColor("blue");
handlePenColor();
} else {
setSignature("");
handleSaveImage("image");
}
setIsImageSelect(false);
setIsDefaultSign(false);
setImage();
handleTab();
};
@@ -719,15 +758,19 @@ function WidgetsValueModal(props) {
loadFont();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fontSelect]);
useEffect(() => {
handleWidgetsResponse();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isTab]);
const handleWidgetsResponse = async () => {
if (currWidgetsDetails?.options?.response) {
let url = currWidgetsDetails?.options?.response;
//'signatureResponse' is save signature/initials widget response to show on response in signature pad
const previousWidgetRes = signatureResponse.find(
(item) => item.type === currWidgetsDetails?.type
);
let url = currWidgetsDetails?.options?.response;
if (currWidgetsDetails?.options?.response || previousWidgetRes) {
//draw widget
if (currWidgetsDetails?.type === drawWidget) {
if (canvasRef.current) {
const iBase64 = isValidBase64(url);
@@ -739,17 +782,36 @@ function WidgetsValueModal(props) {
canvasRef.current.fromDataURL(base64Url);
}
}
} else if (isSignOrInitials) {
if (isTab === "draw" && currWidgetsDetails?.signatureType === "draw") {
setSignature(url);
// Load the default signature after the component mounts
if (canvasRef.current) {
canvasRef.current.fromDataURL(url);
}
//signature and initial widgets
if (isSignOrInitials) {
if (isTab === "draw" || isTab === "image") {
if (
(previousWidgetRes?.tab === "draw" &&
currWidgetsDetails?.signatureType !== "draw" &&
previousWidgetRes?.type === currWidgetsDetails?.type) ||
currWidgetsDetails?.signatureType === "draw"
) {
url = url || previousWidgetRes?.value;
setSignature(url);
canvasRef.current && canvasRef.current.fromDataURL(url);
return;
} else if (
previousWidgetRes?.tab === "image" &&
previousWidgetRes?.type === currWidgetsDetails?.type
) {
setImage({
src: previousWidgetRes?.value,
imgType: previousWidgetRes?.imageType
});
}
} else if (isTab === "uploadImage" && currWidgetsDetails?.ImageType) {
}
if (isTab === "image" && currWidgetsDetails?.ImageType) {
setImage({ imgType: currWidgetsDetails?.ImageType, src: url });
}
} else if (isImageOrStamp) {
}
//image and stamp widget
if (isImageOrStamp) {
const isPrefill = xyPosition.some(
(x) => x.Id === uniqueId && x?.Role === "prefill"
);
@@ -759,9 +821,20 @@ function WidgetsValueModal(props) {
);
url = getPrefillImg?.base64;
}
if (isTab === "myStamp") {
if (
["myStamp", "uploadStamp"].includes(previousWidgetRes?.tab) &&
previousWidgetRes?.type === currWidgetsDetails?.type
) {
setImage({
imgType: previousWidgetRes?.ImageType,
src: previousWidgetRes?.value
});
} else if (isTab === "myStamp") {
setImage(null);
} else {
} else if (
currWidgetsDetails?.options?.response &&
currWidgetsDetails?.type === "image"
) {
setImage({ imgType: currWidgetsDetails?.ImageType, src: url });
}
}
@@ -798,7 +871,7 @@ function WidgetsValueModal(props) {
const fontFamily = fontStyle || typedSignFont || fontSelect || "Fasthand";
const fillColor = color || penColor;
const dataUrl = convertTextToImg(fontFamily, text, fillColor, widgetDims);
setSignature(dataUrl);
// setSignature(dataUrl);
setTextWidth(maxWidth);
setTextHeight(maxHeight);
dispatch(setTypedSignFont(fontFamily));
@@ -1124,10 +1197,10 @@ function WidgetsValueModal(props) {
},
//Upload Image tab
{
id: "uploadImage",
id: "image",
label: t("upload-image"),
onClick: () => {
setIsTab("uploadImage");
setIsTab("image");
setIsImageSelect(true);
},
show:
@@ -1178,7 +1251,12 @@ function WidgetsValueModal(props) {
{
id: "type",
label: t("type"),
onClick: () => setIsTab("type"),
onClick: () => {
setIsTab("type");
if (previousWidgetRes?.tab !== "type") {
setTypedSignature("");
}
},
show: isTabEnabled("typed") && !isStampOrImage,
render: () => (
<>
@@ -1603,7 +1681,7 @@ function WidgetsValueModal(props) {
const isCheckBox =
!currWidgetsDetails.options?.isReadOnly &&
currWidgetsDetails.type === "checkbox";
if (isCheckBox) {
if (isCheckBox && props?.role !== "prefill") {
//get minimum required count if exist
const minCount =
currWidgetsDetails?.options?.validation?.minRequiredCount;
@@ -1711,17 +1789,55 @@ function WidgetsValueModal(props) {
}
};
// focusNextWidget(currWidgetsDetails?.key, xyPosition)
// `clearWidgetResponse` is used to clear response of signature, stamp, image, initials, draw widget based on id
const clearWidgetResponse = () => {
const widgetKey = currWidgetsDetails?.key;
const isOptional =
currWidgetsDetails?.options?.status === "optional" || false;
if (widgetKey && isOptional) {
const isPrefill = xyPosition.some(
({ Id, Role }) => Id === uniqueId && Role === "prefill"
);
if (isPrefill) dispatch(setPrefillImg({ id: widgetKey, base64: "" }));
setXyPosition((prev) =>
prev.map((signer) => {
if (signer.Id !== uniqueId) return signer;
const idx = signer?.placeHolder?.findIndex(
(p) => p.pageNumber === pageNumber
);
if (idx === -1) return signer;
return {
...signer,
placeHolder: clearResponse(widgetKey, signer.placeHolder, idx)
};
})
);
}
};
//function too use on click on next/finish button then update modal UI according to current widgets
const handleClickOnNext = async (isFinishDoc) => {
if (
["signature", "stamp", "image", "initials", drawWidget].includes(
currWidgetsDetails?.type
) &&
(signature || image || myInitial || defaultSignImg || myStamp)
)
) {
//function to save all type draw or image
handleSaveBtn();
if (
signature ||
image ||
myInitial ||
defaultSignImg ||
myStamp ||
typedSignature
) {
//function to save all type draw or image
handleSaveBtn();
} else {
clearWidgetResponse();
}
}
if (isSave) {
handleclose();
@@ -1787,9 +1903,8 @@ function WidgetsValueModal(props) {
const { type } = currWidgetsDetails || {};
//conditions based on widget type or active tab
const conditions = {
image: !image, // Image widget also requires an image
draw: !signature, // Draw tab requires a drawn signature
uploadImage: !image, // Upload tab requires an image
image: !image, // Upload tab requires an image
mysignature: !defaultSignImg, // My Signature tab requires a saved/default signature
type: !(typedSignature?.trim()?.length > 0), // Type tab requires non-empty typed signature
myinitials: !myInitial,
@@ -16,7 +16,9 @@ function PenColorComponent({
convertToImg,
fontSelect,
typedSignature,
setPenColor
setPenColor,
hideLabel = false,
penSize
}) {
const { t } = useTranslation();
@@ -32,9 +34,12 @@ function PenColorComponent({
return filtered.length ? [...new Set(filtered)] : ALLOWED_COLORS;
}, [providedColors]);
const PEN_SIZE = penSize === "sm" ? "text-[14px]" : "text-[16px]";
return (
<div className="flex flex-row items-center m-[5px] gap-3">
<span className="text-base-content">{t("options")}</span>
<div
className={`flex flex-row items-center m-[5px] ${penSize === "sm" ? "gap-2" : "gap-3"}`}
>
{!hideLabel && <span className="text-base-content">{t("options")}</span>}
{pensList.map((color) => {
const selected = penColor === color;
return (
@@ -45,10 +50,9 @@ function PenColorComponent({
aria-label={`Select ${color} pen`}
onClick={() => {
setPenColor?.(color);
convertToImg &&
convertToImg?.(fontSelect, typedSignature, color);
convertToImg && convertToImg?.(fontSelect, typedSignature, color);
}}
className={`${COLOR_CLASS[color] || "text-base-content"} ${selected ? "border-current" : "border-white"} border-b-[2px] pb-0.5 cursor-pointer text-[16px] fa-light fa-pen-nib`}
className={`${COLOR_CLASS[color] || "text-base-content"} ${selected ? "border-current" : "border-white"} border-b-[2px] pb-0.5 cursor-pointer ${PEN_SIZE} fa-light fa-pen-nib`}
></i>
);
})}
@@ -10,16 +10,16 @@ const DateFormatSelector = (props) => {
const dateFormats = [
"MM/DD/YYYY",
"MMMM DD, YYYY",
"DD MMMM, YYYY",
"DD-MM-YYYY",
"DD MMM, YYYY",
"YYYY-MM-DD",
"MM-DD-YYYY",
"MM.DD.YYYY",
"MMM DD, YYYY",
"DD/MM/YYYY",
"DD-MM-YYYY",
"DD.MM.YYYY",
"DD/MM/YYYY"
"YYYY-MM-DD",
"MMM DD, YYYY",
"MMMM DD, YYYY",
"DD MMM, YYYY",
"DD MMMM, YYYY"
];
// Handle format change
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import Parse from "parse";
import ReactQuill from "react-quill-new";
@@ -15,8 +15,6 @@ const MailTemplateEditor = ({
}) => {
const appName = localStorage.getItem("appname") || "OpenSign™";
const { t } = useTranslation();
const editorRef = useRef();
const editorRefCom = useRef();
const [requestBody, setRequestBody] = useState("");
const [requestSubject, setRequestSubject] = useState("");
const [completionBody, setCompletionBody] = useState("");
@@ -188,15 +186,13 @@ const MailTemplateEditor = ({
}
});
//function for handle ontext change and save again text in delta
const handleOnchangeRequest = () => {
if (editorRef.current) {
const html = editorRef.current.editor.root.innerHTML;
const handleOnchangeRequest = (html) => {
if (html) {
setRequestBody(html);
}
};
const handleOnchangeCompletion = () => {
if (editorRefCom.current) {
const html = editorRefCom.current.editor.root.innerHTML;
const handleOnchangeCompletion = (html) => {
if (html) {
setCompletionBody(html);
}
};
@@ -259,10 +255,9 @@ const MailTemplateEditor = ({
theme="snow"
value={requestBody}
placeholder="add body of email"
ref={editorRef}
modules={module1}
formats={formats}
onChange={handleOnchangeRequest}
onChange={(value) => handleOnchangeRequest(value)}
/>
</div>
<div className="flex items-center mt-3 gap-2">
@@ -337,10 +332,9 @@ const MailTemplateEditor = ({
theme="snow"
value={completionBody}
placeholder="add body of email"
ref={editorRefCom}
modules={module2}
formats={formats}
onChange={handleOnchangeCompletion}
onChange={(value) => handleOnchangeCompletion(value)}
/>
</div>
<div className="flex items-center mt-3 gap-2">
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { formatDate, withSessionValidation } from "../../../utils";
import { dateFormat, formatDate, withSessionValidation } from "../../../utils";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import {
@@ -16,19 +16,6 @@ import {
import moment from "moment";
import { Tooltip as ReactTooltip } from "react-tooltip";
const dateFormatArr = [
"L",
"DD-MM-YYYY",
"YYYY-MM-DD",
"MM.DD.YYYY",
"MM-DD-YYYY",
"MMM DD, YYYY",
"LL",
"DD MMM, YYYY",
"DD MMMM, YYYY",
"DD.MM.YYYY",
"DD/MM/YYYY"
];
const WidgetsTab = () => {
const { t } = useTranslation();
@@ -146,7 +133,7 @@ const WidgetsTab = () => {
const formatDateList = (selecteddate) => {
let date = selecteddate || new Date();
const list = dateFormatArr.map((dateFormat) => {
const list = dateFormat.map((dateFormat) => {
const format = selectFormat(dateFormat);
return { date: formatDate({ date, format }), format: format };
});
@@ -0,0 +1,214 @@
import { useEffect, useMemo, useRef, useState } from "react";
/**
* CellsInput (cells for a single word/value)
*
* Props:
* - count: number (how many cells)
* - value: string (controlled)
* - onChange: (nextValue: string) => void
* - hint
* - textSize: number (px) default 12
* - textColor: string default "#000000"
* - only: "all" | "digits" | "letters" | "alphanumeric"
* - autoFocus: boolean
* - disabled: boolean
*/
export default function CellsInput({
count = 1,
value,
onChange,
hint = "",
textSize = 12,
textColor = "#000000",
only = "all",
autoFocus = false,
disabled = false,
className = ""
}) {
const isControlled = value !== undefined && typeof onChange === "function";
const [internal, setInternal] = useState("");
const currentValue = isControlled ? value : internal;
const safeCount = useMemo(() => clampInt(count, 1, 64, 6), [count]);
// refs for focusing next/prev
const inputsRef = useRef([]);
// normalize value length
useEffect(() => {
const normalized = normalizeValue(currentValue, safeCount, only);
if (normalized !== currentValue) emit(normalized);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [safeCount, only]);
useEffect(() => {
if (autoFocus && !disabled) {
queueMicrotask(() => inputsRef.current?.[0]?.focus?.());
}
}, [autoFocus, disabled]);
function emit(next) {
if (isControlled) onChange(next);
else setInternal(next);
}
const chars = useMemo(() => {
const v = normalizeValue(currentValue, safeCount, only);
const arr = Array.from({ length: safeCount }, (_, i) => v[i] || "");
return arr;
}, [currentValue, safeCount, only]);
const inputStyle = {
fontSize: `${Number(textSize || 12)}px`,
color: textColor && String(textColor).trim() ? textColor : "#000000"
};
function focusIndex(i) {
const el = inputsRef.current?.[i];
if (el) {
el.focus();
el.select?.();
}
}
function setCharAt(index, ch) {
const v = normalizeValue(currentValue, safeCount, only);
const nextArr = Array.from({ length: safeCount }, (_, i) => v[i] || "");
nextArr[index] = ch;
emit(nextArr.join(""));
}
function handleChange(index, raw) {
if (disabled) return;
// user typed multiple chars (mobile suggestions/paste) -> distribute
const cleaned = filterString(raw, only);
if (!cleaned) {
setCharAt(index, "");
return;
}
const v = normalizeValue(currentValue, safeCount, only);
const nextArr = Array.from({ length: safeCount }, (_, i) => v[i] || "");
let writeAt = index;
for (const ch of cleaned) {
if (writeAt >= safeCount) break;
nextArr[writeAt] = ch;
writeAt++;
}
emit(nextArr.join(""));
// move focus
const nextFocus = Math.min(writeAt, safeCount - 1);
if (nextFocus !== index) focusIndex(nextFocus);
}
function handleKeyDown(index, e) {
if (disabled) return;
const key = e.key;
if (key === "Backspace") {
e.preventDefault();
const v = normalizeValue(currentValue, safeCount, only);
if (v[index]) {
setCharAt(index, "");
return;
}
// if empty, go previous
if (index > 0) {
focusIndex(index - 1);
setCharAt(index - 1, "");
}
return;
}
if (key === "ArrowLeft") {
e.preventDefault();
if (index > 0) focusIndex(index - 1);
return;
}
if (key === "ArrowRight") {
e.preventDefault();
if (index < safeCount - 1) focusIndex(index + 1);
return;
}
if (key === " " && only !== "all") {
e.preventDefault();
return;
}
}
function handlePaste(index, e) {
if (disabled) return;
e.preventDefault();
const text = e.clipboardData?.getData("text") || "";
const cleaned = filterString(text, only);
if (!cleaned) return;
const v = normalizeValue(currentValue, safeCount, only);
const nextArr = Array.from({ length: safeCount }, (_, i) => v[i] || "");
let writeAt = index;
for (const ch of cleaned) {
if (writeAt >= safeCount) break;
nextArr[writeAt] = ch;
writeAt++;
}
emit(nextArr.join(""));
const nextFocus = Math.min(writeAt, safeCount - 1);
focusIndex(nextFocus);
}
return (
<div className={`w-full ${className}`}>
<div className="flex items-center justify-center gap-0">
{chars.map((ch, i) => (
<input
key={i}
ref={(el) => (inputsRef.current[i] = el)}
type="text"
inputMode={only === "digits" ? "numeric" : "text"}
autoComplete="one-time-code"
className="op-input op-input-bordered op-input-sm w-9 text-center rounded-sm focus:outline-none hover:border-base-content disabled:opacity-60"
style={inputStyle}
value={ch}
// maxLength={safeCount} // allows multi-char suggestions; we still distribute
disabled={disabled}
onChange={(e) => handleChange(i, e.target.value)}
onKeyDown={(e) => handleKeyDown(i, e)}
onPaste={(e) => handlePaste(i, e)}
placeholder={hint[i]}
/>
))}
</div>
</div>
);
}
/* ---------------- helpers ---------------- */
function clampInt(v, min, max, fallback) {
const n = parseInt(String(v), 10);
if (Number.isNaN(n)) return fallback;
return Math.max(min, Math.min(max, n));
}
function filterString(str, only) {
const s = String(str ?? "");
if (only === "digits") return s.replace(/[^0-9]/g, "");
if (only === "letters") return s.replace(/[^a-zA-Z]/g, "");
if (only === "alphanumeric") return s.replace(/[^a-zA-Z0-9]/g, "");
return s.replace(/\s/g, ""); // for "all" we still drop spaces for OTP-like behavior
}
function normalizeValue(v, count, only) {
const cleaned = filterString(v || "", only);
return cleaned.slice(0, count);
}
@@ -1,23 +1,98 @@
import React, { useState, useEffect, useRef } from "react";
import { useState, useLayoutEffect, useEffect, useRef } from "react";
import { findContact } from "../../../constant/Utils";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
function findScrollParent(el) {
let parent = el?.parentElement;
while (parent) {
const style = window.getComputedStyle(parent);
const overflowY = style.overflowY;
if (overflowY === "auto" || overflowY === "scroll") return parent;
parent = parent.parentElement;
}
return window; // fallback
}
const SuggestionInput = (props) => {
const { t } = useTranslation();
const [inputValue, setInputValue] = useState(props?.value || "");
const [suggestions, setSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const ref = useRef(null);
const inputRef = useRef(null);
const dropdownRef = useRef(null);
const [dropdownPos, setDropdownPos] = useState({
top: 0,
left: 0,
width: 0,
placement: "bottom" // bottom | top
});
const updateDropdownPosition = () => {
const inputEl = inputRef.current;
if (!inputEl) return;
const rect = inputEl.getBoundingClientRect();
const desiredHeight = 120; // roughly max-h (adjust if you want)
const gap = 6;
const spaceBelow = window.innerHeight - rect.bottom;
const openUpwards = spaceBelow < desiredHeight + gap;
const top = openUpwards
? Math.max(gap, rect.top - desiredHeight - gap)
: rect.bottom + gap;
setDropdownPos({
top,
left: rect.left,
width: rect.width,
placement: openUpwards ? "top" : "bottom"
});
};
useLayoutEffect(() => {
if (!showSuggestions) return;
updateDropdownPosition();
}, [showSuggestions, inputValue, suggestions.length]);
useEffect(() => {
document.addEventListener("mousedown", Clickout);
return () => {
document.removeEventListener("mousedown", Clickout);
const Clickout = (e) => {
if (
inputRef.current?.contains(e.target) ||
dropdownRef.current?.contains(e.target)
)
return;
setShowSuggestions(false);
};
document.addEventListener("mousedown", Clickout);
return () => document.removeEventListener("mousedown", Clickout);
}, []);
const Clickout = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
setShowSuggestions(false);
}
};
useEffect(() => {
if (!showSuggestions) return;
const inputEl = inputRef.current;
if (!inputEl) return;
const scrollParent = findScrollParent(inputEl);
const onScroll = () => updateDropdownPosition();
const onResize = () => updateDropdownPosition();
// capture scroll on window + scroll parent
window.addEventListener("scroll", onScroll, true);
window.addEventListener("resize", onResize);
if (scrollParent !== window)
scrollParent.addEventListener("scroll", onScroll, { passive: true });
return () => {
window.removeEventListener("scroll", onScroll, true);
window.removeEventListener("resize", onResize);
if (scrollParent !== window)
scrollParent.removeEventListener("scroll", onScroll);
};
}, [showSuggestions]);
// create debounce to avoid unnecessay api calls
useEffect(() => {
let timer;
@@ -63,32 +138,55 @@ const SuggestionInput = (props) => {
props.onChange(suggestion);
}
};
const dropdown =
showSuggestions && suggestions.length > 0
? createPortal(
<ul
ref={dropdownRef}
className="fixed z-[1002] max-h-[250px] overflow-y-auto bg-base-200 border border-gray-300 rounded shadow-md text-base-content"
style={{
top: dropdownPos?.top,
left: dropdownPos?.left,
maxWidth: dropdownPos?.width > 200 ? dropdownPos?.width : 200
}}
>
{suggestions.map((suggestion, index) => (
<li
key={index}
className="block py-2 px-2 w-full text-sm cursor-pointer hover:bg-base-300"
// use onMouseDown so click registers BEFORE input blur
onMouseDown={(e) => {
e.preventDefault();
handleSuggestionClick(suggestion);
}}
>
{suggestion.Name} {"<" + suggestion.Email + ">"}
</li>
))}
</ul>,
document.body
)
: null;
return (
<div className="flex flex-col items-center relative text-base-content">
<input
ref={inputRef}
type={props?.type || "text"}
value={inputValue}
onChange={handleInputChange}
placeholder="Enter Email..."
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs lowercase"
autoComplete="off"
autoCorrect="off"
autoCapitalize="none"
placeholder={`${t("enter-value", { value: t("Email") })}...`}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs lowercase placeholder:capitalize"
required={props.required}
onFocus={() => {
if (suggestions.length) setShowSuggestions(true);
}}
/>
{showSuggestions && (
<ul
ref={ref}
className="absolute z-50 left-0 top-[2.55rem] w-full max-h-[100px] overflow-y-auto bg-base-200 border border-gray-300 rounded shadow-md"
>
{suggestions.map((suggestion, index) => (
<li
key={index}
className="py-2 px-2 w-full text-sm cursor-pointer hover:bg-base-300"
onClick={() => handleSuggestionClick(suggestion)}
>
{suggestion.Name} {"<" + suggestion.Email + ">"}
</li>
))}
</ul>
)}
{dropdown}
</div>
);
};
+130 -145
View File
@@ -1050,9 +1050,7 @@ export const addZIndex = (signerPos, key, setZIndex) => {
zIndex: item.zIndex ? item.zIndex + 1 : 1
};
} else {
return {
...item
};
return { ...item };
}
}
});
@@ -1092,11 +1090,7 @@ export const onChangeInput = (
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const addSignPos = getXYdata.map((position) => {
if (
(position?.options?.name === currentPosition?.options?.name &&
filterSignerPos[0].Role === "prefill") ||
position.key === currentPosition.key
) {
if (position.key === currentPosition.key) {
if (dateFormat) {
return {
...position,
@@ -1108,13 +1102,17 @@ export const onChangeInput = (
? fontColor
: position.options?.fontColor,
isReadOnly:
dateDetails?.isReadOnly !== "undefined"
dateDetails && dateDetails?.isReadOnly !== "undefined"
? dateDetails?.isReadOnly
: position.options?.isReadOnly,
status:
dateDetails?.status !== "undefined"
dateDetails && dateDetails?.status !== "undefined"
? dateDetails?.status
: position.options?.status,
name:
dateDetails && dateDetails?.name !== "undefined"
? dateDetails?.name
: position.options?.name,
validation: {
type: "date-format",
format: dateFormat // This indicates the required date format explicitly.
@@ -1460,7 +1458,6 @@ export function onSaveSign(
typedSignature,
isAutoSign,
widgetsType,
isApplyAll,
typeFont,
fontColor
) {
@@ -1513,7 +1510,7 @@ export function onSaveSign(
return obj;
});
//condition when draw/upload signature/initials then apply it all related to widgets (draw, typed signature or default signature)
if ((isApplyAll || isAutoSign) && widgetsType !== drawWidget) {
if (isAutoSign && widgetsType !== drawWidget) {
const updatedArray = updateXYposition.map((page) => ({
...page,
pos: page.pos.map((item) => {
@@ -1543,6 +1540,26 @@ export function onSaveSign(
}
}
export function clearResponse(widgetKey, placeholder = [], index) {
if (!Array.isArray(placeholder) || !placeholder[index]?.pos) {
return placeholder;
}
const getXYdata = placeholder[index]?.pos;
const updateXYData = getXYdata.map((widget) => {
if (widget?.key !== widgetKey) return widget;
return {
...widget,
options: { ...widget.options, response: "" },
SignUrl: ""
};
});
const updatePlaceholder = placeholder.map((p, ind) => {
if (ind !== index) return p;
return { ...p, pos: updateXYData };
});
return updatePlaceholder;
}
/**
* Scales and centers a base64encoded image into a fixedsize widget
* and returns a new base64 PNG.
@@ -1553,12 +1570,7 @@ export function onSaveSign(
*/
export async function convertBase64ToImg(base64Image, widgetDims) {
const { Width: maxWidth, Height: maxHeight } = widgetDims;
// Detect input format from data URL
const match = base64Image.match(/^data:(image\/(png|jpeg|jpg|webp));base64,/);
const inputMime = match ? match[1] : "image/png"; // fallback to PNG if unknown
// 1. Load the image off-DOM
// Load the image off-DOM
const img = new Image();
img.src = base64Image;
await new Promise((resolve, reject) => {
@@ -1664,7 +1676,6 @@ export function onSaveImage(
image,
isAutoSign,
widgetsType,
isApplyAll,
imgUrl,
defaultStampImg,
defaultStampType
@@ -1696,7 +1707,7 @@ export function onSaveImage(
return obj;
});
// condition when user upload(stamp) then apply it all related to widgets
if (isApplyAll || isAutoSign) {
if (isAutoSign) {
const updatedArray = updateXYposition.map((page) => ({
...page,
pos: page.pos.map(
@@ -2355,11 +2366,18 @@ export const embedWidgetsToDoc = async (
}
});
}
// 5. Preselect a value if provided
if (position?.options?.response) {
radioGroup.select(position.options?.response);
} else if (position?.options?.defaultValue) {
radioGroup.select(position?.options?.defaultValue);
const isOptionExist = position?.options?.values?.some(
(x) =>
x === position?.options?.response ||
position?.options?.defaultValue
);
if (isOptionExist) {
// 5. Preselect a value if provided
if (position?.options?.response) {
radioGroup.select(position.options?.response);
} else if (position?.options?.defaultValue) {
radioGroup.select(position?.options?.defaultValue);
}
}
// 6. Set to readonly (if required)
radioGroup.enableReadOnly();
@@ -2407,11 +2425,7 @@ export const placeholderWidth = (pos) => {
const posWidth = pos.Width || defaultWidth;
//condition to handle old data saved from mobile view to get widthh
if (pos.isMobile && pos.scale) {
if (pos.IsResize) {
return posWidth;
} else {
return posWidth * pos.scale;
}
return pos.IsResize ? posWidth : posWidth * pos.scale;
} else {
return posWidth;
}
@@ -2425,11 +2439,7 @@ export const placeholderHeight = (pos) => {
//condition to handle old data saved from mobile view to get height
if (pos.isMobile && pos.scale) {
if (pos.IsResize) {
return posUpdateHeight;
} else {
return posUpdateHeight * pos.scale;
}
return pos.IsResize ? posUpdateHeight : posUpdateHeight * pos.scale;
} else {
return posUpdateHeight;
}
@@ -2437,30 +2447,20 @@ export const placeholderHeight = (pos) => {
//function for getting contracts_contactbook details
export const contactBook = async (objectId) => {
const result = await axios
.get(
`${localStorage.getItem(
"baseUrl"
)}classes/contracts_Contactbook?where={"objectId":"${objectId}"}`,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
}
)
.then((Listdata) => {
const json = Listdata.data;
const res = json.results;
return res;
})
.catch((err) => {
console.log("Err in contracts_Contactbook class ", err);
return "Error: Something went wrong!";
});
return result;
try {
const url = `${localStorage.getItem("baseUrl")}classes/contracts_Contactbook?where={"objectId":"${objectId}"}`;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
const axiosRes = await axios.get(url, { headers });
const result = axiosRes?.data?.results;
return result;
} catch (error) {
console.error("contracts_Contactbook error", err);
return "Error: Something went wrong!";
}
};
//function for getting document details from contract_Documents class
@@ -2744,6 +2744,9 @@ export const fetchUrl = async (url, fileName) => {
export const getSignedUrl = async (pdfUrl, docId, templateId) => {
//use only axios here due to public template sign
const token = {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
const axiosRes = await axios.post(
`${localStorage.getItem("baseUrl")}/functions/getsignedurl`,
{
@@ -2755,7 +2758,7 @@ export const getSignedUrl = async (pdfUrl, docId, templateId) => {
headers: {
"content-type": "Application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
...token
}
}
);
@@ -2877,6 +2880,22 @@ export const handleToPrint = async (event, setIsDownloading, pdfDetails) => {
alert(i18n.t("something-went-wrong-mssg"));
}
};
const downloadCertificate = async (certificate, isZip, asBlob) => {
try {
const appName = "OpenSign™";
const fetchCertificate = await fetch(certificate);
const certificateUrl = certificate;
if (isZip) {
return certificateUrl;
} else {
// Convert the response into a Blob
const blob = asBlob ? await fetchCertificate.blob() : certificateUrl;
saveAs(blob, `Certificate_signed_by_${appName}.pdf`);
}
} catch (err) {
console.error("download certificate err", err);
}
};
//handle download signed pdf
export const handleDownloadCertificate = async (
@@ -2884,82 +2903,51 @@ export const handleDownloadCertificate = async (
setIsDownloading,
isZip
) => {
const appName = "OpenSign™";
if (pdfDetails?.length > 0 && pdfDetails[0]?.CertificateUrl) {
try {
await fetch(pdfDetails[0] && pdfDetails[0]?.CertificateUrl);
const certificateUrl = pdfDetails[0] && pdfDetails[0]?.CertificateUrl;
if (isZip) {
return certificateUrl;
} else {
saveAs(certificateUrl, `Certificate_signed_by_${appName}.pdf`);
}
} catch (err) {
console.log("err in download in certificate", err);
}
const baseUrl = `${localStorage.getItem("baseUrl")}functions`;
const parseAppId = localStorage.getItem("parseAppId");
const sessionToken = localStorage.getItem("accesstoken");
const docId = pdfDetails?.[0]?.objectId;
const initialCertificateUrl = pdfDetails?.[0]?.CertificateUrl;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseAppId
};
if (initialCertificateUrl) {
await downloadCertificate(initialCertificateUrl, isZip);
} else {
setIsDownloading("certificate");
try {
const data = { docId: pdfDetails[0]?.objectId };
const docDetails = await axios.post(
`${localStorage.getItem("baseUrl")}functions/getDocument`,
data,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessionToken: localStorage.getItem("accesstoken")
}
}
);
if (docDetails.data && docDetails.data.result) {
const doc = docDetails.data.result;
if (doc?.CertificateUrl) {
await fetch(doc?.CertificateUrl);
const certificateUrl = doc?.CertificateUrl;
if (isZip) {
const data = { docId: docId };
const docDetails = await axios.post(`${baseUrl}/getDocument`, data, {
headers: { ...headers, sessionToken }
});
const cert = docDetails?.data?.result?.CertificateUrl;
if (cert) {
await downloadCertificate(cert, isZip);
setIsDownloading("");
} else {
const generateRes = await axios.post(
`${baseUrl}/generatecertificate`,
data,
{ headers }
);
const certificate = generateRes?.data?.result?.CertificateUrl;
if (certificate) {
try {
await downloadCertificate(certificate, isZip, true);
setIsDownloading("");
return certificateUrl;
} else {
saveAs(certificateUrl, `Certificate_signed_by_${appName}.pdf`);
setIsDownloading("");
}
} else {
const generateRes = await axios.post(
`${localStorage.getItem("baseUrl")}functions/generatecertificate`,
data,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId")
}
}
);
if (generateRes?.data?.result?.CertificateUrl) {
try {
const certificateUrl = generateRes.data.result.CertificateUrl;
const fetchCertificate = await fetch(certificateUrl);
if (isZip) {
setIsDownloading("");
return certificateUrl;
} else {
// Convert the response into a Blob
const certificateBlob = await fetchCertificate.blob();
setIsDownloading("");
saveAs(certificateBlob, `Certificate_signed_by_${appName}.pdf`);
}
} catch (err) {
console.log("err in download in certificate", err);
setIsDownloading("certificate_err");
}
} else {
} catch (err) {
console.error("download certificate err", err);
setIsDownloading("certificate_err");
}
} else {
setIsDownloading("certificate_err");
}
}
} catch (err) {
setIsDownloading("certificate_err");
console.log("err in download in certificate", err);
console.error("download certificate err", err);
alert(i18n.t("something-went-wrong-mssg"));
}
}
@@ -3211,17 +3199,12 @@ export const convertBase64ToFile = async (pdfName, pdfBase64, imgType) => {
console.log("error in convertbase64tofile", e);
}
};
export const onClickZoomIn = (scale, zoomPercent, setScale, setZoomPercent) => {
export const onClickZoomIn = (zoomPercent, setScale, setZoomPercent) => {
const newPercent = zoomPercent + 10;
setZoomPercent(newPercent);
setScale(1 + newPercent / 100);
};
export const onClickZoomOut = (
zoomPercent,
scale,
setZoomPercent,
setScale
) => {
export const onClickZoomOut = (zoomPercent, setZoomPercent, setScale) => {
if (zoomPercent > 0) {
const newPercent = Math.max(0, zoomPercent - 10);
setZoomPercent(newPercent);
@@ -3299,11 +3282,7 @@ export const handleRotateWarning = (signerPos, pageNumber) => {
const placeholderExist = signerPos?.some((placeholderObj) =>
placeholderObj?.placeHolder?.some((data) => data?.pageNumber === pageNumber)
);
if (placeholderExist) {
return true;
} else {
return false;
}
return placeholderExist ? true : false;
};
// `generateTitleFromFilename` to generate Title of document from file name
@@ -3584,7 +3563,18 @@ export const updateDateWidgetsRes = (
...ph,
// Sort positions within each page
pos: [...ph.pos]
.sort((a, b) => a.yPosition - b.yPosition)
.sort((a, b) => {
// Sort widgets by Y position (top to bottom) and X position (left to right)
// Treat widgets within 5px Y difference as belonging to the same row
const Y_TOLERANCE = 5;
const yDiff = a.yPosition - b.yPosition;
if (Math.abs(yDiff) <= Y_TOLERANCE) {
return a.xPosition - b.xPosition; // Same row → sort by X
}
return yDiff; // Different rows → sort by Y
})
.map((widget) => {
// Update widget values if needed
if (
@@ -3606,7 +3596,6 @@ export const updateDateWidgetsRes = (
return widget;
})
}));
return { ...item, placeHolder: sortedPlaceHolder };
}
@@ -3786,11 +3775,7 @@ export const handleCheckResponse = (checkUser, setminRequiredCount) => {
break;
}
}
return {
tourPageNumber,
widgetKey,
showAlert
};
return { tourPageNumber, widgetKey, showAlert };
};
/**
+2 -6
View File
@@ -1,19 +1,15 @@
import { useEffect } from "react";
import { useDrag } from "react-dnd";
import { getEmptyImage } from "react-dnd-html5-backend";
import { useGuidelinesContext } from "../context/GuidelinesContext";
import { useDispatch } from "react-redux";
export const useWidgetDrag = (item) => {
const { showGuidelines } = useGuidelinesContext();
const [, dragRef, preview] = useDrag({
type: "BOX",
item,
collect: (monitor) => ({
isDragging: monitor.isDragging()
}),
end: () => {
showGuidelines(false);
}
})
});
// Hide browser default drag preview
+1 -1
View File
@@ -25,7 +25,7 @@ i18n
interpolation: {
escapeValue: false // Not needed for react as it escapes by default
},
whitelist: ["en", "es", "fr", "it", "de", "hi"] // List of allowed languages
whitelist: ["en", "es", "fr", "it", "de", "hi", "kr"] // List of allowed languages
});
export default i18n;
+11 -56
View File
@@ -22,7 +22,7 @@ body {
}
@media screen and (max-width: 766px) {
.reactour__close {
.reactour__close-button {
width: 17px !important;
padding-left: 3px !important;
}
@@ -119,47 +119,24 @@ body {
}
/* React-tour modal styling */
.reactour__helper {
.reactour__popover {
background-color: #1f2937 !important;
color: #e5e7eb !important;
border: 1px solid #374151 !important;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5) !important;
}
.reactour__close {
.reactour__close-button {
color: #e5e7eb !important;
background-color: #374151 !important;
border: 1px solid #4b5563 !important;
}
.reactour__close:hover {
.reactour__close-button:hover {
background-color: #4b5563 !important;
color: #ffffff !important;
}
/* React-tour navigation buttons */
.reactour__controls {
background-color: #1f2937 !important;
border-top: 1px solid #374151 !important;
}
.reactour__controls button {
background-color: #374151 !important;
color: #e5e7eb !important;
border: 1px solid #4b5563 !important;
}
.reactour__controls button:hover {
background-color: #4b5563 !important;
color: #ffffff !important;
}
.reactour__controls button:disabled {
background-color: #3c3c3c !important;
color: #858585 !important;
border-color: #565656 !important;
cursor: not-allowed !important;
}
/* ReactTooltip styling */
.react-tooltip {
@@ -187,41 +164,19 @@ body {
color: #ffffff !important;
}
.reactour__helper h1,
.reactour__helper h2,
.reactour__helper h3,
.reactour__helper h4,
.reactour__helper h5,
.reactour__helper h6 {
.reactour__popover h1,
.reactour__popover h2,
.reactour__popover h3,
.reactour__popover h4,
.reactour__popover h5,
.reactour__popover h6 {
color: #ffffff !important;
}
.reactour__helper p {
.reactour__popover p {
color: #e5e7eb !important;
}
/* VS Code-style tour buttons */
.reactour__controls .op-btn {
background-color: #007acc !important;
color: #ffffff !important;
border: 1px solid #007acc !important;
}
.reactour__controls .op-btn:hover {
background-color: #0086d1 !important;
border-color: #0086d1 !important;
}
.reactour__controls .op-btn-secondary {
background-color: #374151 !important;
color: #e5e7eb !important;
border: 1px solid #4b5563 !important;
}
.reactour__controls .op-btn-secondary:hover {
background-color: #4b5563 !important;
color: #ffffff !important;
}
/* React-datepicker dark mode styling */
.react-datepicker {
+4 -4
View File
@@ -699,7 +699,7 @@ const Forms = (props) => {
value={formData.password}
onChange={(e) => handleStrInput(e)}
className="w-full op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content text-xs"
placeholder="Enter pdf password"
placeholder={t("enter-pdf-password")}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
@@ -769,16 +769,16 @@ const Forms = (props) => {
<div className="flex gap-1 justify-center items-center">
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm w-full h-full text-[13px]">
<div className="break-all cursor-default">
{t("file-selected")}: {selectedFiles.join(", ")}
{t("files-selected")}: {selectedFiles.join(", ")}
</div>
<div
onClick={() => {
setFileUpload("");
setSelectedFiles([]);
}}
className="cursor-pointer px-[10px] text-[20px] font-bold text-red-500"
className="cursor-pointer ml-[10px] text-[20px] font-bold"
>
<i className="fa-light fa-xmark"></i>
<i className="fa-light fa-xmark inline-flex h-7 w-7 items-center justify-center rounded-full hover:bg-red-100 text-red-500" />
</div>
</div>
</div>
+6 -4
View File
@@ -71,11 +71,13 @@ import PlaceholderCopy from "../components/pdf/PlaceholderCopy";
import TextFontSetting from "../components/pdf/TextFontSetting";
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
import * as utils from "../utils";
import { useWindowSize } from "../hook/useWindowSize";
function PdfRequestFiles(
) {
const { t } = useTranslation();
const dispatch = useDispatch();
const windowSize = useWindowSize();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const isShowModal = useSelector((state) => state.widget.isShowModal);
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
@@ -208,10 +210,10 @@ function PdfRequestFiles(
};
// Use setTimeout to wait for the transition to complete
const timer = setTimeout(updateSize, 100); // match the transition duration
const timer = setTimeout(updateSize, 150); // match the transition duration
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [divRef.current, isSidebar]);
}, [divRef.current, isSidebar, windowSize?.width]);
const redirectUrl = pdfDetails?.[0]?.RedirectUrl || "";
useEffect(() => {
if (isredirectCanceled) return; // Stop the redirect timer if canceled
@@ -1394,10 +1396,10 @@ function PdfRequestFiles(
};
const clickOnZoomIn = () => {
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
};
const clickOnZoomOut = () => {
onClickZoomOut(zoomPercent, scale, setZoomPercent, setScale);
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
};
const handleDownloadBtn = async () => {
const url = pdfDetails?.[0]?.SignedUrl || pdfDetails?.[0]?.URL;
+45 -47
View File
@@ -65,11 +65,13 @@ import * as utils from "../utils";
import { resetWidgetState, setPrefillImg } from "../redux/reducers/widgetSlice";
import EditDocument from "../components/pdf/EditTemplate";
import CustomizeMail from "../components/pdf/CustomizeMail";
import { useWindowSize } from "../hook/useWindowSize";
function PlaceHolderSign() {
const { t } = useTranslation();
const copyUrlRef = useRef(null);
const dispatch = useDispatch();
const windowSize = useWindowSize();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const isShowModal = useSelector((state) => state.widget.isShowModal);
const editorRef = useRef();
@@ -130,7 +132,7 @@ function PlaceHolderSign() {
const [isCheckbox, setIsCheckbox] = useState(false);
const [isNameModal, setIsNameModal] = useState(false);
const [mailStatus, setMailStatus] = useState("");
const [isCurrUser, setIsCurrUser] = useState(false);
const [isCurrUser, setIsCurrUser] = useState("");
const [pdfArrayBuffer, setPdfArrayBuffer] = useState("");
const isSidebar = useSelector((state) => state.sidebar.isOpen);
const [showRotateAlert, setShowRotateAlert] = useState({
@@ -224,10 +226,10 @@ function PlaceHolderSign() {
}
};
// Use setTimeout to wait for the transition to complete
const timer = setTimeout(updateSize, 100); // match the transition duration
const timer = setTimeout(updateSize, 150); // match the transition duration
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [divRef.current, isSidebar]);
}, [divRef.current, isSidebar, windowSize?.width]);
//function for get document details
const getDocumentDetails = async () => {
const tenantSignTypes = await fetchTenantDetails();
@@ -1055,16 +1057,6 @@ function PlaceHolderSign() {
: 15;
const currentUser = signersdata.find((x) => x.Email === currentId);
setCurrentId(currentUser?.objectId);
if (
pdfDetails?.[0]?.SendinOrder &&
pdfDetails?.[0]?.SendinOrder === true
) {
const currentUserMail = Parse.User.current()?.getEmail();
const isCurrentUser = signerMail?.[0]?.Email === currentUserMail;
setIsCurrUser(isCurrentUser);
} else {
setIsCurrUser(currentUser?.objectId ? true : false);
}
// Compute expiry date with extra days
let updateExpiryDate = new Date();
updateExpiryDate.setDate(updateExpiryDate.getDate() + addExtraDays);
@@ -1101,12 +1093,16 @@ function PlaceHolderSign() {
const ownerId = pdfDetails[0].ExtUserPtr?.UserId?.objectId;
const firstSigner = signersdata[0];
const isOwner = firstSigner?.UserId?.objectId === ownerId;
const currentUserMail = Parse.User.current()?.getEmail();
const isCurrentUser = signerMail?.[0]?.Email === currentUserMail;
if (pdfDetails[0]?.SendinOrder && isOwner) {
setIsSend(true);
setIsCurrUser(isCurrentUser);
setIsCurrUser(currentUser?.objectId);
} else {
const currentSigner = signersdata?.find(
(x) => x?.UserId?.objectId === ownerId
);
if (currentSigner) {
setIsCurrUser(currentSigner?.objectId);
}
setIsMailModal(true);
}
} catch (e) {
@@ -1727,10 +1723,10 @@ function PlaceHolderSign() {
}
};
const clickOnZoomIn = () => {
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
};
const clickOnZoomOut = () => {
onClickZoomOut(zoomPercent, scale, setZoomPercent, setScale);
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
};
//`handleRotationFun` function is used to roatate pdf particular page
const handleRotationFun = async (rotateDegree) => {
@@ -1958,36 +1954,37 @@ function PlaceHolderSign() {
</div>
)}
{mailStatus !== "quotareached" && (
<div
className={
mailStatus === "success"
? "flex justify-center mt-1"
: "flex items-center justify-center mt-7"
}
>
{isCurrUser && (
<button
onClick={() => handleRecipientSign()}
type="button"
className="op-btn op-btn-primary mr-1"
>
{t("sign-now")}
</button>
)}
<button
onClick={() => {
setIsSend(false);
setSignerPos([]);
navigate("/report/1MwEuxLEkF");
}}
type="button"
className="op-btn op-btn-ghost text-base-content"
{mailStatus !== "quotareached" &&
mailStatus !== "failed" && (
<div
className={
mailStatus === "success"
? "flex justify-center mt-1"
: "flex items-center justify-center mt-7"
}
>
{isCurrUser ? t("no") : t("close")}
</button>
</div>
)}
{isCurrUser && (
<button
onClick={() => handleRecipientSign()}
type="button"
className="op-btn op-btn-primary mr-1"
>
{t("sign-now")}
</button>
)}
<button
onClick={() => {
setIsSend(false);
setSignerPos([]);
navigate("/report/1MwEuxLEkF");
}}
type="button"
className="op-btn op-btn-ghost text-base-content"
>
{isCurrUser ? t("no") : t("close")}
</button>
</div>
)}
</div>
{mailStatus !== "success" &&
isCurrUser &&
@@ -2273,6 +2270,7 @@ function PlaceHolderSign() {
defaultMail={defaultMail}
isMailModal={isMailModal}
handleShareList={handleShareList}
setCurrUserId={setIsCurrUser}
/>
</div>
)}
+25 -3
View File
@@ -34,6 +34,7 @@ const Report = () => {
const [isColumnModal, setIsColumnModal] = useState(false);
const [searchLoader, setSearchLoader] = useState(false);
const debounceTimer = useRef(null);
const searchAbortRef = useRef(null); // holds AbortController
// Number of documents to display per page (should always be half of docLimit for proper pagination)
const docPerPage = 10;
// Number of documents to fetch per API call
@@ -63,6 +64,10 @@ const Report = () => {
setIsNextRecord(false);
// Here it'll abort the fetch
abortController.abort();
if (searchAbortRef.current) {
searchAbortRef.current.abort();
searchAbortRef.current = null;
}
};
// eslint-disable-next-line
}, [id]);
@@ -81,7 +86,16 @@ const Report = () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
// 2) abort any in-flight request triggered by older debounce
if (searchAbortRef.current) {
searchAbortRef.current.abort();
searchAbortRef.current = null;
}
debounceTimer.current = setTimeout(async () => {
// create controller for THIS request
const controller = new AbortController();
searchAbortRef.current = controller;
setSearchLoader(true);
try {
const headers = {
@@ -93,8 +107,11 @@ const Report = () => {
const res = await axios.post(
url,
{ reportId: id, searchTerm: term, skip: 0, limit: docPerPage },
{ headers }
{ headers: headers, signal: controller.signal } // axios abort
);
// if you want to be extra safe (ignore late responses)
if (controller.signal.aborted) return;
const data = res.data?.result || [];
if (!data.error) {
setList(data);
@@ -104,7 +121,12 @@ const Report = () => {
setSearchLoader(false);
}
} catch (err) {
console.error("Search error:", err);
// ignore abort errors
const isAbort =
err?.name === "CanceledError" ||
err?.code === "ERR_CANCELED" ||
err?.message?.toLowerCase?.().includes("canceled");
if (!isAbort) console.error("Search error:", err);
setSearchLoader(false);
}
}, 300);
@@ -222,7 +244,7 @@ const Report = () => {
} catch (err) {
const isCancel = axios.isCancel(err);
if (!isCancel) {
console.log("err ", err);
console.error("getreport error", err);
setIsLoader(false);
}
}
+6 -4
View File
@@ -69,6 +69,7 @@ import {
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
import WidgetNameModal from "../components/pdf/WidgetNameModal";
import CellsSettingModal from "../components/pdf/CellsSettingModal";
import { useWindowSize } from "../hook/useWindowSize";
import {
applyNumberFormulasToPages,
} from "../utils";
@@ -77,6 +78,7 @@ function SignYourSelf() {
const { t } = useTranslation();
const { docId } = useParams();
const dispatch = useDispatch();
const windowSize = useWindowSize();
const isShowModal = useSelector((state) => state.widget.isShowModal);
const appName =
"OpenSign™";
@@ -189,10 +191,10 @@ function SignYourSelf() {
};
// Use setTimeout to wait for the transition to complete
const timer = setTimeout(updateSize, 100); // match the transition duration
const timer = setTimeout(updateSize, 150); // match the transition duration
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [divRef.current, isSidebar]);
}, [divRef.current, isSidebar, windowSize?.width]);
//function for get document details for perticular signer with signer'object id
const getDocumentDetails = async (showComplete) => {
try {
@@ -1164,10 +1166,10 @@ function SignYourSelf() {
setCurrWidgetsDetails({});
};
const clickOnZoomIn = () => {
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
};
const clickOnZoomOut = () => {
onClickZoomOut(zoomPercent, scale, setZoomPercent, setScale);
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
};
//`handleRotationFun` function is used to roatate pdf particular page
const handleRotationFun = async (rotateDegree) => {
@@ -66,12 +66,14 @@ import * as utils from "../utils";
import CustomizeMail from "../components/pdf/CustomizeMail";
import { resetWidgetState, setPrefillImg } from "../redux/reducers/widgetSlice";
import ShareButton from "../primitives/ShareButton";
import { useWindowSize } from "../hook/useWindowSize";
const TemplatePlaceholder = () => {
const { t } = useTranslation();
const { templateId } = useParams();
const windowSize = useWindowSize();
const dispatch = useDispatch();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const { prefillImg, isBulkLoader } = useSelector((state) => state.widget);
const divRef = useRef(null);
const navigate = useNavigate();
const numPages = 1;
@@ -183,11 +185,11 @@ const TemplatePlaceholder = () => {
};
// Use setTimeout to wait for the transition to complete
const timer = setTimeout(updateSize, 100); // match the transition duration
const timer = setTimeout(updateSize, 150); // match the transition duration
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [divRef.current, isSidebar]);
}, [divRef.current, isSidebar, windowSize?.width]);
//function to fetch tenant Details
@@ -978,6 +980,13 @@ const TemplatePlaceholder = () => {
setCurrUserId(firstSigner?.objectId);
setIsSend(true);
return;
} else {
const currentSigner = signersdata?.find(
(x) => x?.UserId?.objectId === ownerId
);
if (currentSigner) {
setCurrUserId(currentSigner?.objectId);
}
}
setIsMailModal(true);
@@ -1606,10 +1615,10 @@ const TemplatePlaceholder = () => {
};
const clickOnZoomIn = () => {
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
onClickZoomIn(zoomPercent, setScale, setZoomPercent);
};
const clickOnZoomOut = () => {
onClickZoomOut(zoomPercent, scale, setZoomPercent, setScale);
onClickZoomOut(zoomPercent, setZoomPercent, setScale);
};
//`handleRotationFun` function is used to roatate pdf particular page
const handleRotationFun = async (rotateDegree) => {
+11 -5
View File
@@ -1,5 +1,5 @@
import React from "react";
import "../styles/signature.css";
import Loader from "./Loader";
const ModalUi = ({
children,
@@ -10,11 +10,14 @@ const ModalUi = ({
showClose = true,
reduceWidth,
position,
crossColor
crossColor,
showScrollBar = false,
isLoader = false
}) => {
const width = reduceWidth;
const isBottom = position === "bottom" ? "items-end pb-2 !bg-black/10" : "";
const crossBtnColor = crossColor ?? "text-base-content";
const hideScrollBar = !showScrollBar ? "hide-scrollbar" : "";
return (
<>
{isOpen && (
@@ -26,10 +29,13 @@ const ModalUi = ({
content: { zIndex: 1001, overflow: "visible" } // Ensure modal doesnt clip content
}}
>
{isLoader && (
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
<Loader />
</div>
)}
<div
className={`${
width || "md:min-w-[500px]"
} op-modal-box p-0 max-h-90 overflow-y-auto hide-scrollbar text-sm`}
className={`${width || "md:min-w-[500px]"} op-modal-box p-0 max-h-90 overflow-y-auto ${hideScrollBar} text-sm`}
>
{showHeader && (
<>
@@ -144,7 +144,7 @@ export default function PasswordResetModal({
<input
type="text"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder="Enter password or click Autogenerate"
placeholder={t("enter-password-or-click-autogenerate")}
value={password}
onChange={(e) => setPassword(e.target.value)}
autoFocus
@@ -15,7 +15,9 @@ const initialState = {
scrollTriggerId: "",
prefillImg: [],
prefillImgLoad: {},
typedSignFont: "Fasthand"
typedSignFont: "Fasthand",
signatureResponse: [],
isBulkLoader: false
};
const widgetSlice = createSlice({
@@ -67,6 +69,28 @@ const widgetSlice = createSlice({
setMyStamp: (state, action) => {
state.myStamp = action.payload;
},
setSignatureRes: (state, action) => {
const payload = action.payload;
const { type } = payload;
const index = state.signatureResponse.findIndex(
(item) => item.type === type
);
if (index !== -1) {
// 🔁 Update existing object by type
state.signatureResponse[index] = {
...state.signatureResponse[index],
...payload
};
return;
}
// Add only if less than 2 objects
if (state.signatureResponse.length < 3) {
state.signatureResponse.push(payload);
}
},
setBulkLoader: (state, action) => {
state.isBulkLoader = action.payload;
},
resetWidgetState: () => initialState
}
});
@@ -83,7 +107,9 @@ export const {
setPrefillImg,
setPrefillImgLoad,
setTypedSignFont,
setMyStamp
setMyStamp,
setSignatureRes,
setBulkLoader
} = widgetSlice.actions;
export default widgetSlice.reducer;
@@ -28,7 +28,7 @@ import EditorToolbar, {
} from "../../components/pdf/EditorToolbar";
import ReactQuill from "react-quill-new";
import "../../styles/quill.css";
import BulkSendUi from "../../components/BulkSendUi";
import BulkSendUi from "../../components/bulksend/BulkSendUi";
import Loader from "../../primitives/Loader";
import { serverUrl_fn } from "../../constant/appinfo";
import { useTranslation } from "react-i18next";
@@ -50,7 +50,7 @@ const DocumentsReport = (props) => {
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const { prefillImg, isBulkLoader } = useSelector((state) => state.widget);
const isDashboard =
location?.pathname === "/dashboard/35KBoSgoAK" ? true : false;
const [currentPage, setCurrentPage] = useState(1);
@@ -252,10 +252,9 @@ const DocumentsReport = (props) => {
return axiosRes;
}
} catch (e) {
console.log("Error to fetch template in report", e);
console.error("fetch template in report error", e);
showAlert("danger", t("something-went-wrong-mssg"));
setActLoader({});
console.log("Error to fetch template in report", e);
}
});
//function is called when there ther no any prefill role widget exist then create direct document and navigate
@@ -300,7 +299,7 @@ const DocumentsReport = (props) => {
setActLoader({});
}
} catch (err) {
console.log("err", err);
console.error("use template error", err);
showAlert("danger", t("something-went-wrong-mssg"));
setActLoader({});
}
@@ -378,7 +377,7 @@ const DocumentsReport = (props) => {
props.setList(upldatedList);
}
} catch (err) {
console.log("err", err);
console.error("delete document error", err);
showAlert("danger", t("something-went-wrong-mssg"));
setActLoader({});
}
@@ -474,7 +473,7 @@ const DocumentsReport = (props) => {
setReason("");
})
.catch((err) => {
console.log("err", err);
console.error("decline document error", err);
setReason("");
showAlert("danger", t("something-went-wrong-mssg"));
setActLoader({});
@@ -514,7 +513,7 @@ const DocumentsReport = (props) => {
}
setActLoader({});
} catch (err) {
console.log("err in getsignedurl", err);
console.error("getsignedurl error", err);
alert(t("something-went-wrong-mssg"));
setActLoader({});
}
@@ -667,7 +666,7 @@ const DocumentsReport = (props) => {
showAlert("danger", t("something-went-wrong-mssg"));
}
} catch (err) {
console.log("err in sendmail", err);
console.error("sendmail error", err);
showAlert("danger", t("something-went-wrong-mssg"));
} finally {
setIsNextStep({});
@@ -747,7 +746,7 @@ const DocumentsReport = (props) => {
}
}
} catch (err) {
console.log("err", err);
console.error("update expiry doc error", err);
showAlert("danger", t("something-went-wrong-mssg"), 2000);
} finally {
setActLoader({});
@@ -806,7 +805,7 @@ const DocumentsReport = (props) => {
setTemplateId(templateRes?.id);
setIsSuccess({ [doc.objectId]: true });
} catch (err) {
console.log("Err in saveastemplate", err);
console.error("saveastemplate error", err);
} finally {
setActLoader({});
}
@@ -828,27 +827,18 @@ const DocumentsReport = (props) => {
try {
const axiosRes = await fetchTemplate(templateId);
const templateRes = axiosRes.data && axiosRes.data.result;
const isPrefillExist = templateRes?.Placeholders.some(
(x) => x.Role === "prefill"
const tenantSignTypes = await fetchTenantDetails();
const docSignTypes = templateRes?.SignatureType || signatureTypes;
const updatedSignatureType = await handleSignatureType(
tenantSignTypes,
docSignTypes
);
if (isPrefillExist) {
setIsBulkSend({});
setIsLoader({});
showAlert("danger", t("prefill-bulk-error"));
} else {
const tenantSignTypes = await fetchTenantDetails();
const docSignTypes = templateRes?.SignatureType || signatureTypes;
const updatedSignatureType = await handleSignatureType(
tenantSignTypes,
docSignTypes
);
setSignatureType(updatedSignatureType);
setPlaceholders(templateRes?.Placeholders);
setTemplateDetails(templateRes);
setIsLoader({});
}
setSignatureType(updatedSignatureType);
setPlaceholders(templateRes?.Placeholders);
setTemplateDetails(templateRes);
setIsLoader({});
} catch (err) {
console.log("err in fetch template in bulk modal", err);
console.error("fetch template in bulk modal error", err);
setIsBulkSend({});
showAlert("danger", t("something-went-wrong-mssg"));
}
@@ -874,7 +864,7 @@ const DocumentsReport = (props) => {
handleCloseModal();
showAlert("danger", err.message);
// showAlert("danger", t("something-went-wrong-mssg"));
console.log("Err while create duplicate template", err);
console.error("create duplicate template error", err);
} finally {
setActLoader({});
}
@@ -966,7 +956,7 @@ const DocumentsReport = (props) => {
try {
await fetchTenantDetails();
} catch (e) {
console.log("error in fetchTenantDetails", e);
console.error("fetchTenantDetails error", e);
alert(t("user-not-exist"));
}
}
@@ -1457,9 +1447,12 @@ const DocumentsReport = (props) => {
{isBulkSend[item.objectId] && (
<ModalUi
isOpen
showScrollBar
title={
t("quick-send")
}
reduceWidth={"md:min-w-[80%]"}
isLoader={isBulkLoader}
handleClose={() => setIsBulkSend({})}
>
{isLoader[item.objectId] ? (
@@ -26,7 +26,7 @@ import EditorToolbar, {
} from "../../components/pdf/EditorToolbar";
import ReactQuill from "react-quill-new";
import "../../styles/quill.css";
import BulkSendUi from "../../components/BulkSendUi";
import BulkSendUi from "../../components/bulksend/BulkSendUi";
import Loader from "../../primitives/Loader";
import { serverUrl_fn } from "../../constant/appinfo";
import {
@@ -46,7 +46,7 @@ const TemplatesReport = (props) => {
const titleRef = useRef(null);
const dispatch = useDispatch();
const titleElement = useElSize(titleRef);
const prefillImg = useSelector((state) => state.widget.prefillImg);
const { prefillImg, isBulkLoader } = useSelector((state) => state.widget);
const appName =
"OpenSign™";
const { t } = useTranslation();
@@ -198,7 +198,7 @@ const TemplatesReport = (props) => {
}
}
} catch (err) {
console.log("Err in fetch top level teamlist", err);
console.error("fetch top level teamlist error", err);
}
};
// below useEffect is used to render next record if IsMoreDoc is true
@@ -301,7 +301,7 @@ const TemplatesReport = (props) => {
return axiosRes;
}
} catch (e) {
console.log("Error to fetch template in report", e);
console.error("fetch template in report error", e);
showAlert("danger", t("something-went-wrong-mssg"));
setActLoader({});
}
@@ -413,7 +413,7 @@ const TemplatesReport = (props) => {
props.setList(upldatedList);
}
} catch (err) {
console.log("err", err);
console.error("delete template error", err);
showAlert("danger", t("something-went-wrong-mssg"));
setActLoader({});
}
@@ -540,7 +540,7 @@ const TemplatesReport = (props) => {
setActLoader({});
} catch (err) {
console.log("err in getsignedurl", err);
console.error("getsignedurl error", err);
alert(t("something-went-wrong-mssg"));
setActLoader({});
}
@@ -692,7 +692,7 @@ const TemplatesReport = (props) => {
showAlert("danger", t("something-went-wrong-mssg"));
}
} catch (err) {
console.log("err in sendmail", err);
console.error("sendmail error", err);
showAlert("danger", t("something-went-wrong-mssg"));
} finally {
setIsNextStep({});
@@ -737,32 +737,25 @@ const TemplatesReport = (props) => {
// `handleBulkSend` is used to open modal as well as fetch template
// and show Ui on the basis template response handleBulkSendTemplate
const handleBulkSend = async (template) => {
const isPrefillExist = template?.Placeholders?.some(
(x) => x.Role === "prefill"
);
if (isPrefillExist) {
showAlert("danger", t("prefill-bulk-error"));
} else {
setIsBulkSend({ [template.objectId]: true });
setIsLoader({ [template.objectId]: true });
try {
const axiosRes = await fetchTemplate(template.objectId);
const templateRes = axiosRes.data && axiosRes.data.result;
const tenantSignTypes = await fetchTenantDetails();
const docSignTypes = templateRes?.SignatureType || signatureTypes;
const updatedSignatureType = await handleSignatureType(
tenantSignTypes,
docSignTypes
);
setSignatureType(updatedSignatureType);
setPlaceholders(templateRes?.Placeholders);
setTemplateDetails(templateRes);
setIsLoader({});
} catch (err) {
console.log("err in fetch template in bulk modal", err);
setIsBulkSend({});
showAlert("danger", t("something-went-wrong-mssg"));
}
setIsBulkSend({ [template.objectId]: true });
setIsLoader({ [template.objectId]: true });
try {
const axiosRes = await fetchTemplate(template.objectId);
const templateRes = axiosRes.data && axiosRes.data.result;
const tenantSignTypes = await fetchTenantDetails();
const docSignTypes = templateRes?.SignatureType || signatureTypes;
const updatedSignatureType = await handleSignatureType(
tenantSignTypes,
docSignTypes
);
setSignatureType(updatedSignatureType);
setPlaceholders(templateRes?.Placeholders);
setTemplateDetails(templateRes);
setIsLoader({});
} catch (err) {
console.error("fetch template in bulk modal err", err);
setIsBulkSend({});
showAlert("danger", t("something-went-wrong-mssg"));
}
};
@@ -808,7 +801,7 @@ const TemplatesReport = (props) => {
}
} catch (err) {
showAlert("danger", t("something-went-wrong-mssg"));
console.log("Err while create duplicate template", err);
console.error("create duplicate template error", err);
} finally {
setActLoader({});
}
@@ -935,6 +928,12 @@ const TemplatesReport = (props) => {
setIsSend(true);
} else {
setIsMailModal(true);
const currentSigner = signerList?.find(
(x) => x?.UserId?.objectId === ownerId
);
if (currentSigner) {
setCurrUserId(currentSigner?.objectId);
}
}
if (user) {
try {
@@ -1350,9 +1349,12 @@ const TemplatesReport = (props) => {
{isBulkSend[item.objectId] && (
<ModalUi
isOpen
showScrollBar
title={
t("quick-send")
}
reduceWidth={"md:min-w-[80%]"}
isLoader={isBulkLoader}
handleClose={() => setIsBulkSend({})}
>
{isLoader[item.objectId] ? (
+53
View File
@@ -123,3 +123,56 @@ export function buildDownloadFilename(formatId, ctx) {
const safeExt = ext.replace(/\.+/g, "").toLowerCase() || "pdf";
return `${stem}.${safeExt}`;
}
export function parseCSV(text) {
const rows = [];
let row = [];
let field = "";
let inQuotes = false;
for (let i = 0; i < text.length; i++) {
const ch = text[i];
const next = text[i + 1];
if (ch === '"') {
// If inside quotes and next is also quote => escaped quote
if (inQuotes && next === '"') {
field += '"';
i++; // skip next quote
} else {
inQuotes = !inQuotes;
}
continue;
}
if (ch === "," && !inQuotes) {
row.push(field.trim());
field = "";
continue;
}
if ((ch === "\n" || ch === "\r") && !inQuotes) {
// handle CRLF (\r\n)
if (ch === "\r" && next === "\n") i++;
row.push(field.trim());
field = "";
// ignore completely empty lines
const hasAny = row.some((c) => c !== "");
if (hasAny) rows.push(row);
row = [];
continue;
}
field += ch;
}
// last field
row.push(field.trim());
const hasAny = row.some((c) => c !== "");
if (hasAny) rows.push(row);
return rows;
}
+5
View File
@@ -45,6 +45,9 @@ export const handleEmbedPrefillToDoc = async (
scale,
prefillImg
);
if (pdfBase64?.error) {
return { error: pdfBase64.error };
}
const pdfName = generatePdfName(16);
const pdfUrl = await convertBase64ToFile(pdfName, pdfBase64);
const tenantId = localStorage.getItem("TenantId");
@@ -232,3 +235,5 @@ export const handleSignersList = (item) => {
});
return updatedSigners;
};
export const normalizeKey = (value) => value?.trim()?.toLowerCase() || "";
+138 -1
View File
@@ -1,7 +1,25 @@
import { generateTitleFromFilename, selectFormat } from "../constant/Utils";
import moment from "moment";
import {
generateTitleFromFilename,
selectFormat,
changeDateToMomentFormat
} from "../constant/Utils";
import { base64StringtoFile, uploadFile } from "./fileUtils";
import { sanitizeFileName } from "./sanitizeFileName";
import Parse from "parse";
export const dateFormat = [
"L",
"MM-DD-YYYY",
"MM.DD.YYYY",
"DD/MM/YYYY",
"DD-MM-YYYY",
"DD.MM.YYYY",
"YYYY-MM-DD",
"MMM DD, YYYY",
"LL",
"DD MMM, YYYY",
"DD MMMM, YYYY"
];
// `handlesavesign` is used to save signature, initials, stamp as a default
export const saveToMySign = async (widget) => {
const base64 = widget?.base64;
@@ -239,3 +257,122 @@ export function formatDate(dateObj) {
.replace(/mm/, tokens.mm)
.replace(/dd/, tokens.dd);
}
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
export const getDatePickerDate = (selectedDate, format = "dd-MM-yyyy") => {
let date;
if (!selectedDate) {
return;
}
if (format && format === "dd-MM-yyyy") {
const [day, month, year] = selectedDate?.split("-");
date = new Date(`${year}-${month}-${day}`);
} else if (format && format === "dd.MM.yyyy") {
const [day, month, year] = selectedDate?.split(".");
date = new Date(`${year}.${month}.${day}`);
} else if (format && format === "dd/MM/yyyy") {
const [day, month, year] = selectedDate?.split("/");
date = new Date(`${year}/${month}/${day}`);
} else {
date = new Date(selectedDate);
}
return date;
};
// Parses strictly "mm/dd/yyyy" (e.g., "01/30/2026")
// Returns "YYYY-MM-DD" (e.g., "2026-01-30") or null if invalid.
export function parseMMDDYYYY(input) {
if (typeof input !== "string") return null;
const match = input.trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
if (!match) return null;
const month = Number(match[1]); // 1..12
const day = Number(match[2]); // 1..31
const year = Number(match[3]);
if (month < 1 || month > 12) return null;
if (day < 1 || day > 31) return null;
// Strict calendar validation (rejects 02/30/2026 etc.)
const dt = new Date(year, month - 1, day);
if (
dt.getFullYear() !== year ||
dt.getMonth() !== month - 1 ||
dt.getDate() !== day
) {
return null;
}
const MM = String(month).padStart(2, "0");
const DD = String(day).padStart(2, "0");
return `${year}-${MM}-${DD}`; // ISO-like (date-only)
}
// Parses strictly "dd-mm-yyyy" (e.g., "30-01-2026")
// Returns "YYYY-MM-DD" (e.g., "2026-01-30") or null if invalid.
export function parseDDMMYYYY(input) {
if (typeof input !== "string") return null;
const match = input.trim().match(/^(\d{1,2})-(\d{1,2})-(\d{4})$/);
if (!match) return null;
const day = Number(match[1]); // 1..31
const month = Number(match[2]); // 1..12
const year = Number(match[3]);
if (month < 1 || month > 12) return null;
if (day < 1 || day > 31) return null;
// Strict calendar validation (rejects 31-02-2026 etc.)
const dt = new Date(year, month - 1, day);
if (
dt.getFullYear() !== year ||
dt.getMonth() !== month - 1 ||
dt.getDate() !== day
) {
return null;
}
const MM = String(month).padStart(2, "0");
const DD = String(day).padStart(2, "0");
return `${year}-${MM}-${DD}`; // ISO-like (date-only)
}
// Turn "/^[A-Z]+$/i" OR "^[A-Z]+$" into a real RegExp
export function toRegExp(maybeRegex, defaultFlags = "") {
if (maybeRegex instanceof RegExp) return maybeRegex;
const s = String(maybeRegex ?? "").trim();
const m = s.match(/^\/([\s\S]*)\/([gimsuy]*)$/); // /pattern/flags
if (m) {
const [, pattern, flags] = m;
return new RegExp(pattern, flags);
}
return new RegExp(s, defaultFlags);
}
// For <input pattern="..."> (must NOT include /.../ delimiters)
export function toHtmlPattern(maybeRegex) {
if (!maybeRegex) return;
const re = toRegExp(maybeRegex);
// pattern attribute is implicitly full-match; stripping anchors is optional
return re.source.replace(/^\^/, "").replace(/\$$/, "");
}
export const formatCSVDate = (widget, response, formatInISO = false) => {
const format = widget?.options?.validation?.format ?? "dd-MM-yyyy";
const input = response === "today" ? new Date() : response;
// moment strict parse; fallback to "today" if invalid
const m = moment(input, changeDateToMomentFormat(format), true);
const date = m.isValid()
? m
: moment(new Date(), changeDateToMomentFormat(format), true);
const finalResponse = formatInISO
? date.format("DD-MM-YYYY")
: date.format(format?.toUpperCase());
return finalResponse;
};
@@ -1,7 +1,6 @@
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import uploadFile from './uploadFile.js';
import docxtopdf, { upload as docxUpload } from './docxtopdf.js';
import decryptpdf, { upload as decryptUpload } from './decryptpdf.js';
@@ -16,7 +15,6 @@ app.use(cors());
app.use(express.json({ limit: '100mb' }));
app.use(express.urlencoded({ limit: '100mb', extended: true }));
app.post('/file_upload', uploadFile);
app.post('/docxtopdf', docxUpload.single('file'), docxtopdf);
app.post('/decryptpdf', decryptUpload.single('file'), decryptpdf);
app.get('/delete-account/:userId', deleteUserGet);
@@ -1,8 +1,31 @@
import axios from 'axios';
import multer from 'multer';
import libre from 'libreoffice-convert';
import { exec } from 'child_process';
import { promisify } from 'util';
import { cloudServerUrl, getSecureUrl, serverAppId } from '../../Utils.js';
const execAsync = promisify(exec);
// -------------------- Process Management --------------------
/**
* Kill stuck LibreOffice processes
*/
async function killStuckProcesses() {
try {
// Kill soffice processes older than 2 minutes
if (process.platform === 'linux' || process.platform === 'darwin') {
await execAsync("pkill -9 -f 'soffice.*--headless' || true");
console.log('[DOCX2PDF] Cleaned up stuck processes');
} else if (process.platform === 'win32') {
await execAsync('taskkill /F /IM soffice.bin /T || exit 0');
console.log('[DOCX2PDF] Cleaned up stuck processes (Windows)');
}
} catch (err) {
console.error('[DOCX2PDF] Error killing processes:', err.message);
}
}
/** @returns {Promise<Buffer>} */
async function convertLibre(input, ext, opts) {
return await new Promise((resolve, reject) => {
@@ -14,7 +37,7 @@ async function convertLibre(input, ext, opts) {
});
}
// -------------------- Concurrency limiter --------------------
// -------------------- Concurrency limiter with queue limits --------------------
// CRITICAL FIX: Reduced to 1 for CPU-intensive LibreOffice conversions
const MAX_CONCURRENCY = Number(process.env.DOCX2PDF_CONCURRENCY || 1);
let active = 0;
@@ -22,22 +45,27 @@ const queue = [];
function runWithLimit(task) {
return new Promise((resolve, reject) => {
const run = () => {
const run = async () => {
active++;
Promise.resolve()
.then(task)
.then(resolve, reject)
.finally(() => {
active--;
if (queue.length) queue.shift()();
});
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
active--;
if (queue.length) {
const next = queue.shift();
next();
}
}
};
if (active < MAX_CONCURRENCY) run();
else queue.push(run);
});
}
// -------------------- Timeout helper --------------------
// -------------------- Timeout helper with cleanup --------------------
/**
* @template T
* @param {Promise<T>} promise
@@ -49,7 +77,11 @@ export async function withTimeout(promise, ms, label = 'operation') {
let timer;
try {
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
timer = setTimeout(async () => {
// Kill stuck processes on timeout
await killStuckProcesses();
reject(new Error(`${label} timed out after ${ms}ms`));
}, ms);
});
return await Promise.race([promise, timeout]);
} finally {
@@ -116,22 +148,29 @@ export default async function docxtopdf(req, res) {
// ---- DOCX -> PDF conversion with concurrency control and timeout ----
const fileName = `${generatePdfName(16)}.pdf`;
// FIX: Increased timeout to 90s for large files, added nice priority
// Adjust timeout based on file size
const timeoutMs = uploadedSizeBytes > 10 * 1024 * 1024 ? 120_000 : 90_000;
// FIX: Increased timeout for large files, added nice priority
const pdfBuffer = await runWithLimit(async () => {
// Log for monitoring
console.log(`[DOCX2PDF] Starting conversion, active: ${active}, queued: ${queue.length}`);
console.log(
`[DOCX2PDF] Starting conversion, size: ${(uploadedSizeBytes / 1024 / 1024).toFixed(2)}MB, active: ${active}, queued: ${queue.length}`
);
const startTime = Date.now();
try {
const result = await withTimeout(
convertLibre(req.file.buffer, '.pdf', undefined),
90_000, // Increased from 60s to 90s
timeoutMs,
'DOCX->PDF'
);
console.log(`[DOCX2PDF] Completed in ${Date.now() - startTime}ms`);
return result;
} catch (error) {
console.error(`[DOCX2PDF] Failed after ${Date.now() - startTime}ms:`, error.message);
// Clean up on error
await killStuckProcesses();
throw error;
}
});
@@ -1,168 +0,0 @@
// npm packages
import multer from 'multer';
import multerS3 from 'multer-s3';
import aws from 'aws-sdk';
import dotenv from 'dotenv';
import { cloudServerUrl, serverAppId, useLocal } from '../../Utils.js';
dotenv.config({ quiet: true });
function sanitizeFileName(fileName) {
// Remove spaces and invalid characters
return fileName.replace(/[^a-zA-Z0-9._-]/g, '');
}
async function uploadFile(req, res) {
try {
//--size extended to 100 mb
const size = 100 * 1024 * 1024;
//console.log(size);
const accepted_extensions = [
'jpg',
'png',
'gif',
'mp4',
'mp3',
'pdf',
'jpeg',
'dwg',
'dxf',
'zip',
'rar',
'txt',
'doc',
'docx',
'pptx',
'ppt',
'xlsx',
'xlsm',
'xlsb',
'xltx',
'xml',
'xls',
'xla',
'xlx',
];
const DO_ENDPOINT = process.env.DO_ENDPOINT;
const DO_ACCESS_KEY_ID = process.env.DO_ACCESS_KEY_ID;
const DO_SECRET_ACCESS_KEY = process.env.DO_SECRET_ACCESS_KEY;
const DO_SPACE = process.env.DO_SPACE;
const parseBaseUrl = cloudServerUrl; //process.env.SERVER_URL;
const parseAppId = serverAppId;
let fileStorage;
if (useLocal === 'true') {
fileStorage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'files/files');
},
metadata: function (req, file, cb) {
cb(null, { fieldName: 'OPENSIGN_METADATA' });
},
filename: function (req, file, cb) {
let filename = file.originalname;
let newFileName = filename.split('.')[0];
let extension = filename.split('.')[1];
newFileName = sanitizeFileName(
newFileName + '_' + new Date().toISOString() + '.' + extension
);
// console.log(newFileName);
cb(null, newFileName);
},
});
} else {
try {
const spacesEndpoint = new aws.Endpoint(DO_ENDPOINT);
const s3 = new aws.S3({
endpoint: spacesEndpoint,
accessKeyId: DO_ACCESS_KEY_ID,
secretAccessKey: DO_SECRET_ACCESS_KEY,
signatureVersion: 'v4',
region: process.env.DO_REGION,
});
fileStorage = multerS3({
acl: 'public-read',
s3,
bucket: DO_SPACE,
metadata: function (req, file, cb) {
cb(null, { fieldName: 'OPENSIGN_METADATA' });
},
key: function (req, file, cb) {
//console.log(file);
let filename = file.originalname;
let newFileName = filename.split('.')[0];
let extension = filename.split('.')[1];
newFileName = sanitizeFileName(
newFileName + '_' + new Date().toISOString() + '.' + extension
);
// console.log(newFileName);
cb(null, newFileName);
},
});
} catch (err) {
fileStorage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'files/files');
},
metadata: function (req, file, cb) {
cb(null, { fieldName: 'OPENSIGN_METADATA' });
},
filename: function (req, file, cb) {
let filename = file.originalname;
let newFileName = filename.split('.')[0];
let extension = filename.split('.')[1];
newFileName = sanitizeFileName(
newFileName + '_' + new Date().toISOString() + '.' + extension
);
// console.log(newFileName);
cb(null, newFileName);
},
});
}
}
// const s3 = new aws.S3();
const upload = multer({
fileFilter: function (req, file, cb) {
if (accepted_extensions.some(ext => file.originalname.toLowerCase().endsWith('.' + ext))) {
return cb(null, true);
}
// otherwise, return error
return cb('Only ' + accepted_extensions.join(', ') + ' files are allowed!');
},
storage: fileStorage,
limits: { fileSize: size },
}).single('file');
//--call upload function--
upload(req, res, function (err, some) {
if (err) {
console.log(err);
const status = 'Error';
const message = err;
const returnCode = 1029;
return res.send({ status, returnCode, message });
}
const status = 'Success';
//res.header("Access-Control-Allow-Headers", "Content-Type");
//res.setHeader("Access-Control-Allow-Origin", "*");
if (useLocal === 'true') {
// console.log(req.file);
var fileUrl = `${parseBaseUrl}/files/${parseAppId}/${req.file.filename}`;
} else {
var fileUrl = req.file.location;
}
return res.json({ status, imageUrl: fileUrl });
});
} catch (err) {
console.log('Exeption in query ' + err.stack);
const status = 'Error';
const returnCode = 1021;
const message = 'Some error occurred';
return res.send({ status, returnCode, message });
}
}
export default uploadFile;
@@ -25,7 +25,7 @@ async function sendMail(document, publicUrl) {
month: 'long',
year: 'numeric',
});
let signerMail = document.Placeholders;
let signerMail = document.Placeholders?.filter(x => x?.Role !== 'prefill');
const senderName = document.ExtUserPtr.Name;
const senderEmail = document.ExtUserPtr.Email;
@@ -109,9 +109,12 @@ async function batchQuery(userId, Documents, Ip, parseConfig, type, publicUrl) {
try {
const requests = Documents.map(x => {
const Signers = x.Signers;
const allSigner = x?.Placeholders?.map(
item => Signers?.find(e => item?.signerPtr?.objectId === e?.objectId) || item?.signerPtr
).filter(signer => Object.keys(signer).length > 0);
const placeholders = x?.Placeholders?.filter(p => p?.Role !== 'prefill');
const allSigner = placeholders
?.map(
item => Signers?.find(e => item?.signerPtr?.objectId === e?.objectId) || item?.signerPtr
)
.filter(signer => Object.keys(signer).length > 0);
const date = new Date();
const isoDate = date.toISOString();
let Acl = { [x.CreatedBy.objectId]: { read: true, write: true } };
@@ -140,7 +143,7 @@ async function batchQuery(userId, Documents, Ip, parseConfig, type, publicUrl) {
className: x.ExtUserPtr.className,
objectId: x.ExtUserPtr?.objectId,
},
Placeholders: x.Placeholders.map(y =>
Placeholders: placeholders.map(y =>
y?.signerPtr?.objectId
? {
...y,
@@ -2,7 +2,6 @@ import { SignPdf } from '@signpdf/signpdf';
import { P12Signer } from '@signpdf/signer-p12';
import { pdflibAddPlaceholder } from '@signpdf/placeholder-pdf-lib';
import { PDFDocument } from 'pdf-lib';
import Parse from 'parse/node.js';
import fs from 'node:fs';
import dotenv from 'dotenv';
import GenerateCertificate from './pdf/GenerateCertificate.js';
@@ -2,6 +2,7 @@ import AWS from 'aws-sdk';
import { useLocal } from '../../Utils.js';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
import { isAuthenticated } from '../../utils/AuthUtils.js';
dotenv.config({ quiet: true });
export default function getPresignedUrl(url) {
@@ -42,6 +43,7 @@ export async function getSignedUrl(request) {
const docId = request.params.docId || '';
const templateId = request.params.templateId || '';
const url = request.params.url;
if (docId || templateId) {
try {
if (url?.includes('files')) {
@@ -56,8 +58,14 @@ export async function getSignedUrl(request) {
const _resDoc = res?.toJSON();
// Ensure user is authenticated if OTP is required
if (_resDoc?.IsEnableOTP && !request?.user) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
if (_resDoc?.IsEnableOTP) {
const isAuth = await isAuthenticated(request?.user);
if (!isAuth) {
throw new Parse.Error(
Parse.Error.INVALID_SESSION_TOKEN,
'User is not authenticated.'
);
}
}
const presignedUrl = getPresignedUrl(url);
@@ -70,7 +78,8 @@ export async function getSignedUrl(request) {
throw err;
}
} else {
if (!request?.user) {
const isAuth = await isAuthenticated(request?.user);
if (!isAuth) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User is not authenticated.');
} else {
if (url?.includes('files')) {
@@ -56,6 +56,7 @@ export default async function saveAsTemplate(request) {
pos: pageItem.pos.map(p => ({
...p,
type: p.type === 'text' ? 'text input' : p.type,
signatureType: '',
options: {
...p.options,
status: 'required',
@@ -92,6 +93,7 @@ export default async function saveAsTemplate(request) {
if (widget.options && widget.options.defaultValue !== undefined) {
return {
...widget,
signatureType: '',
options: {
...widget.options,
defaultValue: '',
-14803
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -18,55 +18,55 @@
"watch": "nodemon index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.958.0",
"@aws-sdk/s3-request-presigner": "^3.958.0",
"@aws-sdk/client-s3": "^3.978.0",
"@aws-sdk/s3-request-presigner": "^3.978.0",
"@parse/fs-files-adapter": "^3.0.0",
"@parse/push-adapter": "^8.1.0",
"@parse/push-adapter": "^8.2.0",
"@parse/s3-files-adapter": "^4.2.0",
"@pdf-lib/fontkit": "^1.1.1",
"@signpdf/placeholder-pdf-lib": "^3.3.0",
"@signpdf/signer-p12": "^3.3.0",
"@signpdf/signpdf": "^3.3.0",
"aws-sdk": "^2.1693.0",
"axios": "^1.13.2",
"axios": "^1.13.4",
"coherentpdf": "^2.5.5",
"cors": "^2.8.5",
"cors": "^2.8.6",
"date-fns-tz": "^3.2.0",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"form-data": "^4.0.5",
"generate-api-key": "^1.0.2",
"googleapis": "^169.0.0",
"libreoffice-convert": "^1.7.0",
"mailgun.js": "^12.4.1",
"googleapis": "^170.1.0",
"libreoffice-convert": "^1.8.1",
"mailgun.js": "^12.7.0",
"moment": "^2.30.1",
"mongodb": "^7.0.0",
"multer": "^2.0.2",
"multer-s3": "^3.0.1",
"node-forge": "^1.3.3",
"nodemailer": "^7.0.12",
"nodemailer": "^7.0.13",
"p-limit": "^7.2.0",
"parse": "^8.0.0",
"parse": "^8.0.3",
"parse-dbtool": "^1.2.0",
"parse-server": "^8.6.2",
"parse-server-api-mail-adapter": "^5.0.2",
"parse-server-api-mail-adapter": "^5.0.4",
"pdf-lib": "^1.17.1",
"posthog-node": "^5.18.1",
"posthog-node": "^5.21.2",
"qrcode": "^1.5.4",
"rate-limiter-flexible": "^9.0.1",
"sharp": "^0.34.5",
"speakeasy": "^2.0.0",
"ws": "^8.18.3"
"ws": "^8.19.0"
},
"type": "module",
"devDependencies": {
"@babel/eslint-parser": "^7.28.5",
"@babel/eslint-parser": "^7.28.6",
"eslint": "^9.39.2",
"jasmine": "^5.12.0",
"mongodb-runner": "^6.5.2",
"jasmine": "^6.0.0",
"mongodb-runner": "^6.5.4",
"nodemon": "^3.1.11",
"nyc": "^17.1.0",
"prettier": "^3.7.4"
"prettier": "^3.8.1"
},
"overrides": {
"ws": "$ws",
+8
View File
@@ -0,0 +1,8 @@
export const isAuthenticated = async sessionUser => {
try {
return sessionUser ? true : false;
} catch (err) {
console.log(`authentication error ${err?.message}`);
return false;
}
};
+214
View File
@@ -0,0 +1,214 @@
export function getPattern(option) {
const validateType = option?.validation?.type;
let regexValidation;
switch (validateType) {
case 'email':
regexValidation = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
return regexValidation;
case 'number':
regexValidation = /^[0-9\s]*$/;
return regexValidation;
case 'ssn':
regexValidation = /^(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}$/;
return regexValidation;
default:
regexValidation = option?.validation?.pattern;
return regexValidation;
}
}
//function used in getTemplate API to update placeholders options field
export function getWidgetOptions(option, type, role) {
const isPrefill = role === 'prefill';
const status = option?.status === 'required' ? true : false;
const widgetname = option?.name;
const defaultValue = option?.defaultValue || option?.response || '';
switch (type) {
case 'stamp':
return { required: status, name: widgetname };
case 'initials':
return { required: status, name: widgetname };
case 'image':
return {
required: status,
name: widgetname,
...(isPrefill &&
option?.response && {
response: option.response,
}),
};
case 'email':
return {
required: status,
name: widgetname,
color: option?.fontColor || 'black',
fontSize: option?.fontSize || 12,
hide_text_with_asterisks: option?.isHideText || false,
};
case 'name':
return {
required: status,
name: widgetname,
color: option?.fontColor || 'black',
fontSize: option?.fontSize || 12,
hide_text_with_asterisks: option?.isHideText || false,
};
case 'job title':
return {
required: status,
name: widgetname,
color: option?.fontColor || 'black',
fontSize: option?.fontSize || 12,
hide_text_with_asterisks: option?.isHideText || false,
};
case 'company':
return {
required: status,
name: widgetname,
color: option?.fontColor || 'black',
fontSize: option?.fontSize || 12,
hide_text_with_asterisks: option?.isHideText || false,
};
case 'date': {
const format = option?.validation?.format;
return {
required: status,
name: widgetname,
color: option?.fontColor || 'black',
fontSize: option?.fontSize || 12,
format: format || 'mm-dd-yyyy',
...(!isPrefill && {
...{ min_date: option?.minDate || '' },
...{ max_date: option?.maxDate || '' },
...(defaultValue !== 'today' && { default: defaultValue }),
...(defaultValue === 'today' && { signing_date: true }),
...{ readonly: option?.isReadOnly || false },
}),
...(isPrefill &&
option?.response && {
response: option.response,
}),
};
}
case 'text input':
case 'text':
return {
required: status,
name: widgetname,
color: option?.fontColor || 'black',
fontSize: option?.fontSize || 12,
...(!isPrefill && {
...{ hint: option?.hint || '' },
...{ hide_text_with_asterisks: option?.isHideText || false },
...(defaultValue && { default: defaultValue }),
...{ regularexpression: getPattern(option) },
...{ readonly: option?.isReadOnly || false },
}),
...(isPrefill &&
option?.response && {
response: option.response,
}),
};
case 'checkbox': {
const optionsArr = option?.values || [];
// defaultValue = index numbers e.g. [0, 1]
let selectedvalues =
Array.isArray(defaultValue) && defaultValue.length > 0 ? defaultValue : [];
// convert index numbers back to actual values
let getSelectedValue = [];
if (selectedvalues.length > 0) {
for (const index of selectedvalues) {
if (optionsArr[index] !== undefined) {
getSelectedValue.push(optionsArr[index]);
}
}
}
return {
required: status,
name: widgetname,
values: optionsArr,
...(!isPrefill && {
...(getSelectedValue && { selectedvalues: getSelectedValue || [] }),
...{
validation: {
minselections: option?.validation?.minRequiredCount || 0,
maxselections: option?.validation?.maxRequiredCount || 0,
},
},
...{ readonly: option?.isReadOnly || false },
}),
hidelabel: option?.isHideLabel || false,
color: option?.fontColor || 'black',
fontSize: option?.fontSize || 12,
layout: option?.layout || 'vertical',
...(isPrefill &&
getSelectedValue && {
response: getSelectedValue,
}),
};
}
case 'radio button': {
return {
required: status,
name: widgetname,
color: option?.fontColor || 'black',
fontSize: option?.fontSize || 12,
layout: option?.layout || 'vertical',
values: option?.values || [],
...(!isPrefill && {
...{ default: defaultValue || '' },
...{ readonly: option?.isReadOnly || false },
}),
...(isPrefill && {
...{ response: defaultValue },
}),
};
}
case 'dropdown':
return {
required: status,
name: widgetname,
color: option?.fontColor || 'black',
fontSize: option?.fontSize || 12,
...(!isPrefill && {
...{ default: defaultValue || '' },
...{ readonly: option?.isReadOnly || false },
}),
values: option?.values || [],
...(isPrefill &&
defaultValue && {
response: defaultValue,
}),
};
case 'number':
return {
required: status,
name: widgetname,
color: option?.fontColor || 'black',
fontSize: option?.fontSize || 12,
readonly: option?.isReadOnly || false,
default: defaultValue || '',
hint: option?.hint || '',
formula: option?.formula || '',
decimalplaces: option?.decimalPlaces || 0,
hide_text_with_asterisks: option?.isHideText || false,
};
case 'cells':
return {
required: status,
name: widgetname,
color: option?.fontColor || 'black',
fontSize: option?.fontSize || 12,
hide_text_with_asterisks: option?.isHideText || false,
readonly: option?.isReadOnly || false,
default: defaultValue || '',
hint: option?.hint || '',
cell_count: option?.cellCount || 0,
regularexpression: getPattern(option),
};
default:
break;
}
}