Compare commits

...
Author SHA1 Message Date
prafull-opensignlabsandGitHub 18a4c8523a Update Docker.yml 2025-04-25 13:03:50 +05:30
prafull-opensignlabsandGitHub f7b39409a8 Merge pull request #1692 from OpenSignLabs/updates-14590419587
fix: normalize emails, migrate user logic to cloud function
2025-04-22 14:09:18 +05:30
prafull-opensignlabs 6283524d2f Merge pull request #524 from OpenSignLabs/publicurl_adduser
fix: correct public URL, normalize emails, migrate user/orgAdmin logic to cloud function
2025-04-22 08:36:56 +00:00
prafull-opensignlabsandGitHub 216288fd09 Merge pull request #1689 from OpenSignLabs/updates-14509601184
v2.18.0
2025-04-17 12:06:43 +05:30
prafull-opensignlabs 3b1d8a48b7 v2.18.0
New Features:
1. Added support to recreate a new document from declined document without having to redo everything from scratch.
2. Added replace pdf file feature for Templates. PDF underlying a Template can now be replaced while keeping all the fields the same.
3. Introduced the ability to replace pdf file for in UpdateTemplate and CreateDocumentfromtemplate API.
4. Revamped the preferences menu UI.

Bug Fixes:
1. Fixed issue with capital letters in contact emails via API.
2. Better validations & error handling in APIs
2025-04-17 06:34:06 +00:00
prafull-opensignlabsandGitHub ec68770ff0 Merge pull request #1681 from OpenSignLabs/updates-14398222591
v2.17.0
2025-04-11 13:20:43 +05:30
prafull-opensignlabs f5d2027e3a refactor: resolve warnings 2025-04-11 07:45:23 +00:00
42 changed files with 1541 additions and 951 deletions
+1
View File
@@ -3,6 +3,7 @@ on:
push:
branches:
- 'main'
- 'staging'
jobs:
docker:
@@ -152,7 +152,8 @@
"Duplicate Template": "Vorlage duplizieren",
"Duplicate": "Duplikat",
"daily-mail-quota": "Tägliches E-Mail-Kontingent",
"Save as template": "Als Vorlage speichern"
"Save as template": "Als Vorlage speichern",
"Fix & resend": "Korrigieren und erneut senden"
},
"report-heading": {
"Sr.No": "Nr.",
@@ -284,6 +285,7 @@
"make-template-public": "Vorlage öffentlich machen",
"make-template-private": "Vorlage privat machen",
"make-template-public-alert": "Sind Sie sicher, dass Sie diese Vorlage öffentlich machen möchten?",
"make-template-private-alert-non": "Sind Sie sicher, dass Sie diese Vorlage privat machen möchten?",
"make-template-private-alert": "Sind Sie sicher, dass Sie diese Vorlage privat machen möchten? Dadurch wird sie aus Ihrem öffentlichen Profil entfernt.",
"public-role": "Öffentliche Rolle",
"public-url": "Öffentliches Profil",
@@ -873,5 +875,17 @@
"document-has-been-signed": "Das Dokument wurde erfolgreich unterschrieben!",
"document-has-been-signed-by-you": "Das Dokument wurde erfolgreich von Ihnen unterschrieben!",
"participant-completed-signing": "Alle Teilnehmer haben den Signaturprozess abgeschlossen.",
"you-will-receive-email-shortly": "✅ Das war's! Sie erhalten in Kürze eine Bestätigungs-E-Mail."
"you-will-receive-email-shortly": "✅ Das war's! Sie erhalten in Kürze eine Bestätigungs-E-Mail.",
"please-provide-templateid": "Bitte geben Sie templateid an",
"this-template-is-not-public": "Dieses template ist nicht öffentlich",
"invalid-templateid": "Ungültige templateid",
"contact-billing-at-opensign": "Um weitere Plätze hinzuzufügen, kontaktieren Sie bitte OpenSign™ unter <1>billing@opensignlabs.com</1> für Unterstützung.",
"title-length-alert": "Der Titel darf höchstens 250 Zeichen lang sein.",
"note-length-alert": "Die Notiz darf höchstens 200 Zeichen lang sein.",
"description-length-alert": "Die Beschreibung darf höchstens 500 Zeichen lang sein.",
"fix-&-resend-document": "Dokument korrigieren und erneut senden",
"do-you-want-recreate-document?": "Dadurch wird ein Entwurf aus diesem Dokument mit allen vorhandenen Feldern erstellt. Sind Sie sicher, dass Sie dieses Dokument neu erstellen möchten?",
"start-editing": "Bearbeitung starten",
"unsaved-changes-discard-them?": "Sie haben ungespeicherte Änderungen. Verwerfen?",
"yes-discard": "Ja, verwerfen"
}
@@ -152,7 +152,8 @@
"Duplicate Template": "Duplicate template",
"Duplicate": "Duplicate",
"daily-mail-quota": "Daily Email Quota",
"Save as template": "Save as template"
"Save as template": "Save as template",
"Fix & resend": "Fix & Resend"
},
"report-heading": {
"Sr.No": "Sr.No",
@@ -284,11 +285,12 @@
"make-template-public": "Make template public",
"make-template-private": "Make template private",
"make-template-public-alert": "Are you sure you want to make this template public?",
"make-template-private-alert-non": "Are you sure you want to make this template private?",
"make-template-private-alert": "Are you sure you want to make this template private? This will remove it from your public profile.",
"public-role": "Public role",
"public-url": "Public profile",
"embed-template": "Embed template",
"public-url-copy": "Heres your public URL: ",
"public-url-copy": "Here's your public URL: ",
"public-url-copy-mssg": "Copy it or share it with the signer, and you will be able to see all your publicly set templates.",
"add-public-url-alert": "You can generate your {{appName}} public profile",
"share-with-alert": "You cannot share a template if any roles already have contacts assigned. Please remove all contact assignments from the roles before sharing the template.",
@@ -314,7 +316,7 @@
"body": "Body",
"add-contact": "Add contact",
"edit-contact": "Edit contact",
"add-signer-alert": "Contact already exist! Please select it from Signers dropdown",
"add-signer-alert": "Contact already exist! Please select it from 'Signers' dropdown",
"record-delete-alert": "Record deleted successfully!",
"record-revoke-alert": "Record revoked successfully!",
"mail-sent-alert": "Mail sent successfully.",
@@ -586,7 +588,7 @@
},
"tour-mssg": {
"home-layout-1": "You have logged in successfully! Let's take a look.",
"home-layout-2": "To upload documents for self-signing or to request others signatures, simply select the respective buttons.",
"home-layout-2": "To upload documents for self-signing or to request others' signatures, simply select the respective buttons.",
"home-layout-3": "You are ready to start using {{appName}}! If you need support feel free to contact us.",
"generate-token": "Upgrade now to generate production API token.",
"opensign-drive-1": "Click on the breadcrumb links to easily navigate through the folder hierarchy and view the documents within each folder.",
@@ -595,7 +597,7 @@
"opensign-drive-4": "Click on this menu to display the documents in list view.",
"opensign-drive-5": "The document list is displayed according to the selected sorting option. Icons next to each document indicate its current status.",
"opensign-drive-6": "Right-click on a document to see options such as Download, Rename, Move, and Delete. Click on the document to open it.",
"opensign-drive-7": "Right-click on any folder to see options. Choose Rename to change the folders name or click on the folder to navigate through its contents.",
"opensign-drive-7": "Right-click on any folder to see options. Choose 'Rename' to change the folder's name or click on the folder to navigate through its contents.",
"pdf-request-file-1": "List of signers who still need to sign the document .",
"pdf-request-file-2": "Click any of the placeholders appearing on the document to sign. You will then see options to draw your signature, type it, or upload an image .",
"pdf-request-file-3": "Click Decline, or Finish buttons to navigate your document. Use the ellipsis menu for additional options, including the Download button .",
@@ -608,24 +610,24 @@
"placeholder-sign-4": "Drag or click on a field to add it to the document.",
"placeholder-sign-5": "The PDF content area already displays the template's existing placeholders. For your convenience, these placeholders will match the color of the recipient's name, making them easily identifiable.",
"placeholder-sign-6": "Clicking 'Next' will save the document. In the next step you can customize the emails to be sent out to the recipients or copy the signing links and share those with the recipients yourself.",
"report-1": "Click the 'Add' button to create a new template. Templates are reusable documents designed to quickly generate new documents with the same structure and varying signers. For example, an HR template for onboarding could have predefined roles like HR Manager and New Employee. Each time you use the template, you can assign the New Employee role to different incoming staff members, while the HR Manager role remains constant, facilitating a seamless onboarding process for each recruit. ",
"report-1": "Click the 'Add' button to create a new template. Templates are reusable documents designed to quickly generate new documents with the same structure and varying signers. For example, an HR template for onboarding could have predefined roles like 'HR Manager' and 'New Employee'. Each time you use the template, you can assign the 'New Employee' role to different incoming staff members, while the 'HR Manager' role remains constant, facilitating a seamless onboarding process for each recruit. ",
"redirect": "Click the 'Use' button to create a new document from an existing template.",
"bulksend": "To quickly send multiple documents using an existing template by just creating the recipient email addresses, click the 'Bulk Send' button.",
"option": "This menu reveals more options such as Edit & Delete. Use the 'Edit' button to add signer roles, modify fields, and update your template. Changes will apply to all future documents created from this template but wont affect existing documents.Use the Delete button you can delete template. ",
"option": "This menu reveals more options such as Edit & Delete. Use the 'Edit' button to add signer roles, modify fields, and update your template. Changes will apply to all future documents created from this template but won't affect existing documents.Use the Delete button you can delete template. ",
"signyour-self-1": "Select and drag your preferred widgets onto the PDF to customize your document before signing. Choose the perfect spots for each modification to tailor the document to your needs.",
"signyour-self-2": "Drag and drop anywhere in this area. You can resize and move it later.",
"template-placeholder-1": "Clicking 'Add role' button will allow you to add various signer roles. You can attach users to each role in subsequent steps.",
"template-placeholder-2": "Once roles are added, select a role from list to add a place-holder where he is supposed to sign. The placeholder will appear in the same colour as the role name once you drop it on the document.",
"template-placeholder-3": "Drag or click on a field to add it to the document.",
"template-placeholder-4": "Drag the placeholder for a role anywhere on the document.Remember, it will appear in the same colour as the name of the recipient for easy reference.",
"template-placeholder-5": "Clicking 'Next' will store the current template. After saving, youll be prompted to create a new document from this template if you wish.",
"template-placeholder-5": "Clicking 'Next' will store the current template. After saving, you'll be prompted to create a new document from this template if you wish.",
"webhook-1": "Upgrade now to set webhook",
"Need your Signature": "Clicking on this card will take you to the list of documents awaiting your review.",
"Out for signatures": "Clicking on this card will take you to a list of documents awaiting signature.",
"Recent signature requests": "This is a list of documents that are waiting for your signature.",
"Recently sent for signatures": "This is a list of documents you've sent to other parties for signature.",
"Drafts": "This are documents you have started but have not finalized for sending.",
"public-template": "This video demonstrates how to set up your personalized public profile, such as https://opensign.me/your-username. Youll also learn how to customize your tagline and make your templates available for public signing.",
"public-template": "This video demonstrates how to set up your personalized public profile, such as 'https://opensign.me/your-username'. You'll also learn how to customize your tagline and make your templates available for public signing.",
"allowModify-widgets": "You can drag and drop any of these fields onto the document, in addition to the fields already designated for you by the document creator."
},
"enter-email-plaholder": "Add an email address and hit enter",
@@ -659,13 +661,13 @@
"bulk-send-subcription-alert": "Please upgrade to Professional or Team plan to use bulk send.",
"generate-test-token": "Generate test token",
"regenerate-test-token": "Regenerate test token",
"help-test-token": "This token can be used to test the APIs at the https://sandbox.opensignlabs.com/api/v1 endpoint, allowing you to conduct unlimited document signatures. Please note that the sandbox API will sign your documents with self-signed certificates, which may not be recognized as valid by Adobe. Once youve completed your testing, you can upgrade to one of our paid plans to generate a production token.",
"help-test-token": "This token can be used to test the APIs at the https://sandbox.opensignlabs.com/api/v1 endpoint, allowing you to conduct unlimited document signatures. Please note that the sandbox API will sign your documents with self-signed certificates, which may not be recognized as valid by Adobe. Once you've completed your testing, you can upgrade to one of our paid plans to generate a production token.",
"help-api-token": "This token can be used to access the production APIs at the {{origin}}/api/v1 endpoint. It can only be generated on one of our paid plans.",
"reason": "Reason",
"decline-by": "Declined/revoked by",
"document-declined": "Document declined",
"public-template-mssg-1": "To integrate OpenSign into your React or Next.js project, simply run the following command:",
"public-template-mssg-2": "Ensure you have npm or yarn set up in your project. If youre using Yarn, you can replace npm install with yarn add @opensign/react.",
"public-template-mssg-2": "Ensure you have npm or yarn set up in your project. If you're using Yarn, you can replace npm install with yarn add @opensign/react.",
"public-template-mssg-3": "Need more details or examples?",
"public-template-mssg-4": "Visit the",
"public-template-mssg-5": " npm for the latest updates, detailed documentation, and version history.",
@@ -793,7 +795,7 @@
"term-cond-p22": "Understand that {{appName}} is a platform facilitating the transaction and is not a party to the agreement.",
"term-cond-h7": "7. Legal Effect",
"term-cond-p23": "Your electronic signature facilitated through {{appName}}:",
"term-cond-p24": "Complies with applicable electronic signature laws, including but not limited to the E-SIGN Act in the United States, the EU eIDAS Regulation, and Indias Information Technology Act.",
"term-cond-p24": "Complies with applicable electronic signature laws, including but not limited to the E-SIGN Act in the United States, the EU eIDAS Regulation, and India's Information Technology Act.",
"term-cond-p25": "Is legally binding between You and the Sender for the signed document(s).",
"term-cond-h8": "8. Platform Role and Limitation of Liability",
"term-cond-p26": "{{appName}} serves as a platform to facilitate electronic transactions. It is not responsible for the content, validity, or enforceability of the documents sent by the Sender. Any disputes or issues related to the document or its signing must be resolved directly between You and the Sender.",
@@ -853,7 +855,7 @@
"draft-template-info-p1": "To make your template public, it must either contain a single role, or, if it includes multiple roles, all additional roles must already be assigned to signers. The unassigned public role should remain empty and must be placed in the first position.",
"visit-below-link": "Visit below link to know more -",
"storage-help": "Enabling BYOC lets you connect your own S3 storage so your files remain entirely under your control—no external copies retained. If data autonomy matters to you, consider upgrading to Teams to unlock this feature.",
"daily-quota-reached": "Youve reached your daily quota. For assistance, please contact quotas@opensignlabs.com.",
"daily-quota-reached": "You've reached your daily quota. For assistance, please contact quotas@opensignlabs.com.",
"enabled-signature-type": "Enabled Signature Types",
"enabled-signature-type-help": "The 'Enabled Signature Types' setting determines which signature options are available across your organization. For example, if you disable the 'Draw' option, members of your organization will not see it in the signature widget, while the other three options will remain accessible.",
"indexing-public-profile": "Allow indexing of public profile by search engines",
@@ -873,5 +875,17 @@
"document-has-been-signed": "The document has been signed successfully!",
"document-has-been-signed-by-you": "The document has been successfully signed by you!",
"participant-completed-signing": "All participants have completed the signing process.",
"you-will-receive-email-shortly": "✅ That's it! You'll receive a confirmation email shortly."
"you-will-receive-email-shortly": "✅ That's it! You'll receive a confirmation email shortly.",
"please-provide-templateid": "Please provide templateid",
"this-template-is-not-public": "This template is not public",
"invalid-templateid": "Invaldi templateid",
"contact-billing-at-opensign": "To add more seats, please contact OpenSign™ at <1>billing@opensignlabs.com</1> for assistance",
"title-length-alert": "Title must be at most 250 characters long.",
"note-length-alert": "Note must be at most 200 characters long.",
"description-length-alert": "Description must be at most 500 characters long.",
"fix-&-resend-document": "Fix & Resend Document",
"do-you-want-recreate-document?": "This will create a draft from this document with all fields intact. Are you sure you want to recreate this document?",
"start-editing": "Start Editing",
"unsaved-changes-discard-them?": "You have unsaved changes. Discard them?",
"yes-discard": "Yes, Discard"
}
@@ -152,7 +152,8 @@
"Duplicate Template": "Plantilla duplicada",
"Duplicate": "Duplicada",
"daily-mail-quota": "Cuota diaria de correos electrónicos",
"Save as template": "Guardar como plantilla"
"Save as template": "Guardar como plantilla",
"Fix & resend": "Corregir y reenviar"
},
"report-heading": {
"Sr.No": "Nº",
@@ -285,6 +286,7 @@
"make-template-public": "Convertir la plantilla en pública",
"make-template-private": "Convertir la plantilla en privada",
"make-template-public-alert": "¿En definitiva quieres convertir esta plantilla en pública?",
"make-template-private-alert-non": "¿Está seguro de que desea hacer este plantilla privado?",
"make-template-private-alert": "¿En definitiva quieres convertir esta plantilla en privada? Esto lo removerá de tu perfil público.",
"public-role": "Rol público",
"public-url": "Perfil publico",
@@ -873,5 +875,17 @@
"document-has-been-signed": "¡El documento ha sido firmado con éxito!",
"document-has-been-signed-by-you": "¡El documento ha sido firmado con éxito por usted!",
"participant-completed-signing": "Todos los participantes han completado el proceso de firma.",
"you-will-receive-email-shortly": "✅ ¡Eso es todo! Recibirá un correo de confirmación en breve."
"you-will-receive-email-shortly": "✅ ¡Eso es todo! Recibirá un correo de confirmación en breve.",
"please-provide-templateid": "Por favor, proporcione templateid",
"this-template-is-not-public": "Esta template no es pública",
"invalid-templateid": "templateid no válida",
"contact-billing-at-opensign": "Para agregar más asientos, comuníquese con OpenSign™ a <1>billing@opensignlabs.com</1> para obtener ayuda.",
"title-length-alert": "El título debe tener como máximo 250 caracteres.",
"note-length-alert": "La nota debe tener como máximo 200 caracteres.",
"description-length-alert": "La descripción debe tener como máximo 500 caracteres.",
"fix-&-resend-document": "Corregir y reenviar el documento",
"do-you-want-recreate-document?": "Esto creará un borrador a partir de este documento con todos los campos intactos. ¿Está seguro de que desea recrear este documento?",
"start-editing": "Comenzar a editar",
"unsaved-changes-discard-them?": "Tienes cambios sin guardar. ¿Deseas descartarlos?",
"yes-discard": "Sí, descartar"
}
@@ -173,7 +173,8 @@
"Duplicate Template": "dupliquer le modèle",
"Duplicate": "Double",
"daily-mail-quota": "Quota d'e-mails quotidien",
"Save as template": "Enregistrer comme modèle"
"Save as template": "Enregistrer comme modèle",
"Fix & resend": "Corriger et renvoyer"
},
"report-help": {
"Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.",
@@ -201,7 +202,7 @@
"send-in-order-help": {
"p1": "Choisissez la manière dont vous souhaitez que les demandes de signature soient envoyées aux signataires du document :",
"p2": "La sélection de cette option enverra initialement la demande de signature au premier signataire. Une fois que le premier signataire a terminé sa partie, le prochain signataire de la séquence recevra la demande. Ce processus se poursuit jusqu'à ce que tous les signataires aient signé le document. Cette méthode garantit que le document est signé dans un ordre spécifique.",
"p3": "La sélection de cette option enverra les liens de signature à tous les signataires simultanément. Chaque signataire peut signer le document à sa convenance, que d'autres signataires aient ou non complété leur signature. Cette méthode est plus rapide mais nimpose aucun ordre de signature entre les participants.",
"p3": "La sélection de cette option enverra les liens de signature à tous les signataires simultanément. Chaque signataire peut signer le document à sa convenance, que d'autres signataires aient ou non complété leur signature. Cette méthode est plus rapide mais n'impose aucun ordre de signature entre les participants.",
"p4": "Sélectionnez l'option qui correspond le mieux aux besoins de votre traitement de documents."
},
"no": "Non",
@@ -227,7 +228,7 @@
"create": "Créer",
"signers": "Signataires",
"signers-help": "Commencez à saisir le nom d'un contact pour voir les signataires suggérés par vos contacts enregistrés ou en ajouter de nouveaux. Organisez l'ordre de signature en ajoutant des signataires dans l'ordre souhaité. Utilisez le bouton « + » pour inclure les signataires et le « x » pour les supprimer. Chaque signataire recevra un e-mail invité à signer le document dans l'ordre indiqué.",
"bcc-help": "Commencez à taper le nom dun contact pour voir les suggestions parmi vos contacts enregistrés ou en ajouter de nouveaux. Utilisez le bouton '+' pour ajouter un utilisateur et le bouton 'x' pour le supprimer. Ladresse e-mail de lutilisateur sélectionné sera ajoutée en Bcc (copie carbone invisible). Chaque utilisateur recevra une notification par e-mail une fois le document terminé.",
"bcc-help": "Commencez à taper le nom d'un contact pour voir les suggestions parmi vos contacts enregistrés ou en ajouter de nouveaux. Utilisez le bouton '+' pour ajouter un utilisateur et le bouton 'x' pour le supprimer. L'adresse e-mail de l'utilisateur sélectionné sera ajoutée en Bcc (copie carbone invisible). Chaque utilisateur recevra une notification par e-mail une fois le document terminé.",
"add-signer": "Ajouter un signataire",
"contact-not-found": "Contact introuvable",
"add-yourself": "Ajoutez-vous",
@@ -284,6 +285,7 @@
"make-template-public": "Rendre le modèle public",
"make-template-private": "Rendre le modèle privé",
"make-template-public-alert": "Êtes-vous sûr de vouloir rendre ce modèle public ?",
"make-template-private-alert-non": "Êtes-vous sûr de vouloir rendre ce modèle privé ?",
"make-template-private-alert": "Êtes-vous sûr de vouloir rendre ce modèle privé ? Cela le supprimera de votre profil public.",
"public-role": "Rôle public",
"public-url": "Profil public",
@@ -659,7 +661,7 @@
"bulk-send-subcription-alert": "Veuillez passer au forfait Professionnel ou Équipe pour utiliser Quicksend.",
"generate-test-token": "Générer jeton de test",
"regenerate-test-token": "Régénérer le jeton de test",
"help-test-token": "Ce jeton peut être utilisé pour tester les API au niveau du point de terminaison https://sandbox.opensignlabs.com/api/v1, vous permettant ainsi d'effectuer un nombre illimité de signatures de documents. Veuillez noter que l'API sandbox signera vos documents avec des certificats auto-signés, qui peuvent ne pas être reconnus comme valides par Adobe. Une fois vos tests terminés, vous pouvez passer à lun de nos forfaits payants pour générer un jeton de production.",
"help-test-token": "Ce jeton peut être utilisé pour tester les API au niveau du point de terminaison https://sandbox.opensignlabs.com/api/v1, vous permettant ainsi d'effectuer un nombre illimité de signatures de documents. Veuillez noter que l'API sandbox signera vos documents avec des certificats auto-signés, qui peuvent ne pas être reconnus comme valides par Adobe. Une fois vos tests terminés, vous pouvez passer à l'un de nos forfaits payants pour générer un jeton de production.",
"help-api-token": "Ce jeton peut être utilisé pour accéder aux API de production au point de terminaison {{origin}}/api/v1. Il ne peut être généré que sur l'un de nos forfaits payants.",
"reason": "Raison",
"decline-by": "Refusé/révoqué par",
@@ -873,5 +875,17 @@
"document-has-been-signed": "Le document a été signé avec succès !",
"document-has-been-signed-by-you": "Le document a été signé avec succès par vous !",
"participant-completed-signing": "Tous les participants ont terminé le processus de signature.",
"you-will-receive-email-shortly": "✅ Voilà, c'est fait ! Vous recevrez un e-mail de confirmation sous peu."
"you-will-receive-email-shortly": "✅ Voilà, c'est fait ! Vous recevrez un e-mail de confirmation sous peu.",
"please-provide-templateid": "Veuillez fournir templateid",
"this-template-is-not-public": "Ce template n'est pas public",
"invalid-templateid": "templateid invalide",
"contact-billing-at-opensign": "Pour ajouter plus de places, veuillez contacter OpenSign™ à l'adresse <1>billing@opensignlabs.com</1> pour obtenir de l'aide.",
"title-length-alert": "Le titre doit comporter au maximum 250 caractères.",
"note-length-alert": "La note doit comporter au maximum 200 caractères.",
"description-length-alert": "La description doit comporter au maximum 500 caractères",
"fix-&-resend-document": "Corriger et renvoyer le document",
"do-you-want-recreate-document?": "Cela créera un brouillon à partir de ce document avec tous les champs intacts. Êtes-vous sûr de vouloir recréer ce document ?",
"start-editing": "Commencer l'édition",
"unsaved-changes-discard-them?": "Vous avez des modifications non enregistrées. Les supprimer ?",
"yes-discard": "Oui, supprimer"
}
@@ -152,7 +152,8 @@
"Duplicate Template": "Duplica modello",
"Duplicate": "Duplica",
"daily-mail-quota": "Quota e-mail giornaliera",
"Save as template": "Salva come modello"
"Save as template": "Salva come modello",
"Fix & resend": "Correggi e reinvia"
},
"report-heading": {
"Sr.No": "Nr.",
@@ -284,6 +285,7 @@
"make-template-public": "Rendi il modello pubblico",
"make-template-private": "Rendi il modello privato",
"make-template-public-alert": "Sei sicuro di voler rendere pubblico questo modello?",
"make-template-private-alert-non": "Sei sicuro di voler rendere privato questo modello?",
"make-template-private-alert": "Sei sicuro di voler rendere privato questo modello? Questo lo rimuoverà dal tuo profilo pubblico.",
"public-role": "Ruolo pubblico",
"public-url": "Profilo pubblico",
@@ -314,7 +316,7 @@
"body": "Corpo del messaggio",
"add-contact": "Aggiungi contatto",
"edit-contact": "Modifica contatto",
"add-signer-alert": "Il contatto esiste già! Selezionalo dal menu a tendina Firmatari.",
"add-signer-alert": "Il contatto esiste già! Selezionalo dal menu a tendina 'Firmatari'.",
"record-delete-alert": "Record eliminato con successo!",
"record-revoke-alert": "Record revocato con successo!",
"mail-sent-alert": "Email inviata con successo.",
@@ -608,7 +610,7 @@
"placeholder-sign-4": "Trascina o fai clic su un campo per aggiungerlo al documento.",
"placeholder-sign-5": "L'area del contenuto PDF visualizza già i segnaposti esistenti del modello. Per tua comodità, questi segnaposti corrisponderanno al colore del nome del destinatario, rendendoli facilmente identificabili.",
"placeholder-sign-6": "Facendo clic su 'Invia' il documento verrà salvato. Nel passaggio successivo potrai personalizzare le email da inviare ai destinatari o copiare i link di firma e condividerli direttamente con i destinatari.",
"report-1": "Fai clic sul pulsante 'Aggiungi' per creare un nuovo modello. I modelli sono documenti riutilizzabili progettati per generare rapidamente nuovi documenti con la stessa struttura e firmatari diversi. Ad esempio, un modello HR per l'onboarding potrebbe avere ruoli predefinita come Responsabile HR e Nuovo Dipendente. Ogni volta che usi il modello, puoi assegnare il ruolo Nuovo Dipendente a membri dello staff in arrivo, mentre il ruolo Responsabile HR rimane costante, facilitando un processo di onboarding fluido per ogni nuovo assunto.",
"report-1": "Fai clic sul pulsante 'Aggiungi' per creare un nuovo modello. I modelli sono documenti riutilizzabili progettati per generare rapidamente nuovi documenti con la stessa struttura e firmatari diversi. Ad esempio, un modello HR per l'onboarding potrebbe avere ruoli predefinita come 'Responsabile HR' e 'Nuovo Dipendente'. Ogni volta che usi il modello, puoi assegnare il ruolo 'Nuovo Dipendente' a membri dello staff in arrivo, mentre il ruolo 'Responsabile HR' rimane costante, facilitando un processo di onboarding fluido per ogni nuovo assunto.",
"redirect": "Fai clic sul pulsante 'Usa' per creare un nuovo documento da un modello esistente.",
"bulksend": "Per inviare rapidamente più documenti utilizzando un modello esistente creando semplicemente gli indirizzi e-mail dei destinatari, fai clic sul pulsante 'Invio Multiplo'.",
"option": "Questo menu rivela altre opzioni come Modifica ed Elimina. Usa il pulsante 'Modifica' per aggiungere ruoli di firmatari, modificare i campi e aggiornare il modello. Le modifiche si applicheranno a tutti i futuri documenti creati da questo modello ma non influiranno sui documenti esistenti. Usa il pulsante Elimina per eliminare il modello.",
@@ -625,7 +627,7 @@
"Recent signature requests": "Questo è un elenco di documenti che aspettano la tua firma.",
"Recently sent for signatures": "Questo è un elenco di documenti che hai inviato ad altre parti per la firma.",
"Drafts": "Questi sono documenti che hai iniziato ma non hai finalizzato per l'invio.",
"public-template": "Questo video dimostra come configurare il tuo profilo pubblico personalizzato, come https://opensign.me/tuo-username. Imparerai anche come personalizzare il tuo slogan e rendere i tuoi modelli disponibili per la firma pubblica.",
"public-template": "Questo video dimostra come configurare il tuo profilo pubblico personalizzato, come 'https://opensign.me/tuo-username'. Imparerai anche come personalizzare il tuo slogan e rendere i tuoi modelli disponibili per la firma pubblica.",
"allowModify-widgets": "È possibile trascinare e rilasciare uno qualsiasi di questi campi nel documento, oltre ai campi già designati dal creatore del documento."
},
"enter-email-plaholder": "Aggiungi un indirizzo email e premi invio",
@@ -873,5 +875,17 @@
"document-has-been-signed": "Il documento è stato firmato con successo!",
"document-has-been-signed-by-you": "Il documento è stato firmato con successo da lei!",
"participant-completed-signing": "Tutti i partecipanti hanno completato il processo di firma.",
"you-will-receive-email-shortly": "✅ È tutto! Riceverà a breve un'e-mail di conferma."
"you-will-receive-email-shortly": "✅ È tutto! Riceverà a breve un'e-mail di conferma.",
"please-provide-templateid": "Si prega di fornire templateid",
"this-template-is-not-public": "Questo template non è pubblico",
"invalid-templateid": "templateid non valido",
"contact-billing-at-opensign": " Per aggiungere altri posti, contattare OpenSign™ all'indirizzo <1>billing@opensignlabs.com</1> per assistenza.",
"title-length-alert": "Il titolo può contenere al massimo 250 caratteri.",
"note-length-alert": "La nota può contenere al massimo 200 caratteri.",
"description-length-alert": " La descrizione può contenere al massimo 500 caratteri.",
"fix-&-resend-document": "Correggi e reinvia il documento",
"do-you-want-recreate-document?": "Questo creerà una bozza da questo documento con tutti i campi intatti. Sei sicuro di voler ricreare questo documento?",
"start-editing": "Inizia a modificare",
"unsaved-changes-discard-them?": "Hai modifiche non salvate. Vuoi scartarle?",
"yes-discard": "Sì, scarta"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+1 -1
View File
@@ -24,7 +24,7 @@ const AddSigner = (props) => {
if (savedUserDetails && addYourself) {
setName(savedUserDetails.name);
setPhone(savedUserDetails?.phone || "");
setEmail(savedUserDetails.email);
setEmail(savedUserDetails.email?.toLowerCase()?.replace(/\s/g, ""));
}
}, [addYourself]);
+35 -145
View File
@@ -75,156 +75,46 @@ const AddUser = (props) => {
setIsFormLoader(false);
} else {
if (localStorage.getItem("TenantId")) {
const timezone = usertimezone;
try {
const extUser = new Parse.Object("contracts_Users");
extUser.set("Name", formdata.name);
if (formdata.phone) {
extUser.set("Phone", formdata.phone);
}
extUser.set("Email", formdata.email);
extUser.set("UserRole", `contracts_${formdata.role}`);
if (formdata?.team) {
extUser.set("TeamIds", [
{
__type: "Pointer",
className: "contracts_Teams",
objectId: formdata.team
}
]);
}
if (localUser && localUser.OrganizationId) {
extUser.set("OrganizationId", {
__type: "Pointer",
className: "contracts_Organizations",
objectId: localUser.OrganizationId.objectId
});
}
if (localUser && localUser.Company) {
extUser.set("Company", localUser.Company);
}
if (localStorage.getItem("TenantId")) {
extUser.set("TenantId", {
__type: "Pointer",
className: "partners_Tenant",
objectId: localStorage.getItem("TenantId")
});
}
const timezone = usertimezone;
if (timezone) {
extUser.set("Timezone", timezone);
}
try {
const _users = Parse.Object.extend("User");
const _user = new _users();
_user.set("name", formdata.name);
_user.set("username", formdata.email);
_user.set("email", formdata.email);
_user.set("password", formdata.password);
if (formdata.phone) {
_user.set("phone", formdata.phone);
const params = {
name: formdata.name,
email: formdata.email,
phone: formdata.phone,
password: formdata.password,
role: formdata.role,
team: formdata.team,
timezone: timezone,
tenantId: localStorage.getItem("TenantId"),
organization: {
objectId: localUser?.OrganizationId?.objectId,
company: localUser?.Company
}
const user = await _user.save();
if (user) {
const currentUser = Parse.User.current();
extUser.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
};
const res = await Parse.Cloud.run("adduser", params);
const parseData = JSON.parse(JSON.stringify(res));
console.log("parseData ", parseData);
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
if (formdata?.team) {
const team = teamList.find((x) => x.objectId === formdata.team);
parseData.TeamIds = parseData.TeamIds.map((y) =>
y.objectId === team.objectId ? team : y
);
extUser.set("UserId", user);
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(currentUser.id, true);
acl.setWriteAccess(currentUser.id, true);
extUser.setACL(acl);
const res = await extUser.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
if (formdata?.team) {
const team = teamList.find(
(x) => x.objectId === formdata.team
);
parseData.TeamIds = parseData.TeamIds.map((y) =>
y.objectId === team.objectId ? team : y
);
}
props.handleUserData(parseData);
}
setIsFormLoader(false);
setFormdata({
name: "",
email: "",
phone: "",
team: "",
role: ""
});
props.showAlert("success", t("user-created-successfully"));
}
} catch (err) {
console.log("err ", err);
if (err.code === 202) {
const params = { email: formdata.email };
const userRes = await Parse.Cloud.run("getUserId", params);
const currentUser = Parse.User.current();
extUser.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
);
extUser.set("UserId", {
__type: "Pointer",
className: "_User",
objectId: userRes.id
});
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(currentUser.id, true);
acl.setWriteAccess(currentUser.id, true);
extUser.setACL(acl);
const res = await extUser.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
if (formdata?.team) {
const team = teamList.find(
(x) => x.objectId === formdata.team
);
parseData.TeamIds = parseData.TeamIds.map((y) =>
y.objectId === team.objectId ? team : y
);
}
props.handleUserData(parseData);
}
setIsFormLoader(false);
setFormdata({
name: "",
email: "",
phone: "",
team: "",
role: ""
});
props.showAlert("success", t("user-created-successfully"));
} else {
setIsFormLoader(false);
props.showAlert("danger", t("something-went-wrong-mssg"));
}
props.handleUserData(parseData);
}
setIsFormLoader(false);
setFormdata({
name: "",
email: "",
phone: "",
team: "",
role: ""
});
props.showAlert("success", t("user-created-successfully"));
} catch (err) {
console.log("err", err);
setIsFormLoader(false);
+80 -81
View File
@@ -3,9 +3,7 @@ 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 { emailRegex } from "../constant/const";
const BulkSendUi = (props) => {
const { t } = useTranslation();
const [forms, setForms] = useState([]);
@@ -22,15 +20,15 @@ const BulkSendUi = (props) => {
//function to check atleast one signature field exist
const signatureExist = async () => {
setIsDisableBulkSend(false);
const getPlaceholder = props?.Placeholders;
const checkIsSignatureExistt = getPlaceholder?.every((placeholderObj) =>
placeholderObj?.placeHolder?.some((holder) =>
holder?.pos?.some((posItem) => posItem?.type === "signature")
)
);
setIsSignatureExist(checkIsSignatureExistt);
setIsLoader(false);
setIsDisableBulkSend(false);
const getPlaceholder = props?.Placeholders;
const checkIsSignatureExistt = getPlaceholder?.every((placeholderObj) =>
placeholderObj?.placeHolder?.some((holder) =>
holder?.pos?.some((posItem) => posItem?.type === "signature")
)
);
setIsSignatureExist(checkIsSignatureExistt);
setIsLoader(false);
};
useEffect(() => {
if (scrollOnNextUpdate && formRef.current) {
@@ -74,7 +72,6 @@ const BulkSendUi = (props) => {
setForms(newForms);
};
const handleRemoveForm = (index) => {
const updatedForms = forms.filter((_, i) => i !== index);
setForms(updatedForms);
@@ -95,82 +92,83 @@ const BulkSendUi = (props) => {
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: props.item.Signers
? [...props.item.Signers, ...existSigner]
: [...existSigner]
});
} else {
Documents.push({
...props.item,
Placeholders: updatedPlaceholders,
SignatureType: props.signatureType
});
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);
}
});
await batchQuery(Documents);
} else {
setIsSubmit(false);
}
// 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: props.item.Signers
? [...props.item.Signers, ...existSigner]
: [...existSigner]
});
} else {
Documents.push({
...props.item,
Placeholders: updatedPlaceholders,
SignatureType: props.signatureType
});
}
});
await batchQuery(Documents);
} else {
setIsSubmit(false);
}
};
const batchQuery = async (Documents) => {
const token =
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
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,
...token
};
const params = { Documents: JSON.stringify(Documents) };
try {
@@ -218,7 +216,9 @@ const BulkSendUi = (props) => {
className="flex flex-col"
key={field.fieldId}
>
<label>{field.label}</label>
<label className="block text-xs font-semibold">
{field.label}
</label>
<SuggestionInput
required
type="email"
@@ -275,8 +275,7 @@ const BulkSendUi = (props) => {
)}
</>
) : (
<>
</>
<></>
)}
</>
)}
+411 -205
View File
@@ -1,12 +1,34 @@
import React, { useState } from "react";
import { getFileName } from "../../constant/Utils";
import React, { useState, useRef } from "react";
import {
base64ToArrayBuffer,
convertBase64ToFile,
generatePdfName,
getFileName
} from "../../constant/Utils";
import {
maxDescriptionLength,
maxNoteLength,
maxTitleLength
} from "../../constant/const";
import { useTranslation } from "react-i18next";
import { Tooltip } from "react-tooltip";
import SignersInput from "../shared/fields/SignersInput";
import { PDFDocument } from "pdf-lib";
import ModalUi from "../../primitives/ModalUi";
import { SaveFileSize } from "../../constant/saveFileSize";
const EditTemplate = ({ template, onSuccess }) => {
const EditTemplate = ({
title,
handleClose,
pdfbase64,
template,
onSuccess,
setPdfArrayBuffer,
setPdfBase64Url
}) => {
const appName = "OpenSign™";
const { t } = useTranslation();
const inputFileRef = useRef(null);
const [formData, setFormData] = useState({
Name: template?.Name || "",
Note: template?.Note || "",
@@ -27,6 +49,31 @@ const EditTemplate = ({ template, onSuccess }) => {
AllowModifications: template?.AllowModifications || false,
TimeToCompleteDays: template?.TimeToCompleteDays || 15
});
const [isUpdate, setIsUpdate] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [uploadPdf, setUploadPdf] = useState({
name: "",
base64: "",
url: ""
});
const handleDrop = (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
handleFile(file);
};
const handleFile = (file) => {
if (file && file.type === "application/pdf") {
handleReplaceFileValdition(file);
// You can handle the file here
} else {
alert("Only pdf files are allowed.");
if (inputFileRef.current) inputFileRef.current.value = "";
}
};
const handleDragOver = (e) => {
e.preventDefault();
};
// `isValidURL` is used to check valid webhook url
function isValidURL(value) {
@@ -39,9 +86,63 @@ const EditTemplate = ({ template, onSuccess }) => {
}
const handleStrInput = (e) => {
setIsUpdate(true);
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const getPdfMetadataHash = async (pdfBytes) => {
const pdfDoc = await PDFDocument.load(pdfBytes);
const pages = pdfDoc.getPages();
const metaString = pages
.map((page, index) => {
const { width, height } = page.getSize();
return `${index + 1}:${Math.round(width)}x${Math.round(height)}`;
})
.join("|");
const encoder = new TextEncoder();
const data = encoder.encode(metaString);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
const handleFileInput = async (e) => {
const file = e.target.files[0];
if (!file) return;
handleReplaceFileValdition(file);
};
const handleReplaceFileValdition = async (file) => {
try {
const basePdfBytes = base64ToArrayBuffer(pdfbase64);
const expectedHash = await getPdfMetadataHash(basePdfBytes);
const fileReader = new FileReader();
fileReader.onload = async (event) => {
const uploadedPdfBytes = event.target.result;
const uploadedHash = await getPdfMetadataHash(uploadedPdfBytes);
if (expectedHash === uploadedHash) {
const arrayBuffer = uploadedPdfBytes;
const uint8Array = new Uint8Array(arrayBuffer);
const binaryString = Array.from(uint8Array)
.map((b) => String.fromCharCode(b))
.join("");
const base64 = btoa(binaryString);
const pdfName = generatePdfName(16);
setIsUpdate(true);
setUploadPdf((prev) => ({ ...prev, name: pdfName, base64: base64 }));
// alert("✅ PDFs match (based on page number, width, height)");
} else {
alert("❌ PDF do NOT match based on page number, width, height");
if (inputFileRef.current) inputFileRef.current.value = "";
}
};
fileReader.readAsArrayBuffer(file);
} catch (err) {
alert("Error: " + err.message);
if (inputFileRef.current) inputFileRef.current.value = "";
}
};
// Define a function to handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
@@ -50,6 +151,31 @@ const EditTemplate = ({ template, onSuccess }) => {
alert(t("invalid-redirect-url"));
return;
}
if (formData?.Name?.length > maxTitleLength) {
alert(t("title-length-alert"));
return;
}
if (formData?.Note?.length > maxNoteLength) {
alert(t("note-length-alert"));
return;
}
if (formData?.Description?.length > maxDescriptionLength) {
alert(t("description-length-alert"));
return;
}
let pdfUrl;
if (uploadPdf?.base64) {
pdfUrl = await convertBase64ToFile(uploadPdf.name, uploadPdf.base64);
setUploadPdf((prev) => ({ ...prev, url: pdfUrl }));
const pdfBuffer = base64ToArrayBuffer(uploadPdf.base64);
setPdfArrayBuffer && setPdfArrayBuffer(pdfBuffer);
setPdfBase64Url && setPdfBase64Url(uploadPdf.base64);
const tenantId =
localStorage.getItem("TenantId") ||
template?.ExtUserPtr?.TenantId?.objectId;
const buffer = atob(uploadPdf.base64);
SaveFileSize(buffer.length, pdfUrl, tenantId);
}
const isChecked = formData.SendinOrder === "true" ? true : false;
const isTourEnabled = formData?.IsTourEnabled === "false" ? false : true;
const AutoReminder = formData?.AutomaticReminders || false;
@@ -71,6 +197,7 @@ const EditTemplate = ({ template, onSuccess }) => {
}
const data = {
...formData,
...(pdfUrl ? { URL: pdfUrl } : {}),
SendinOrder: isChecked,
IsEnableOTP: IsEnableOTP,
IsTourEnabled: isTourEnabled,
@@ -82,6 +209,7 @@ const EditTemplate = ({ template, onSuccess }) => {
// `handleNotifySignChange` is trigger when user change radio of notify on signatures
const handleNotifySignChange = (value) => {
setIsUpdate(true);
setFormData((obj) => ({ ...obj, NotifyOnSignatures: value }));
};
const handleBcc = (data) => {
@@ -91,226 +219,304 @@ const EditTemplate = ({ template, onSuccess }) => {
Name: item?.label,
Email: item?.email
}));
setIsUpdate(true);
setFormData((prev) => ({ ...prev, Bcc: trimEmail }));
}
};
const handleEditTemplateClose = () => {
if (isUpdate) {
setShowConfirm(true);
} else {
handleClose();
}
};
const discardChanges = () => {
setShowConfirm(false);
handleClose();
};
return (
<div className="max-h-[300px] md:max-h-[400px] overflow-y-scroll p-[10px]">
<div className="text-base-content">
<form onSubmit={handleSubmit}>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("report-heading.File")}
</label>
<div className="op-input op-input-bordered op-input-sm focus:outline-none py-2 font-semibold w-full text-xs">
{getFileName(template.URL)}
</div>
<ModalUi
isOpen
title={isUpdate ? `${title} (unsaved)` : title}
handleClose={handleEditTemplateClose}
>
<ModalUi isOpen={showConfirm} showClose={false}>
<div className="p-[20px]">
<p className="text-base font-normal text-base-content py-[5px] md:py-[6px] px-[5px]">
{t("unsaved-changes-discard-them?")}
</p>
<div className="flex items-center mt-2.5 gap-2 md:gap-3 text-white">
<button
className="op-btn op-btn-primary px-6"
onClick={discardChanges}
>
{t("yes-discard")}
</button>
<button
className="op-btn op-btn-secondary px-4 md:px-6"
onClick={() => setShowConfirm(false)}
>
{t("cancel")}
</button>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("Title")}
<span className="text-[13px] text-[red]"> *</span>
</label>
<input
type="text"
name="Name"
value={formData.Name}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Note" className="text-[13px]">
{t("report-heading.Note")}
</label>
<input
type="text"
name="Note"
id="Note"
value={formData.Note}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Description" className="text-[13px]">
{t("description")}
</label>
<input
type="text"
name="Description"
id="Description"
value={formData.Description}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label className="text-[13px]">{t("send-in-order")}</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="SendinOrder"
checked={formData.SendinOrder === "true"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("yes")}</div>
</div>
</ModalUi>
<div className="max-h-[300px] md:max-h-[400px] overflow-y-scroll p-[10px]">
<div className="text-base-content">
<form onSubmit={handleSubmit}>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("report-heading.File")}
</label>
<div
className="border-[1.5px] border-dashed border-gray-300 rounded-lg px-4 py-6 text-center text-gray-500 bg-white cursor-pointer hover:border-base-content transition"
onDrop={handleDrop}
onDragOver={handleDragOver}
onClick={() => inputFileRef?.current?.click()}
>
<label
htmlFor="fileUpload"
className="cursor-pointer text-center mb-0"
>
Browse or drag & drop a new file to replace the existng one
</label>
</div>
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"false"}
name="SendinOrder"
className="op-radio op-radio-xs"
checked={formData.SendinOrder === "false"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("no")}</div>
<input
ref={inputFileRef}
type="file"
className="hidden"
accept="application/pdf"
onChange={(e) => handleFileInput(e)}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
/>
{uploadPdf?.name && (
<div
onClick={() => inputFileRef?.current?.click()}
className="mt-2 cursor-pointer op-input op-input-bordered op-input-sm focus:outline-none py-2 font-semibold w-full text-xs"
>
selected:{" "}
{uploadPdf?.url
? `${uploadPdf?.name}.pdf`
: getFileName(template.URL)}
</div>
)}
</div>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("Title")}
<span className="text-[13px] text-[red]"> *</span>
</label>
<input
type="text"
name="Name"
value={formData.Name}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
required
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Note" className="text-[13px]">
{t("report-heading.Note")}
</label>
<input
type="text"
name="Note"
id="Note"
value={formData.Note}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Description" className="text-[13px]">
{t("description")}
</label>
<input
type="text"
name="Description"
id="Description"
value={formData.Description}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label className="text-[13px]">{t("send-in-order")}</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="SendinOrder"
checked={formData.SendinOrder === "true"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("yes")}</div>
</div>
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"false"}
name="SendinOrder"
className="op-radio op-radio-xs"
checked={formData.SendinOrder === "false"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("no")}</div>
</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<label className="block">
<span>
{t("enable-tour")}
<a data-tooltip-id="istourenabled-tooltip" className="ml-1">
<div className="text-xs mt-3">
<label className="block">
<span>
{t("enable-tour")}
<a data-tooltip-id="istourenabled-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
</span>
<Tooltip id="istourenabled-tooltip" className="z-50">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("enable-tour")}</p>
<div className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">{t("yes")}: </span>
<span>{t("istourenabled-help.p1")}</span>
</li>
<li>
<span className="font-bold">{t("no")}: </span>
<span>{t("istourenabled-help.p2")}</span>
</li>
</ol>
</div>
<p>{t("istourenabled-help.p3", { appName: appName })}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="IsTourEnabled"
checked={formData.IsTourEnabled === "true"}
onChange={handleStrInput}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"false"}
name="IsTourEnabled"
className="op-radio op-radio-xs"
checked={formData.IsTourEnabled === "false"}
onChange={handleStrInput}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<label>
{t("notify-on-signatures")}
<a data-tooltip-id="nos-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
</span>
<Tooltip id="istourenabled-tooltip" className="z-50">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("enable-tour")}</p>
<div className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">{t("yes")}: </span>
<span>{t("istourenabled-help.p1")}</span>
</li>
<li>
<span className="font-bold">{t("no")}: </span>
<span>{t("istourenabled-help.p2")}</span>
</li>
</ol>
<Tooltip id="nos-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
<p className="font-bold">{t("notify-on-signatures")}</p>
<p>{t("notify-on-signatures-help.p1")}</p>
<p>{t("notify-on-signatures-help.note")}</p>
</div>
<p>{t("istourenabled-help.p3", { appName: appName })}</p>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className={`flex items-center gap-2 ml-2 mb-1`}>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(true)}
checked={formData.NotifyOnSignatures === true}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className={`flex items-center gap-2 ml-2 mb-1`}>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(false)}
checked={formData.NotifyOnSignatures === false}
/>
<div className="text-center">{t("no")}</div>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="IsTourEnabled"
checked={formData.IsTourEnabled === "true"}
onChange={handleStrInput}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"false"}
name="IsTourEnabled"
className="op-radio op-radio-xs"
checked={formData.IsTourEnabled === "false"}
onChange={handleStrInput}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<label>
{t("notify-on-signatures")}
<a data-tooltip-id="nos-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
<Tooltip id="nos-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
<p className="font-bold">{t("notify-on-signatures")}</p>
<p>{t("notify-on-signatures-help.p1")}</p>
<p>{t("notify-on-signatures-help.note")}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className={`flex items-center gap-2 ml-2 mb-1`}>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(true)}
checked={formData.NotifyOnSignatures === true}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className={`flex items-center gap-2 ml-2 mb-1`}>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(false)}
checked={formData.NotifyOnSignatures === false}
/>
<div className="text-center">{t("no")}</div>
</div>
<div className="text-xs mt-3">
<SignersInput
label={t("Bcc")}
initialData={template?.Bcc}
onChange={handleBcc}
helptextZindex={50}
helpText={t("bcc-help")}
isCaptureAllData
/>
</div>
</div>
<div className="text-xs mt-3">
<SignersInput
label={t("Bcc")}
initialData={template?.Bcc}
onChange={handleBcc}
helptextZindex={50}
helpText={t("bcc-help")}
isCaptureAllData
/>
</div>
<div className="text-xs mt-2">
<label className="block">Redirect Url</label>
<input
name="RedirectUrl"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.RedirectUrl}
onChange={handleStrInput}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
/>
</div>
<div className="text-xs mt-2">
<label className="block">
{t("time-to-complete")}
<span className="text-red-500 text-[13px]">*</span>
</label>
<input
type="number"
name="TimeToCompleteDays"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.TimeToCompleteDays}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
/>
</div>
<div className="mt-[1rem] flex justify-start">
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
</div>
</form>
<div className="text-xs mt-2">
<label className="block">Redirect Url</label>
<input
name="RedirectUrl"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.RedirectUrl}
onChange={handleStrInput}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
/>
</div>
<div className="text-xs mt-2">
<label className="block">
{t("time-to-complete")}
<span className="text-red-500 text-[13px]">*</span>
</label>
<input
type="number"
name="TimeToCompleteDays"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.TimeToCompleteDays}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
required
/>
</div>
<div className="mt-[1rem] flex justify-start">
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
</div>
</form>
</div>
</div>
</div>
</ModalUi>
);
};
@@ -45,7 +45,7 @@ const DateFormatSelector = (props) => {
</option>
))}
</select>
<div className="flex flex-col md:flex-row md:gap-4 mt-[0.75rem] text-[12px]">
<div className="flex flex-row gap-4 mt-[0.75rem] text-[12px]">
<div className="flex items-center gap-2 ml-2">
<input
type="radio"
+11 -1
View File
@@ -643,6 +643,15 @@ export const createDocument = async (
const RedirectUrl = Doc?.RedirectUrl
? { RedirectUrl: Doc?.RedirectUrl }
: {};
const TemplateId = Doc?.objectId
? {
TemplateId: {
__type: "Pointer",
className: "contracts_Template",
objectId: Doc?.objectId
}
}
: {};
const data = {
Name: Doc.Name,
URL: pdfUrl,
@@ -672,7 +681,8 @@ export const createDocument = async (
...SignatureType,
...NotifyOnSignatures,
...Bcc,
...RedirectUrl
...RedirectUrl,
...TemplateId
};
const remindOnceInEvery = Doc?.RemindOnceInEvery;
const TimeToCompleteDays = parseInt(Doc?.TimeToCompleteDays);
+3
View File
@@ -5,3 +5,6 @@ export const themeColor = "#47a3ad";
export const iconColor = "#686968";
export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
export const maxFileSize = 10; // 10MB
export const maxTitleLength = 250; // 250 characters
export const maxNoteLength = 200; // 200 characters
export const maxDescriptionLength = 500; // 500 characters
+9 -1
View File
@@ -219,7 +219,6 @@ export default function reportJson(id) {
btnId: "44534",
hoverLabel: "option",
btnColor: "",
restrictBtn: true,
textColor: "black",
btnIcon: "fa-light fa-ellipsis-vertical fa-lg",
action: "option",
@@ -228,9 +227,18 @@ export default function reportJson(id) {
btnId: "0940",
btnLabel: "Save as template",
hoverLabel: "Save as template",
restrictBtn: true,
btnIcon: "fa-light fa-envelope",
redirectUrl: "",
action: "saveastemplate"
},
{
btnId: "8440",
btnLabel: "Fix & resend",
hoverLabel: "Fix & resend",
btnIcon: "fa-light fa-paper-plane",
redirectUrl: "",
action: "recreatedocument"
}
]
}
+5 -6
View File
@@ -16,8 +16,7 @@ import { useTranslation } from "react-i18next";
import { emailRegex } from "../constant/const";
const AddAdmin = () => {
const appName =
"OpenSign™";
const appName = "OpenSign™";
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const dispatch = useDispatch();
@@ -96,7 +95,7 @@ const AddAdmin = () => {
setState({ loading: true });
const userDetails = {
name: name,
email: email,
email: email?.toLowerCase()?.replace(/\s/g, ""),
phone: phone,
company: company,
jobTitle: jobTitle
@@ -106,10 +105,10 @@ const AddAdmin = () => {
event.preventDefault();
const user = new Parse.User();
user.set("name", name);
user.set("email", email);
user.set("email", email?.toLowerCase()?.replace(/\s/g, ""));
user.set("password", password);
user.set("phone", phone);
user.set("username", email);
user.set("username", email?.toLowerCase()?.replace(/\s/g, ""));
const userRes = await user.save();
if (userRes) {
const params = {
@@ -117,7 +116,7 @@ const AddAdmin = () => {
jobTitle: jobTitle,
company: company,
name: name,
email: email,
email: email?.toLowerCase()?.replace(/\s/g, ""),
phone: phone,
role: "contracts_Admin",
timezone: usertimezone
+19 -14
View File
@@ -7,17 +7,17 @@ import Alert from "../primitives/Alert";
import { appInfo } from "../constant/appinfo";
import { useDispatch } from "react-redux";
import { fetchAppInfo } from "../redux/reducers/infoReducer";
import {
emailRegex,
} from "../constant/const";
import { emailRegex } from "../constant/const";
import { useTranslation } from "react-i18next";
import Loader from "../primitives/Loader";
function ForgotPassword() {
const { t } = useTranslation();
const dispatch = useDispatch();
const navigate = useNavigate();
const [state, setState] = useState({ email: "", password: "", hideNav: "" });
const [sentStatus, setSentStatus] = useState("");
const [toast, setToast] = useState({ type: "", message: "" });
const [isLoading, setIsLoading] = useState(false);
const [image, setImage] = useState();
const handleChange = (event) => {
@@ -40,18 +40,23 @@ function ForgotPassword() {
if (!emailRegex.test(state.email)) {
alert("Please enter a valid email address.");
} else {
setIsLoading(true);
localStorage.setItem("appLogo", appInfo.applogo);
localStorage.setItem("userSettings", JSON.stringify(appInfo.settings));
if (state.email) {
const username = state.email;
try {
await Parse.User.requestPasswordReset(username);
setSentStatus("success");
setToast({ type: "success", message: t("reset-password-alert-1") });
} catch (err) {
console.log("err ", err.code);
setSentStatus("failed");
setToast({
type: "danger",
message: err.message || t("reset-password-alert-2")
});
} finally {
setTimeout(() => setSentStatus(""), 1000);
setIsLoading(false);
setTimeout(() => setToast({ type: "", message: "" }), 1000);
}
}
}
@@ -71,17 +76,17 @@ function ForgotPassword() {
} catch (err) {
console.log("err while logging out ", err);
}
setImage(appInfo?.applogo || undefined);
setImage(appInfo?.applogo || undefined);
};
return (
<div>
{isLoading && (
<div className="fixed w-full h-full flex justify-center items-center bg-black bg-opacity-30 z-50">
<Loader />
</div>
)}
<Title title="Forgot password" />
{sentStatus === "success" && (
<Alert type="success">{t("reset-password-alert-1")}</Alert>
)}
{sentStatus === "failed" && (
<Alert type={"danger"}>{t("reset-password-alert-2")}</Alert>
)}
{toast?.message && <Alert type={toast.type}>{toast.message}</Alert>}
<div className="md:p-10 lg:p-16">
<div className="md:p-4 lg:p-10 p-4 bg-base-100 text-base-content op-card">
<div className="w-[250px] h-[66px] inline-block overflow-hidden">
+18 -1
View File
@@ -18,7 +18,12 @@ import {
} from "../constant/Utils";
import { PDFDocument } from "pdf-lib";
import axios from "axios";
import { maxFileSize } from "../constant/const";
import {
maxFileSize,
maxDescriptionLength,
maxNoteLength,
maxTitleLength
} from "../constant/const";
import ModalUi from "../primitives/ModalUi";
import { Tooltip } from "react-tooltip";
import Loader from "../primitives/Loader";
@@ -354,6 +359,18 @@ const Forms = (props) => {
e.preventDefault();
e.stopPropagation();
if (fileupload) {
if (formData?.Name?.length > maxTitleLength) {
alert(t("title-length-alert"));
return;
}
if (formData?.Note?.length > maxNoteLength) {
alert(t("note-length-alert"));
return;
}
if (formData?.Description?.length > maxDescriptionLength) {
alert(t("description-length-alert"));
return;
}
if (formData.RedirectUrl && !isValidURL(formData?.RedirectUrl)) {
alert(t("invalid-redirect-url"));
return;
+19 -8
View File
@@ -15,7 +15,9 @@ function GuestLogin() {
const { t, i18n } = useTranslation();
const { id, userMail, contactBookId, base64url } = useParams();
const navigate = useNavigate();
const [email, setEmail] = useState(userMail);
const [email, setEmail] = useState(
userMail?.toLowerCase()?.replace(/\s/g, "")
);
const [OTP, setOTP] = useState("");
const [EnterOTP, setEnterOtp] = useState(false);
const [loading, setLoading] = useState(false);
@@ -76,12 +78,18 @@ function GuestLogin() {
//split url in array from '/'
const checkSplit = decodebase64.split("/");
setDocumentId(checkSplit[0]);
setContact((prev) => ({ ...prev, email: checkSplit[1] }));
setEmail(checkSplit[1]);
setContact((prev) => ({
...prev,
email: checkSplit[1]?.toLowerCase()?.replace(/\s/g, "")
}));
setEmail(checkSplit[1]?.toLowerCase()?.replace(/\s/g, ""));
const contactId = checkSplit?.[2];
setSendmail(checkSplit[3]);
if (!contactId) {
const params = { email: checkSplit[1], docId: checkSplit[0] };
const params = {
email: checkSplit[1]?.toLowerCase()?.replace(/\s/g, ""),
docId: checkSplit[0]
};
try {
const linkContactRes = await Parse.Cloud.run(
"linkcontacttodoc",
@@ -103,10 +111,10 @@ function GuestLogin() {
//send email OTP function
const SendOtp = async () => {
setLoading(true);
setEmail(email);
setEmail(email?.toLowerCase()?.replace(/\s/g, ""));
try {
const params = {
email: email.toString(),
email: email?.toLowerCase()?.replace(/\s/g, "")?.toString(),
docId: documentId
};
const Otp = await Parse.Cloud.run("SendOTPMailV1", params);
@@ -139,7 +147,10 @@ function GuestLogin() {
"Content-Type": "application/json",
"X-Parse-Application-Id": parseId
};
let body = { email: email, otp: OTP };
let body = {
email: email?.toLowerCase()?.replace(/\s/g, ""),
otp: OTP
};
let user = await axios.post(url, body, { headers: headers });
if (user.data.result === "Invalid Otp") {
alert(t("invalid-otp"));
@@ -184,7 +195,7 @@ function GuestLogin() {
};
const handleUserData = async (e) => {
e.preventDefault();
if (!emailRegex.test(contact.email)) {
if (!emailRegex.test(contact.email?.toLowerCase()?.replace(/\s/g, ""))) {
alert("Please enter a valid email address.");
} else {
const params = { ...contact, docId: documentId };
+55 -17
View File
@@ -3,6 +3,7 @@ import axios from "axios";
import Parse from "parse";
import "../styles/signature.css";
import { PDFDocument } from "pdf-lib";
import { maxTitleLength } from "../constant/const";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useDrop } from "react-dnd";
@@ -164,6 +165,7 @@ function PlaceHolderSign() {
const [isAttchSignerModal, setIsAttchSignerModal] = useState(false);
const [isNewContact, setIsNewContact] = useState({ status: false, id: "" });
const [owner, setOwner] = useState({});
const [docTitle, setDocTitle] = useState("");
const isMobile = window.innerWidth < 767;
const [, drop] = useDrop({
accept: "BOX",
@@ -243,6 +245,7 @@ function PlaceHolderSign() {
//getting document details
const documentData = await contractDocument(documentId);
if (documentData && documentData.length > 0) {
setDocTitle(documentData?.[0]?.Name);
if (documentData[0]?.Placeholders?.length > 0) {
const signerNotExist = documentData[0]?.Placeholders.some(
(data) => !data.signerObjId && data.Role !== "prefill"
@@ -1010,6 +1013,7 @@ function PlaceHolderSign() {
const saveDocumentDetails = async () => {
setIsUiLoading(true);
let signerMail = signersdata.slice();
// For "Send in order", only consider the first signer
if (pdfDetails?.[0]?.SendinOrder && pdfDetails?.[0]?.SendinOrder === true) {
signerMail.splice(1);
}
@@ -1022,7 +1026,7 @@ function PlaceHolderSign() {
objectId: x.objectId
};
});
const addExtraDays = pdfDetails[0]?.TimeToCompleteDays
const addExtraDays = pdfDetails?.[0]?.TimeToCompleteDays
? pdfDetails[0].TimeToCompleteDays
: 15;
const currentUser = signersdata.find((x) => x.Email === currentId);
@@ -1037,22 +1041,22 @@ function PlaceHolderSign() {
} else {
setIsCurrUser(currentUser?.objectId ? true : false);
}
let updateExpiryDate, data;
updateExpiryDate = new Date();
// Compute expiry date with extra days
let updateExpiryDate = new Date();
updateExpiryDate.setDate(updateExpiryDate.getDate() + addExtraDays);
//filter label widgets after add label widgets data on pdf
// Filter out prefill roles
const filterPrefill = signerPos.filter((data) => data.Role !== "prefill");
try {
data = {
const data = {
Name: docTitle || pdfDetails?.[0]?.Name,
Placeholders: filterPrefill,
SignedUrl: pdfUrl,
Signers: signers,
SentToOthers: true,
SignatureType: pdfDetails?.[0]?.SignatureType
SignatureType: pdfDetails?.[0]?.SignatureType,
ExpiryDate: { iso: updateExpiryDate, __type: "Date" }
};
if (updateExpiryDate) {
data["ExpiryDate"] = { iso: updateExpiryDate, __type: "Date" };
}
await axios.put(
`${localStorage.getItem(
"baseUrl"
@@ -1071,6 +1075,11 @@ function PlaceHolderSign() {
setIsUiLoading(false);
setSignerPos([]);
setIsSendAlert({ mssg: "confirm", alert: true });
if (docTitle) {
const updatedPdfDetails = [...pdfDetails];
updatedPdfDetails[0].Name = docTitle;
setPdfDetails(updatedPdfDetails);
}
} catch (e) {
console.log("error", e);
alert(t("something-went-wrong-mssg"));
@@ -1844,6 +1853,10 @@ function PlaceHolderSign() {
setPdfBase64Url(urlDetails.base64);
};
const handleSendDoc = () => {
if (docTitle?.length > maxTitleLength) {
alert(t("title-length-alert"));
return;
}
setIsAttchSignerModal(false);
setCheckTourStatus(true);
alertSendEmail();
@@ -1906,6 +1919,10 @@ function PlaceHolderSign() {
return isAllSigner;
};
const handleCloseAttachSigner = () => {
if (docTitle?.length > maxTitleLength) {
alert(t("title-length-alert"));
return;
}
setIsAttchSignerModal(false);
};
return (
@@ -2180,24 +2197,45 @@ function PlaceHolderSign() {
</ModalUi>
<ModalUi
isOpen={isAttchSignerModal}
title={t("add-signer")}
title={t("create-document")}
handleClose={() => handleCloseAttachSigner()}
>
<div className="h-[100%] p-[20px]">
<div className="h-[100%] px-[20px] py-[10px]">
<div>
<label
htmlFor="doctitle"
className="block text-xs font-semibold"
>
Title
</label>
<input
type="text"
name="doctitle"
value={docTitle}
onChange={(e) => setDocTitle(e.target.value)}
required
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-[11px] py-[18px]"
/>
</div>
{pdfDetails[0].Placeholders?.some(
(x) => !x.signerObjId
) && (
<>
{/* grid grid-cols-1 md:grid-cols-2 */}
<div className="min-h-max max-h-[250px] overflow-y-auto">
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400 mt-3 md:mx-3 mb-4 bg-base-200 text-base-content flex flex-col gap-2 relative">
<div className="py-2 text-base-content flex flex-col gap-2 relative">
{forms?.map((field, id) => {
return (
<div
className="flex flex-col"
key={field?.value}
>
<label>{field?.role}</label>
<label className="block text-xs font-semibold">
{field?.role}
</label>
<div className="flex justify-between items-center gap-1">
<div className="flex-1">
<AsyncSelect
@@ -2240,7 +2278,7 @@ function PlaceHolderSign() {
onClick={(e) =>
handleCreateNew(e, field.value)
}
className="op-btn op-btn-accent op-btn-outline op-btn-sm "
className="op-btn op-btn-accent op-btn-outline op-btn-sm"
>
<i className="fa-light fa-plus"></i>
</button>
@@ -2250,8 +2288,8 @@ function PlaceHolderSign() {
})}
</div>
</div>
<div className="w-full h-[1px] bg-[#9f9f9f] my-[15px]"></div>
<div className="flex mx-4 mb-4 gap-3">
<div className="w-full h-[0.5px] bg-[#9f9f9f] mt-[8px] mb-[15px]"></div>
<div className="flex mx-2 mb-2 gap-3">
<button
disabled={handleDisable()}
onClick={() => handleSendDoc()}
+310 -253
View File
@@ -344,7 +344,7 @@ const Preferences = () => {
<Loader />
</div>
)}
<h1 className="ml-4 mt-3 mb-2 font-semibold">
<h1 className="ml-4 mt-3 text-lg mb-2 font-semibold text-base-content">
{appName} {t("Preferences")}{" "}
<span>
<Tooltip message={`${appName} ${t("Preferences")}`} />
@@ -362,7 +362,9 @@ const Preferences = () => {
role="tab"
className={`${
activeTab === ind ? "op-tab-active" : ""
} op-tab text-xs md:text-base pb-2 md:pb-0 }`}
} op-tab text-xs md:text-base pb-2 md:pb-0 transition-all `}
aria-selected={activeTab === ind}
aria-controls={`panel-${tabData.title}`}
>
<i className={tabData.icon}></i>
<span className="ml-1 md:ml-2">{tabData.title}</span>
@@ -370,275 +372,330 @@ const Preferences = () => {
))}
</div>
</div>
<div className="flex justify-center md:justify-start mt-3 md:mt-4 break-all">
<div
id="panel-general"
className="px-6 pt-4 pb-6"
aria-labelledby="tab-general"
role="tabpanel"
>
{activeTab === 0 ? (
<div className="ml-4 mt-1 mb-2 flex flex-col">
<div className="mb-[0.75rem]">
<label
className="mb-[0.7rem] text-[12px]"
htmlFor="signaturetype"
>
<span className="font-medium text-[14px]">
<div className="grid grid-cols-1 md:grid-cols-12 md:gap-x-8">
{/* Left Column - Signature Settings */}
<div className="md:col-span-5 flex flex-col">
{/* Signature Types Section */}
<div className="mb-6">
<label
className="text-[14px] mb-[0.7rem] font-medium"
htmlFor="signaturetype"
>
{t("allowed-signature-types")}
</span>
<a data-tooltip-id="signtypes-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<ReactTooltip
id="signtypes-tooltip"
className="z-[999]"
>
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">
{t("allowed-signature-types")}
</p>
<p>{t("allowed-signature-types-help.p1")}</p>
<div className="p-[5px] ml-2">
<ol className="list-disc">
<li>
<span className="font-bold">Draw: </span>
<span>
{t("allowed-signature-types-help.l1")}
</span>
</li>
<li>
<span className="font-bold">Type: </span>
<span>
{t("allowed-signature-types-help.l2")}
</span>
</li>
<li>
<span className="font-bold">Upload: </span>
<span>
{t("allowed-signature-types-help.l3")}
</span>
</li>
<li>
<span className="font-bold">Default: </span>
<span>
{t("allowed-signature-types-help.l4")}
</span>
</li>
</ol>
</div>
</div>
</ReactTooltip>
</label>
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
{signatureType.map((type, i) => (
<div
key={i}
className="flex flex-row gap-[5px] items-center"
<a
data-tooltip-id="signtypes-tooltip"
className="ml-1"
>
<input
className="mr-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
name="signaturetype"
onChange={() => handleCheckboxChange(i)}
checked={type.enabled}
/>
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<ReactTooltip
id="signtypes-tooltip"
className="z-[999]"
>
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">
{t("allowed-signature-types")}
</p>
<p>{t("allowed-signature-types-help.p1")}</p>
<div className="p-[5px] ml-2">
<ol className="list-disc">
<li>
<span className="font-bold">Draw: </span>
<span>
{t("allowed-signature-types-help.l1")}
</span>
</li>
<li>
<span className="font-bold">Type: </span>
<span>
{t("allowed-signature-types-help.l2")}
</span>
</li>
<li>
<span className="font-bold">Upload: </span>
<span>
{t("allowed-signature-types-help.l3")}
</span>
</li>
<li>
<span className="font-bold">Default: </span>
<span>
{t("allowed-signature-types-help.l4")}
</span>
</li>
</ol>
</div>
</div>
</ReactTooltip>
</label>
<div className="flex flex-col md:flex-row gap-3 mb-2">
{signatureType.map((type, i) => (
<div
className="text-[13px] font-medium hover:underline underline-offset-2 cursor-default capitalize"
title={`Enabling this allow signers to ${type.name} signature`}
key={i}
className="flex flex-row gap-2 items-center"
>
{type?.name === "typed" ? "type" : type?.name}
<input
className="op-checkbox op-checkbox-xs"
type="checkbox"
id={`signature-type-${type.name}`}
name="signaturetype"
onChange={() => handleCheckboxChange(i)}
checked={type.enabled}
/>
<label
htmlFor={`signature-type-${type.name}`}
className="text-sm font-medium text-base-content hover:underline underline-offset-2 cursor-pointer capitalize mb-0"
title={`Enabling this allows signers to ${type.name} signature`}
>
{type?.name === "typed" ? "type" : type?.name}
</label>
</div>
</div>
))}
))}
</div>
</div>
</div>
<div className="mb-[0.75rem]">
<label className="mb-[0.7rem] text-[12px]">
<span className="text-[14px] font-medium">
{/* Notify on Signatures Section */}
<div className="mb-6">
<label className="text-[14px] mb-[0.7rem] font-medium">
{t("notify-on-signatures")}
</span>
<a data-tooltip-id="nos-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<ReactTooltip id="nos-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">
{t("notify-on-signatures")}
</p>
<p>{t("notify-on-signatures-help.p1")}</p>
<p>{t("notify-on-signatures-help.note")}</p>
<a data-tooltip-id="nos-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<ReactTooltip id="nos-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">
{t("notify-on-signatures")}
</p>
<p>{t("notify-on-signatures-help.p1")}</p>
<p>{t("notify-on-signatures-help.note")}</p>
</div>
</ReactTooltip>
</label>
<div className="flex flex-row gap-6">
<div className={"flex items-center gap-2"}>
<input
id="notify-yes"
className="op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(true)}
checked={isNotifyOnSignatures === true}
/>
<label
htmlFor="notify-yes"
className="text-sm text-base-content cursor-pointer mb-0"
>
{t("yes")}
</label>
</div>
</ReactTooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className={"flex items-center gap-2 ml-2 mb-1"}>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(true)}
checked={isNotifyOnSignatures === true}
/>
<div className="text-[13px] cursor-default capitalize">
{t("yes")}
</div>
</div>
<div className={"flex items-center gap-2 ml-2 mb-1"}>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(false)}
checked={isNotifyOnSignatures === false}
/>
<div className="text-[13px] cursor-default capitalize">
{t("no")}
<div className={"flex items-center gap-2"}>
<input
id="notify-no"
className="op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(false)}
checked={isNotifyOnSignatures === false}
/>
<label
htmlFor="notify-no"
className="text-sm text-base-content cursor-pointer mb-0"
>
{t("no")}
</label>
</div>
</div>
</div>
</div>
<div className="mb-[0.75rem]">
<TimezoneSelector
timezone={timezone}
setTimezone={setTimezone}
/>
</div>
<div className="mb-[0.75rem]">
<DateFormatSelector
timezone={timezone}
dateFormat={dateFormat}
is12HourTime={is12HourTime}
setIs12HourTime={setIs12HourTime}
setDateFormat={setDateFormat}
/>
</div>
<div className="mb-[0.75rem] text-[12px]">
<label className="block mb-[0.7rem]">
<span className="text-[14px] font-medium">
{/* Send in Order Section */}
<div className="mb-6">
<label className="text-[14px] mb-[0.7rem] font-medium">
{t("send-in-order")}
</span>
<a
data-tooltip-id="sendInOrder-tooltip"
className="ml-1"
>
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<ReactTooltip
id="sendInOrder-tooltip"
className="z-[999]"
>
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("send-in-order")}</p>
<p>{t("send-in-order-help.p1")}</p>
<div className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">
{t("yes")}:{" "}
</span>
<span>{t("send-in-order-help.p2")}</span>
</li>
<li>
<span className="font-bold">{t("no")}: </span>
<span>{t("send-in-order-help.p3")}</span>
</li>
</ol>
<a
data-tooltip-id="sendInOrder-tooltip"
className="ml-1"
>
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<ReactTooltip
id="sendInOrder-tooltip"
className="z-[999]"
>
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("send-in-order")}</p>
<p>{t("send-in-order-help.p1")}</p>
<div className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">
{t("yes")}:{" "}
</span>
<span>{t("send-in-order-help.p2")}</span>
</li>
<li>
<span className="font-bold">
{t("no")}:{" "}
</span>
<span>{t("send-in-order-help.p3")}</span>
</li>
</ol>
</div>
<p>{t("send-in-order-help.p4")}</p>
</div>
<p>{t("send-in-order-help.p4")}</p>
</ReactTooltip>
</label>
<div className="flex flex-row gap-6">
<div className="flex items-center gap-2">
<input
id="order-yes"
type="radio"
value={true}
className="op-radio op-radio-xs"
name="SendinOrder"
checked={sendinOrder}
onChange={handleSendinOrderInput}
/>
<label
htmlFor="order-yes"
className="text-sm text-base-content cursor-pointer mb-0"
>
{t("yes")}
</label>
</div>
<div className="flex items-center gap-2">
<input
id="order-no"
type="radio"
value={false}
name="SendinOrder"
className="op-radio op-radio-xs"
checked={!sendinOrder}
onChange={handleSendinOrderInput}
/>
<label
htmlFor="order-no"
className="text-sm text-base-content cursor-pointer mb-0"
>
{t("no")}
</label>
</div>
</ReactTooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={true}
className="op-radio op-radio-xs"
name="SendinOrder"
checked={sendinOrder}
onChange={handleSendinOrderInput}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={false}
name="SendinOrder"
className="op-radio op-radio-xs"
checked={!sendinOrder}
onChange={handleSendinOrderInput}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="mb-[0.75rem] text-[12px]">
<label className="block mb-[0.7rem]">
<span className="text-[14px] font-medium">
{/* Enable Tour Section */}
<div className="mb-6">
<label className="text-[14px] mb-[0.7rem] font-medium">
{t("enable-tour")}
</span>
<a
data-tooltip-id="istourenabled-tooltip"
className="ml-1"
>
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<ReactTooltip
id="istourenabled-tooltip"
className="z-[999]"
>
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("enable-tour")}</p>
<div className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">
{t("yes")}:{" "}
</span>
<span>{t("istourenabled-help.p1")}</span>
</li>
<li>
<span className="font-bold">{t("no")}: </span>
<span>{t("istourenabled-help.p2")}</span>
</li>
</ol>
<a
data-tooltip-id="istourenabled-tooltip"
className="ml-1"
>
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<ReactTooltip
id="istourenabled-tooltip"
className="z-[999]"
>
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("enable-tour")}</p>
<div className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">
{t("yes")}:{" "}
</span>
<span>{t("istourenabled-help.p1")}</span>
</li>
<li>
<span className="font-bold">
{t("no")}:{" "}
</span>
<span>{t("istourenabled-help.p2")}</span>
</li>
</ol>
</div>
<p>
{t("istourenabled-help.p3", {
appName: appName
})}
</p>
</div>
<p>
{t("istourenabled-help.p3", {
appName: appName
})}
</p>
</ReactTooltip>
</label>
<div className="flex flex-row gap-6">
<div className="flex items-center gap-2">
<input
id="tour-yes"
type="radio"
value={true}
className="op-radio op-radio-xs"
name="IsTourEnabled"
checked={isTourEnabled}
onChange={handleTourInput}
/>
<label
htmlFor="tour-yes"
className="text-sm text-base-content cursor-pointer mb-0"
>
{t("yes")}
</label>
</div>
<div className="flex items-center gap-2">
<input
id="tour-no"
type="radio"
value={false}
name="IsTourEnabled"
className="op-radio op-radio-xs"
checked={!isTourEnabled}
onChange={handleTourInput}
/>
<label
htmlFor="tour-no"
className="text-sm text-base-content cursor-pointer mb-0"
>
{t("no")}
</label>
</div>
</ReactTooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={true}
className="op-radio op-radio-xs"
name="IsTourEnabled"
checked={isTourEnabled}
onChange={handleTourInput}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={false}
name="IsTourEnabled"
className="op-radio op-radio-xs"
checked={!isTourEnabled}
onChange={handleTourInput}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="mb-[0.75rem]">
{/* Right Column - Timezone & Date Settings */}
<div className="md:col-span-7 flex flex-col">
<div className="mb-6">
<TimezoneSelector
timezone={timezone}
setTimezone={setTimezone}
/>
</div>
<div className="mb-6">
<DateFormatSelector
timezone={timezone}
dateFormat={dateFormat}
is12HourTime={is12HourTime}
setIs12HourTime={setIs12HourTime}
setDateFormat={setDateFormat}
/>
</div>
</div>
{/* Save Button - Full Width */}
<div className="md:col-span-12 flex justify-start mt-2">
<button
className="op-btn op-btn-primary w-[110px]"
onClick={handleSave}
@@ -648,7 +705,7 @@ const Preferences = () => {
</div>
</div>
) : (
<div className="flex flex-col mx-4 mb-4">
<div className="flex flex-col mb-4">
<div className="flex flex-col">
<h1 className="text-[14px] mb-[0.7rem] font-medium">
{t("request-email")}
@@ -660,7 +717,7 @@ const Preferences = () => {
>
<div className="text-lg font-normal">
<label className="text-sm">
{t("subject")}
{t("subject")}{" "}
<Tooltip
id={"request-sub-tooltip"}
message={`${t(
@@ -682,7 +739,7 @@ const Preferences = () => {
</div>
<div className="text-lg font-normal py-2">
<label className="text-sm mt-3">
{t("body")}
{t("body")}{" "}
<Tooltip
id={"request-body-tooltip"}
message={`${t(
@@ -730,7 +787,7 @@ const Preferences = () => {
>
<div className="text-lg font-normal">
<label className="text-sm">
{t("subject")}
{t("subject")}{" "}
<Tooltip
id={"complete-sub-tooltip"}
message={`${t(
@@ -752,7 +809,7 @@ const Preferences = () => {
</div>
<div className="text-lg font-normal py-2">
<label className="text-sm mt-3">
{t("body")}
{t("body")}{" "}
<Tooltip
id={"complete-body-tooltip"}
message={`${t(
+19 -19
View File
@@ -733,20 +733,18 @@ const TemplatePlaceholder = () => {
let isLabel = false;
let unfilledTextWidgetId = "";
//condition is used to check text widget data is empty or have response
if (getPrefill) {
if (prefillPlaceholder) {
prefillPlaceholder.map((data) => {
if (!isLabel) {
const unfilledTextWidgets = data.pos.find(
(position) => !position.options.response
);
if (unfilledTextWidgets) {
isLabel = true;
unfilledTextWidgetId = unfilledTextWidgets.key;
}
if (getPrefill && prefillPlaceholder) {
prefillPlaceholder.map((data) => {
if (!isLabel) {
const unfilledTextWidgets = data.pos.find(
(position) => !position.options.response
);
if (unfilledTextWidgets) {
isLabel = true;
unfilledTextWidgetId = unfilledTextWidgets.key;
}
});
}
}
});
}
if (getPrefill && isLabel) {
setUnSignedWidgetId(unfilledTextWidgetId);
@@ -1250,6 +1248,7 @@ const TemplatePlaceholder = () => {
? { RedirectUrl: updateTemplate?.[0]?.RedirectUrl }
: {};
const data = {
...(updateTemplate?.[0]?.URL ? { URL: updateTemplate?.[0]?.URL } : {}),
Name: updateTemplate?.[0]?.Name || "",
Note: updateTemplate?.[0]?.Note || "",
Description: updateTemplate?.[0]?.Description || "",
@@ -1964,16 +1963,17 @@ const TemplatePlaceholder = () => {
/>
)}
</div>
<ModalUi
title={t("edit-template")}
isOpen={isEditTemplate}
handleClose={handleEditTemplateModal}
>
{isEditTemplate && (
<EditTemplate
title={t("edit-template")}
handleClose={handleEditTemplateModal}
pdfbase64={pdfBase64Url}
template={pdfDetails?.[0]}
onSuccess={handleEditTemplateForm}
setPdfArrayBuffer={setPdfArrayBuffer}
setPdfBase64Url={setPdfBase64Url}
/>
</ModalUi>
)}
<WidgetNameModal
signatureType={signatureType}
widgetName={widgetName}
+6 -12
View File
@@ -1,7 +1,4 @@
import React, {
useState,
useEffect,
} from "react";
import React, { useState, useEffect } from "react";
import { Navigate, useNavigate } from "react-router";
import Parse from "parse";
import { SaveFileSize } from "../constant/saveFileSize";
@@ -10,10 +7,7 @@ import Title from "../components/Title";
import sanitizeFileName from "../primitives/sanitizeFileName";
import axios from "axios";
import Tooltip from "../primitives/Tooltip";
import {
getSecureUrl,
handleSendOTP,
} from "../constant/Utils";
import { getSecureUrl, handleSendOTP } from "../constant/Utils";
import ModalUi from "../primitives/ModalUi";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
@@ -121,13 +115,13 @@ function UserProfile() {
const updateExtUser = async (obj) => {
try {
const extData = JSON.parse(localStorage.getItem("Extand_Class"));
const ExtUserId = extData[0].objectId;
const ExtUserId = extData?.[0]?.objectId;
const body = {
Phone: obj?.Phone || "",
Name: obj.Name,
JobTitle: jobTitle,
Company: company,
Language: obj?.language || "",
Language: obj?.language || ""
};
await axios.put(
@@ -235,7 +229,7 @@ function UserProfile() {
const handleResend = async (e) => {
e.preventDefault();
setOtpLoader(true);
await handleSendOTP();
await handleSendOTP(Parse.User.current().getEmail());
setOtpLoader(false);
alert(t("otp-sent-alert"));
};
@@ -412,7 +406,7 @@ function UserProfile() {
<button
type="button"
onClick={(e) => {
editmode ? handleSubmit(e) : setEditMode(true);
editmode ? handleSubmit(e) : setEditMode(true);
}}
className="op-btn op-btn-primary w-[100px]"
>
+100 -27
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from "react";
import pad from "../assets/images/pad.svg";
import recreatedoc from "../assets/images/recreatedoc.png";
import { Link, useLocation, useNavigate } from "react-router";
import axios from "axios";
import ModalUi from "./ModalUi";
@@ -19,7 +20,8 @@ import {
getTenantDetails,
handleSignatureType,
replaceMailVaribles,
signatureTypes
signatureTypes,
openInNewTab
} from "../constant/Utils";
import EditorToolbar, {
module1,
@@ -82,6 +84,7 @@ const ReportTable = (props) => {
const [contact, setContact] = useState({ Name: "", Email: "", Phone: "" });
const [isSuccess, setIsSuccess] = useState({});
const [templateId, setTemplateId] = useState("");
const isTemplateReport = props.ReportName === "Templates";
const recordsPerPage = 5;
const startIndex = (currentPage - 1) * props.docPerPage;
const { isMoreDocs, setIsNextRecord } = props;
@@ -152,7 +155,7 @@ const ReportTable = (props) => {
// `fetchTeamList` is used to fetch team list for share with functionality
const fetchTeamList = async () => {
if (props.ReportName === "Templates") {
if (isTemplateReport) {
try {
const extUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
if (extUser?.OrganizationId?.objectId) {
@@ -225,7 +228,7 @@ const ReportTable = (props) => {
// `handleURL` is used to open microapp
const handleURL = async (item, act) => {
if (props.ReportName === "Templates") {
if (isTemplateReport) {
if (act.hoverLabel === "Edit") {
navigate(`/${act.redirectUrl}/${item.objectId}`);
} else {
@@ -298,6 +301,15 @@ const ReportTable = (props) => {
const RedirectUrl = Doc?.RedirectUrl
? { RedirectUrl: Doc?.RedirectUrl }
: {};
const TemplateId = Doc?.objectId
? {
TemplateId: {
__type: "Pointer",
className: "contracts_Template",
objectId: Doc?.objectId
}
}
: {};
let placeholdersArr = [];
if (Doc.Placeholders?.length > 0) {
placeholdersArr = Doc.Placeholders;
@@ -329,7 +341,8 @@ const ReportTable = (props) => {
...SignatureType,
...NotifyOnSignatures,
...Bcc,
...RedirectUrl
...RedirectUrl,
...TemplateId
};
try {
const res = await axios.post(
@@ -401,6 +414,8 @@ const ReportTable = (props) => {
setIsModal({ [`edit_${item.objectId}`]: true });
} else if (act.action === "saveastemplate") {
setIsModal({ [`saveastemplate_${item.objectId}`]: true });
} else if (act.action === "recreatedocument") {
setIsModal({ [`recreatedocument_${item.objectId}`]: true });
} else if (act.action) {
setIsModal({ [`extendexpiry_${item.objectId}`]: true });
}
@@ -656,7 +671,6 @@ const ReportTable = (props) => {
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>`
};
const res = replaceMailVaribles(subject, "", variables);
setMail((prev) => ({ ...prev, subject: res.subject }));
};
// `handlebodyChange` is used to add or change body of resend mail
@@ -1235,10 +1249,9 @@ const ReportTable = (props) => {
const handleRenameDoc = async (item) => {
setActLoader({ [item.objectId]: true });
setIsModal({});
const className =
props.ReportName === "Templates"
? "contracts_Template"
: "contracts_Document";
const className = isTemplateReport
? "contracts_Template"
: "contracts_Document";
try {
const query = new Parse.Query(className);
const docObj = await query.get(item.objectId);
@@ -1336,6 +1349,31 @@ const ReportTable = (props) => {
showAlert("danger", t("something-went-wrong-mssg"));
}
};
const handleResendClose = () => {
setIsResendMail({});
setIsNextStep({});
setUserDetails({});
};
const handleRecreateDoc = async (item) => {
setActLoader({ [item.objectId]: true });
try {
const res = await Parse.Cloud.run("recreatedoc", {
docId: item.objectId
});
if (res) {
openInNewTab(`/placeHolderSign/${res.objectId}`, "_self");
}
} catch (err) {
handleCloseModal();
showAlert("danger", err.message);
// showAlert("danger", t("something-went-wrong-mssg"));
console.log("Err while create duplicate template", err);
} finally {
setActLoader({});
}
};
return (
<div className="relative">
{Object.keys(actLoader)?.length > 0 && (
@@ -1347,7 +1385,7 @@ const ReportTable = (props) => {
{alertMsg.message && (
<Alert type={alertMsg.type}>{alertMsg.message}</Alert>
)}
{props.tourData && props.ReportName === "Templates" && (
{props.tourData && isTemplateReport && (
<>
<Tour
onRequestClose={closeTour}
@@ -1384,7 +1422,7 @@ const ReportTable = (props) => {
<i className="fa-light fa-square-plus text-accent text-[30px] md:text-[35px]"></i>
</div>
)}
{props.ReportName === "Templates" && (
{isTemplateReport && (
<i
data-tut="reactourFirst"
onClick={() => navigate("/form/template")}
@@ -1583,7 +1621,9 @@ const ReportTable = (props) => {
</th>
)}
<td className="p-2 min-w-56 max-w-56">
<div className="font-semibold">{item?.Name}</div>
<div className="font-semibold break-words">
{item?.Name}
</div>
{item?.ExpiryDate?.iso && (
<div className="text-gray-500">
Expires {formatDate(item?.ExpiryDate?.iso)}
@@ -1653,7 +1693,7 @@ const ReportTable = (props) => {
<div className="text-base-content min-w-max flex flex-row gap-x-2 gap-y-1 justify-start items-center">
{props.actions?.length > 0 &&
props.actions.map((act, index) =>
props.ReportName === "Templates" ? (
isTemplateReport ? (
<React.Fragment key={index}>
{(item.ExtUserPtr?.objectId ===
extClass?.[0]?.objectId ||
@@ -1808,6 +1848,45 @@ const ReportTable = (props) => {
)
)}
</div>
{isModal["recreatedocument_" + item.objectId] && (
<ModalUi isOpen handleClose={handleCloseModal}>
{actLoader[item.objectId] && (
<div className="absolute h-full w-full flex justify-center items-center rounded-box bg-black/30">
<Loader />
</div>
)}
<h3 className="text-base-content font-bold text-lg pt-[15px] px-[20px]">
{t("fix-&-resend-document")}
</h3>
<div className="p-[15px] md:p-[20px]">
<div className="text-lg font-normal text-center">
<img
src={recreatedoc}
alt="recreate-doc"
className="mx-auto w-[200px] h-auto"
/>
<p className="text-sm md:text-base md:px-2 mt-2">
{t("do-you-want-recreate-document?")}
</p>
</div>
<hr className="bg-[#ccc] mt-2.5" />
<div className="flex items-center justify-center mt-[14px] md:mt-[16px] gap-2 text-white">
<button
onClick={() => handleRecreateDoc(item)}
className="op-btn op-btn-primary focus:outline-none text-sm relative px-4"
>
{t("start-editing")}
</button>
<button
onClick={handleCloseModal}
className="op-btn op-btn-secondary focus:outline-none text-sm relative px-8"
>
{t("cancel")}
</button>
</div>
</div>
</ModalUi>
)}
{isModal["saveastemplate_" + item.objectId] && (
<ModalUi
isOpen
@@ -1953,14 +2032,12 @@ const ReportTable = (props) => {
{isViewShare[item.objectId] && (
<ModalUi
isOpen
showHeader={
props.ReportName === "Templates" && true
}
showHeader={isTemplateReport}
title={t("signers")}
reduceWidth={"md:max-w-[450px]"}
handleClose={() => setIsViewShare({})}
>
{props.ReportName !== "Templates" && (
{!isTemplateReport && (
<div
className="op-btn op-btn-sm op-btn-circle op-btn-ghost text-base-content absolute right-2 top-1 z-40"
onClick={() => setIsViewShare({})}
@@ -1971,13 +2048,13 @@ const ReportTable = (props) => {
<table className="op-table w-full overflow-auto">
<thead className="h-[38px] sticky top-0 text-base-content text-sm pt-[15px] px-[20px]">
<tr>
{props.ReportName === "Templates" && (
{isTemplateReport && (
<th className="p-2 pl-3 w-[30%]">
{t("roles")}
</th>
)}
<th className="pl-3 py-2">
{props.ReportName === "Templates"
{isTemplateReport
? t("email")
: t("signers")}
</th>
@@ -1991,7 +2068,7 @@ const ReportTable = (props) => {
key={i}
className="text-sm font-medium"
>
{props.ReportName === "Templates" && (
{isTemplateReport && (
<td className="text-[12px] p-2 pl-3 w-[30%]">
{x.Role && x.Role}
</td>
@@ -2147,13 +2224,9 @@ const ReportTable = (props) => {
<ModalUi
isOpen
title={t("resend-mail")}
handleClose={() => {
setIsResendMail({});
setIsNextStep({});
setUserDetails({});
}}
handleClose={handleResendClose}
>
<div className=" overflow-y-auto max-h-[340px] md:max-h-[400px]">
<div className="overflow-y-auto max-h-[340px] md:max-h-[400px]">
{item?.Placeholders?.map((user) => (
<React.Fragment key={user.Id}>
{isNextStep[user.Id] && (
@@ -2255,7 +2328,7 @@ const ReportTable = (props) => {
isOpen={isModal["rename_" + item.objectId]}
handleClose={handleCloseModal}
>
<div className=" flex flex-col px-4 pb-3 pt-2 ">
<div className="flex flex-col px-4 pb-3 pt-2">
<div className="flex flex-col gap-2">
<input
maxLength={200}
+1 -1
View File
@@ -2,7 +2,7 @@
FROM node:22.14.0
# install java
RUN wget https://download.oracle.com/java/23/latest/jdk-23_linux-x64_bin.deb \
RUN wget https://downloads.opensign.me/jdk-23_linux-x64_bin.deb \
&& dpkg -i jdk-23_linux-x64_bin.deb
# Set the working directory inside the container
Binary file not shown.
+42
View File
@@ -2,11 +2,16 @@ import dotenv from 'dotenv';
import { format, toZonedTime } from 'date-fns-tz';
import { getSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
import { PDFDocument } from 'pdf-lib';
import crypto from 'node:crypto';
import axios from 'axios';
dotenv.config();
export const cloudServerUrl = 'http://localhost:8080/app';
export const appName = 'OpenSign™';
export const MAX_NAME_LENGTH = 250;
export const MAX_NOTE_LENGTH = 200;
export const MAX_DESCRIPTION_LENGTH = 500;
export const color = [
'#93a3db',
'#e6c3db',
@@ -145,6 +150,7 @@ export const useLocal = process.env.USE_LOCAL ? process.env.USE_LOCAL.toLowerCas
export const smtpsecure = process.env.SMTP_PORT && process.env.SMTP_PORT !== '465' ? false : true;
export const smtpenable =
process.env.SMTP_ENABLE && process.env.SMTP_ENABLE.toLowerCase() === 'true' ? true : false;
export const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// `generateId` is used to unique Id for fileAdapter
export function generateId(length) {
@@ -294,3 +300,39 @@ export function formatDateTime(date, dateFormat, timeZone, is12Hour) {
? format(zonedDate, `${selectFormat(dateFormat)}, ${timeFormat} 'GMT' XXX`, { timeZone })
: formatTimeInTimezone(date, timeZone);
}
// Utility: Convert base64 to buffer
export const base64ToBuffer = base64 => Buffer.from(base64, 'base64');
// Utility: Generate SHA-256 hash from PDF page metadata
const getPdfMetadataHash = async pdfBytes => {
const pdfDoc = await PDFDocument.load(pdfBytes);
const metaString = pdfDoc
.getPages()
.map((page, index) => {
const { width, height } = page.getSize();
return `${index + 1}:${Math.round(width)}x${Math.round(height)}`;
})
.join('|');
return crypto.createHash('sha256').update(metaString).digest('hex');
};
// Utility: Validate if uploaded file matches original template PDF
export const handleReplaceFileValidation = async (baseFileUrl, newFileBase64) => {
try {
const { data } = await axios.get(baseFileUrl, { responseType: 'arraybuffer' });
const basePdfBytes = Buffer.from(data);
const uploadedPdfBytes = base64ToBuffer(newFileBase64);
const baseHash = await getPdfMetadataHash(basePdfBytes);
const uploadedHash = await getPdfMetadataHash(uploadedPdfBytes);
if (baseHash === uploadedHash) {
return { base64: newFileBase64 };
}
return { error: 'PDFs do NOT match based on page number, width, and height' };
} catch (err) {
console.error('Validation Error:', err.message);
return { error: err.message };
}
};
+5 -1
View File
@@ -11,7 +11,11 @@ export const SSOAuth = {
Authorization: `Bearer ${authData.access_token}`,
},
});
if (response.data && response.data.id && response.data.email === authData.id) {
if (
response.data &&
response.data.id &&
response.data.email?.toLowerCase()?.replace(/\s/g, '') === authData.id
) {
return;
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'SSO auth is invalid for this user.');
+4
View File
@@ -51,6 +51,8 @@ import editContact from './parsefunction/editContact.js';
import forwardDoc from './parsefunction/ForwardDoc.js';
import saveAsTemplate from './parsefunction/saveAsTemplate.js';
import updateTenant from './parsefunction/updateTenant.js';
import recreateDocument from './parsefunction/recreateDocument.js';
import addUser from './parsefunction/addUser.js';
// This afterSave function triggers after an object is added or updated in the specified class, allowing for post-processing logic.
Parse.Cloud.afterSave('contracts_Document', DocumentAftersave);
@@ -112,3 +114,5 @@ Parse.Cloud.define('editcontact', editContact);
Parse.Cloud.define('forwarddoc', forwardDoc);
Parse.Cloud.define('saveastemplate', saveAsTemplate);
Parse.Cloud.define('updatetenant', updateTenant);
Parse.Cloud.define('recreatedoc', recreateDocument);
Parse.Cloud.define('adduser', addUser);
@@ -1,6 +1,24 @@
import { MAX_DESCRIPTION_LENGTH, MAX_NAME_LENGTH, MAX_NOTE_LENGTH } from '../../Utils.js';
async function DocumentBeforesave(request) {
if (!request.original) {
const TimeToCompleteDays = request.object.get('TimeToCompleteDays') || 15;
const validations = [
{ field: 'Name', max: MAX_NAME_LENGTH },
{ field: 'Note', max: MAX_NOTE_LENGTH },
{ field: 'Description', max: MAX_DESCRIPTION_LENGTH },
];
for (const { field, max } of validations) {
const value = request?.object?.get(field);
if (value && value.length > max) {
throw new Parse.Error(
Parse.Error.VALIDATION_ERROR,
`The "${field}" field must be at most ${max} characters long.`
);
}
}
const TimeToCompleteDays = request?.object?.get('TimeToCompleteDays') || 15;
const RemindOnceInEvery = request?.object?.get('RemindOnceInEvery') || 5;
const AutoReminder = request?.object?.get('AutomaticReminders') || false;
const reminderCount = TimeToCompleteDays / RemindOnceInEvery;
@@ -36,7 +54,6 @@ async function DocumentBeforesave(request) {
}
if (document?.get('Signers') && document.get('Signers').length > 0) {
document.set('DocSentAt', new Date());
document.save(null, { useMasterKey: true });
}
}
} catch (err) {
@@ -1,112 +1,82 @@
import axios from 'axios';
import {
cloudServerUrl,
} from '../../Utils.js';
import { cloudServerUrl } from '../../Utils.js';
export default async function GetTemplate(request) {
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const templateId = request.params.templateId;
const ispublic = request.params.ispublic;
const sessiontoken = request.headers?.sessiontoken;
try {
if (!ispublic) {
let userEmail;
if (sessiontoken) {
const userRes = await axios.get(serverUrl + '/users/me', {
headers: {
'X-Parse-Application-Id': process.env.APP_ID,
'X-Parse-Session-Token': sessiontoken,
},
});
userEmail = userRes.data && userRes.data.email;
}
if (templateId && userEmail) {
try {
let template = new Parse.Query('contracts_Template');
template.equalTo('objectId', templateId);
template.include('ExtUserPtr');
template.include('Signers');
template.include('CreatedBy');
template.include('ExtUserPtr.TenantId');
template.include('Bcc');
const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('Email', userEmail);
extUserQuery.include('TeamIds');
const extUser = await extUserQuery.first({ useMasterKey: true });
if (extUser) {
const _extUser = JSON.parse(JSON.stringify(extUser));
if (_extUser?.TeamIds && _extUser.TeamIds?.length > 0) {
let teamsArr = [];
_extUser?.TeamIds?.forEach(x => (teamsArr = [...teamsArr, ...x.Ancestors]));
// Create the first query
const sharedWithQuery = new Parse.Query('contracts_Template');
sharedWithQuery.containedIn('SharedWith', teamsArr);
// Create the second query
const createdByQuery = new Parse.Query('contracts_Template');
createdByQuery.equalTo('ExtUserPtr', {
__type: 'Pointer',
className: 'contracts_Users',
objectId: extUser.id,
});
template = Parse.Query.or(sharedWithQuery, createdByQuery);
template.equalTo('objectId', templateId);
template.include('ExtUserPtr');
template.include('Signers');
template.include('CreatedBy');
template.include('ExtUserPtr.TenantId');
template.include('Placeholders.signerPtr');
template.include('Bcc');
}
}
const res = await template.first({ useMasterKey: true });
if (res) {
const templateRes = JSON.parse(JSON.stringify(res));
delete templateRes?.ExtUserPtr?.TenantId?.FileAdapters;
delete templateRes?.ExtUserPtr?.TenantId?.PfxFile;
return templateRes;
} else {
return { error: "You don't have access of this document!" };
}
} catch (err) {
console.log('err', err);
return err;
}
} else {
return { error: "You don't have access of this document!" };
}
} else if (templateId && ispublic) {
let userEmail;
if (sessiontoken) {
const userRes = await axios.get(serverUrl + '/users/me', {
headers: {
'X-Parse-Application-Id': process.env.APP_ID,
'X-Parse-Session-Token': sessiontoken,
},
});
userEmail = userRes.data && userRes.data.email;
}
if (templateId && userEmail) {
try {
const template = new Parse.Query('contracts_Template');
let template = new Parse.Query('contracts_Template');
template.equalTo('objectId', templateId);
template.include('ExtUserPtr');
template.include('Signers');
template.include('CreatedBy');
template.include('ExtUserPtr.TenantId');
template.include('Bcc');
const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('Email', userEmail);
extUserQuery.include('TeamIds');
const extUser = await extUserQuery.first({ useMasterKey: true });
if (extUser) {
const _extUser = JSON.parse(JSON.stringify(extUser));
if (_extUser?.TeamIds && _extUser.TeamIds?.length > 0) {
let teamsArr = [];
_extUser?.TeamIds?.forEach(x => (teamsArr = [...teamsArr, ...x.Ancestors]));
// Create the first query
const sharedWithQuery = new Parse.Query('contracts_Template');
sharedWithQuery.containedIn('SharedWith', teamsArr);
// Create the second query
const createdByQuery = new Parse.Query('contracts_Template');
createdByQuery.equalTo('ExtUserPtr', {
__type: 'Pointer',
className: 'contracts_Users',
objectId: extUser.id,
});
template = Parse.Query.or(sharedWithQuery, createdByQuery);
template.equalTo('objectId', templateId);
template.include('ExtUserPtr');
template.include('Signers');
template.include('CreatedBy');
template.include('ExtUserPtr.TenantId');
template.include('Placeholders.signerPtr');
template.include('Bcc');
}
}
const res = await template.first({ useMasterKey: true });
// console.log("res ", res)
if (res) {
const templateRes = JSON.parse(JSON.stringify(res));
delete templateRes?.ExtUserPtr?.TenantId?.FileAdapters;
delete templateRes?.ExtUserPtr?.TenantId?.PfxFile;
return templateRes;
} else {
return { error: "You don't have access of this document!" };
return { error: "You don't have access of this template!" };
}
} catch (err) {
console.log('err', err);
return err;
}
} else {
return { error: 'Please pass required parameters!' };
return { error: "You don't have access of this template!" };
}
} catch (err) {
console.log('err', err);
if (err?.response?.data?.code === 209 || err.code == 209) {
return { error: 'Invalid session token' };
} else {
return { error: "You don't have access of this document!" };
return { error: "You don't have access of this template!" };
}
}
}
@@ -19,7 +19,6 @@ async function getDocument(docId) {
}
async function sendMailOTPv1(request) {
try {
//--for elearning app side
let code = Math.floor(1000 + Math.random() * 9000);
let email = request.params.email;
let TenantId = request.params.TenantId ? request.params.TenantId : undefined;
@@ -30,14 +29,14 @@ async function sendMailOTPv1(request) {
const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER;
try {
await Parse.Cloud.sendEmail({
from: AppName + ' <' + mailsender + '>',
sender: AppName + ' <' + mailsender + '>',
recipient: recipient,
subject: `Your ${AppName} OTP`,
text: 'This email is a test.',
text: 'otp email',
html:
`<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body><div style='background-color:#f5f5f5;padding:20px'><div style='box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;background-color:white;'><div style='background-color:red;padding:2px;font-family:system-ui; background-color:#47a3ad;'> <p style='font-size:20px;font-weight:400;color:white;padding-left:20px',>OTP Verification</p></div><div style='padding:20px'><p style='font-family:system-ui;font-size:14px'>Your OTP for ${AppName} verification is:</p><p style=' text-decoration: none; font-weight: bolder; color:blue;font-size:45px;margin:20px'>` +
`<html><head><meta http-equiv='Content-Type' content='text/html;charset=UTF-8' /></head><body><div style='background-color:#f5f5f5;padding:20px'><div style='background-color:white;'><div style='background-color:red;padding:2px;font-family:system-ui;background-color:#47a3ad;'><p style='font-size:20px;font-weight:400;color:white;padding-left:20px;'>OTP Verification</p></div><div style='padding:20px;'><p style='font-family:system-ui;font-size:14px;'>Your OTP for ${AppName} verification is:</p><p style='text-decoration:none;font-weight:bolder;color:blue;font-size:45px;margin:20px;'>` +
code +
'</p></div> </div> </div></body></html>',
'</p></div></div></div></body></html>',
});
console.log('OTP sent', code);
if (request.params?.docId) {
@@ -1,5 +1,23 @@
import { MAX_DESCRIPTION_LENGTH, MAX_NAME_LENGTH, MAX_NOTE_LENGTH } from '../../Utils.js';
async function TemplateBeforeSave(request) {
if (!request.original) {
const validations = [
{ field: 'Name', max: MAX_NAME_LENGTH },
{ field: 'Note', max: MAX_NOTE_LENGTH },
{ field: 'Description', max: MAX_DESCRIPTION_LENGTH },
];
for (const { field, max } of validations) {
const value = request.object?.get(field);
if (value && value.length > max) {
throw new Parse.Error(
Parse.Error.VALIDATION_ERROR,
`The "${field}" field must be at most ${max} characters long.`
);
}
}
const TimeToCompleteDays = request.object.get('TimeToCompleteDays') || 15;
const RemindOnceInEvery = request?.object?.get('RemindOnceInEvery') || 5;
const AutoReminder = request?.object?.get('AutomaticReminders') || false;
@@ -0,0 +1,106 @@
export default async function addUser(request) {
const { phone, name, password, organization, team, tenantId, timezone, role } = request.params;
const email = request.params?.email?.toLowerCase()?.replace(/\s/g, '');
if (!request.user) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token.');
}
const currentUser = { __type: 'Pointer', className: '_User', objectId: request.user.id };
if (name && email && password && organization && team && role && tenantId) {
try {
const extUser = new Parse.Object('contracts_Users');
extUser.set('Name', name);
if (phone) {
extUser.set('Phone', phone);
}
extUser.set('Email', email);
extUser.set('UserRole', `contracts_${role}`);
if (team) {
extUser.set('TeamIds', [
{
__type: 'Pointer',
className: 'contracts_Teams',
objectId: team,
},
]);
}
if (organization.objectId) {
extUser.set('OrganizationId', {
__type: 'Pointer',
className: 'contracts_Organizations',
objectId: organization.objectId,
});
}
if (organization.company) {
extUser.set('Company', organization.company);
}
if (tenantId) {
extUser.set('TenantId', {
__type: 'Pointer',
className: 'partners_Tenant',
objectId: tenantId,
});
}
if (timezone) {
extUser.set('Timezone', timezone);
}
try {
const _users = Parse.Object.extend('User');
const _user = new _users();
_user.set('name', name);
_user.set('username', email);
_user.set('email', email);
_user.set('password', password);
if (phone) {
_user.set('phone', phone);
}
const user = await _user.save();
if (user) {
extUser.set('CreatedBy', currentUser);
extUser.set('UserId', user);
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(request.user.id, true);
acl.setWriteAccess(request.user.id, true);
extUser.setACL(acl);
const extUserRes = await extUser.save();
const parseData = JSON.parse(JSON.stringify(extUserRes));
return parseData;
}
} catch (err) {
console.log('err ', err);
if (err.code === 202) {
const userQuery = new Parse.Query(Parse.User);
userQuery.equalTo('email', email);
const userRes = await userQuery.first({ useMasterKey: true });
userRes.setPassword(password);
await userRes.save(null, { useMasterKey: true });
extUser.set('CreatedBy', currentUser);
extUser.set('UserId', { __type: 'Pointer', className: '_User', objectId: userRes.id });
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(request.user.id, true);
acl.setWriteAccess(request.user.id, true);
extUser.setACL(acl);
const res = await extUser.save();
const parseData = JSON.parse(JSON.stringify(res));
return parseData;
} else {
throw new Parse.Error(400, err?.message || 'something went wrong');
}
}
} catch (err) {
console.log('err', err);
throw new Parse.Error(400, err?.message || 'something went wrong');
}
} else {
throw new Parse.Error(400, 'Please provide all required fields.');
}
}
@@ -182,6 +182,15 @@ async function batchQuery(userId, Documents, Ip, parseConfig, type, publicUrl) {
...(x?.RedirectUrl ? { RedirectUrl: x?.RedirectUrl } : {}),
...(mailBody ? { RequestBody: mailBody } : {}),
...(mailSubject ? { RequestSubject: mailSubject } : {}),
...(x?.objectId
? {
TemplateId: {
__type: 'Pointer',
className: 'contracts_Template',
objectId: x?.objectId,
},
}
: {}),
},
};
});
@@ -1,35 +0,0 @@
import { exec } from 'child_process';
export default function DigitalSign(pdf, pfx, details) {
const signcmd = `java -jar PDFDigitalSigner.jar "${pdf}" "${pfx.name}" "${pfx.passphrase}" "${details.name}" "${details.location}" "${details.reason}"`;
// const signcmd = `java -jar PDFDigitalSigner.jar "${pdf}" keystore.pfx opensign "${details.name}" "${details.location}" "${details.reason}"`;
return new Promise((resolve, reject) => {
exec(signcmd, (error, stdout, stderr) => {
if (error) {
reject(`Error: ${error.message}`);
}
if (stderr) {
reject(`stderr: ${stderr}`);
}
// Resolve the promise with the output
resolve(stdout.trim());
});
});
}
export async function retryAsync(fn, args = [], retries = 3, delay = 2000) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fn(...args); // Try executing the async function
if (response) return { response, attempt }; // Stop retrying if response is received
} catch (error) {
if (attempt < retries) {
console.log(`Attempt ${attempt} failed. Retrying in ${delay / 1000} seconds...\n`);
await new Promise(resolve => setTimeout(resolve, delay)); // Wait before retrying
} else {
return { error, attempt };
// throw new Error(error);
}
}
}
}
@@ -20,6 +20,16 @@ const masterKEY = process.env.MASTER_KEY;
const eSignName = 'OpenSign';
const eSigncontact = 'hello@opensignlabs.com';
async function unlinkFile(path) {
if (fs.existsSync(path)) {
try {
fs.unlinkSync(path);
} catch (err) {
console.log('Err in unlink file: ', path);
}
}
}
// `updateDoc` is used to create url in from pdfFile
async function uploadFile(pdfName, filepath) {
try {
@@ -33,8 +43,8 @@ async function uploadFile(pdfName, filepath) {
return { imageUrl: fileUrl };
} catch (err) {
console.log('Err ', err);
// `fs.unlinkSync` is used to remove exported signed pdf file from exports folder
fs.unlinkSync(filepath);
// below line of code is used to remove exported signed pdf file from exports folder
unlinkFile(filepath);
}
}
@@ -240,10 +250,10 @@ async function sendCompletedMail(obj) {
});
// console.log('res', res.data.result);
if (res.data?.result?.status !== 'success') {
fs.unlinkSync(`./exports/signed_certificate_${doc.objectId}.pdf`);
unlinkFile(`./exports/signed_certificate_${doc.objectId}.pdf`);
}
} catch (err) {
fs.unlinkSync(`./exports/signed_certificate_${doc.objectId}.pdf`);
unlinkFile(`./exports/signed_certificate_${doc.objectId}.pdf`);
}
}
@@ -288,7 +298,7 @@ async function sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, file
sendCompletedMail({ isCustomMail, doc, mailProvider, filename });
}
saveFileUsage(CertificateBuffer.length, file.imageUrl, doc?.CreatedBy?.objectId);
fs.unlinkSync(pfx.name);
unlinkFile(pfx.name);
}
/**
*
@@ -438,10 +448,10 @@ async function PDF(req) {
const doc = { ..._resDoc, AuditTrail: updatedDoc.AuditTrail, SignedUrl: data.imageUrl };
sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, `signed_${name}`);
} else {
fs.unlinkSync(pfxname);
unlinkFile(pfxname);
}
// `fs.unlinkSync` is used to remove exported signed pdf file from exports folder
fs.unlinkSync(signedFilePath);
// below code is used to remove exported signed pdf file from exports folder
unlinkFile(signedFilePath);
// console.log(`New Signed PDF created called: ${filePath}`);
if (updatedDoc.message === 'success') {
return { status: 'success', data: data.imageUrl };
@@ -466,7 +476,7 @@ async function PDF(req) {
} catch (err) {
console.log('err in saving debugginglog', err);
}
fs.unlinkSync(pfxname);
unlinkFile(pfxname);
throw err;
}
}
@@ -0,0 +1,36 @@
export default async function recreateDocument(request) {
const { docId } = request.params;
if (!docId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing docId parameter');
}
if (!request.user) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'User not aunthenticated');
}
try {
const docQuery = new Parse.Query('contracts_Document');
docQuery.equalTo('objectId', docId);
const doc = await docQuery.first({ useMasterKey: true });
if (!doc) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Document not found');
}
const _docRes = doc?.toJSON();
const { objectId, SignedUrl, AuditTrail, ACL, DeclineBy, DeclineReason, ...docRes } = _docRes;
const createDoc = new Parse.Object('contracts_Document');
Object.entries(docRes).forEach(([key, value]) => {
if (key === 'IsDeclined') {
createDoc.set(key, false);
} else {
createDoc.set(key, value);
}
// console.log(`${key}: ${value}`);
});
const createDocRes = await createDoc.save(null, { useMasterKey: true });
// console.log('createDocRes', createDocRes);
const newDoc = JSON.parse(JSON.stringify(createDocRes));
return { objectId: newDoc.objectId, createdAt: newDoc.createdAt, updatedAt: newDoc.updatedAt };
} catch (err) {
console.log('err in recreate document', err);
throw err;
}
}
@@ -25,9 +25,9 @@ export default function reportJson(id, userId) {
'Signers.Phone',
'Placeholders',
'IsSignyourself',
'TemplateId',
],
};
// Need your sign report
case '4Hhwbp482K':
return {
@@ -61,6 +61,7 @@ export default function reportJson(id, userId) {
'AuditTrail',
'Placeholders',
'SignedUrl',
'TemplateId',
'ExpiryDate',
],
};
@@ -94,6 +95,7 @@ export default function reportJson(id, userId) {
'SendMail',
'Placeholders',
'SignedUrl',
'TemplateId',
],
};
// completed documents report
@@ -140,6 +142,7 @@ export default function reportJson(id, userId) {
'Placeholders',
'IsSignyourself',
'IsCompleted',
'TemplateId',
],
};
// declined documents report
@@ -164,6 +167,7 @@ export default function reportJson(id, userId) {
'Placeholders',
'DeclineReason',
'SignedUrl',
'TemplateId',
],
};
// Expired Documents report
@@ -190,6 +194,7 @@ export default function reportJson(id, userId) {
'Signers.Phone',
'Placeholders',
'SignedUrl',
'TemplateId',
'ExpiryDate',
],
};
@@ -221,6 +226,7 @@ export default function reportJson(id, userId) {
'ExpiryDate',
'Placeholders',
'SignedUrl',
'TemplateId',
],
};
// Recent signature requests report show on dashboard
@@ -254,6 +260,7 @@ export default function reportJson(id, userId) {
'Signers.Phone',
'Placeholders',
'SignedUrl',
'TemplateId',
'ExpiryDate',
],
};
@@ -279,6 +286,7 @@ export default function reportJson(id, userId) {
'Signers.Email',
'Signers.Phone',
'Placeholders',
'TemplateId',
],
};
// contact book report
@@ -0,0 +1,21 @@
/**
*
* @param {Parse} Parse
*/
exports.up = async Parse => {
const className = 'contracts_Document';
const schema = new Parse.Schema(className);
schema.addPointer('TemplateId', 'contracts_Template');
return schema.update();
};
/**
*
* @param {Parse} Parse
*/
exports.down = async Parse => {
const className = 'contracts_Document';
const schema = new Parse.Schema(className);
schema.deleteField('TemplateId');
return schema.update();
};
+1 -1
View File
@@ -160,7 +160,7 @@ app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
app.use(function (req, res, next) {
req.headers['x-real-ip'] = getUserIP(req);
const publicUrl = req.protocol + '://' + req.get('host');
const publicUrl = req?.protocol + '://' + req?.get('host');
req.headers['public_url'] = publicUrl; // process.env.PUBLIC_URL
next();
});
Binary file not shown.