Merge pull request #2024 from OpenSignLabs/staging

v2.31.0
This commit is contained in:
Amol
2025-11-20 12:20:55 +05:30
committed by GitHub
59 changed files with 21233 additions and 3539 deletions
+9
View File
@@ -241,6 +241,15 @@
"contributions": [
"translation"
]
},
{
"login": "ehowe",
"name": "Eugene Howe",
"avatar_url": "https://avatars.githubusercontent.com/u/756984?v=4",
"profile": "https://github.com/ehowe",
"contributions": [
"code"
]
}
]
}
+24 -12
View File
@@ -21,34 +21,46 @@ jobs:
dockerfile: apps/OpenSign/Dockerhubfile
- image: opensign/opensignserver
dockerfile: apps/OpenSignServer/Dockerhubfile
steps:
-
name: Checkout
- name: Checkout
uses: actions/checkout@v3
-
name: Login to Docker Hub
# 👇 Enable emulation so we can build for arm64 on amd64 runners
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: linux/amd64,linux/arm64
# 👇 Enable Buildx for multi-arch builds
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Extract metadata (tags, labels) for Docker
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ matrix.image }}
-
name: Debug - List files opensign
- name: Debug - List files opensign
run: ls -R apps/OpenSign/ # Adjust the path as needed
-
name: Debug - List files opensignserver
- name: Debug - List files opensignserver
run: ls -R apps/OpenSignServer/ # Adjust the path as needed
-
name: Build and push
- name: Build and push (multi-arch)
uses: docker/build-push-action@v4
with:
context: .
file: ${{ matrix.dockerfile }}
push: true
# 👇 Build for both AMD64 & ARM64
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+1
View File
@@ -165,6 +165,7 @@ We would like to thank all our contributors and users for their support and feed
<td align="center" valign="top" width="14.28%"><a href="https://github.com/daniel-mutwiri"><img src="https://avatars.githubusercontent.com/u/8936960?v=4?s=100" width="100px;" alt="Daniel Mutwiri"/><br /><sub><b>Daniel Mutwiri</b></sub></a><br /><a href="#code-daniel-mutwiri" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zathiel"><img src="https://avatars.githubusercontent.com/u/26553418?v=4?s=100" width="100px;" alt="Zathiel"/><br /><sub><b>Zathiel</b></sub></a><br /><a href="#code-Zathiel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1024mb"><img src="https://avatars.githubusercontent.com/u/9301204?v=4?s=100" width="100px;" alt="1024mb"/><br /><sub><b>1024mb</b></sub></a><br /><a href="#translation-1024mb" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ehowe"><img src="https://avatars.githubusercontent.com/u/756984?v=4?s=100" width="100px;" alt="Eugene Howe"/><br /><sub><b>Eugene Howe</b></sub></a><br /><a href="#code-ehowe" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+17237
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -32,7 +32,6 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dnd-multi-backend": "^9.0.0",
"react-dnd-touch-backend": "^16.0.1",
"react-dom": "^18.3.1",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
@@ -120,7 +120,8 @@
"Teams": "Teams",
"Team-update-successfully": "Team erfolgreich aktualisiert.",
"Users": "Benutzer"
}
},
"Global SMTP": "Globales SMTP"
},
"dashboard-card": {
"Need your Signature": "Benötigt Ihre Unterschrift",
@@ -1256,5 +1257,21 @@
"Please provide required parameters.": "Bitte geben Sie die erforderlichen Parameter an.",
"reset-password": "Benutzerpasswort zurücksetzen",
"enter-strong-password": "Geben Sie ein sicheres Passwort ein oder generieren Sie eines automatisch.",
"autogenerate": "Automatisch generieren"
"autogenerate": "Automatisch generieren",
"extend": "Verlängern",
"console-general-help": "Um einzelnen Benutzern zu ermöglichen, ihre eigenen E-Mail-Vorlagen anzupassen, navigieren Sie zu",
"console-mail-help": "Um unternehmensweite E-Mail-Vorlagen zu aktualisieren, gehen Sie zu",
"admin-setting-disabled": "Diese Einstellung ist deaktiviert. Bitte wenden Sie sich für Unterstützung an den Administrator.",
"company-email-settings": "Unternehmens-E-Mail-Einstellungen",
"allow-email-template-individually": "E-Mail-Vorlagenanpassung für Benutzer individuell erlauben ",
"user-email-template-general-help": "Wenn diese Option deaktiviert ist, können Benutzer ihre E-Mail-Vorlagen nicht mehr anpassen.",
"disable-individual-user-smtp": "individuelle benutzer-smtp-einstellungen deaktivieren",
"enable-individual-user-smtp": "individuelle benutzer-smtp-einstellungen aktivieren",
"user-smtp-general-help": "Wenn deaktiviert, können Benutzer ihre eigenen SMTP-Einstellungen nicht konfigurieren. Alle E-Mails verwenden dann die globale SMTP-Konfiguration.",
"global-smtp-settings": "Globale SMTP-Einstellungen",
"global-smtp-settings-help-p1": "Diese Einstellungen gelten global für Ihr gesamtes {{appName}}-Konto. Wenn ein Benutzer keine eigenen E-Mail-Zustellungs­einstellungen konfiguriert hat, verwendet das System automatisch die hier definierten globalen SMTP-Einstellungen.",
"global-smtp-settings-help-p2": "Um eine bessere Zustellbarkeit aller Signaturanfragen sicherzustellen, können Sie Ihren bevorzugten E-Mail-Anbieter verbinden. Wählen Sie eine der folgenden Optionen:",
"global-connect-to-gmail-help": "Verwenden Sie ein Gmail-Konto, um Signaturanfragen direkt aus dem Posteingang Ihrer Organisation zu senden. Dies verbessert die Zustellrate, Reputation und Zuverlässigkeit.",
"global-connect-to-smtp-help": "Verwenden Sie Ihren eigenen SMTP-Server, um E-Mails über Ihre Domain zu senden. Dies bietet volle Kontrolle über Ihre E-Mail-Infrastruktur, verbessert die Zustellbarkeit und sorgt für konsistentes Branding.",
"global-smtp-settings-help-p3": "Wenn keine individuellen Benutzereinstellungen konfiguriert sind, werden automatisch diese globalen Einstellungen angewendet."
}
@@ -120,7 +120,8 @@
"Teams": "Teams",
"Team-update-successfully": "Team Updated successfully.",
"Users": "Users"
}
},
"Global SMTP": "Global SMTP"
},
"dashboard-card": {
"Need your Signature": "Need your signature",
@@ -1257,5 +1258,21 @@
"Please provide required parameters.": "Please provide required parameters.",
"reset-password": "Reset User Password",
"enter-strong-password": "Enter a strong password or auto-generate one.",
"autogenerate": "Autogenerate"
"autogenerate": "Autogenerate",
"extend": "Extend",
"console-general-help": "To let individual users customize their own email templates, navigate to",
"console-mail-help": "To update company-wide email templates, go to",
"admin-setting-disabled": "This setting is disabled. Please contact admin for assistance.",
"company-email-settings": "Company email settings",
"allow-email-template-individually": "Allow email template customization for users individually ",
"user-email-template-general-help": "Disabling this will prevent users from customizing their email templates.",
"disable-individual-user-smtp": "Disable individual user smtp settings",
"enable-individual-user-smtp": "Enable individual user smtp settings",
"user-smtp-general-help": "When disabled, users will not be able to configure their own SMTP settings. All emails will use the global SMTP configuration instead.",
"global-smtp-settings": "Global SMTP Settings",
"global-smtp-settings-help-p1": "These settings apply globally across your entire {{appName}} account. If an individual user has not configured their own email delivery settings, the system will automatically use the Global SMTP Settings defined here.",
"global-smtp-settings-help-p2": "To ensure better inbox deliverability for all signature request emails, you can connect your preferred email provider. Choose one of the following options:",
"global-connect-to-gmail-help": "Use a Gmail account to send signature request emails directly from your organization's inbox. This improves delivery rates, reputation, and overall reliability.",
"global-connect-to-smtp-help": "Use your own SMTP server to send emails through your domain. This provides full control over your email infrastructure, enhances deliverability, and maintains consistent branding.",
"global-smtp-settings-help-p3": "If no individual user-level settings are configured, these global settings will be applied automatically."
}
@@ -120,7 +120,8 @@
"Teams": "Equipos",
"Team-update-successfully": "Equipo actualizado correctamente.",
"Users": "Usuarios"
}
},
"Global SMTP": "SMTP Global"
},
"dashboard-card": {
"Need your Signature": "Necesitan tu firma",
@@ -1257,5 +1258,21 @@
"Please provide required parameters.": "Por favor, proporcione los parámetros requeridos.",
"reset-password": "Restablecer contraseña de usuario",
"enter-strong-password": "Ingrese una contraseña segura o genere una automáticamente.",
"autogenerate": "Generar automáticamente"
"autogenerate": "Generar automáticamente",
"extend": "Extender",
"console-general-help": "Para permitir que los usuarios individuales personalicen sus propias plantillas de correo electrónico, vaya a",
"console-mail-help": "Para actualizar las plantillas de correo electrónico de toda la empresa, vaya a",
"admin-setting-disabled": "Esta configuración está deshabilitada. Comuníquese con el administrador para obtener ayuda.",
"company-email-settings": "Configuración de correo electrónico de la empresa",
"allow-email-template-individually": "Permitir la personalización individual de plantillas de correo electrónico para los usuarios ",
"user-email-template-general-help": "Si se desactiva, los usuarios no podrán personalizar sus plantillas de correo electrónico.",
"disable-individual-user-smtp": "desactivar la configuración smtp individual del usuario",
"enable-individual-user-smtp": "activar la configuración smtp individual del usuario",
"user-smtp-general-help": "Cuando está desactivado, los usuarios no podrán configurar su propio SMTP. Todos los correos utilizarán la configuración SMTP global.",
"global-smtp-settings": "Configuración SMTP Global",
"global-smtp-settings-help-p1": "Estas configuraciones se aplican globalmente en toda su cuenta de {{appName}}. Si un usuario no ha configurado sus propios ajustes de entrega de correo, el sistema utilizará automáticamente la configuración SMTP global definida aquí.",
"global-smtp-settings-help-p2": "Para garantizar una mejor entregabilidad de todos los correos de solicitud de firma, puede conectar su proveedor de correo preferido. Elija una de las siguientes opciones:",
"global-connect-to-gmail-help": "Use una cuenta de Gmail para enviar correos de solicitud de firma directamente desde la bandeja de entrada de su organización. Esto mejora la entregabilidad, la reputación y la fiabilidad.",
"global-connect-to-smtp-help": "Use su propio servidor SMTP para enviar correos a través de su dominio. Esto proporciona control total sobre su infraestructura de correo, mejora la entregabilidad y mantiene la coherencia de marca.",
"global-smtp-settings-help-p3": "Si no se configuran ajustes a nivel de usuario, se aplicarán automáticamente estos ajustes globales."
}
@@ -120,7 +120,8 @@
"Teams": "Équipe",
"Team-update-successfully": "Équipe mise à jour avec succès.",
"Users": "Utilisateurs"
}
},
"Global SMTP": "SMTP Global"
},
"dashboard-card": {
"Need your Signature": "Besoin de votre signature",
@@ -1256,5 +1257,21 @@
"Please provide required parameters.": "Veuillez fournir les paramètres requis.",
"reset-password": "Réinitialiser le mot de passe utilisateur",
"enter-strong-password": "Entrez un mot de passe sécurisé ou générez-en un automatiquement.",
"autogenerate": "Générer automatiquement"
"autogenerate": "Générer automatiquement",
"extend": "Prolonger",
"console-general-help": "Pour permettre aux utilisateurs individuels de personnaliser leurs propres modèles d'e-mails, accédez à",
"console-mail-help": "Pour mettre à jour les modèles d'e-mails à l'échelle de l'entreprise, allez dans",
"admin-setting-disabled": "Ce paramètre est désactivé. Veuillez contacter l'administrateur pour obtenir de l'aide.",
"company-email-settings": "Paramètres de messagerie de l'entreprise",
"allow-email-template-individually": "Autoriser la personnalisation individuelle des modèles d'e-mails pour chaque utilisateur ",
"user-email-template-general-help": "Si cette option est désactivée, les utilisateurs ne pourront plus personnaliser leurs modèles d'e-mails.",
"disable-individual-user-smtp": "désactiver les paramètres smtp individuels de l'utilisateur",
"enable-individual-user-smtp": "activer les paramètres smtp individuels de l'utilisateur",
"user-smtp-general-help": "Lorsqu'elle est désactivée, les utilisateurs ne pourront pas configurer leur propre SMTP. Tous les e-mails utiliseront la configuration SMTP globale.",
"global-smtp-settings": "Paramètres SMTP globaux",
"global-smtp-settings-help-p1": "Ces paramètres s'appliquent globalement à l'ensemble de votre compte {{appName}}. Si un utilisateur n'a pas configuré ses propres paramètres de messagerie, le système utilisera automatiquement les paramètres SMTP globaux définis ici.",
"global-smtp-settings-help-p2": "Pour assurer une meilleure délivrabilité de toutes les demandes de signature, vous pouvez connecter votre fournisseur de messagerie préféré. Choisissez l'une des options suivantes :",
"global-connect-to-gmail-help": "Utilisez un compte Gmail pour envoyer des demandes de signature directement depuis la boîte de réception de votre organisation. Cela améliore la délivrabilité, la réputation et la fiabilité.",
"global-connect-to-smtp-help": "Utilisez votre propre serveur SMTP pour envoyer des e-mails via votre domaine. Cela offre un contrôle total sur votre infrastructure de messagerie, améliore la délivrabilité et garantit une cohérence de marque.",
"global-smtp-settings-help-p3": "Si aucun paramètre utilisateur individuel n'est configuré, ces paramètres globaux seront appliqués automatiquement."
}
@@ -120,7 +120,8 @@
"Teams": "टीमें",
"Team-update-successfully": "टीम सफलतापूर्वक अपडेट की गई।",
"Users": "उपयोगकर्ता"
}
},
"Global SMTP": "ग्लोबल SMTP"
},
"dashboard-card": {
"Need your Signature": "आपके हस्ताक्षर की आवश्यकता है",
@@ -1256,5 +1257,21 @@
"Please provide required parameters.": "कृपया आवश्यक पैरामीटर प्रदान करें।",
"reset-password": "उपयोगकर्ता पासवर्ड रीसेट करें",
"enter-strong-password": "एक मज़बूत पासवर्ड दर्ज करें या स्वचालित रूप से उत्पन्न करें।",
"autogenerate": "स्वतः उत्पन्न करें"
"autogenerate": "स्वतः उत्पन्न करें",
"extend": "बढ़ाएँ",
"console-general-help": "व्यक्तिगत उपयोगकर्ताओं को अपनी स्वयं की ईमेल टेम्पलेट्स अनुकूलित करने की अनुमति देने के लिए, जाएँ",
"console-mail-help": "कंपनी-व्यापी ईमेल टेम्पलेट्स अपडेट करने के लिए, जाएँ",
"admin-setting-disabled": "यह सेटिंग अक्षम है। सहायता के लिए कृपया व्यवस्थापक से संपर्क करें।",
"company-email-settings": "कंपनी ईमेल सेटिंग्स",
"allow-email-template-individually": "उपयोगकर्ताओं को व्यक्तिगत रूप से ईमेल टेम्पलेट अनुकूलित करने की अनुमति दें ",
"user-email-template-general-help": "इसे निष्क्रिय करने पर उपयोगकर्ता अपनी ईमेल टेम्पलेट्स को अनुकूलित नहीं कर पाएंगे।",
"disable-individual-user-smtp": "व्यक्तिगत उपयोगकर्ता SMTP सेटिंग्स अक्षम करें",
"enable-individual-user-smtp": "व्यक्तिगत उपयोगकर्ता SMTP सेटिंग्स सक्षम करें",
"user-smtp-general-help": "जब यह निष्क्रिय हो, तो उपयोगकर्ता अपनी स्वयं की SMTP सेटिंग्स कॉन्फ़िगर नहीं कर पाएंगे। सभी ईमेल ग्लोबल SMTP कॉन्फ़िगरेशन का उपयोग करेंगे।",
"global-smtp-settings": "ग्लोबल SMTP सेटिंग्स",
"global-smtp-settings-help-p1": "ये सेटिंग्स आपके पूरे {{appName}} खाते में वैश्विक रूप से लागू होती हैं। यदि किसी उपयोगकर्ता ने अपनी स्वयं की ईमेल डिलीवरी सेटिंग्स कॉन्फ़िगर नहीं की हैं, तो सिस्टम स्वचालित रूप से यहाँ परिभाषित ग्लोबल SMTP सेटिंग्स का उपयोग करेगा।",
"global-smtp-settings-help-p2": "सभी हस्ताक्षर अनुरोध ईमेल की बेहतर इनबॉक्स डिलीवरी सुनिश्चित करने के लिए, आप अपना पसंदीदा ईमेल प्रदाता जोड़ सकते हैं। निम्न विकल्पों में से एक चुनें:",
"global-connect-to-gmail-help": "अपनी संस्था की इनबॉक्स से सीधे हस्ताक्षर अनुरोध ईमेल भेजने के लिए Gmail खाते का उपयोग करें। यह डिलीवरी दर, प्रतिष्ठा और विश्वसनीयता में सुधार करता है।",
"global-connect-to-smtp-help": "अपने डोमेन के माध्यम से ईमेल भेजने के लिए अपना स्वयं का SMTP सर्वर उपयोग करें। यह आपकी ईमेल संरचना पर पूर्ण नियंत्रण प्रदान करता है, डिलीवरी को बेहतर बनाता है और ब्रांड की निरंतरता बनाए रखता है।",
"global-smtp-settings-help-p3": "यदि कोई व्यक्तिगत उपयोगकर्ता-स्तरीय सेटिंग्स कॉन्फ़िगर नहीं की गई हैं, तो ये वैश्विक सेटिंग्स स्वचालित रूप से लागू होंगी।"
}
@@ -120,7 +120,8 @@
"Teams": "Team",
"Team-update-successfully": "Team aggiornato con successo.",
"Users": "Utenti"
}
},
"Global SMTP": "SMTP Globale"
},
"dashboard-card": {
"Need your Signature": "Necessita della tua firma",
@@ -263,7 +264,7 @@
"my-signature": "La mia firma",
"signature": "Firma",
"upload-image": "Carica immagine",
"clear": "Chiara",
"clear": "Cancella",
"upload-signature/Image": "Carica firma/immagine",
"initials": "Iniziali",
"API": "API",
@@ -1256,5 +1257,21 @@
"Please provide required parameters.": "Fornisci i parametri richiesti.",
"reset-password": "Reimposta password utente",
"enter-strong-password": "Inserisci una password sicura o generane una automaticamente.",
"autogenerate": "Genera automaticamente"
"autogenerate": "Genera automaticamente",
"extend": "Estendi",
"console-general-help": "Per consentire ai singoli utenti di personalizzare i propri modelli di e-mail, vai a",
"console-mail-help": "Per aggiornare i modelli di e-mail aziendali, vai a",
"admin-setting-disabled": "Questa impostazione è disabilitata. Contatta l'amministratore per assistenza.",
"company-email-settings": "Impostazioni e-mail aziendali",
"allow-email-template-individually": "Consenti la personalizzazione individuale dei modelli di e-mail per gli utenti ",
"user-email-template-general-help": "Disabilitando questa opzione, gli utenti non potranno più personalizzare i loro modelli di e-mail.",
"disable-individual-user-smtp": "disabilita le impostazioni smtp individuali dell'utente",
"enable-individual-user-smtp": "abilita le impostazioni smtp individuali dell'utente",
"user-smtp-general-help": "Quando disattivato, gli utenti non potranno configurare il proprio SMTP. Tutte le e-mail utilizzeranno la configurazione SMTP globale.",
"global-smtp-settings": "Impostazioni SMTP Globali",
"global-smtp-settings-help-p1": "Queste impostazioni si applicano globalmente all'intero account {{appName}}. Se un utente non ha configurato le proprie impostazioni di consegna e-mail, il sistema utilizzerà automaticamente le impostazioni SMTP globali definite qui.",
"global-smtp-settings-help-p2": "Per garantire una migliore consegna delle e-mail di richiesta firma, puoi collegare il tuo provider di posta preferito. Scegli una delle seguenti opzioni:",
"global-connect-to-gmail-help": "Utilizza un account Gmail per inviare e-mail di richiesta firma direttamente dalla casella di posta della tua organizzazione. Migliora la consegna, la reputazione e l'affidabilità.",
"global-connect-to-smtp-help": "Utilizza il tuo server SMTP per inviare e-mail tramite il tuo dominio. Questo offre pieno controllo sulla tua infrastruttura e-mail, migliora la consegna e mantiene la coerenza del marchio.",
"global-smtp-settings-help-p3": "Se non sono configurate impostazioni a livello utente, verranno applicate automaticamente queste impostazioni globali."
}
-302
View File
@@ -1,302 +0,0 @@
import React, { useState, useEffect } from "react";
import Parse from "parse";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
import { emailRegex } from "../constant/const";
const AddSigner = (props) => {
const { t } = useTranslation();
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [email, setEmail] = useState("");
const [addYourself, setAddYourself] = useState(false);
const [isLoader, setIsLoader] = useState(false);
const [isUserExist, setIsUserExist] = useState(false);
useEffect(() => {
checkUserExist();
}, []);
// Load user details from localStorage when the component mounts
useEffect(() => {
const savedUserDetails = JSON.parse(
localStorage.getItem("UserInformation")
);
if (savedUserDetails && addYourself) {
setName(savedUserDetails.name);
setPhone(savedUserDetails?.phone || "");
setEmail(savedUserDetails.email?.toLowerCase()?.replace(/\s/g, ""));
}
}, [addYourself]);
const checkUserExist = async () => {
const user = Parse.User.current();
try {
const query = new Parse.Query("contracts_Contactbook");
query.equalTo("CreatedBy", user);
query.notEqualTo("IsDeleted", true);
query.equalTo("Email", user.getEmail());
const res = await query.first();
// console.log(res);
if (!res) {
setIsUserExist(true);
}
} catch (err) {
console.log("err", err);
}
};
// Define a function to handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
if (!emailRegex.test(email)) {
alert(t("valid-email-alert"));
} else {
setIsLoader(true);
if (localStorage.getItem("TenantId")) {
try {
const user = Parse.User.current();
const query = new Parse.Query("contracts_Contactbook");
query.equalTo("CreatedBy", user);
query.notEqualTo("IsDeleted", true);
query.equalTo("Email", email);
const res = await query.first();
// console.log(res);
if (!res) {
const contactQuery = new Parse.Object("contracts_Contactbook");
contactQuery.set("Name", name);
if (phone) {
contactQuery.set("Phone", phone);
}
contactQuery.set("Email", email);
contactQuery.set("UserRole", "contracts_Guest");
contactQuery.set("TenantId", {
__type: "Pointer",
className: "partners_Tenant",
objectId: localStorage.getItem("TenantId")
});
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", email);
if (phone) {
_user.set("phone", phone);
}
const user = await _user.save();
if (user) {
const currentUser = Parse.User.current();
contactQuery.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
);
contactQuery.set("UserId", user);
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(currentUser.id, true);
acl.setWriteAccess(currentUser.id, true);
contactQuery.setACL(acl);
const res = await contactQuery.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.details) {
props.details({
value: parseData[props.valueKey],
label: parseData[props.displayKey],
email: parseData?.Email
});
}
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
props.handleUserData(parseData);
}
setIsLoader(false);
// Reset the form fields
setAddYourself(false);
setName("");
setPhone("");
setEmail("");
}
} catch (err) {
console.log("err ", err);
if (err.code === 202) {
const params = { email: email };
const userRes = await Parse.Cloud.run("getUserId", params);
const currentUser = Parse.User.current();
contactQuery.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
);
contactQuery.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);
contactQuery.setACL(acl);
const res = await contactQuery.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.details) {
props.details({
value: parseData[props.valueKey],
label: parseData[props.displayKey],
email: parseData?.Email
});
}
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
props.handleUserData(parseData);
}
setIsLoader(false);
// Reset the form fields
setAddYourself(false);
setName("");
setPhone("");
setEmail("");
}
}
} else {
alert(t("add-signer-alert"));
setIsLoader(false);
}
} catch (err) {
console.log("err in fetch contact", err);
setIsLoader(false);
alert(t("something-went-wrong-mssg"));
}
} else {
setIsLoader(false);
alert(t("something-went-wrong-mssg"));
}
}
};
// Define a function to handle the "add yourself" checkbox
const handleAddYourselfChange = () => {
if (addYourself) {
setAddYourself(false);
setName("");
setPhone("");
setEmail("");
} else {
setAddYourself(true);
}
};
const handleReset = () => {
setAddYourself(false);
};
return (
<div className="h-full p-[20px]">
{isLoader && (
<div className="fixed inset-0 flex justify-center items-center bg-black bg-opacity-30">
<Loader />
</div>
)}
<div className="w-full mx-auto p-2 text-base-content">
<p className="mb-3 text-xs text-gray-500">{t("add-signer-note")}</p>
{isUserExist && (
<div className="mb-3 flex items-center">
<input
type="checkbox"
id="addYourself"
checked={addYourself}
onChange={handleAddYourselfChange}
className="op-checkbox op-checkbox-sm"
/>
<label htmlFor="addYourself" className="ml-2 mb-0">
{t("add-yourself")}
</label>
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label htmlFor="name" className="block text-xs font-semibold">
{t("name")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
disabled={addYourself}
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-xs"
placeholder={t("enter-name")}
/>
</div>
<div className="mb-3">
<label htmlFor="email" className="block text-xs font-semibold">
{t("email")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) =>
setEmail(e.target.value?.toLowerCase()?.replace(/\s/g, ""))
}
required
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-email")}
/>
</div>
<div className="mb-3">
<label htmlFor="phone" className="block text-xs font-semibold">
{t("phone")}
</label>
<input
type="text"
id="phone"
value={phone}
onChange={(e) => setPhone(e.target.value)}
disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("phone-optional")}
/>
</div>
<div className="mt-4 flex gap-x-2 justify-start">
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
<button
type="button"
onClick={() => handleReset()}
className="op-btn op-btn-secondary"
>
{t("reset")}
</button>
</div>
</form>
</div>
</div>
);
};
export default AddSigner;
@@ -42,7 +42,7 @@ const ColumnSelector = ({
type="checkbox"
checked={selected.includes(col)}
onChange={() => handleChange(col)}
className="mb-0"
className="mb-0 cursor-pointer"
/>
<span className="whitespace-nowrap ml-1">
{t(`report-heading.${col}`, { defaultValue: col })}
+1 -25
View File
@@ -1,12 +1,8 @@
import { HTML5Backend } from "react-dnd-html5-backend";
import { TouchBackend } from "react-dnd-touch-backend";
import {
DndProvider,
TouchTransition,
MouseTransition,
Preview
MouseTransition
} from "react-dnd-multi-backend";
import DragElement from "./pdf/DragElement";
import LazyPage from "../primitives/LazyPage";
import { GuidelinesProvider } from "../context/GuidelinesContext";
@@ -16,33 +12,13 @@ const HTML5toTouch = {
id: "html5",
backend: HTML5Backend,
transition: MouseTransition
},
{
id: "touch",
backend: TouchBackend,
options: { enableMouseEvents: true },
preview: true,
transition: TouchTransition
}
]
};
/* This handling in only for devices which support touch */
const generatePreview = (props) => {
const { item, style } = props;
const newStyle = { ...style };
return (
<div style={newStyle}>
<DragElement {...item} />
</div>
);
};
export default function DragProvider({ Page, lazy = false }) {
return (
<DndProvider options={HTML5toTouch}>
<Preview>{generatePreview}</Preview>
<GuidelinesProvider>
{lazy ? <LazyPage Page={Page} /> : <Page />}
</GuidelinesProvider>
+6 -3
View File
@@ -13,8 +13,8 @@ import {
import { useTranslation } from "react-i18next";
import { appInfo } from "../constant/appinfo";
import { useDispatch } from "react-redux";
import { toggleSidebar } from "../redux/reducers/sidebarReducer.js";
import { sessionStatus } from "../redux/reducers/userReducer.js";
import { toggleSidebar } from "../redux/reducers/sidebarReducer";
import { sessionStatus } from "../redux/reducers/userReducer";
const Header = ({ isConsole, setIsLoggingOut }) => {
const { t, i18n } = useTranslation();
@@ -141,7 +141,10 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
</button>
</div>
<div className="flex-1 ml-2">
<div className="h-[25px] md:h-[40px] w-auto overflow-hidden">
<div
onClick={() => navigate("/dashboard/35KBoSgoAK")}
className="h-[25px] md:h-[40px] w-auto overflow-hidden cursor-pointer"
>
{applogo && (
<img
className="object-contain h-full w-auto"
@@ -7,7 +7,6 @@ import Loader from "../../primitives/Loader";
import { useTranslation } from "react-i18next";
function DashboardReport(props) {
const { t } = useTranslation();
// console.log("props ", props)
const [List, setList] = useState([]);
const [isLoader, setIsLoader] = useState(true);
const [reportName, setReportName] = useState("");
@@ -1,4 +1,4 @@
import React, { Suspense } from "react";
import { useState, useEffect, Suspense } from "react";
import { lazyWithRetry } from "../../utils";
import { useTranslation } from "react-i18next";
const DashboardButton = lazyWithRetry(() => import("./DashboardButton"));
@@ -20,6 +20,7 @@ const buttonList = [
];
const GetDashboard = (props) => {
const { t } = useTranslation();
const Button = ({ label, redirectId, redirectType, icon }) => (
<DashboardButton
Icon={icon}
@@ -59,7 +60,9 @@ const GetDashboard = (props) => {
<div data-tut={col.widget.data.tourSection}>
<Suspense fallback={<div>please wait</div>}>
<div className="mb-3 md:mb-0">
<DashboardReport Record={col.widget} />
<DashboardReport
Record={col.widget}
/>
</div>
</Suspense>
</div>
@@ -93,7 +96,9 @@ const GetDashboard = (props) => {
return (
<Suspense fallback={<div>please wait</div>}>
<div className="mb-3 md:mb-0">
<DashboardReport Record={col.widget} />
<DashboardReport
Record={col.widget}
/>
</div>
</Suspense>
);
@@ -3,6 +3,8 @@ import ModalUi from "../../primitives/ModalUi";
import { EmailBody } from "./EmailBody";
import {
contractDocument,
defaultMailBody,
defaultMailSubject,
sendEmailToSigners
} from "../../constant/Utils";
import { useTranslation } from "react-i18next";
@@ -101,7 +103,10 @@ function CustomizeMail(props) {
<div
className="flex justify-end items-center gap-1 mt-2 op-link op-link-primary"
onClick={() => {
props?.setCustomizeMail(props?.defaultMail);
props?.setCustomizeMail({
subject: defaultMailSubject,
body: defaultMailBody
});
}}
>
<span>{t("reset-to-default")}</span>
@@ -0,0 +1,76 @@
import { useDragLayer } from "react-dnd";
import { defaultWidthHeight } from "../../constant/Utils";
import { useEffect, useRef } from "react";
function DragGuideLinesLayer(props) {
const { showGuidelines } = props;
const prevDragState = useRef({ isDragging: false, x: 0, y: 0 });
const { isDraggingWidgets, offset, itemType } = useDragLayer((monitor) => ({
isDraggingWidgets: monitor.isDragging(),
offset: monitor.getClientOffset(),
itemType: monitor.getItem()
}));
useEffect(() => {
if (!offset) return;
//getting container by id
const container = document.getElementById("container");
if (!container) return;
if (itemType?.text) {
// Calculate the position of the dragged element relative to the container
// Get the container's bounding rectangle (position and size in the viewport)
const containerRect = container.getBoundingClientRect();
// Compute the X and Y coordinates of the dragged item inside the container
// by subtracting the container's top-left offset from the current drag offset
const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top;
// Check if theres any change in the dragging state or the dragged elements position
// Compare the current drag state with the previously stored one
const hasStateChanged =
prevDragState.current.isDragging !== isDraggingWidgets || // Drag started or stopped
prevDragState.current.x !== x || // X position changed
prevDragState.current.y !== y; // Y position changed
if (hasStateChanged) {
// Update the previous drag state with the latest values
prevDragState.current = { isDragging: isDraggingWidgets, x, y };
if (isDraggingWidgets) {
// Adjust the current drag coordinates relative to the initial widgets button position
const getXPosition = props?.signBtnPosition?.[0]
? x - props.signBtnPosition[0].xPos
: x;
const getYPosition = props?.signBtnPosition?.[0]
? y - props.signBtnPosition[0].yPos
: y;
// Get the default width and height of the dragged checkbox/radio widget type
const el = document.getElementById("checkbox&radio-preview");
let rect;
if (el) {
rect = el.getBoundingClientRect();
}
const widget = {
type: itemType?.text,
width: defaultWidthHeight(itemType?.text).width,
height: defaultWidthHeight(itemType?.text).height
};
const getWidth =
rect?.width || props?.posWidth(widget, props?.isSignYourself);
const getHeight =
rect?.height || props?.posHeight(widget, props?.isSignYourself);
// Display the alignment guidelines based on the current drag position and widget size
showGuidelines(true, getXPosition, getYPosition, getWidth, getHeight);
} else {
// Hide guidelines when dragging stops
showGuidelines(false);
}
}
}
}, [isDraggingWidgets, offset, itemType, showGuidelines]);
}
export default DragGuideLinesLayer;
@@ -7,7 +7,6 @@ import {
fontColorArr,
fontsizeArr,
getContainerScale,
handleCopyNextToWidget,
handleHeighlightWidget,
onChangeInput,
radioButtonWidget,
@@ -15,13 +14,13 @@ import {
cellsWidget,
textWidget,
selectFormat,
randomId,
getYear,
getMonth,
months,
years,
getDefaultDate,
getDefaultFormat
getDefaultFormat,
handleBackground
} from "../../constant/Utils";
import PlaceholderType from "./PlaceholderType";
import moment from "moment";
@@ -29,10 +28,7 @@ import "../../styles/opensigndrive.css";
import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import {
setIsShowModal,
setPrefillImg
} from "../../redux/reducers/widgetSlice";
import { setIsShowModal } from "../../redux/reducers/widgetSlice";
import { themeColor } from "../../constant/const";
import { useGuidelinesContext } from "../../context/GuidelinesContext";
import DatePicker from "react-datepicker";
@@ -40,7 +36,6 @@ import DatePicker from "react-datepicker";
function Placeholder(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const { showGuidelines } = useGuidelinesContext();
const statusArr = ["Required", "Optional"];
const widgetData =
@@ -306,31 +301,8 @@ function Placeholder(props) {
) {
props.setUniqueId(props?.data?.Id);
}
if (props?.data?.Role !== "prefill") {
props.setIsPageCopy(true);
props.setCurrWidgetsDetails(props.pos);
} else {
const newId = randomId();
//function to create new widget next to just widget
handleCopyNextToWidget(
newId,
props.pos,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id
);
//condiiton is used to store copied prefill image base64 url in redux for display image
if (props?.pos?.type === "image") {
const getPrefillImg = prefillImg?.find((x) => x.id === props.pos.key);
dispatch(
setPrefillImg({
id: newId,
base64: getPrefillImg?.base64
})
);
}
}
props.setIsPageCopy(true);
props.setCurrWidgetsDetails(props.pos);
};
const setCellCount = (key, newCount) => {
@@ -594,7 +566,7 @@ function Placeholder(props) {
}
}
};
//function to calculate font size
//function to calculate font size
const calculateFont = (size, isMinHeight) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
@@ -624,22 +596,6 @@ function Placeholder(props) {
}
};
//function to handle widget background color
const handleBackground = () => {
if (props.data) {
if (props.isNeedSign) {
if (props.data?.Id === props?.uniqueId) {
return props.data?.blockColor + "b0";
} else {
return "#dedddc";
}
} else {
return props.data?.blockColor + "b0";
}
} else {
return "rgba(203, 233, 237, 0.69)";
}
};
const fontSize = calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
@@ -773,7 +729,11 @@ function Placeholder(props) {
background:
props?.data?.Role === "prefill"
? "transparent"
: handleBackground() //handle block color of widget for
: handleBackground(
props?.data,
props?.isNeedSign,
props?.uniqueId
) //handle block color of widget for
}}
onDrag={(_, d) => {
props?.handleTabDrag?.(props.pos.key);
@@ -2,9 +2,12 @@ import { useState } from "react";
import ModalUi from "../../primitives/ModalUi";
import { handleCopyNextToWidget, randomId } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
import { setPrefillImg } from "../../redux/reducers/widgetSlice";
import { useDispatch } from "react-redux";
function PlaceholderCopy(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
const copyType = [
{ id: 1, type: "All pages" },
{ id: 2, type: "All pages but last" },
@@ -121,7 +124,7 @@ function PlaceholderCopy(props) {
);
for (let i = 0; i < props.allPages; i++) {
const newId = randomId();
const nameId = randomId(2)
const nameId = randomId(2);
const widgetName = `${currentPlaceholder?.options?.name}${nameId}`;
const newPlaceholder = {
...currentPlaceholder,
@@ -134,7 +137,6 @@ function PlaceholderCopy(props) {
);
const existPlaceholderPosition =
existPlaceholder[0] && existPlaceholder[0].pos;
//function for get copy to requested location of placeholder position
const getPlaceholderObj = getCopyPlaceholderPosition(
type,
@@ -146,6 +148,18 @@ function PlaceholderCopy(props) {
newPlaceholderPosition.push(getPlaceholderObj);
}
newPageNumber++;
if (
filterSignerPosition[0]?.Role === "prefill" &&
currentPlaceholder?.options?.response &&
currentPlaceholder?.type === "image"
) {
dispatch(
setPrefillImg({
id: newId,
base64: currentPlaceholder?.options?.response
})
);
}
}
let updatedSignerPlaceholder;
if (props?.signerObjId) {
@@ -240,6 +254,18 @@ function PlaceholderCopy(props) {
props.setXyPosition,
props?.Id
);
if (
filterSignerPosition[0]?.Role === "prefill" &&
currentXYposition?.options?.response &&
currentXYposition?.type === "image"
) {
dispatch(
setPrefillImg({
id: newId,
base64: currentXYposition?.options?.response
})
);
}
} else {
const getIndex = props?.xyPosition.findIndex(
(data) => data.pageNumber === props.pageNumber
+28 -3
View File
@@ -22,19 +22,25 @@ import { useDispatch, useSelector } from "react-redux";
import Guidelines from "./Guidelines";
import { useGuidelinesContext } from "../../context/GuidelinesContext";
import { toggleSidebar } from "../../redux/reducers/sidebarReducer";
import DragGuidelinesLayer from "./DragGridLinesLayer";
import { useDrop } from "react-dnd";
import WidgetsDragPreview from "./WidgetsDragPreview";
function RenderPdf(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
const [, drop] = useDrop({
accept: "BOX",
drop: (item, monitor) => props?.addPositionOfSignature(item, monitor)
});
const [scaledHeight, setScaledHeight] = useState();
const { guideline } = useGuidelinesContext();
const { guideline, showGuidelines } = useGuidelinesContext();
//check isGuestSigner is present in local if yes than handle login flow header in mobile view
const isGuestSigner = localStorage.getItem("isGuestSigner");
const scrollTriggerId = useSelector((state) => state.widget.scrollTriggerId);
const isOpen = useSelector((state) => state.sidebar.isOpen);
const scrollRef = useRef(null);
const pdfContainerRef = useRef(null);
useEffect(() => {
dispatch(toggleSidebar(false));
return () => {
@@ -335,7 +341,7 @@ function RenderPdf(props) {
}}
ref={(node) => {
pdfContainerRef.current = node;
props.drop && props.drop(node);
drop && drop(node);
}}
id="container"
>
@@ -532,6 +538,25 @@ function RenderPdf(props) {
y2={guideline.y2}
/>
)}
<DragGuidelinesLayer
signBtnPosition={props.signBtnPosition}
showGuidelines={showGuidelines}
posWidth={posWidth}
posHeight={posHeight}
isSignYourself={props.signerPos?.length > 0 ? true : false}
/>
<WidgetsDragPreview
posWidth={posWidth}
posHeight={posHeight}
isSignYourself={props.signerPos?.length > 0 ? true : false}
uniqueId={props?.uniqueId}
data={props?.signerPos?.find((x) => x.Id === props?.uniqueId)}
isNeedSign={props?.pdfRequest}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
containerWH={props.containerWH}
scale={props?.scale}
/>
</div>
</RSC>
</>
@@ -1,7 +1,6 @@
import React, { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect } from "react";
import ModalUi from "../../primitives/ModalUi";
import RecipientList from "./RecipientList";
import { useDrag } from "react-dnd";
import WidgetList from "./WidgetList";
import {
isMobile,
@@ -11,106 +10,54 @@ import {
textWidget,
widgets
} from "../../constant/Utils";
import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next";
import { useWidgetDrag } from "../../hook/useWidgetDrag";
function WidgetComponent(props) {
const { t } = useTranslation();
const signRef = useRef(null);
const userInformation = localStorage.getItem("UserInformation");
const [isSignersModal, setIsSignersModal] = useState(false);
const [, signature] = useDrag({
type: "BOX",
item: { type: "BOX", id: 1, text: "signature" }
});
const [, stamp] = useDrag({
type: "BOX",
item: { type: "BOX", id: 2, text: "stamp" }
});
const [, dropdown] = useDrag({
type: "BOX",
item: { type: "BOX", id: 5, text: "dropdown" }
});
const [, checkbox] = useDrag({
type: "BOX",
item: { type: "BOX", id: 6, text: "checkbox" }
});
const [, textInput] = useDrag({
type: "BOX",
item: { type: "BOX", id: 7, text: textInputWidget }
});
const [, cells] = useDrag({
type: "BOX",
item: { type: "BOX", id: 17, text: cellsWidget }
});
const [, initials] = useDrag({
type: "BOX",
item: { type: "BOX", id: 8, text: "initials" }
});
const [, name] = useDrag({
type: "BOX",
item: { type: "BOX", id: 9, text: "name" }
});
const [, company] = useDrag({
type: "BOX",
item: { type: "BOX", id: 10, text: "company" }
});
const [, jobTitle] = useDrag({
type: "BOX",
item: { type: "BOX", id: 11, text: "job title" }
});
const [, date] = useDrag({
type: "BOX",
item: { type: "BOX", id: 12, text: "date" }
});
const [, image] = useDrag({
type: "BOX",
item: { type: "BOX", id: 13, text: "image" }
});
const [, email] = useDrag({
type: "BOX",
item: { type: "BOX", id: 14, text: "email" }
});
const [, radioButton] = useDrag({
type: "BOX",
item: { type: "BOX", id: 15, text: radioButtonWidget }
});
const [, text] = useDrag({
type: "BOX",
item: { type: "BOX", id: 16, text: textWidget }
});
const [widget, setWidget] = useState([]);
// Define all draggable widget configurations
const draggableItems = [
{ id: 1, text: "signature" },
{ id: 2, text: "stamp" },
{ id: 3, text: "initials" },
{ id: 4, text: textInputWidget },
{ id: 6, text: "name" },
{ id: 7, text: "job title" },
{ id: 8, text: "company" },
{ id: 9, text: "email" },
{ id: 10, text: "date" },
{ id: 11, text: textWidget },
{ id: 12, text: cellsWidget },
{ id: 13, text: "checkbox" },
{ id: 14, text: "dropdown" },
{ id: 15, text: radioButtonWidget },
{ id: 16, text: "image" }
];
// Create all drag refs in one go
const widgetRefs = draggableItems.map((item) =>
useWidgetDrag({ type: "BOX", ...item })
);
// Map your widgets with the generated dragRefs
const [widgetList, setWidgetList] = useState([]);
const handleModal = () => {
setIsSignersModal(!isSignersModal);
};
useEffect(() => {
const widgetRef = [
signature,
stamp,
initials,
textInput,
name,
jobTitle,
company,
email,
date,
text,
cells,
checkbox,
dropdown,
radioButton,
image
];
const getWidgetArray = widgets;
const newUpdateSigner = getWidgetArray.map((obj, ind) => {
return { ...obj, ref: widgetRef[ind] };
});
setWidget(newUpdateSigner);
const updated = widgets.map((obj, index) => ({
...obj,
ref: widgetRefs[index]?.dragRef || null
}));
setWidgetList(updated);
// eslint-disable-next-line
}, []);
// allow only (signature, stamp, initials, text, name, job title, company, email, cells) widget when isAllowModification true and user have session token
const modifiedWidgets = widget.filter(
const modifiedWidgets = widgetList.filter(
(data) =>
![
"dropdown",
@@ -122,7 +69,7 @@ function WidgetComponent(props) {
].includes(data.type)
);
// allow only (signature, stamp, initials, text, cells) widget when isAllowModification true and user does not have session token
const unlogedInUserWidgets = widget.filter(
const unlogedInUserWidgets = widgetList.filter(
(data) =>
![
"dropdown",
@@ -137,13 +84,13 @@ function WidgetComponent(props) {
"company"
].includes(data.type)
);
const selfSignWidgets = widget.filter(
const selfSignWidgets = widgetList.filter(
(data) =>
!["dropdown", radioButtonWidget, textInputWidget].includes(data.type)
);
//if user select prefill role then allow only date,image,text,checkbox,radio,dropdownAdd commentMore actions
//dropdown widget should only be show in template flow
const prefillAllowWidgets = widget.filter((data) =>
const prefillAllowWidgets = widgetList.filter((data) =>
(props.isPrefillDropdown ? ["dropdown"] : [])
.concat([radioButtonWidget, textWidget, "date", "image", "checkbox"])
.includes(data.type)
@@ -161,7 +108,7 @@ function WidgetComponent(props) {
return unlogedInUserWidgets;
}
} else if (props?.roleName !== "prefill") {
return widget.filter((data) => ![textWidget].includes(data.type));
return widgetList.filter((data) => ![textWidget].includes(data.type));
}
};
const handleSelectRecipient = () => {
@@ -0,0 +1,105 @@
import { useDragLayer } from "react-dnd";
import {
defaultWidthHeight,
getContainerScale,
handleBackground,
radioButtonWidget
} from "../../constant/Utils";
export default function WidgetsDragPreview(props) {
const { widgetType, isDragging, initialOffset, currentOffset } = useDragLayer(
(monitor) => ({
widgetType: monitor.getItem(),
isDragging: monitor.isDragging(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset()
})
);
const options = [{ name: "Option-1" }, { name: "Option-2" }];
if (!isDragging) return null;
if (!initialOffset || !currentOffset) return null;
const { x, y } = currentOffset;
const getWidgetDetails = defaultWidthHeight(widgetType?.text);
const widget = {
type: widgetType?.text,
width: getWidgetDetails?.width,
height: getWidgetDetails?.height
};
const calculateFont = () => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const fontSize = 12 * containerScale * props.scale;
return fontSize;
};
return (
<div className="fixed inset-0 pointer-events-none z-[9999]">
<div
className="absolute"
style={{
transform: `translate(${x}px, ${y}px)`
}}
>
{widgetType.text === "checkbox" ||
widgetType.text === radioButtonWidget ? (
<div
id="checkbox&radio-preview"
style={{
background: handleBackground(
props?.data,
props?.isNeedSign,
props?.uniqueId
)
}}
className="border-red-600 rounded-[2px] border-[1px]"
>
{options.map((x, id) => {
return (
<div key={id}>
<label
style={{ fontSize: calculateFont(), color: "black" }}
className={`mb-0 flex items-center gap-1`}
>
<input
style={{
width: calculateFont(),
height: calculateFont()
}}
className={`${widgetType.text === "checkbox" ? "op-checkbox" : "op-radio rounded-full border-black appearance-none bg-white inline-block align-middle relative"} rounded-[1px]`}
type={
widgetType.text === "checkbox" ? "checkbox" : "radio"
}
/>
<span>{x?.name}</span>
</label>
</div>
);
})}
</div>
) : (
<div
style={{
width: props?.posWidth(widget, props?.isSignYourself),
height: props?.posHeight(widget, props?.isSignYourself),
background: handleBackground(
props?.data,
props?.isNeedSign,
props?.uniqueId
)
}}
className="border-red-600 rounded-[2px] border-[1px] flex items-center justify-center"
>
<span className="text-center text-[10px] font-medium">
{widgetType?.text}
</span>
</div>
)}
</div>
</div>
);
}
@@ -10,13 +10,13 @@ const getWidgetType = (item, widgetName) => {
<div className="w-full h-full flex md:justify-between items-center">
<div className="flex justify-start items-center ml-1 overflow-hidden">
{!isMobile && (
<i className="fa-light fa-grip-vertical ml-[3px] text-[13px]"></i>
<i className="fa-light fa-grip-vertical ml-[3px] text-[13px] lg:text-[13px]"></i>
)}
<span className="md:inline-block text-center text-[11px] mx-[3px] truncate font-medium capitalize">
<span className="md:inline-block text-center text-[11px] lg:text-[13px] mx-[3px] truncate font-medium capitalize">
{widgetName}
</span>
</div>
<div className="text-[13px] op-btn op-btn-primary rounded-none w-[20px] h-full flex justify-center items-center">
<div className="text-[13px] lg:text-[14px] op-btn op-btn-primary rounded-none w-[20px] h-full flex justify-center items-center">
<i className={item.icon}></i>
</div>
</div>
@@ -0,0 +1,368 @@
import { useState, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import Parse from "parse";
import ReactQuill from "react-quill-new";
import "../../styles/quill.css";
import EditorToolbar, { module1, module2, formats } from "../pdf/EditorToolbar";
import Tooltip from "../../primitives/Tooltip";
import Alert from "../../primitives/Alert";
import Loader from "../../primitives/Loader";
const MailTemplateEditor = ({
info,
tenantId,
}) => {
const appName = localStorage.getItem("appname") || "OpenSign™";
const { t } = useTranslation();
const editorRef = useRef();
const editorRefCom = useRef();
const [requestBody, setRequestBody] = useState("");
const [requestSubject, setRequestSubject] = useState("");
const [completionBody, setCompletionBody] = useState("");
const [completionSubject, setCompletionSubject] = useState("");
const [isDefaultMail, setIsDefaultMail] = useState({
requestMail: false,
completionMail: false
});
const [isMailLoader, setIsMailLoader] = useState({
request: false,
completion: false
});
const [isalert, setIsAlert] = useState({ type: "success", msg: "" });
const defaultRequestSubject = `{{sender_name}} has requested you to sign {{document_title}}`;
const defaultRequestBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}}&nbsp;has requested you to review and sign&nbsp;{{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p><a href='{{signing_url}}' rel='noopener noreferrer' target='_blank'>Sign here</a></p><br><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
const defaultCompletionSubject = `Document {{document_title}} has been signed by all parties`;
const defaultCompletionBody = `<p>Hi {{sender_name}},</p><br><p>All parties have successfully signed the document {{document_title}}. Kindly download the document from the attachment.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
const cloudfunction =
"updatetenant";
useEffect(() => {
fetchSubscription();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
info
]);
const handleModifyMail = (mode) => {
mode === "request"
? setIsDefaultMail((p) => ({ ...p, requestMail: !p?.requestMail }))
: setIsDefaultMail((p) => ({ ...p, completionMail: !p?.completionMail }));
};
const fetchSubscription = async () => {
await tenantEmailTemplate(info);
};
const tenantEmailTemplate = async (tenantRes) => {
if (tenantRes === "user does not exist!") {
alert(t("user-not-exist"));
} else if (tenantRes) {
const updateRes = tenantRes;
const defaultRequestBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}}&nbsp;has requested you to review and sign&nbsp;{{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p><a href='{{signing_url}}' rel='noopener noreferrer' target='_blank'>Sign here</a></p><br><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
if (updateRes?.RequestBody) {
setRequestBody(updateRes?.RequestBody);
setRequestSubject(updateRes?.RequestSubject);
setIsDefaultMail((prev) => ({ ...prev, requestMail: false }));
} else {
setRequestBody(defaultRequestBody);
setRequestSubject(defaultRequestSubject);
setIsDefaultMail((prev) => ({ ...prev, requestMail: true }));
}
if (updateRes?.CompletionBody) {
setCompletionBody(updateRes?.CompletionBody);
setCompletionSubject(updateRes?.CompletionSubject);
setIsDefaultMail((prev) => ({ ...prev, completionMail: false }));
} else {
setCompletionBody(defaultCompletionBody);
setCompletionSubject(defaultCompletionSubject);
setIsDefaultMail((prev) => ({ ...prev, completionMail: true }));
}
}
};
//function to save completion email template
const handleSaveCompletionEmail = async (e) => {
e.preventDefault();
try {
const replacedHtmlBody = completionBody.replace(/"/g, "'");
const htmlBody = `<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>${replacedHtmlBody}</body></html>`;
const updateTenant = await Parse.Cloud.run(cloudfunction, {
tenantId: tenantId,
details: {
CompletionBody: htmlBody,
CompletionSubject: completionSubject
}
});
if (updateTenant) {
const updateRes = JSON.parse(JSON.stringify(updateTenant));
setCompletionBody(updateRes?.CompletionBody);
setCompletionSubject(updateRes?.CompletionSubject);
setIsAlert({ type: "success", msg: t("saved-successfully") });
setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500);
}
} catch (err) {
console.error("Error while saving completion email template: ", err);
setIsAlert({ type: "danger", msg: t("something-went-wrong-mssg") });
setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500);
}
};
//function to save request email template
const handleSaveRequestEmail = async (e) => {
e.preventDefault();
try {
const replacedHtmlBody = requestBody.replace(/"/g, "'");
const htmlBody = `<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>${replacedHtmlBody}</body></html>`;
const updateTenant = await Parse.Cloud.run(cloudfunction, {
tenantId: tenantId,
details: { RequestBody: htmlBody, RequestSubject: requestSubject }
});
if (updateTenant) {
const updateRes = JSON.parse(JSON.stringify(updateTenant));
setRequestBody(updateRes?.RequestBody);
setRequestSubject(updateRes?.RequestSubject);
let extUser =
localStorage.getItem("Extand_Class") &&
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
if (extUser && extUser?.objectId) {
extUser.TenantId.RequestBody = updateRes?.RequestBody;
extUser.TenantId.RequestSubject = updateRes?.RequestSubject;
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
setIsAlert({ type: "success", msg: t("saved-successfully") });
setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500);
}
} catch (err) {
console.error("Error while saving request email template: ", err);
setIsAlert({ type: "danger", msg: t("something-went-wrong-mssg") });
setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500);
}
};
//function to use reset form
const handleReset = async (request, completion) => {
let extUser =
localStorage.getItem("Extand_Class") &&
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
handleModifyMail(request);
if (request && !isDefaultMail?.requestMail) {
setRequestBody(defaultRequestBody);
setRequestSubject(defaultRequestSubject);
setIsMailLoader((p) => ({ ...p, request: true }));
try {
await Parse.Cloud.run(cloudfunction, {
tenantId: tenantId,
details: { RequestBody: "", RequestSubject: "" }
});
if (extUser && extUser?.objectId) {
extUser.TenantId.RequestBody = "";
extUser.TenantId.RequestSubject = "";
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
} catch (err) {
console.error("Error while resetting request mail: ", err);
} finally {
setIsMailLoader((p) => ({ ...p, request: false }));
}
} else if (completion && !isDefaultMail?.completionMail) {
setCompletionSubject(defaultCompletionSubject);
setCompletionBody(defaultCompletionBody);
setIsMailLoader((p) => ({ ...p, completion: true }));
try {
await Parse.Cloud.run(cloudfunction, {
tenantId: tenantId,
details: { CompletionBody: "", CompletionSubject: "" }
});
if (extUser && extUser?.objectId) {
extUser.TenantId.CompletionBody = "";
extUser.TenantId.CompletionSubject = "";
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
} catch (err) {
console.error("Error while resetting completion mail: ", err);
} finally {
setIsMailLoader((p) => ({ ...p, completion: false }));
}
}
};
//function for handle ontext change and save again text in delta
const handleOnchangeRequest = () => {
if (editorRef.current) {
const html = editorRef.current.editor.root.innerHTML;
setRequestBody(html);
}
};
const handleOnchangeCompletion = () => {
if (editorRefCom.current) {
const html = editorRefCom.current.editor.root.innerHTML;
setCompletionBody(html);
}
};
return (
<>
{isalert.msg && <Alert type={isalert.type}>{isalert.msg}</Alert>}
<div className="flex flex-col mb-4">
<div className="flex flex-col">
<h1 className="text-[14px] mb-[0.7rem] font-medium">
{t("request-email")}
</h1>
<div className="relative mt-2 mb-4">
{isMailLoader.request && (
<div className="flex z-[100] justify-center items-center absolute w-full h-full rounded-box bg-black/30">
<Loader />
</div>
)}
{
isDefaultMail?.requestMail && (
<div className="absolute backdrop-blur-[2px] flex w-full h-full justify-center items-center bg-black/10 rounded-box select-none z-20">
<button
onClick={() => handleModifyMail("request")}
className="op-btn op-btn-primary shadow-lg"
>
{t("modify")}
</button>
</div>
)
}
<form
onSubmit={handleSaveRequestEmail}
className="p-3 border-[1px] border-base-content rounded-box"
>
<div className="text-lg font-normal">
<label className="text-sm">
{t("subject")}{" "}
<Tooltip
id={"request-sub-tooltip"}
message={`${t("variables-use")}: {{sender_name}} {{document_title}}`}
/>
</label>
<input
required
value={requestSubject}
onChange={(e) => setRequestSubject(e.target.value)}
placeholder={`{{sender_name}} ${t("send-to-sign")} {{document_title}}`}
className="w-full op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content text-xs"
/>
</div>
<div className="text-lg font-normal py-2">
<label className="text-sm mt-3">
{t("body")}{" "}
<Tooltip
id={"request-body-tooltip"}
message={`${t("variables-use")}: {{sender_name}} {{document_title}}`}
/>
</label>
<EditorToolbar containerId="toolbar1" />
<ReactQuill
theme="snow"
value={requestBody}
placeholder="add body of email"
ref={editorRef}
modules={module1}
formats={formats}
onChange={handleOnchangeRequest}
/>
</div>
<div className="flex items-center mt-3 gap-2">
<button
disabled={!requestBody || !requestSubject}
className="op-btn op-btn-primary"
type="submit"
>
{t("save")}
</button>
<button
type="button"
className="op-btn op-btn-secondary"
onClick={() => handleReset("request")}
>
{t("reset")}
</button>
</div>
</form>
</div>
<h1 className="text-[14px] mb-[0.7rem] font-medium">
{t("completion-email")}
</h1>
<div className="relative my-2">
{isMailLoader.completion && (
<div className="flex z-[100] justify-center items-center absolute w-full h-full rounded-box bg-black/30">
<Loader />
</div>
)}
{
isDefaultMail?.completionMail && (
<div className="absolute backdrop-blur-[2px] flex w-full h-full justify-center items-center bg-black/10 rounded-box select-none z-20">
<button
onClick={() => handleModifyMail("completion")}
className="op-btn op-btn-primary shadow-lg"
>
{t("modify")}
</button>
</div>
)
}
<form
onSubmit={handleSaveCompletionEmail}
className="p-3 border-[1px] border-base-content rounded-box"
>
<div className="text-lg font-normal">
<label className="text-sm">
{t("subject")}{" "}
<Tooltip
id={"complete-sub-tooltip"}
message={`${t("variables-use")}:{{sender_name}} {{document_title}}`}
/>
</label>
<input
required
value={completionSubject}
onChange={(e) => setCompletionSubject(e.target.value)}
placeholder={`{{sender_name}} ${t("send-to-sign")} {{document_title}}`}
className="w-full op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content text-xs"
/>
</div>
<div className="text-lg font-normal py-2">
<label className="text-sm mt-3">
{t("body")}{" "}
<Tooltip
id={"complete-body-tooltip"}
message={`${t("variables-use")}:{{sender_name}} {{document_title}} {{signing_url}}`}
/>
</label>
<EditorToolbar containerId="toolbar2" />
<ReactQuill
theme="snow"
value={completionBody}
placeholder="add body of email"
ref={editorRefCom}
modules={module2}
formats={formats}
onChange={handleOnchangeCompletion}
/>
</div>
<div className="flex items-center mt-3 gap-2">
<button
disabled={!completionBody || !completionSubject}
className="op-btn op-btn-primary"
type="submit"
>
{t("save")}
</button>
<button
type="button"
className="op-btn op-btn-secondary"
onClick={() => handleReset(null, "completion")}
>
{t("reset")}
</button>
</div>
</form>
</div>
</div>
</div>
</>
);
};
export default MailTemplateEditor;
+88 -50
View File
@@ -2980,34 +2980,34 @@ export const saveLanguageInLocal = (i18n) => {
const detectedLanguage = i18n.language || "en";
localStorage.setItem("i18nextLng", detectedLanguage);
};
//function to get default signatur eof current user from `contracts_Signature` class
// function to get default signature of current user from `contracts_Signature` class
export const getDefaultSignature = async (objectId) => {
try {
const query = new Parse.Query("contracts_Signature");
query.equalTo("UserId", {
__type: "Pointer",
className: "_User",
objectId: objectId
});
if (objectId) {
const result = await Parse.Cloud.run("getdefaultsignature", {
userId: objectId
});
if (result) {
const res = JSON.parse(JSON.stringify(result));
const defaultSignature = res?.ImageURL
? await getBase64FromUrl(res?.ImageURL, true)
: "";
const defaultInitial = res?.Initials
? await getBase64FromUrl(res?.Initials, true)
: "";
const result = await query.first();
if (result) {
const res = JSON.parse(JSON.stringify(result));
const defaultSignature = res?.ImageURL
? await getBase64FromUrl(res?.ImageURL, true)
: "";
const defaultInitial = res?.Initials
? await getBase64FromUrl(res?.Initials, true)
: "";
return {
status: "success",
res: {
id: result?.id,
defaultSignature: defaultSignature,
defaultInitial: defaultInitial
}
};
return {
status: "success",
res: {
id: result?.id,
defaultSignature: defaultSignature,
defaultInitial: defaultInitial
}
};
}
} else {
return { status: "error" };
}
} catch (err) {
console.log(
@@ -3430,39 +3430,49 @@ export const updateDateWidgetsRes = (
const contactUser = documentData?.Signers?.find(
(data) => data.objectId === signerId
);
const extUser = JSON.stringify([contactUser]);
const extUser =
localStorage.getItem("Extand_Class") || JSON.stringify([contactUser]);
let placeHolders = documentData?.Placeholders;
if (isRemovePrefill) {
placeHolders = placeHolders?.filter((x) => x.Role !== "prefill");
}
const userDetails = extUser ? JSON.parse(extUser)[0] : contactUser;
return placeHolders?.map((item) => {
if (item?.signerObjId === signerId) {
return {
...item,
placeHolder: item?.placeHolder?.map((ph) => ({
if (item?.signerObjId === signerId || item?.Id === signerId) {
// Sort page number placeholders
const sortedPlaceHolder = [...item.placeHolder]
.sort((a, b) => a.pageNumber - b.pageNumber)
.map((ph) => ({
...ph,
pos: ph?.pos?.map((widget) => {
if (
["name", "email", "job title", "company"].includes(widget.type) &&
!widget.options.defaultValue &&
!widget.options.response
) {
return {
...widget,
options: {
...widget.options,
response: widgetDataValue(widget.type, userDetails)
}
};
}
return widget;
})
}))
};
} else {
return item;
// Sort positions within each page
pos: [...ph.pos]
.sort((a, b) => a.yPosition - b.yPosition)
.map((widget) => {
// Update widget values if needed
if (
["name", "email", "job title", "company"].includes(
widget.type
) &&
!widget.options.defaultValue &&
!widget.options.response &&
userDetails
) {
return {
...widget,
options: {
...widget.options,
response: widgetDataValue(widget.type, userDetails)
}
};
}
return widget;
})
}));
return { ...item, placeHolder: sortedPlaceHolder };
}
return item;
});
};
@@ -4038,3 +4048,31 @@ export const getDefaultDate = (dateStr, format) => {
};
//function to get default format
export const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
export function generateId(length) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
//function to handle widget background color
export const handleBackground = (data, isNeedSign, uniqueId) => {
if (data) {
if (isNeedSign) {
if (data?.Id === uniqueId) {
return data?.blockColor + "b0";
} else {
return "#dedddc";
}
} else {
return data?.blockColor + "b0";
}
} else {
return "rgba(203, 233, 237, 0.69)";
}
};
+25
View File
@@ -0,0 +1,25 @@
import { useEffect } from "react";
import { useDrag } from "react-dnd";
import { getEmptyImage } from "react-dnd-html5-backend";
import { useGuidelinesContext } from "../context/GuidelinesContext";
export const useWidgetDrag = (item) => {
const { showGuidelines } = useGuidelinesContext();
const [, dragRef, preview] = useDrag({
type: "BOX",
item,
collect: (monitor) => ({
isDragging: monitor.isDragging()
}),
end: () => {
showGuidelines(false);
}
});
// Hide browser default drag preview
useEffect(() => {
preview(getEmptyImage(), { captureDraggingState: true });
}, [preview]);
return { dragRef };
};
+10 -7
View File
@@ -388,13 +388,16 @@ const Forms = (props) => {
object.set("Description", formData?.Description);
object.set("Note", formData?.Note);
if (props.title === "Request Signatures") {
if (
extUserData?.TenantId?.RequestBody &&
extUserData?.TenantId?.RequestSubject
) {
object.set("RequestBody", extUserData?.TenantId?.RequestBody);
object.set("RequestSubject", extUserData?.TenantId?.RequestSubject);
}
if (
extUserData?.TenantId?.RequestBody &&
extUserData?.TenantId?.RequestSubject
) {
object.set("RequestBody", extUserData?.TenantId?.RequestBody);
object.set(
"RequestSubject",
extUserData?.TenantId?.RequestSubject
);
}
}
if (props.title !== "Sign Yourself") {
const isChecked = formData.SendinOrder === "false" ? false : true;
+5 -1
View File
@@ -47,10 +47,14 @@ function Login() {
const [image, setImage] = useState();
const [errMsg, setErrMsg] = useState();
useEffect(() => {
checkUserExt();
handleUserExist();
// eslint-disable-next-line
}, []);
const handleUserExist = async () => {
checkUserExt();
};
const setLocalVar = (user) => {
localStorage.setItem("accesstoken", user.sessionToken);
+20 -42
View File
@@ -46,10 +46,9 @@ const ManageSign = () => {
objectId: User.id
};
try {
const signCls = "contracts_Signature";
const signQuery = new Parse.Query(signCls);
signQuery.equalTo("UserId", userId);
const signRes = await signQuery.first();
const signRes = await Parse.Cloud.run("getdefaultsignature", {
userId: User.id
});
if (signRes) {
const res = signRes.toJSON();
setId(res.objectId);
@@ -226,44 +225,23 @@ const ManageSign = () => {
};
const saveEntry = async (obj) => {
const signCls = "contracts_Signature";
const User = Parse?.User?.current()?.id;
const userId = { __type: "Pointer", className: "_User", objectId: User };
if (id) {
try {
const updateSign = new Parse.Object(signCls);
updateSign.id = id;
updateSign.set("Initials", obj.initialsUrl ? obj.initialsUrl : "");
updateSign.set("ImageURL", obj.url ? obj.url : "");
updateSign.set("SignatureName", obj.name);
updateSign.set("UserId", userId);
const res = await updateSign.save();
setIsAlert({ type: "success", message: t("signature-saved-alert") });
return res;
} catch (err) {
console.log(err);
setIsAlert({ type: "danger", message: `${err.message}` });
} finally {
setIsLoader(false);
setTimeout(() => setIsAlert({}), 2000);
}
} else {
try {
const updateSign = new Parse.Object(signCls);
updateSign.set("Initials", obj.initialsUrl ? obj.initialsUrl : "");
updateSign.set("ImageURL", obj.url);
updateSign.set("SignatureName", obj.name);
updateSign.set("UserId", userId);
const res = await updateSign.save();
setIsAlert({ type: "success", message: t("signature-saved-alert") });
return res;
} catch (err) {
console.log(err);
setIsAlert({ type: "success", message: `${err.message}` });
} finally {
setIsLoader(false);
setTimeout(() => setIsAlert({}), 2000);
}
try {
const User = Parse?.User?.current()?.id;
const res = await Parse.Cloud.run("managesign", {
signature: obj.url,
userId: User,
initials: obj.initialsUrl,
id: id,
title: obj.name
});
setIsAlert({ type: "success", message: t("signature-saved-alert") });
return res;
} catch (err) {
console.log(err);
setIsAlert({ type: "danger", message: `${err.message}` });
} finally {
setIsLoader(false);
setTimeout(() => setIsAlert({}), 2000);
}
};
+38 -21
View File
@@ -3,7 +3,6 @@ import { PDFDocument } from "pdf-lib";
import "../styles/signature.css";
import Parse from "parse";
import axios from "axios";
import { useDrop } from "react-dnd";
import { useDispatch, useSelector } from "react-redux";
import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import Tour from "../primitives/Tour";
@@ -49,7 +48,7 @@ import {
widgetDataValue,
getOriginalWH,
handleCheckResponse,
convertJpegToPng,
convertJpegToPng
} from "../constant/Utils";
import Header from "../components/pdf/PdfHeader";
import RenderPdf from "../components/pdf/RenderPdf";
@@ -156,16 +155,12 @@ function PdfRequestFiles(
const [assignedWidgetId, setAssignedWidgetId] = useState([]);
const [showSignPagenumber, setShowSignPagenumber] = useState([]);
const [owner, setOwner] = useState({});
const [tenant, setTenant] = useState({});
const [tenantMailTemplate, setTenantMailTemplate] = useState({
body: "",
subject: ""
});
const [isOptionalDetails, setIsOptionalDetails] = useState(false);
const [, drop] = useDrop({
accept: "BOX",
drop: (item, monitor) => addPositionOfSignature(item, monitor),
collect: (monitor) => ({ isOver: !!monitor.isOver() })
});
const isMobile = window.innerWidth < 767;
let isGuestSignFlow = false;
let sendmail;
@@ -250,6 +245,7 @@ function PdfRequestFiles(
const filterSignTypes = signatureType?.filter(
(x) => x.enabled === true
);
setTenant(tenantDetails || {});
if (tenantDetails?.RequestBody) {
setTenantMailTemplate({
body: tenantDetails?.RequestBody,
@@ -340,7 +336,6 @@ function PdfRequestFiles(
const getCurrentSigner = getSigners?.find(
(data) => data.UserId.objectId === jsonSender?.objectId
);
currUserId = getCurrentSigner?.objectId
? getCurrentSigner.objectId
: contactBookId ||
@@ -770,12 +765,16 @@ function PdfRequestFiles(
sendmail !== "false" &&
sendInOrder
) {
const mailBody =
tenantMailTemplate?.body;
const mailSubject =
tenantMailTemplate?.subject;
const requestBody =
updatedDoc.updatedPdfDetails?.[0]?.RequestBody ||
tenantMailTemplate?.body;
mailBody;
const requestSubject =
updatedDoc.updatedPdfDetails?.[0]?.RequestSubject ||
tenantMailTemplate?.subject;
mailSubject;
if (user) {
const expireDate = expiry;
const newDate = new Date(expireDate);
@@ -1099,10 +1098,11 @@ function PdfRequestFiles(
const userId =
pdfDetails?.[0].Signers?.find((x) => x.objectId === signerObjectId)
?.UserId?.objectId || jsonSender?.objectId;
const params = {
docId: pdfDetails?.[0].objectId,
reason: reason,
userId: userId
userId: userId,
};
await axios
.post(`${localStorage.getItem("baseUrl")}functions/declinedoc`, params, {
@@ -1488,7 +1488,10 @@ function PdfRequestFiles(
const widgetHeight =
defaultWidthHeight(dragTypeValue).height * containerScale;
const extUser = localStorage.getItem("Extand_Class");
const parseUser = extUser && JSON.parse(extUser)[0];
let parseUser =
(extUser && JSON.parse(extUser)[0]) ||
JSON.parse(localStorage.getItem("signer"));
const widgetValue = widgetDataValue(dragTypeValue, parseUser);
//adding and updating drop position in array when user drop signature button in div
if (item === "onclick") {
@@ -1530,12 +1533,16 @@ function PdfRequestFiles(
//`containerRect.top` The distance from the top of the viewport to the top of the element.
const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top;
const getXPosition = signBtnPosition[0]
? x - signBtnPosition[0].xPos
: x;
const getYPosition = signBtnPosition[0]
? y - signBtnPosition[0].yPos
: y;
let getXPosition = signBtnPosition[0] ? x - signBtnPosition[0].xPos : x;
let getYPosition = signBtnPosition[0] ? y - signBtnPosition[0].yPos : y;
// to avoid negative position values (half portion of widget should not be out of pdf container)
if (getXPosition < 0) {
getXPosition = 0;
}
if (getYPosition < 0) {
getYPosition = 0;
}
dropObj = {
xPosition: getXPosition / (containerScale * scale),
yPosition: getYPosition / (containerScale * scale),
@@ -1572,6 +1579,7 @@ function PdfRequestFiles(
);
const getPos = currentPagePosition?.pos;
const newSignPos = getPos.concat(dropData);
newSignPos.sort((a, b) => a.yPosition - b.yPosition);
let xyPos = { pageNumber: pageNumber, pos: newSignPos };
updatePlace.push(xyPos);
const updatesignerPos = signerPos.map((x) =>
@@ -1582,9 +1590,17 @@ function PdfRequestFiles(
//else condition to add placeholder widgets on multiple page first time
const updatesignerPos = signerPos.map((x) =>
x.Id === uniqueId && x?.placeHolder
? { ...x, placeHolder: [...x.placeHolder, placeHolder] }
? {
...x,
placeHolder: [...x.placeHolder, placeHolder].sort(
(a, b) => a.pageNumber - b.pageNumber
)
}
: x.Id === uniqueId
? { ...x, placeHolder: [placeHolder] }
? {
...x,
placeHolder: [placeHolder]
}
: x
);
setSignerPos(updatesignerPos);
@@ -2033,7 +2049,6 @@ function PdfRequestFiles(
{containerWH && (
<RenderPdf
setIsPageCopy={setIsPageCopy}
drop={drop}
pageNumber={pageNumber}
pdfOriginalWH={pdfOriginalWH}
pdfNewWidth={pdfNewWidth}
@@ -2071,6 +2086,8 @@ function PdfRequestFiles(
setIsReqSignTourDisabled={handleCloseTour}
currWidgetsDetails={currWidgetsDetails}
isShowModal={isShowModal}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
/>
)}
</div>
+26 -9
View File
@@ -3,7 +3,6 @@ import axios from "axios";
import Parse from "parse";
import "../styles/signature.css";
import { PDFDocument } from "pdf-lib";
import { useDrop } from "react-dnd";
import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import WidgetComponent from "../components/pdf/WidgetComponent";
import Tour from "../primitives/Tour";
@@ -164,11 +163,6 @@ function PlaceHolderSign() {
const [docTitle, setDocTitle] = useState("");
const [isEditDoc, setIsEditDoc] = useState(false);
const isMobile = window.innerWidth < 767;
const [, drop] = useDrop({
accept: "BOX",
drop: (item, monitor) => addPositionOfSignature(item, monitor),
collect: (monitor) => ({ isOver: !!monitor.isOver() })
});
const currentUser = localStorage.getItem(
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
);
@@ -571,12 +565,33 @@ function PlaceHolderSign() {
//`containerRect.top` The distance from the top of the viewport to the top of the element.
const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top;
const getXPosition = signBtnPosition[0]
let getXPosition = signBtnPosition[0]
? x - signBtnPosition[0].xPos
: x;
const getYPosition = signBtnPosition[0]
let getYPosition = signBtnPosition[0]
? y - signBtnPosition[0].yPos
: y;
// to avoid negative position values (half portion of widget should not be out of pdf container)
const calculateWidth =
getXPosition + widgetWidth - containerRect.width;
const calculateHeight =
getYPosition + widgetHeight - containerRect.height;
if (getXPosition < 0) {
getXPosition = 0;
}
if (getYPosition < 0) {
getYPosition = 0;
}
if (calculateWidth > 0) {
getXPosition = getXPosition - calculateWidth;
}
if (calculateHeight > 0) {
getYPosition = getYPosition - calculateHeight;
}
dropObj = {
xPosition: getXPosition / (containerScale * scale),
yPosition: getYPosition / (containerScale * scale),
@@ -1931,6 +1946,7 @@ function PlaceHolderSign() {
return { ...x, ...data };
});
setPdfDetails(updateDocument);
setDocTitle(updateDocument?.[0]?.Name);
try {
const Bcc = updateDocument?.[0]?.Bcc?.length
? {
@@ -2332,7 +2348,6 @@ function PlaceHolderSign() {
numPages={numPages}
pageDetails={pageDetails}
placeholder={true}
drop={drop}
handleDeleteWidget={handleDeleteWidget}
handleTabDrag={handleTabDrag}
handleStop={handleStop}
@@ -2368,6 +2383,8 @@ function PlaceHolderSign() {
currWidgetsDetails={currWidgetsDetails}
setRoleName={setRoleName}
isShowModal={isShowModal}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
/>
)}
</div>
+50 -369
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef } from "react";
import React, { useEffect, useState } from "react";
import Alert from "../primitives/Alert";
import { useTranslation } from "react-i18next";
import Loader from "../primitives/Loader";
@@ -6,37 +6,24 @@ import Tooltip from "../primitives/Tooltip";
import {
getTenantDetails,
handleSignatureType,
openInNewTab,
signatureTypes,
usertimezone
} from "../constant/Utils";
import Parse from "parse";
import { Tooltip as ReactTooltip } from "react-tooltip";
import ReactQuill from "react-quill-new";
import "../styles/quill.css";
import EditorToolbar, {
module1,
module2,
formats
} from "../components/pdf/EditorToolbar";
import TimezoneSelector from "../components/preferences/TimezoneSelector";
import DateFormatSelector from "../components/preferences/DateFormatSelector";
import FilenameFormatSelector from "../components/preferences/FilenameFormatSelector";
import axios from "axios";
import MailTemplateEditor from "../components/preferences/MailTemplateEditor";
const Preferences = () => {
const appName =
"OpenSign™";
const { t } = useTranslation();
const editorRef = useRef();
const editorRefCom = useRef();
const Extand_Class = localStorage.getItem("Extand_Class");
const extClass = Extand_Class && JSON.parse(Extand_Class);
const [requestSubject, setRequestSubject] = useState("");
const [completionBody, SetCompletionBody] = useState("");
const [completionsubject, setCompletionSubject] = useState("");
const [requestBody, setRequestBody] = useState("");
const [defaultCompHtml, setDefaultCompHtml] = useState({});
const [tenantId, setTenantId] = useState("");
const [defaultReqHtml, setDefaultReqHtml] = useState({});
const [isalert, setIsAlert] = useState({ type: "success", msg: "" });
const [isTopLoader, setIsTopLoader] = useState(false);
const [isLoader, setIsLoader] = useState(false);
@@ -56,11 +43,9 @@ const Preferences = () => {
const [dateFormat, setDateFormat] = useState("MM/DD/YYYY");
const [is12HourTime, setIs12HourTime] = useState(false);
const [isLTVEnabled, setIsLTVEnabled] = useState(false);
const [isDefaultMail, setIsDefaultMail] = useState({
requestMail: false,
completionMail: false
});
const [fileNameFormat, setFileNameFormat] = useState("DOCNAME");
const [userInfo, setUserInfo] = useState({});
const [tenantInfo, setTenantInfo] = useState({});
useEffect(() => {
fetchSignType();
@@ -69,10 +54,10 @@ const Preferences = () => {
const fetchSignType = async () => {
setIsTopLoader(true);
const EmailTab =
extClass?.[0]?.UserRole === "contracts_Admin"
? [{ name: "email", title: t("email"), icon: "fa-light fa-envelope" }]
: [];
const EmailTab = [
{ name: "email", title: t("email"), icon: "fa-light fa-envelope" }
];
const arr = [
generaltab,
...EmailTab,
@@ -85,17 +70,29 @@ const Preferences = () => {
)
);
const tenantDetails = await getTenantDetails(user?.objectId);
await tenantEmailTemplate(tenantDetails);
setTenantInfo(tenantDetails);
const signatureType = tenantDetails?.SignatureType || [];
const tenantSignTypes = signatureType?.filter((x) => x.enabled === true);
const getUser = await Parse.Cloud.run("getUserDetails");
const extUser = await axios.post(
`${localStorage.getItem("baseUrl")}functions/getUserDetails`,
{},
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
}
);
const getUser = extUser.data?.result;
if (getUser) {
const _getUser = JSON.parse(JSON.stringify(getUser));
const notifyOnSignatures =
setUserInfo(_getUser);
setIsNotifyOnSignatures(
_getUser?.NotifyOnSignatures !== undefined
? _getUser?.NotifyOnSignatures
: true;
setIsNotifyOnSignatures(notifyOnSignatures);
: true
);
setTimezone(_getUser?.Timezone || usertimezone);
if (tenantSignTypes?.length > 0) {
const signatureType = _getUser?.SignatureType || signatureTypes;
@@ -108,31 +105,29 @@ const Preferences = () => {
const SignatureType = _getUser?.SignatureType || signatureTypes;
setSignatureType(SignatureType);
}
const sendinorder =
_getUser?.SendinOrder !== undefined ? _getUser?.SendinOrder : true;
setSendinOrder(sendinorder);
const istourenabled =
_getUser?.IsTourEnabled !== undefined
? _getUser?.IsTourEnabled
: true;
setIsTourEnabled(istourenabled);
const DateFormat =
setSendinOrder(
_getUser?.SendinOrder !== undefined ? _getUser?.SendinOrder : true
);
setIsTourEnabled(
_getUser?.IsTourEnabled !== undefined ? _getUser?.IsTourEnabled : true
);
setDateFormat(
_getUser?.DateFormat !== undefined
? _getUser?.DateFormat
: "MM/DD/YYYY";
setDateFormat(DateFormat);
const is12Hr =
_getUser?.Is12HourTime !== undefined ? _getUser?.Is12HourTime : false;
setIs12HourTime(is12Hr);
const isLTVEnabled =
_getUser?.IsLTVEnabled !== undefined ? _getUser?.IsLTVEnabled : false;
setIsLTVEnabled(isLTVEnabled);
: "MM/DD/YYYY"
);
setIs12HourTime(
_getUser?.Is12HourTime !== undefined ? _getUser?.Is12HourTime : false
);
setIsLTVEnabled(
_getUser?.IsLTVEnabled !== undefined ? _getUser?.IsLTVEnabled : false
);
const downloadFilenameFormat =
_getUser?.DownloadFilenameFormat || "DOCNAME";
setFileNameFormat(downloadFilenameFormat);
}
} catch (err) {
console.log("err while getting user details", err);
console.error("Error while getting user details: ", err);
setErrMsg(t("something-went-wrong-mssg"));
} finally {
setIsTopLoader(false);
@@ -215,7 +210,7 @@ const Preferences = () => {
}
}
} catch (err) {
console.log("Error updating signature type", err);
console.error("Error updating signature type: ", err);
setIsAlert({ type: "danger", msg: err.message });
}
@@ -228,178 +223,12 @@ const Preferences = () => {
const handleNotifySignChange = (value) => {
setIsNotifyOnSignatures(value);
};
const tenantEmailTemplate = async (tenantRes) => {
if (tenantRes === "user does not exist!") {
alert(t("user-not-exist"));
} else if (tenantRes) {
setIsLoader(true);
const updateRes = tenantRes;
setTenantId(updateRes?.objectId);
const defaultRequestBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}}&nbsp;has requested you to review and sign&nbsp;{{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p><a href='{{signing_url}}' rel='noopener noreferrer' target='_blank'>Sign here</a></p><br><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
if (updateRes?.RequestBody) {
setRequestBody(updateRes?.RequestBody);
setRequestSubject(updateRes?.RequestSubject);
} else {
setRequestBody(defaultRequestBody);
setRequestSubject(
`{{sender_name}} has requested you to sign {{document_title}}`
);
setIsDefaultMail((prev) => ({ ...prev, requestMail: true }));
}
setDefaultReqHtml({
body: defaultRequestBody,
subject: `{{sender_name}} has requested you to sign {{document_title}}`
});
const defaultCompletionBody = `<p>Hi {{sender_name}},</p><br><p>All parties have successfully signed the document {{document_title}}. Kindly download the document from the attachment.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
if (updateRes?.CompletionBody) {
SetCompletionBody(updateRes?.CompletionBody);
setCompletionSubject(updateRes?.CompletionSubject);
} else {
SetCompletionBody(defaultCompletionBody);
setCompletionSubject(
`Document {{document_title}} has been signed by all parties`
);
setIsDefaultMail((prev) => ({ ...prev, completionMail: true }));
}
setDefaultCompHtml({
body: defaultCompletionBody,
subject: `Document {{document_title}} has been signed by all parties`
});
setIsLoader(false);
}
};
//function to save completion email template
const handleSaveCompletionEmail = async (e) => {
e.preventDefault();
try {
setIsLoader(true);
const replacedHtmlBody = completionBody.replace(/"/g, "'");
const htmlBody = `<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>${replacedHtmlBody}</body></html>`;
const updateTenant = await Parse.Cloud.run("updatetenant", {
tenantId: tenantId,
details: {
CompletionBody: htmlBody,
CompletionSubject: completionsubject
}
});
if (updateTenant) {
const updateRes = JSON.parse(JSON.stringify(updateTenant));
SetCompletionBody(updateRes?.CompletionBody);
setCompletionSubject(updateRes?.CompletionSubject);
setIsAlert({ type: "success", msg: t("saved-successfully") });
setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500);
}
} catch (err) {
console.log("Err", err);
setIsAlert({ type: "danger", msg: t("something-went-wrong-mssg") });
setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500);
} finally {
setIsLoader(false);
}
};
//function to save request email template
const handleSaveRequestEmail = async (e) => {
e.preventDefault();
try {
setIsLoader(true);
const replacedHtmlBody = requestBody.replace(/"/g, "'");
const htmlBody = `<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>${replacedHtmlBody}</body></html>`;
const updateTenant = await Parse.Cloud.run("updatetenant", {
tenantId: tenantId,
details: {
RequestBody: htmlBody,
RequestSubject: requestSubject
}
});
if (updateTenant) {
const updateRes = JSON.parse(JSON.stringify(updateTenant));
setRequestBody(updateRes?.RequestBody);
setRequestSubject(updateRes?.RequestSubject);
let extUser =
localStorage.getItem("Extand_Class") &&
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
if (extUser && extUser?.objectId) {
extUser.TenantId.RequestBody = updateRes?.RequestBody;
extUser.TenantId.RequestSubject = updateRes?.RequestSubject;
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
setIsAlert({ type: "success", msg: t("saved-successfully") });
setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500);
}
} catch (err) {
console.log("Err", err);
setIsAlert({ type: "danger", msg: t("something-went-wrong-mssg") });
setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500);
} finally {
setIsLoader(false);
}
};
//function to use reset form
const handleReset = async (request, completion) => {
let extUser =
localStorage.getItem("Extand_Class") &&
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
handleModifyMail(request);
if (request && !isDefaultMail?.requestMail) {
setRequestBody(defaultReqHtml?.body);
setRequestSubject(defaultReqHtml?.subject);
try {
await Parse.Cloud.run("updatetenant", {
tenantId: tenantId,
details: { RequestBody: "", RequestSubject: "" }
});
if (extUser && extUser?.objectId) {
extUser.TenantId.RequestBody = "";
extUser.TenantId.RequestSubject = "";
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
} catch (err) {
console.log("Err in reseting request mail", err);
}
} else if (completion && !isDefaultMail?.completionMail) {
setCompletionSubject(defaultCompHtml?.subject);
SetCompletionBody(defaultCompHtml?.body);
try {
await Parse.Cloud.run("updatetenant", {
tenantId: tenantId,
details: { CompletionBody: "", CompletionSubject: "" }
});
if (extUser && extUser?.objectId) {
extUser.TenantId.CompletionBody = "";
extUser.TenantId.CompletionSubject = "";
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
} catch (err) {
console.log("Err in reseting completion mail", err);
}
}
};
//function for handle ontext change and save again text in delta
const handleOnchangeRequest = () => {
if (editorRef.current) {
const html = editorRef.current.editor.root.innerHTML;
setRequestBody(html);
}
};
const handleOnchangeCompletion = () => {
if (editorRefCom.current) {
const html = editorRefCom.current.editor.root.innerHTML;
SetCompletionBody(html);
}
};
const handleTourInput = () => setIsTourEnabled(!isTourEnabled);
const handleSendinOrderInput = () => setSendinOrder(!sendinOrder);
const tabName = (ind) => tab.find((t, i) => i === ind)?.name;
const handleModifyMail = (mode) => {
mode === "request"
? setIsDefaultMail((p) => ({ ...p, requestMail: !p?.requestMail }))
: setIsDefaultMail((p) => ({ ...p, completionMail: !p?.completionMail }));
};
return (
<React.Fragment>
{isalert.msg && <Alert type={isalert.type}>{isalert.msg}</Alert>}
@@ -793,160 +622,12 @@ const Preferences = () => {
)}
{tabName(activeTab) === "email" && (
<div className="flex flex-col mb-4">
<div className="flex flex-col">
<h1 className="text-[14px] mb-[0.7rem] font-medium">
{t("request-email")}
</h1>
<div className="relative mt-2 mb-4">
{
isDefaultMail?.requestMail && (
<div className="absolute backdrop-blur-[2px] flex w-full h-full justify-center items-center bg-black/10 rounded-box select-none z-20">
<button
onClick={() => handleModifyMail("request")}
className="op-btn op-btn-primary shadow-lg"
>
{t("modify")}
</button>
</div>
)
<MailTemplateEditor
info={
tenantInfo
}
<form
onSubmit={handleSaveRequestEmail}
className="p-3 border-[1px] border-base-content rounded-box"
>
<div className="text-lg font-normal">
<label className="text-sm">
{t("subject")}{" "}
<Tooltip
id={"request-sub-tooltip"}
message={`${t("variables-use")}: {{sender_name}} {{document_title}}`}
/>
</label>
<input
required
value={requestSubject}
onChange={(e) =>
setRequestSubject(e.target.value)
}
placeholder={`{{sender_name}} ${t("send-to-sign")} {{document_title}}`}
className="w-full op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content text-xs"
/>
</div>
<div className="text-lg font-normal py-2">
<label className="text-sm mt-3">
{t("body")}{" "}
<Tooltip
id={"request-body-tooltip"}
message={`${t("variables-use")}: {{sender_name}} {{document_title}}`}
/>
</label>
<EditorToolbar containerId="toolbar1" />
<ReactQuill
theme="snow"
value={requestBody}
placeholder="add body of email"
// onChangeSelection={handleTextSelection} // Listen for text selection
ref={editorRef}
modules={module1}
formats={formats}
onChange={handleOnchangeRequest}
/>
</div>
<div className="flex items-center mt-3 gap-2">
<button
disabled={!requestBody || !requestSubject}
className="op-btn op-btn-primary"
type="submit"
>
{t("save")}
</button>
<button
type="button"
className="op-btn op-btn-secondary"
onClick={() => handleReset("request")}
>
{t("reset")}
</button>
</div>
</form>
</div>
<h1 className="text-[14px] mb-[0.7rem] font-medium">
{t("completion-email")}
</h1>
<div className="relative my-2">
{
isDefaultMail?.completionMail && (
<div className="absolute backdrop-blur-[2px] flex w-full h-full justify-center items-center bg-black/10 rounded-box select-none z-20">
<button
onClick={() => handleModifyMail("completion")}
className="op-btn op-btn-primary shadow-lg"
>
{t("modify")}
</button>
</div>
)
}
<form
onSubmit={handleSaveCompletionEmail}
className="p-3 border-[1px] border-base-content rounded-box"
>
<div className="text-lg font-normal">
<label className="text-sm">
{t("subject")}{" "}
<Tooltip
id={"complete-sub-tooltip"}
message={`${t("variables-use")}:{{sender_name}} {{document_title}}`}
/>
</label>
<input
required
value={completionsubject}
onChange={(e) =>
setCompletionSubject(e.target.value)
}
placeholder={`{{sender_name}} ${t("send-to-sign")} {{document_title}}`}
className="w-full op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content text-xs"
/>
</div>
<div className="text-lg font-normal py-2">
<label className="text-sm mt-3">
{t("body")}{" "}
<Tooltip
id={"complete-body-tooltip"}
message={`${t("variables-use")}:{{sender_name}} {{document_title}} {{signing_url}}`}
/>
</label>
<EditorToolbar containerId="toolbar2" />
<ReactQuill
theme="snow"
value={completionBody}
placeholder="add body of email "
// onChangeSelection={handleTextSelection} // Listen for text selection
ref={editorRefCom}
modules={module2}
formats={formats}
onChange={handleOnchangeCompletion}
/>
</div>
<div className="flex items-center mt-3 gap-2">
<button
disabled={!completionBody || !completionsubject}
className="op-btn op-btn-primary"
type="submit"
>
{t("save")}
</button>
<button
type="button"
className="op-btn op-btn-secondary"
onClick={() => handleReset(null, "completion")}
>
{t("reset")}
</button>
</div>
</form>
</div>
</div>
tenantId={tenantInfo?.objectId}
/>
</div>
)}
{tabName(activeTab) === "security" && (
+27 -13
View File
@@ -5,7 +5,6 @@ import Parse from "parse";
import Confetti from "react-confetti";
import axios from "axios";
import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import { useDrop } from "react-dnd";
import EmailComponent from "../components/pdf/EmailComponent";
import WidgetComponent from "../components/pdf/WidgetComponent";
import {
@@ -69,7 +68,9 @@ import {
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
import WidgetNameModal from "../components/pdf/WidgetNameModal";
import CellsSettingModal from "../components/pdf/CellsSettingModal";
import { applyNumberFormulasToPages } from "../utils";
import {
applyNumberFormulasToPages,
} from "../utils";
//For signYourself inProgress section signer can add sign and complete doc sign.
function SignYourSelf() {
const { t } = useTranslation();
@@ -145,11 +146,6 @@ function SignYourSelf() {
const [widgetsTour, setWidgetsTour] = useState(false);
const [owner, setOwner] = useState({});
const [, drop] = useDrop({
accept: "BOX",
drop: (item, monitor) => addPositionOfSignature(item, monitor),
collect: (monitor) => ({ isOver: !!monitor.isOver() })
});
const index = xyPosition?.findIndex((object) => {
return object.pageNumber === pageNumber;
});
@@ -397,15 +393,34 @@ function SignYourSelf() {
.getBoundingClientRect();
//`containerRect.left`, The distance from the left of the viewport to the left side of the element.
//`containerRect.top` The distance from the top of the viewport to the top of the element.
const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top;
const getXPosition = signBtnPosition[0] ? x - signBtnPosition[0].xPos : x;
const getYPosition = signBtnPosition[0] ? y - signBtnPosition[0].yPos : y;
let getXPosition = signBtnPosition[0] ? x - signBtnPosition[0].xPos : x;
let getYPosition = signBtnPosition[0] ? y - signBtnPosition[0].yPos : y;
const getWidth = widgetTypeExist
? calculateInitialWidthHeight(widgetValue).getWidth
: defaultWidthHeight(dragTypeValue).width;
const getHeight = defaultWidthHeight(dragTypeValue).height;
// to avoid negative position values (half portion of widget should not be out of pdf container)
const calculateWidth = getXPosition + getWidth - containerRect.width;
const calculateHeight = getYPosition + getWidth - containerRect.height;
// to avoid negative position values (half portion of widget should not be out of pdf container)
if (getXPosition < 0) {
getXPosition = 0;
}
if (getYPosition < 0) {
getYPosition = 0;
}
if (calculateWidth > 0) {
getXPosition = getXPosition - calculateWidth;
}
if (calculateHeight > 0) {
getYPosition = getYPosition - calculateHeight;
}
dropObj = {
xPosition: getXPosition / (containerScale * scale),
yPosition: getYPosition / (containerScale * scale),
@@ -454,9 +469,7 @@ function SignYourSelf() {
setFontColor("black");
}
dispatch(setIsShowModal({ [key]: true }));
// if (dragTypeValue !== "checkbox") {
setCurrWidgetsDetails(dropObj);
// }
};
//`handleResend` function is used to resend otp for email verification
@@ -1388,7 +1401,6 @@ function SignYourSelf() {
pageNumber={pageNumber}
pdfOriginalWH={pdfOriginalWH}
pdfNewWidth={pdfNewWidth}
drop={drop}
successEmail={successEmail}
nodeRef={nodeRef}
handleTabDrag={handleTabDrag}
@@ -1427,6 +1439,8 @@ function SignYourSelf() {
currWidgetsDetails={currWidgetsDetails}
isShowModal={isShowModal}
unSignedWidgetId={unSignedWidgetId}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
/>
)}
</div>
+45 -35
View File
@@ -3,7 +3,6 @@ import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import { useParams, useNavigate } from "react-router";
import axios from "axios";
import "../styles/signature.css";
import { useDrop } from "react-dnd";
import WidgetComponent from "../components/pdf/WidgetComponent";
import Tour from "../primitives/Tour";
import SignerListPlace from "../components/pdf/SignerListPlace";
@@ -124,11 +123,6 @@ const TemplatePlaceholder = () => {
const [pdfBase64Url, setPdfBase64Url] = useState("");
const [isUploadPdf, setIsUploadPdf] = useState(false);
const isMobile = window.innerWidth < 767;
const [, drop] = useDrop({
accept: "BOX",
drop: (item, monitor) => addPositionOfSignature(item, monitor),
collect: (monitor) => ({ isOver: !!monitor.isOver() })
});
const [uniqueId, setUniqueId] = useState("");
const [isModalRole, setIsModalRole] = useState(false);
const [roleName, setRoleName] = useState("");
@@ -215,28 +209,21 @@ const TemplatePlaceholder = () => {
const filterSignTypes = signatureType?.filter(
(x) => x.enabled === true
);
if (tenantDetails?.RequestBody) {
//customize mail state is handle to when user want to customize already set tenant email format then use that format
setCustomizeMail({
subject: tenantDetails?.RequestSubject,
body: tenantDetails?.RequestBody
});
setDefaultMail({
subject: tenantDetails?.RequestSubject,
body: tenantDetails?.RequestBody
});
} else {
const defaultRequestBody = defaultMailBody;
const defaultSubject = defaultMailSubject;
setCustomizeMail({
subject: defaultSubject,
body: defaultRequestBody
});
setDefaultMail({
subject: defaultSubject,
body: defaultRequestBody
});
}
const extUser =
localStorage.getItem("Extand_Class") &&
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
const subject = tenantDetails?.RequestSubject ?? "";
const body = tenantDetails?.RequestBody ?? "";
//customize mail state is handle to when user want to customize already set tenant email format then use that format
const userSubject =
subject;
const userBody =
body;
setCustomizeMail({
subject: userSubject ?? defaultMailSubject,
body: userBody ?? defaultMailBody
});
setDefaultMail({ subject: userSubject, body: userBody });
return filterSignTypes;
}
} catch (e) {
@@ -498,12 +485,33 @@ const TemplatePlaceholder = () => {
.getBoundingClientRect();
const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top;
const getXPosition = signBtnPosition[0]
let getXPosition = signBtnPosition[0]
? x - signBtnPosition[0].xPos
: x;
const getYPosition = signBtnPosition[0]
let getYPosition = signBtnPosition[0]
? y - signBtnPosition[0].yPos
: y;
// to avoid negative position values (half portion of widget should not be out of pdf container)
const calculateWidth =
getXPosition + widgetWidth - containerRect.width;
const calculateHeight =
getYPosition + widgetHeight - containerRect.height;
if (getXPosition < 0) {
getXPosition = 0;
}
if (getYPosition < 0) {
getYPosition = 0;
}
if (calculateWidth > 0) {
getXPosition = getXPosition - calculateWidth;
}
if (calculateHeight > 0) {
getYPosition = getYPosition - calculateHeight;
}
dropObj = {
xPosition: getXPosition / (containerScale * scale),
yPosition: getYPosition / (containerScale * scale),
@@ -754,7 +762,11 @@ const TemplatePlaceholder = () => {
}
};
useEffect(() => {
if (pdfDetails?.[0]?.CreatedBy?.objectId !== user?.objectId || isPrefillModal) return;
if (
pdfDetails?.[0]?.CreatedBy?.objectId !== user?.objectId ||
isPrefillModal
)
return;
const timer = setTimeout(() => {
autosavedetails();
}, 2000);
@@ -1926,7 +1938,6 @@ const TemplatePlaceholder = () => {
numPages={numPages}
pageDetails={pageDetails}
placeholder={true}
drop={drop}
handleDeleteWidget={handleDeleteWidgetObj}
handleTabDrag={handleTabDrag}
handleStop={handleStop}
@@ -1963,6 +1974,8 @@ const TemplatePlaceholder = () => {
currWidgetsDetails={currWidgetsDetails}
setRoleName={setRoleName}
isShowModal={isShowModal}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
/>
)}
</div>
@@ -2039,9 +2052,6 @@ const TemplatePlaceholder = () => {
initial={true}
isTemplateFlow={true}
roleName={roleName}
Add
commentMore
actions
isPrefillDropdown={true}
/>
</div>
@@ -55,7 +55,7 @@ function CustomModal(props) {
className="op-btn op-btn-primary px-6 ml-[20px] mb-3 mt-1"
onClick={() => handleExtendBtn()}
>
Extend
{t("extend")}
</button>
)}
{props.isDownloadBtn && (
@@ -32,7 +32,8 @@ export const RenderReportCell = ({
startIndex,
handleDownload,
handleRemovePrefill,
reportName
reportName,
handleItemClick
}) => {
const { t } = useTranslation();
const appName =
@@ -59,7 +60,11 @@ export const RenderReportCell = ({
);
case "Reason":
return (
<td key={col} className="p-2 text-center">
<td
key={col}
className="p-2 text-center cursor-pointer"
onClick={() => handleItemClick(col, rowData?.DeclineReason)}
>
{rowData?.DeclineReason?.length > 25
? rowData?.DeclineReason?.slice(0, 25) + "..."
: rowData?.DeclineReason || "-"}
@@ -68,7 +73,12 @@ export const RenderReportCell = ({
case "Note":
return (
<td key={col} className="p-2 text-center">
<p className="truncate w-[100px]">{rowData?.Note || "-"}</p>
<p
className="truncate w-[100px] cursor-pointer"
onClick={() => handleItemClick(col, rowData?.Note)}
>
{rowData?.Note || "-"}
</p>
</td>
);
case "Folder":
@@ -97,6 +97,7 @@ const DocumentsReport = (props) => {
const [defaultMail, setDefaultMail] = useState({ body: "", subject: "" });
const [currUserId, setCurrUserId] = useState(false);
const [documentDetails, setDocumentDetails] = useState();
const [objInfoModal, setObjInfoModal] = useState({ title: "", info: "" });
const startIndex = (currentPage - 1) * props.docPerPage;
const { isMoreDocs, setIsNextRecord } = props;
@@ -202,29 +203,21 @@ const DocumentsReport = (props) => {
const filterSignTypes = signatureType?.filter(
(x) => x.enabled === true
);
//condition to check tenant have some already set any email template
if (tenantDetails?.RequestBody) {
//customize mail state is handle to when user want to customize already set tenant email format then use that format
setCustomizeMail({
subject: tenantDetails?.RequestSubject,
body: tenantDetails?.RequestBody
});
setDefaultMail({
subject: tenantDetails?.RequestSubject,
body: tenantDetails?.RequestBody
});
} else {
const defaultRequestBody = defaultMailBody;
const defaultSubject = defaultMailSubject;
setCustomizeMail({
subject: defaultSubject,
body: defaultRequestBody
});
setDefaultMail({
subject: defaultSubject,
body: defaultRequestBody
});
}
const extUser =
localStorage.getItem("Extand_Class") &&
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
const subject = tenantDetails?.RequestSubject ?? "";
const body = tenantDetails?.RequestBody ?? "";
//customize mail state is handle to when user want to customize already set tenant email format then use that format
const userSubject =
subject;
const userBody =
body;
setCustomizeMail({
subject: userSubject ?? defaultMailSubject,
body: userBody ?? defaultMailBody
});
setDefaultMail({ subject: userSubject, body: userBody });
return filterSignTypes;
}
} catch (e) {
@@ -1049,6 +1042,10 @@ const DocumentsReport = (props) => {
);
}
};
const handleItemClick = (title, info) => {
setObjInfoModal({ title, info });
};
return (
<div className="relative">
{Object.keys(actLoader)?.length > 0 && (
@@ -1173,6 +1170,7 @@ const DocumentsReport = (props) => {
handleDownload={handleDownload}
handleRemovePrefill={handleRemovePrefill}
reportName={props.ReportName}
handleItemClick={handleItemClick}
/>
))}
{/* actions */}
@@ -1852,6 +1850,13 @@ const DocumentsReport = (props) => {
)}
</div>
</ModalUi>
<ModalUi
title={t(`report-heading.${objInfoModal.title}`)}
isOpen={objInfoModal.title}
handleClose={() => setObjInfoModal({ title: "", info: "" })}
>
<div className="p-[20px]">{objInfoModal.info || "-"}</div>
</ModalUi>
</div>
</div>
);
@@ -86,6 +86,7 @@ const TemplatesReport = (props) => {
const [isNewContact, setIsNewContact] = useState({ status: false, id: "" });
const [isPrefillModal, setIsPrefillModal] = useState({});
const [isSubmit, setIsSubmit] = useState(false);
const [objInfoModal, setObjInfoModal] = useState({ title: "", info: "" });
const startIndex = (currentPage - 1) * props.docPerPage;
const { isMoreDocs, setIsNextRecord } = props;
const [isMailModal, setIsMailModal] = useState(false);
@@ -919,29 +920,21 @@ const TemplatesReport = (props) => {
if (tenantDetails && tenantDetails === "user does not exist!") {
alert(t("user-not-exist"));
} else if (tenantDetails) {
//condition to check tenant have some already set any email template
if (tenantDetails?.RequestBody) {
//customize mail state is handle to when user want to customize already set tenant email format then use that format
setCustomizeMail({
subject: tenantDetails?.RequestSubject,
body: tenantDetails?.RequestBody
});
setDefaultMail({
subject: tenantDetails?.RequestSubject,
body: tenantDetails?.RequestBody
});
} else {
const defaultRequestBody = defaultMailBody;
const defaultSubject = defaultMailSubject;
setCustomizeMail({
subject: defaultSubject,
body: defaultRequestBody
});
setDefaultMail({
subject: defaultSubject,
body: defaultRequestBody
});
}
const extUser =
localStorage.getItem("Extand_Class") &&
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
const subject = tenantDetails?.RequestSubject ?? "";
const body = tenantDetails?.RequestBody ?? "";
//customize mail state is handle to when user want to customize already set tenant email format then use that format
const userSubject =
subject;
const userBody =
body;
setCustomizeMail({
subject: userSubject ?? defaultMailSubject,
body: userBody ?? defaultMailBody
});
setDefaultMail({ subject: userSubject, body: userBody });
}
} catch (e) {
alert(t("user-not-exist"));
@@ -1011,6 +1004,10 @@ const TemplatesReport = (props) => {
navigate(`/recipientSignPdf/${docId}`);
}
};
const handleItemClick = (title, info) => {
setObjInfoModal({ title, info });
};
return (
<div className="relative">
{Object.keys(actLoader)?.length > 0 && (
@@ -1147,6 +1144,7 @@ const TemplatesReport = (props) => {
handleDownload={handleDownload}
handleRemovePrefill={handleRemovePrefill}
reportName={props.ReportName}
handleItemClick={handleItemClick}
/>
))}
<td className="px-2 py-2">
@@ -1682,6 +1680,13 @@ const TemplatesReport = (props) => {
)}
</div>
</ModalUi>
<ModalUi
title={t(`report-heading.${objInfoModal.title}`)}
isOpen={objInfoModal.title}
handleClose={() => setObjInfoModal({ title: "", info: "" })}
>
<div className="p-[20px]">{objInfoModal.info || "-"}</div>
</ModalUi>
</div>
</div>
);
+8 -9
View File
@@ -17,21 +17,20 @@ export const saveToMySign = async (widget) => {
const file = base64StringtoFile(base64, fileName);
const fileUrl = await uploadFile(file, User?.id);
// below code is used to save or update default signature, initials, stamp
const signCls = new Parse.Object("contracts_Signature");
if (widget?.defaultSignId) {
signCls.id = widget.defaultSignId;
}
const params = {
id: widget?.defaultSignId, // pass id to update existing signature/initials
userId: User.id
};
if (widget?.type === "initials") {
signCls.set("Initials", fileUrl);
params.initials = fileUrl; // save initials image url
} else if (widget?.type === "signature") {
signCls.set("ImageURL", fileUrl);
params.signature = fileUrl; // save signature image url
}
signCls.set("UserId", Parse.User.createWithoutData(User.id));
const signRes = await signCls.save();
const signRes = await Parse.Cloud.run("savesignature", params);
return { base64File: base64, id: signRes?.id };
} catch (err) {
console.log("Err while saving signature", err);
return url;
return err;
}
}
};
-1
View File
@@ -1,7 +1,6 @@
# Use an official Node runtime as the base image
FROM node:22.14.0
# Install LibreOffice for DOCX to PDF conversions
RUN apt-get update \
&& apt-get install -y libreoffice \
@@ -11,6 +11,7 @@ export const deleteUserOtp = async (req, res) => {
const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
extUserQuery.include('TenantId');
const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) return res.status(404).json({ error: 'User not found' });
@@ -18,6 +18,7 @@ export function generateOtp(len = OTP_LENGTH) {
}
export async function sendDeleteOtpEmail(extUser, otp) {
const _extUser = extUser && JSON.parse(JSON.stringify(extUser));
const params = {
extUserId: extUser.id,
from: appName,
+8
View File
@@ -57,6 +57,10 @@ import addUser from './parsefunction/addUser.js';
import filterDocs from './parsefunction/filterDocs.js';
import sendDeleteUserMail from './parsefunction/sendDeleteUserMail.js';
import resetPassword from './parsefunction/resetPassword.js';
import saveSignature from './parsefunction/saveSignature.js';
import manageSign from './parsefunction/manageSign.js';
import getSignature from './parsefunction/getSignature.js';
import updateEmailTemplates from './parsefunction/updateEmailTemplates.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);
@@ -124,3 +128,7 @@ Parse.Cloud.define('adduser', addUser);
Parse.Cloud.define('filterdocs', filterDocs);
Parse.Cloud.define('senddeleterequest', sendDeleteUserMail);
Parse.Cloud.define('resetpassword', resetPassword);
Parse.Cloud.define('savesignature', saveSignature);
Parse.Cloud.define('managesign', manageSign);
Parse.Cloud.define('getdefaultsignature', getSignature);
Parse.Cloud.define('updateemailtemplates', updateEmailTemplates);
@@ -49,8 +49,8 @@ async function sendMail(document, publicUrl) {
let signPdf = `${hostUrl}/login/${encodeBase64}`;
const orgName = document.ExtUserPtr.Company ? document.ExtUserPtr.Company : '';
const senderObj = document?.ExtUserPtr;
const mailBody = document?.ExtUserPtr?.TenantId?.RequestBody || '';
const mailSubject = document?.ExtUserPtr?.TenantId?.RequestSubject || '';
let mailBody = senderObj?.TenantId?.RequestBody || '';
let mailSubject = senderObj?.TenantId?.RequestSubject || '';
let replaceVar;
if (mailBody && mailSubject) {
const replacedRequestBody = mailBody.replace(/"/g, "'");
@@ -126,8 +126,8 @@ async function batchQuery(userId, Documents, Ip, parseConfig, type, publicUrl) {
}
});
}
const mailBody = x?.ExtUserPtr?.TenantId?.RequestBody || '';
const mailSubject = x?.ExtUserPtr?.TenantId?.RequestSubject || '';
let mailBody = x?.ExtUserPtr?.TenantId?.RequestBody || '';
let mailSubject = x?.ExtUserPtr?.TenantId?.RequestSubject || '';
return {
method: 'POST',
path: '/app/classes/contracts_Document',
@@ -1,26 +1,78 @@
import axios from 'axios';
import { appName, cloudServerUrl, serverAppId } from '../../Utils.js';
const serverUrl = cloudServerUrl;
const APPID = serverAppId;
const masterKEY = process.env.MASTER_KEY;
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': APPID,
'X-Parse-Master-Key': masterKEY,
};
async function sendDeclineMail(doc, publicUrl, userId, reason) {
try {
const TenantAppName = appName;
const logo =
"<img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' style='padding:20px'/>";
const opurl = ` <a href=www.opensignlabs.com target=_blank>here</a>`;
const removePrefill =
doc?.Placeholders?.length > 0 && doc?.Placeholders?.filter(x => x?.Role !== 'prefill');
const signUser =
removePrefill?.length > 0 &&
removePrefill?.find(x => x?.signerPtr?.UserId?.objectId === userId);
const sender = doc.ExtUserPtr;
const pdfName = doc.Name;
const creatorName = doc.ExtUserPtr.Name;
const creatorEmail = doc.ExtUserPtr.Email;
const signerName = signUser?.signerPtr?.Name || '';
const signerEmail = signUser?.signerPtr?.Email || signUser?.email || '';
const viewDocUrl = `${publicUrl}/recipientSignPdf/${doc.objectId}`;
const subject = `Document "${pdfName}" has been declined by ${signerName}`;
const body =
"<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>${logo}</div><div style='padding:2px;font-family:system-ui;background-color:#47a3ad'><p style='font-size:20px;font-weight:400;color:white;padding-left:20px'>Document declined by ${signerName}</p>` +
`</div><div style='padding:20px;font-family:system-ui;font-size:14px'><p>Dear ${creatorName},</p>` +
`<p>${pdfName} has been declined by ${signerName} "${signerEmail}" on ${new Date().toLocaleDateString()}.</p>` +
`<p>Decline Reason: ${reason || 'Not specified'}</p>` +
`<p><a href=${viewDocUrl} target=_blank>View Document</a></p></div></div><div><p>This is an automated email from ${TenantAppName}. For any queries regarding this email, ` +
`please contact the sender ${creatorEmail} directly. If you think this email is inappropriate or spam, you may file a complaint with ${TenantAppName}${opurl}.</p></div></div></body></html>`;
const params = {
extUserId: sender.objectId,
from: TenantAppName,
recipient: creatorEmail,
subject: subject,
pdfName: pdfName,
html: body,
};
await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
} catch (err) {
console.log('err in sendnotifymail', err);
}
}
export default async function declinedocument(request) {
const docId = request.params.docId;
const reason = request.params?.reason || '';
const declineBy = {
__type: 'Pointer',
className: '_User',
objectId: request.params?.userId,
};
const userId = request.params.userId;
const declineBy = { __type: 'Pointer', className: '_User', objectId: userId };
const publicUrl = request.headers.public_url;
if (!docId) {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'missing parameter docId.');
}
try {
const docCls = new Parse.Query('contracts_Document');
docCls.include('ExtUserPtr.TenantId');
docCls.include('ExtUserPtr.TenantId,Placeholders.signerPtr,Signers');
const updateDoc = await docCls.get(docId, { useMasterKey: true });
if (updateDoc) {
const _doc = JSON.parse(JSON.stringify(updateDoc));
const isEnableOTP = updateDoc?.get('IsEnableOTP') || false;
if (!isEnableOTP) {
updateDoc.set('IsDeclined', true);
updateDoc.set('DeclineReason', reason);
updateDoc.set('DeclineBy', declineBy);
await updateDoc.save(null, { useMasterKey: true });
sendDeclineMail(_doc, publicUrl, userId, reason);
return 'document declined';
} else {
if (!request?.user) {
@@ -30,6 +82,7 @@ export default async function declinedocument(request) {
updateDoc.set('DeclineReason', reason);
updateDoc.set('DeclineBy', declineBy);
await updateDoc.save(null, { useMasterKey: true });
sendDeclineMail(_doc, publicUrl, userId, reason);
return 'document declined';
}
} else {
@@ -0,0 +1,18 @@
export default async function getSignature(request) {
const { userId } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
}
if (userId !== request.user?.id) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot save signature for the current user.');
}
try {
const query = new Parse.Query('contracts_Signature');
query.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
const result = await query.first({ useMasterKey: true });
return result;
} catch (err) {
console.error('Error fetching signature:', err);
throw err;
}
}
@@ -0,0 +1,28 @@
export default async function manageSign(request) {
const { signature, userId, initials, id, title } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
}
if (userId !== request.user?.id) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot save signature for the current user.');
}
const userPtr = { __type: 'Pointer', className: '_User', objectId: userId };
try {
const signatureCls = new Parse.Object('contracts_Signature');
if (id) {
signatureCls.id = id;
}
signatureCls.set('Initials', initials ? initials : '');
signatureCls.set('ImageURL', signature ? signature : '');
signatureCls.set('SignatureName', title ? title : '');
if (userPtr) {
signatureCls.set('UserId', userPtr);
}
const signRes = await signatureCls.save(null, { useMasterKey: true });
return signRes;
} catch (err) {
console.error('Error saving signature:', err);
throw err;
}
}
@@ -41,6 +41,7 @@ export default async function GenerateCertificate(docDetails) {
const maxX = width - margin - textWidth; // Ensures text stays inside the border with 30px margin
const OriginIp = docDetails?.OriginIp || '';
const company = docDetails?.ExtUserPtr?.Company || '';
const documentHash = docDetails?.DocumentHash || '';
const createdAt = docDetails?.DocSentAt?.iso || docDetails.createdAt;
const createdAtperTimezone = formatDateTime(createdAt, DateFormat, timezone, Is12Hr);
const IsEnableOTP = docDetails?.IsEnableOTP || false;
@@ -150,9 +151,36 @@ export default async function GenerateCertificate(docDetails) {
color: textValueColor,
});
if (documentHash) {
page.drawText('Document hash (sha256) :', {
x: 30,
y: 670,
size: text,
font: timesRomanFont,
color: textKeyColor,
});
page.drawText(documentHash, {
x: 170,
y: 670,
size: text,
font: timesRomanFont,
color: textValueColor,
});
}
const organizationY = documentHash ? 650 : 670;
const createdOnY = organizationY - 20;
const completedOnY = createdOnY - 20;
const signersY = completedOnY - 20;
const originatorHeaderY = signersY - 20;
const nameY = originatorHeaderY - 17;
const emailY = nameY - 20;
const ipY = emailY - 20;
page.drawText('Organization :', {
x: 30,
y: 670,
y: organizationY,
size: text,
font: timesRomanFont,
color: textKeyColor,
@@ -160,14 +188,14 @@ export default async function GenerateCertificate(docDetails) {
page.drawText(company, {
x: 110,
y: 670,
y: organizationY,
size: text,
font: timesRomanFont,
color: textValueColor,
});
page.drawText('Created on :', {
x: 30,
y: 650,
y: createdOnY,
size: text,
font: timesRomanFont,
color: textKeyColor,
@@ -175,14 +203,14 @@ export default async function GenerateCertificate(docDetails) {
page.drawText(`${createdAtperTimezone}`, {
x: 97,
y: 650,
y: createdOnY,
size: text,
font: timesRomanFont,
color: textValueColor,
});
page.drawText('Completed on :', {
x: 30,
y: 630,
y: completedOnY,
size: text,
font: timesRomanFont,
color: textKeyColor,
@@ -190,14 +218,14 @@ export default async function GenerateCertificate(docDetails) {
page.drawText(`${completedUTCtime}`, {
x: 115,
y: 630,
y: completedOnY,
size: text,
font: timesRomanFont,
color: textValueColor,
});
page.drawText('Signers :', {
x: 30,
y: 610,
y: signersY,
size: text,
font: timesRomanFont,
color: textKeyColor,
@@ -205,75 +233,75 @@ export default async function GenerateCertificate(docDetails) {
page.drawText(`${signersCount}`, {
x: 80,
y: 610,
y: signersY,
size: text,
font: timesRomanFont,
color: textValueColor,
});
page.drawText('Document originator', {
x: 30,
y: 590,
y: originatorHeaderY,
size: 17,
font: timesRomanFont,
color: titleColor,
});
page.drawText('Name :', {
x: 60,
y: 573,
y: nameY,
size: text,
font: timesRomanFont,
color: textKeyColor,
});
page.drawText(ownerName, {
x: 105,
y: 573,
y: nameY,
size: text,
font: timesRomanFont,
color: textValueColor,
});
page.drawText('Email :', {
x: 60,
y: 553,
y: emailY,
size: text,
font: timesRomanFont,
color: textKeyColor,
});
page.drawText(ownerEmail, {
x: 105,
y: 553,
y: emailY,
size: text,
font: timesRomanFont,
color: textValueColor,
});
page.drawText('IP address :', {
x: 60,
y: 533,
y: ipY,
size: text,
font: timesRomanFont,
color: textKeyColor,
});
page.drawText(`${OriginIp}`, {
x: 125,
y: 533,
y: ipY,
size: text,
font: timesRomanFont,
color: textValueColor,
});
page.drawLine({
start: { x: 30, y: 527 },
end: { x: width - 30, y: 527 },
start: { x: 30, y: ipY - 6 },
end: { x: width - 30, y: ipY - 6 },
color: rgb(0.12, 0.12, 0.12),
thickness: 0.5,
});
let yPosition1 = 512;
let yPosition2 = 498;
let yPosition3 = 478;
let yPosition4 = 458;
let yPosition5 = 438;
let yPosition6 = 418;
let yPosition7 = 398;
let yPosition8 = 363;
let yPosition1 = ipY - 21;
let yPosition2 = yPosition1 - 14;
let yPosition3 = yPosition2 - 20;
let yPosition4 = yPosition3 - 20;
let yPosition5 = yPosition4 - 20;
let yPosition6 = yPosition5 - 20;
let yPosition7 = yPosition6 - 20;
let yPosition8 = yPosition7 - 35;
auditTrail.slice(0, 3).forEach(async (x, i) => {
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : '';
@@ -1,4 +1,5 @@
import fs from 'node:fs';
import { createHash } from 'node:crypto';
import axios from 'axios';
import { PDFDocument } from 'pdf-lib';
import {
@@ -28,6 +29,10 @@ const headers = {
'X-Parse-Master-Key': masterKEY,
};
function generateDocumentHash(buffer) {
return createHash('sha256').update(buffer).digest('hex');
}
async function unlinkFile(path) {
if (fs.existsSync(path)) {
try {
@@ -44,11 +49,6 @@ async function uploadFile(pdfName, filepath) {
const filedata = fs.readFileSync(filepath);
let fileUrl;
// const file = new Parse.File(pdfName, [...filedata], 'application/pdf');
// await file.save({ useMasterKey: true });
// const fileRes = getSecureUrl(file.url());
// fileUrl = fileRes.url;
const fileRes = await parseUploadFile(pdfName, filedata, 'application/pdf');
fileUrl = getSecureUrl(fileRes?.url)?.url;
@@ -61,7 +61,7 @@ async function uploadFile(pdfName, filepath) {
}
// `updateDoc` is used to update signedUrl, AuditTrail, Iscompleted in document
async function updateDoc(docId, url, userId, ipAddress, data, className, sign) {
async function updateDoc(docId, url, userId, ipAddress, data, className, sign, documentHash) {
try {
const UserPtr = { __type: 'Pointer', className: className, objectId: userId };
const obj = {
@@ -100,8 +100,16 @@ async function updateDoc(docId, url, userId, ipAddress, data, className, sign) {
isCompleted = true;
}
const body = { SignedUrl: url, AuditTrail: updateAuditTrail, IsCompleted: isCompleted };
if (documentHash && isCompleted) {
body.DocumentHash = documentHash;
}
const signedRes = await axios.put(`${docUrl}/${docId}`, body, { headers });
return { isCompleted: isCompleted, message: 'success', AuditTrail: updateAuditTrail };
return {
isCompleted: isCompleted,
message: 'success',
AuditTrail: updateAuditTrail,
DocumentHash: documentHash && isCompleted ? documentHash : undefined,
};
} catch (err) {
console.log('update doc err ', err);
return 'err';
@@ -183,8 +191,8 @@ async function sendCompletedMail(obj) {
if (obj?.isCustomMail) {
const tenant = sender?.TenantId;
if (tenant) {
subject = tenant?.CompletionSubject || '';
body = tenant?.CompletionBody || '';
subject = tenant?.CompletionSubject ? tenant?.CompletionSubject : subject;
body = tenant?.CompletionBody ? tenant?.CompletionBody : body;
} else {
const userId = sender?.CreatedBy?.objectId || sender?.UserId?.objectId;
if (userId) {
@@ -198,8 +206,8 @@ async function sendCompletedMail(obj) {
const tenantRes = await tenantQuery.first({ useMasterKey: true });
if (tenantRes) {
const _tenantRes = JSON.parse(JSON.stringify(tenantRes));
subject = _tenantRes?.CompletionSubject || '';
body = _tenantRes?.CompletionBody || '';
subject = _tenantRes?.CompletionSubject ? tenant?.CompletionSubject : subject;
body = _tenantRes?.CompletionBody ? tenant?.CompletionBody : body;
}
} catch (err) {
console.log('error in fetch tenant in signpdf', err.message);
@@ -427,6 +435,7 @@ async function PDF(req) {
let filePath = `./exports/${name}`;
let signedFilePath = `./exports/signed_${name}`;
let pdfSize = PdfBuffer.length;
let documentHash;
if (isCompleted) {
const signersName = _resDoc.Signers?.map(x => x.Name + ' <' + x.Email + '>');
const reason =
@@ -444,6 +453,7 @@ async function PDF(req) {
//`saveUrl` is used to save signed pdf in exports folder
fs.writeFileSync(signedFilePath, signedDocs);
pdfSize = signedDocs.length;
documentHash = generateDocumentHash(signedDocs);
console.log(`✅ PDF digitally signed created: ${signedFilePath} \n`);
} else {
//`saveUrl` is used to save signed pdf in exports folder
@@ -464,12 +474,17 @@ async function PDF(req) {
userIP, // client ipAddress,
_resDoc, // auditTrail, signers, etc data
className, // className based on flow
sign // sign base64
sign, // sign base64
isCompleted ? documentHash : undefined
);
sendNotifyMail(_resDoc, signUser, mailProvider, publicUrl);
saveFileUsage(pdfSize, data.imageUrl, _resDoc?.CreatedBy?.objectId);
if (updatedDoc && updatedDoc.isCompleted) {
const hashForDoc = documentHash || updatedDoc?.DocumentHash;
const doc = { ..._resDoc, AuditTrail: updatedDoc.AuditTrail, SignedUrl: data.imageUrl };
if (hashForDoc) {
doc.DocumentHash = hashForDoc;
}
sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, `signed_${name}`);
} else {
unlinkFile(pfxname);
@@ -0,0 +1,33 @@
export default async function saveSignature(request) {
const { signature, userId, initials, id, title } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
}
if (userId !== request.user?.id) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot save signature for the current user.');
}
const userPtr = { __type: 'Pointer', className: '_User', objectId: userId };
try {
const signatureCls = new Parse.Object('contracts_Signature');
if (id) {
signatureCls.id = id;
}
if (initials) {
signatureCls.set('Initials', initials);
}
if (signature) {
signatureCls.set('ImageURL', signature);
}
if (title) {
signatureCls.set('SignatureName', title);
}
if (userPtr) {
signatureCls.set('UserId', userPtr);
}
const signRes = await signatureCls.save(null, { useMasterKey: true });
return signRes;
} catch (err) {
throw err;
}
}
@@ -159,7 +159,8 @@ export default async function sendMailGmailProvider(_extRes, template) {
template;
if (_extRes) {
const refresh_token = _extRes.google_refresh_token;
let refresh_token = '';
refresh_token = _extRes?.TenantId?.google_refresh_token;
// generate access token
const access_token = await refreshAccessToken(refresh_token);
@@ -12,15 +12,23 @@ async function sendMailProvider(req, plan, monthchange) {
let mailgunClient;
let mailgunDomain;
if (smtpenable) {
transporterSMTP = createTransport({
let transporterConfig = {
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT || 465,
secure: smtpsecure,
auth: {
};
// ✅ Add auth only if BOTH username & password exist
const smtpUser = process.env.SMTP_USERNAME;
const smtpPass = process.env.SMTP_PASS;
if (smtpUser && smtpPass) {
transporterConfig.auth = {
user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL,
pass: process.env.SMTP_PASS,
},
});
pass: smtpPass,
};
}
transporterSMTP = createTransport(transporterConfig);
} else {
if (mailgunApiKey) {
const mailgun = new Mailgun(formData);
@@ -185,7 +193,7 @@ async function sendMailProvider(req, plan, monthchange) {
}
}
} catch (err) {
console.log('err in sendmailv3', err);
console.log(`Error in sendmailv3: ${err}`);
if (fs.existsSync(testPdf)) {
try {
fs.unlinkSync(testPdf);
@@ -236,7 +244,7 @@ async function sendMailProvider(req, plan, monthchange) {
}
}
} catch (err) {
console.log('err in sendmailv3', err);
console.log(`Error in sendmailv3: ${err}`);
if (err) {
return { status: 'error' };
}
@@ -0,0 +1,36 @@
export default async function updateEmailTemplates(request) {
const { tenantId, details } = request.params;
if (!tenantId || !details) {
throw new Parse.Error(400, 'Missing tenantId or details.');
}
if (!request.user) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'unauthorized');
}
try {
const extUser = new Parse.Query('contracts_Users');
extUser.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: request.user.id });
extUser.include('TenantId');
const extUserRes = await extUser.first({ useMasterKey: true });
const mailKeys = ['CompletionBody', 'CompletionSubject', 'RequestBody', 'RequestSubject'];
Object.keys(details).forEach(key => {
if (mailKeys.includes(key)) {
if (details?.[key]) {
extUserRes.set(key, details?.[key]);
} else {
extUserRes.unset(key);
}
}
});
const updateExtRes = await extUserRes.save(null, { useMasterKey: true });
if (updateExtRes) {
const res = JSON.parse(JSON.stringify(updateExtRes));
return res;
}
} catch (error) {
console.log('error while updating email templates:', error);
throw error;
}
}
@@ -15,7 +15,11 @@ export default async function updateTenant(request) {
// Update tenant details
Object.keys(details).forEach(key => {
if (validKeys.includes(key)) {
tenant.set(key, details?.[key]);
if (details?.[key] !== undefined) {
tenant.set(key, details?.[key]);
} else {
tenant.unset(key);
}
}
});
+14 -6
View File
@@ -61,20 +61,28 @@ let mailgunDomain;
let isMailAdapter = false;
if (smtpenable) {
try {
transporterMail = createTransport({
let transporterConfig = {
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT || 465,
secure: smtpsecure,
auth: {
};
// ✅ Add auth only if BOTH username & password exist
const smtpUser = process.env.SMTP_USERNAME;
const smtpPass = process.env.SMTP_PASS;
if (smtpUser && smtpPass) {
transporterConfig.auth = {
user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL,
pass: process.env.SMTP_PASS,
},
});
pass: smtpPass,
};
}
transporterMail = createTransport(transporterConfig);
await transporterMail.verify();
isMailAdapter = true;
} catch (err) {
isMailAdapter = false;
console.log('Please provide valid SMTP credentials');
console.log(`Please provide valid SMTP credentials: ${err}`);
}
} else if (process.env.MAILGUN_API_KEY) {
try {
+2433 -2339
View File
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -18,8 +18,8 @@
"watch": "nodemon index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.901.0",
"@aws-sdk/s3-request-presigner": "^3.901.0",
"@aws-sdk/client-s3": "^3.931.0",
"@aws-sdk/s3-request-presigner": "^3.931.0",
"@parse/fs-files-adapter": "^3.0.0",
"@parse/s3-files-adapter": "^4.2.0",
"@pdf-lib/fontkit": "^1.1.1",
@@ -27,7 +27,7 @@
"@signpdf/signer-p12": "^3.2.4",
"@signpdf/signpdf": "^3.2.5",
"aws-sdk": "^2.1692.0",
"axios": "^1.12.2",
"axios": "^1.13.2",
"coherentpdf": "^2.5.5",
"cors": "^2.8.5",
"date-fns-tz": "^3.2.0",
@@ -37,32 +37,32 @@
"generate-api-key": "^1.0.2",
"googleapis": "^161.0.0",
"libreoffice-convert": "^1.7.0",
"mailgun.js": "^12.1.0",
"mongodb": "^6.20.0",
"mailgun.js": "^12.1.1",
"mongodb": "^5.9.2",
"multer": "^2.0.2",
"multer-s3": "^3.0.1",
"node-forge": "^1.3.1",
"nodemailer": "^7.0.9",
"p-limit": "^7.1.1",
"nodemailer": "^7.0.10",
"p-limit": "^7.2.0",
"parse": "^6.1.1",
"parse-dbtool": "^1.2.0",
"parse-server": "^8.2.5",
"parse-server": "^8.4.0",
"parse-server-api-mail-adapter": "^4.1.0",
"pdf-lib": "^1.17.1",
"posthog-node": "^5.9.3",
"posthog-node": "^5.11.2",
"qrcode": "^1.5.4",
"rate-limiter-flexible": "^8.0.1",
"sharp": "^0.34.4",
"rate-limiter-flexible": "^8.2.0",
"sharp": "^0.34.5",
"speakeasy": "^2.0.0",
"ws": "^8.18.3"
},
"type": "module",
"devDependencies": {
"@babel/eslint-parser": "^7.28.4",
"eslint": "^9.37.0",
"@babel/eslint-parser": "^7.28.5",
"eslint": "^9.39.1",
"jasmine": "^5.12.0",
"mongodb-runner": "^5.10.0",
"nodemon": "^3.1.10",
"mongodb-runner": "^5.11.1",
"nodemon": "^3.1.11",
"nyc": "^17.1.0",
"prettier": "^3.6.2"
},
@@ -74,4 +74,4 @@
"engines": {
"node": "18 || 20 || 22"
}
}
}