mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-17 21:25:54 +02:00
Generated
-16871
File diff suppressed because it is too large
Load Diff
+26
-26
@@ -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.12.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?"
|
||||
}
|
||||
@@ -1341,5 +1341,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 업로드",
|
||||
"files-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": "웹훅 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": "문서를 보내시겠습니까?"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 there’s any change in the dragging state or the dragged element’s 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 base64‐encoded image into a fixed‐size 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. Pre‐select 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. Pre‐select 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 read‐only (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 };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
-56
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 doesn’t 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] ? (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() || "";
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
Generated
-14803
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const isAuthenticated = async sessionUser => {
|
||||
try {
|
||||
return sessionUser ? true : false;
|
||||
} catch (err) {
|
||||
console.log(`authentication error ${err?.message}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user