Compare commits

...
57 changed files with 21346 additions and 3983 deletions
+9
View File
@@ -241,6 +241,15 @@
"contributions": [ "contributions": [
"translation" "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 dockerfile: apps/OpenSign/Dockerhubfile
- image: opensign/opensignserver - image: opensign/opensignserver
dockerfile: apps/OpenSignServer/Dockerhubfile dockerfile: apps/OpenSignServer/Dockerhubfile
steps: steps:
- - name: Checkout
name: Checkout
uses: actions/checkout@v3 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 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
images: ${{ matrix.image }} images: ${{ matrix.image }}
-
name: Debug - List files opensign - name: Debug - List files opensign
run: ls -R apps/OpenSign/ # Adjust the path as needed 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 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 uses: docker/build-push-action@v4
with: with:
context: . context: .
file: ${{ matrix.dockerfile }} file: ${{ matrix.dockerfile }}
push: true push: true
# 👇 Build for both AMD64 & ARM64
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} 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/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/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/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> </tr>
</tbody> </tbody>
</table> </table>
+17180
View File
File diff suppressed because it is too large Load Diff
+17 -18
View File
@@ -8,19 +8,19 @@
"@lottiefiles/dotlottie-react": "^0.16.2", "@lottiefiles/dotlottie-react": "^0.16.2",
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.2.1", "@radix-ui/themes": "^3.2.1",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.10.1",
"axios": "^1.12.2", "axios": "^1.13.2",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^25.4.0", "i18next": "^25.6.3",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"parse": "^6.1.1", "parse": "^7.0.2",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pkijs": "^3.0.8", "pkijs": "^3.3.3",
"print-js": "^1.6.0", "print-js": "^1.6.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"quill-html-edit-button": "^3.0.0", "quill-html-edit-button": "^3.0.0",
@@ -28,11 +28,10 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-confetti": "^6.4.0", "react-confetti": "^6.4.0",
"react-datepicker": "^8.7.0", "react-datepicker": "^8.9.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dnd-multi-backend": "^9.0.0", "react-dnd-multi-backend": "^9.0.0",
"react-dnd-touch-backend": "^16.0.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-gtm-module": "^2.0.11", "react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
@@ -42,12 +41,12 @@
"react-quill-new": "^3.6.0", "react-quill-new": "^3.6.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-rnd": "^10.5.2", "react-rnd": "^10.5.2",
"react-router": "^7.9.3", "react-router": "^7.9.6",
"react-scrollbars-custom": "^4.1.1", "react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.2", "react-select": "^5.10.2",
"react-signature-canvas": "^1.1.0-alpha.2", "react-signature-canvas": "^1.1.0-alpha.2",
"react-timezone-select": "^3.2.8", "react-timezone-select": "^3.2.8",
"react-tooltip": "^5.29.1", "react-tooltip": "^5.30.0",
"reactour": "^1.19.4", "reactour": "^1.19.4",
"redux": "^5.0.1", "redux": "^5.0.1",
"regex-parser": "^2.3.1", "regex-parser": "^2.3.1",
@@ -96,31 +95,31 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.28.3", "@babel/core": "^7.28.3",
"@babel/preset-env": "^7.28.3", "@babel/preset-env": "^7.28.3",
"@babel/preset-react": "^7.27.1", "@babel/preset-react": "^7.28.5",
"@babel/runtime-corejs2": "^7.28.3", "@babel/runtime-corejs2": "^7.28.3",
"@testing-library/jest-dom": "^6.8.0", "@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/react": "^19.1.13", "@types/react": "^19.2.6",
"@vitejs/plugin-react": "^4.7.0", "@vitejs/plugin-react": "^4.7.0",
"@vitest/ui": "^3.2.4", "@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.22",
"babel-loader": "^10.0.0", "babel-loader": "^10.0.0",
"commitizen": "^4.3.1", "commitizen": "^4.3.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"daisyui": "^4.12.24", "daisyui": "^4.12.24",
"eslint": "^9.37.0", "eslint": "^9.39.1",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"jsdom": "^27.0.0", "jsdom": "^27.2.0",
"lint-staged": "^16.1.6", "lint-staged": "^16.2.7",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"pretty-quick": "^4.2.2", "pretty-quick": "^4.2.2",
"rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.18",
"vite": "^7.1.4", "vite": "^7.2.4",
"vite-plugin-svgr": "^4.5.0", "vite-plugin-svgr": "^4.5.0",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4" "vitest": "^3.2.4"
@@ -128,4 +127,4 @@
"engines": { "engines": {
"node": "18 || 20 || 22" "node": "18 || 20 || 22"
} }
} }
@@ -71,6 +71,7 @@
"login-page": "Anmeldeseite", "login-page": "Anmeldeseite",
"or": "ODER", "or": "ODER",
"signup-page": "Registrierungsseite", "signup-page": "Registrierungsseite",
"password-match-length": "Neues Passwort und Bestätigungspasswort sollten übereinstimmen",
"password-length": "Das Passwort sollte 8 Zeichen lang sein", "password-length": "Das Passwort sollte 8 Zeichen lang sein",
"password-case": "Das Passwort sollte einen Großbuchstaben, einen Kleinbuchstaben und eine Zahl enthalten", "password-case": "Das Passwort sollte einen Großbuchstaben, einen Kleinbuchstaben und eine Zahl enthalten",
"password-special-char": "Das Passwort sollte ein Sonderzeichen enthalten", "password-special-char": "Das Passwort sollte ein Sonderzeichen enthalten",
@@ -120,7 +121,8 @@
"Teams": "Teams", "Teams": "Teams",
"Team-update-successfully": "Team erfolgreich aktualisiert.", "Team-update-successfully": "Team erfolgreich aktualisiert.",
"Users": "Benutzer" "Users": "Benutzer"
} },
"Global SMTP": "Globales SMTP"
}, },
"dashboard-card": { "dashboard-card": {
"Need your Signature": "Benötigt Ihre Unterschrift", "Need your Signature": "Benötigt Ihre Unterschrift",
@@ -1256,5 +1258,21 @@
"Please provide required parameters.": "Bitte geben Sie die erforderlichen Parameter an.", "Please provide required parameters.": "Bitte geben Sie die erforderlichen Parameter an.",
"reset-password": "Benutzerpasswort zurücksetzen", "reset-password": "Benutzerpasswort zurücksetzen",
"enter-strong-password": "Geben Sie ein sicheres Passwort ein oder generieren Sie eines automatisch.", "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."
} }
@@ -71,6 +71,7 @@
"login-page": "Login P+age", "login-page": "Login P+age",
"or": "OR", "or": "OR",
"signup-page": "Signup page", "signup-page": "Signup page",
"password-match-length": "New password and confirmation password should match",
"password-length": "Password should be 8 characters long", "password-length": "Password should be 8 characters long",
"password-case": "Password should contain uppercase letter, lowercase letter, digit", "password-case": "Password should contain uppercase letter, lowercase letter, digit",
"password-special-char": " Password should contain special character", "password-special-char": " Password should contain special character",
@@ -120,7 +121,8 @@
"Teams": "Teams", "Teams": "Teams",
"Team-update-successfully": "Team Updated successfully.", "Team-update-successfully": "Team Updated successfully.",
"Users": "Users" "Users": "Users"
} },
"Global SMTP": "Global SMTP"
}, },
"dashboard-card": { "dashboard-card": {
"Need your Signature": "Need your signature", "Need your Signature": "Need your signature",
@@ -1257,5 +1259,21 @@
"Please provide required parameters.": "Please provide required parameters.", "Please provide required parameters.": "Please provide required parameters.",
"reset-password": "Reset User Password", "reset-password": "Reset User Password",
"enter-strong-password": "Enter a strong password or auto-generate one.", "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."
} }
@@ -71,6 +71,7 @@
"login-page": "Página de inicio de sesión", "login-page": "Página de inicio de sesión",
"or": "O", "or": "O",
"signup-page": "Página de registro", "signup-page": "Página de registro",
"password-match-length": "La nueva contraseña y la confirmación deben coincidir",
"password-length": "La contraseña debe tener 8 caracteres", "password-length": "La contraseña debe tener 8 caracteres",
"password-case": "La contraseña debe incluir mayúsculas, minúsculas y números", "password-case": "La contraseña debe incluir mayúsculas, minúsculas y números",
"password-special-char": " La contraseña debe incluir caracteres especiales", "password-special-char": " La contraseña debe incluir caracteres especiales",
@@ -120,7 +121,8 @@
"Teams": "Equipos", "Teams": "Equipos",
"Team-update-successfully": "Equipo actualizado correctamente.", "Team-update-successfully": "Equipo actualizado correctamente.",
"Users": "Usuarios" "Users": "Usuarios"
} },
"Global SMTP": "SMTP Global"
}, },
"dashboard-card": { "dashboard-card": {
"Need your Signature": "Necesitan tu firma", "Need your Signature": "Necesitan tu firma",
@@ -1257,5 +1259,21 @@
"Please provide required parameters.": "Por favor, proporcione los parámetros requeridos.", "Please provide required parameters.": "Por favor, proporcione los parámetros requeridos.",
"reset-password": "Restablecer contraseña de usuario", "reset-password": "Restablecer contraseña de usuario",
"enter-strong-password": "Ingrese una contraseña segura o genere una automáticamente.", "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."
} }
@@ -71,6 +71,7 @@
"login-page": "Page de connexion", "login-page": "Page de connexion",
"or": "OU", "or": "OU",
"signup-page": "Page d'inscription", "signup-page": "Page d'inscription",
"password-match-length": "Le nouveau mot de passe et la confirmation doivent correspondre",
"password-length": "Le mot de passe doit être de plus de 8 caractères", "password-length": "Le mot de passe doit être de plus de 8 caractères",
"password-case": "Le mot de passe doit contenir une lettre majuscule, minuscule et un chiffre ", "password-case": "Le mot de passe doit contenir une lettre majuscule, minuscule et un chiffre ",
"password-special-char": "Le mot de passe doit contenir un caractère spécial", "password-special-char": "Le mot de passe doit contenir un caractère spécial",
@@ -120,7 +121,8 @@
"Teams": "Équipe", "Teams": "Équipe",
"Team-update-successfully": "Équipe mise à jour avec succès.", "Team-update-successfully": "Équipe mise à jour avec succès.",
"Users": "Utilisateurs" "Users": "Utilisateurs"
} },
"Global SMTP": "SMTP Global"
}, },
"dashboard-card": { "dashboard-card": {
"Need your Signature": "Besoin de votre signature", "Need your Signature": "Besoin de votre signature",
@@ -1256,5 +1258,21 @@
"Please provide required parameters.": "Veuillez fournir les paramètres requis.", "Please provide required parameters.": "Veuillez fournir les paramètres requis.",
"reset-password": "Réinitialiser le mot de passe utilisateur", "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.", "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."
} }
@@ -71,6 +71,7 @@
"login-page": "लॉगिन पृष्ठ", "login-page": "लॉगिन पृष्ठ",
"or": "या", "or": "या",
"signup-page": "साइनअप पृष्ठ", "signup-page": "साइनअप पृष्ठ",
"password-match-length": "नया पासवर्ड और पुष्टि पासवर्ड मेल खाना चाहिए",
"password-length": "पासवर्ड 8 वर्ण लंबा होना चाहिए", "password-length": "पासवर्ड 8 वर्ण लंबा होना चाहिए",
"password-case": "पासवर्ड में अपरकेस अक्षर, लोअरकेस अक्षर, अंक होना चाहिए", "password-case": "पासवर्ड में अपरकेस अक्षर, लोअरकेस अक्षर, अंक होना चाहिए",
"password-special-char": " पासवर्ड में विशेष वर्ण होना चाहिए", "password-special-char": " पासवर्ड में विशेष वर्ण होना चाहिए",
@@ -120,7 +121,8 @@
"Teams": "टीमें", "Teams": "टीमें",
"Team-update-successfully": "टीम सफलतापूर्वक अपडेट की गई।", "Team-update-successfully": "टीम सफलतापूर्वक अपडेट की गई।",
"Users": "उपयोगकर्ता" "Users": "उपयोगकर्ता"
} },
"Global SMTP": "ग्लोबल SMTP"
}, },
"dashboard-card": { "dashboard-card": {
"Need your Signature": "आपके हस्ताक्षर की आवश्यकता है", "Need your Signature": "आपके हस्ताक्षर की आवश्यकता है",
@@ -1256,5 +1258,21 @@
"Please provide required parameters.": "कृपया आवश्यक पैरामीटर प्रदान करें।", "Please provide required parameters.": "कृपया आवश्यक पैरामीटर प्रदान करें।",
"reset-password": "उपयोगकर्ता पासवर्ड रीसेट करें", "reset-password": "उपयोगकर्ता पासवर्ड रीसेट करें",
"enter-strong-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": "यदि कोई व्यक्तिगत उपयोगकर्ता-स्तरीय सेटिंग्स कॉन्फ़िगर नहीं की गई हैं, तो ये वैश्विक सेटिंग्स स्वचालित रूप से लागू होंगी।"
} }
@@ -71,6 +71,7 @@
"login-page": "Pagina di accesso", "login-page": "Pagina di accesso",
"or": "O", "or": "O",
"signup-page": "Pagina di registrazione", "signup-page": "Pagina di registrazione",
"password-match-length": "La nuova password e la conferma devono corrispondere",
"password-length": "La password deve contenere almeno 8 caratteri", "password-length": "La password deve contenere almeno 8 caratteri",
"password-case": "La password deve contenere una lettera maiuscola, una minuscola e un numero", "password-case": "La password deve contenere una lettera maiuscola, una minuscola e un numero",
"password-special-char": "La password deve contenere un carattere speciale", "password-special-char": "La password deve contenere un carattere speciale",
@@ -120,7 +121,8 @@
"Teams": "Team", "Teams": "Team",
"Team-update-successfully": "Team aggiornato con successo.", "Team-update-successfully": "Team aggiornato con successo.",
"Users": "Utenti" "Users": "Utenti"
} },
"Global SMTP": "SMTP Globale"
}, },
"dashboard-card": { "dashboard-card": {
"Need your Signature": "Necessita della tua firma", "Need your Signature": "Necessita della tua firma",
@@ -263,7 +265,7 @@
"my-signature": "La mia firma", "my-signature": "La mia firma",
"signature": "Firma", "signature": "Firma",
"upload-image": "Carica immagine", "upload-image": "Carica immagine",
"clear": "Chiara", "clear": "Cancella",
"upload-signature/Image": "Carica firma/immagine", "upload-signature/Image": "Carica firma/immagine",
"initials": "Iniziali", "initials": "Iniziali",
"API": "API", "API": "API",
@@ -1256,5 +1258,21 @@
"Please provide required parameters.": "Fornisci i parametri richiesti.", "Please provide required parameters.": "Fornisci i parametri richiesti.",
"reset-password": "Reimposta password utente", "reset-password": "Reimposta password utente",
"enter-strong-password": "Inserisci una password sicura o generane una automaticamente.", "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" type="checkbox"
checked={selected.includes(col)} checked={selected.includes(col)}
onChange={() => handleChange(col)} onChange={() => handleChange(col)}
className="mb-0" className="mb-0 cursor-pointer"
/> />
<span className="whitespace-nowrap ml-1"> <span className="whitespace-nowrap ml-1">
{t(`report-heading.${col}`, { defaultValue: col })} {t(`report-heading.${col}`, { defaultValue: col })}
+1 -25
View File
@@ -1,12 +1,8 @@
import { HTML5Backend } from "react-dnd-html5-backend"; import { HTML5Backend } from "react-dnd-html5-backend";
import { TouchBackend } from "react-dnd-touch-backend";
import { import {
DndProvider, DndProvider,
TouchTransition, MouseTransition
MouseTransition,
Preview
} from "react-dnd-multi-backend"; } from "react-dnd-multi-backend";
import DragElement from "./pdf/DragElement";
import LazyPage from "../primitives/LazyPage"; import LazyPage from "../primitives/LazyPage";
import { GuidelinesProvider } from "../context/GuidelinesContext"; import { GuidelinesProvider } from "../context/GuidelinesContext";
@@ -16,33 +12,13 @@ const HTML5toTouch = {
id: "html5", id: "html5",
backend: HTML5Backend, backend: HTML5Backend,
transition: MouseTransition 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 }) { export default function DragProvider({ Page, lazy = false }) {
return ( return (
<DndProvider options={HTML5toTouch}> <DndProvider options={HTML5toTouch}>
<Preview>{generatePreview}</Preview>
<GuidelinesProvider> <GuidelinesProvider>
{lazy ? <LazyPage Page={Page} /> : <Page />} {lazy ? <LazyPage Page={Page} /> : <Page />}
</GuidelinesProvider> </GuidelinesProvider>
+6 -3
View File
@@ -13,8 +13,8 @@ import {
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { appInfo } from "../constant/appinfo"; import { appInfo } from "../constant/appinfo";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { toggleSidebar } from "../redux/reducers/sidebarReducer.js"; import { toggleSidebar } from "../redux/reducers/sidebarReducer";
import { sessionStatus } from "../redux/reducers/userReducer.js"; import { sessionStatus } from "../redux/reducers/userReducer";
const Header = ({ isConsole, setIsLoggingOut }) => { const Header = ({ isConsole, setIsLoggingOut }) => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
@@ -141,7 +141,10 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
</button> </button>
</div> </div>
<div className="flex-1 ml-2"> <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 && ( {applogo && (
<img <img
className="object-contain h-full w-auto" className="object-contain h-full w-auto"
@@ -7,7 +7,6 @@ import Loader from "../../primitives/Loader";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
function DashboardReport(props) { function DashboardReport(props) {
const { t } = useTranslation(); const { t } = useTranslation();
// console.log("props ", props)
const [List, setList] = useState([]); const [List, setList] = useState([]);
const [isLoader, setIsLoader] = useState(true); const [isLoader, setIsLoader] = useState(true);
const [reportName, setReportName] = useState(""); const [reportName, setReportName] = useState("");
@@ -1,4 +1,4 @@
import React, { Suspense } from "react"; import { useState, useEffect, Suspense } from "react";
import { lazyWithRetry } from "../../utils"; import { lazyWithRetry } from "../../utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const DashboardButton = lazyWithRetry(() => import("./DashboardButton")); const DashboardButton = lazyWithRetry(() => import("./DashboardButton"));
@@ -20,6 +20,7 @@ const buttonList = [
]; ];
const GetDashboard = (props) => { const GetDashboard = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const Button = ({ label, redirectId, redirectType, icon }) => ( const Button = ({ label, redirectId, redirectType, icon }) => (
<DashboardButton <DashboardButton
Icon={icon} Icon={icon}
@@ -59,7 +60,9 @@ const GetDashboard = (props) => {
<div data-tut={col.widget.data.tourSection}> <div data-tut={col.widget.data.tourSection}>
<Suspense fallback={<div>please wait</div>}> <Suspense fallback={<div>please wait</div>}>
<div className="mb-3 md:mb-0"> <div className="mb-3 md:mb-0">
<DashboardReport Record={col.widget} /> <DashboardReport
Record={col.widget}
/>
</div> </div>
</Suspense> </Suspense>
</div> </div>
@@ -93,7 +96,9 @@ const GetDashboard = (props) => {
return ( return (
<Suspense fallback={<div>please wait</div>}> <Suspense fallback={<div>please wait</div>}>
<div className="mb-3 md:mb-0"> <div className="mb-3 md:mb-0">
<DashboardReport Record={col.widget} /> <DashboardReport
Record={col.widget}
/>
</div> </div>
</Suspense> </Suspense>
); );
@@ -3,6 +3,8 @@ import ModalUi from "../../primitives/ModalUi";
import { EmailBody } from "./EmailBody"; import { EmailBody } from "./EmailBody";
import { import {
contractDocument, contractDocument,
defaultMailBody,
defaultMailSubject,
sendEmailToSigners sendEmailToSigners
} from "../../constant/Utils"; } from "../../constant/Utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -101,7 +103,10 @@ function CustomizeMail(props) {
<div <div
className="flex justify-end items-center gap-1 mt-2 op-link op-link-primary" className="flex justify-end items-center gap-1 mt-2 op-link op-link-primary"
onClick={() => { onClick={() => {
props?.setCustomizeMail(props?.defaultMail); props?.setCustomizeMail({
subject: defaultMailSubject,
body: defaultMailBody
});
}} }}
> >
<span>{t("reset-to-default")}</span> <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, fontColorArr,
fontsizeArr, fontsizeArr,
getContainerScale, getContainerScale,
handleCopyNextToWidget,
handleHeighlightWidget, handleHeighlightWidget,
onChangeInput, onChangeInput,
radioButtonWidget, radioButtonWidget,
@@ -15,13 +14,13 @@ import {
cellsWidget, cellsWidget,
textWidget, textWidget,
selectFormat, selectFormat,
randomId,
getYear, getYear,
getMonth, getMonth,
months, months,
years, years,
getDefaultDate, getDefaultDate,
getDefaultFormat getDefaultFormat,
handleBackground
} from "../../constant/Utils"; } from "../../constant/Utils";
import PlaceholderType from "./PlaceholderType"; import PlaceholderType from "./PlaceholderType";
import moment from "moment"; import moment from "moment";
@@ -29,10 +28,7 @@ import "../../styles/opensigndrive.css";
import ModalUi from "../../primitives/ModalUi"; import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import { setIsShowModal } from "../../redux/reducers/widgetSlice";
setIsShowModal,
setPrefillImg
} from "../../redux/reducers/widgetSlice";
import { themeColor } from "../../constant/const"; import { themeColor } from "../../constant/const";
import { useGuidelinesContext } from "../../context/GuidelinesContext"; import { useGuidelinesContext } from "../../context/GuidelinesContext";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
@@ -40,7 +36,6 @@ import DatePicker from "react-datepicker";
function Placeholder(props) { function Placeholder(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const { showGuidelines } = useGuidelinesContext(); const { showGuidelines } = useGuidelinesContext();
const statusArr = ["Required", "Optional"]; const statusArr = ["Required", "Optional"];
const widgetData = const widgetData =
@@ -306,31 +301,8 @@ function Placeholder(props) {
) { ) {
props.setUniqueId(props?.data?.Id); props.setUniqueId(props?.data?.Id);
} }
if (props?.data?.Role !== "prefill") { props.setIsPageCopy(true);
props.setIsPageCopy(true); props.setCurrWidgetsDetails(props.pos);
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
})
);
}
}
}; };
const setCellCount = (key, newCount) => { 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 calculateFont = (size, isMinHeight) => {
const containerScale = getContainerScale( const containerScale = getContainerScale(
props.pdfOriginalWH, 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 fontSize = calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black"; const fontColor = props.pos.options?.fontColor || "black";
@@ -773,7 +729,11 @@ function Placeholder(props) {
background: background:
props?.data?.Role === "prefill" props?.data?.Role === "prefill"
? "transparent" ? "transparent"
: handleBackground() //handle block color of widget for : handleBackground(
props?.data,
props?.isNeedSign,
props?.uniqueId
) //handle block color of widget for
}} }}
onDrag={(_, d) => { onDrag={(_, d) => {
props?.handleTabDrag?.(props.pos.key); props?.handleTabDrag?.(props.pos.key);
@@ -2,9 +2,12 @@ import { useState } from "react";
import ModalUi from "../../primitives/ModalUi"; import ModalUi from "../../primitives/ModalUi";
import { handleCopyNextToWidget, randomId } from "../../constant/Utils"; import { handleCopyNextToWidget, randomId } from "../../constant/Utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { setPrefillImg } from "../../redux/reducers/widgetSlice";
import { useDispatch } from "react-redux";
function PlaceholderCopy(props) { function PlaceholderCopy(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch();
const copyType = [ const copyType = [
{ id: 1, type: "All pages" }, { id: 1, type: "All pages" },
{ id: 2, type: "All pages but last" }, { id: 2, type: "All pages but last" },
@@ -121,7 +124,7 @@ function PlaceholderCopy(props) {
); );
for (let i = 0; i < props.allPages; i++) { for (let i = 0; i < props.allPages; i++) {
const newId = randomId(); const newId = randomId();
const nameId = randomId(2) const nameId = randomId(2);
const widgetName = `${currentPlaceholder?.options?.name}${nameId}`; const widgetName = `${currentPlaceholder?.options?.name}${nameId}`;
const newPlaceholder = { const newPlaceholder = {
...currentPlaceholder, ...currentPlaceholder,
@@ -134,7 +137,6 @@ function PlaceholderCopy(props) {
); );
const existPlaceholderPosition = const existPlaceholderPosition =
existPlaceholder[0] && existPlaceholder[0].pos; existPlaceholder[0] && existPlaceholder[0].pos;
//function for get copy to requested location of placeholder position //function for get copy to requested location of placeholder position
const getPlaceholderObj = getCopyPlaceholderPosition( const getPlaceholderObj = getCopyPlaceholderPosition(
type, type,
@@ -146,6 +148,18 @@ function PlaceholderCopy(props) {
newPlaceholderPosition.push(getPlaceholderObj); newPlaceholderPosition.push(getPlaceholderObj);
} }
newPageNumber++; newPageNumber++;
if (
filterSignerPosition[0]?.Role === "prefill" &&
currentPlaceholder?.options?.response &&
currentPlaceholder?.type === "image"
) {
dispatch(
setPrefillImg({
id: newId,
base64: currentPlaceholder?.options?.response
})
);
}
} }
let updatedSignerPlaceholder; let updatedSignerPlaceholder;
if (props?.signerObjId) { if (props?.signerObjId) {
@@ -240,6 +254,18 @@ function PlaceholderCopy(props) {
props.setXyPosition, props.setXyPosition,
props?.Id props?.Id
); );
if (
filterSignerPosition[0]?.Role === "prefill" &&
currentXYposition?.options?.response &&
currentXYposition?.type === "image"
) {
dispatch(
setPrefillImg({
id: newId,
base64: currentXYposition?.options?.response
})
);
}
} else { } else {
const getIndex = props?.xyPosition.findIndex( const getIndex = props?.xyPosition.findIndex(
(data) => data.pageNumber === props.pageNumber (data) => data.pageNumber === props.pageNumber
+28 -3
View File
@@ -22,19 +22,25 @@ import { useDispatch, useSelector } from "react-redux";
import Guidelines from "./Guidelines"; import Guidelines from "./Guidelines";
import { useGuidelinesContext } from "../../context/GuidelinesContext"; import { useGuidelinesContext } from "../../context/GuidelinesContext";
import { toggleSidebar } from "../../redux/reducers/sidebarReducer"; import { toggleSidebar } from "../../redux/reducers/sidebarReducer";
import DragGuidelinesLayer from "./DragGridLinesLayer";
import { useDrop } from "react-dnd";
import WidgetsDragPreview from "./WidgetsDragPreview";
function RenderPdf(props) { function RenderPdf(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [, drop] = useDrop({
accept: "BOX",
drop: (item, monitor) => props?.addPositionOfSignature(item, monitor)
});
const [scaledHeight, setScaledHeight] = useState(); 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 //check isGuestSigner is present in local if yes than handle login flow header in mobile view
const isGuestSigner = localStorage.getItem("isGuestSigner"); const isGuestSigner = localStorage.getItem("isGuestSigner");
const scrollTriggerId = useSelector((state) => state.widget.scrollTriggerId); const scrollTriggerId = useSelector((state) => state.widget.scrollTriggerId);
const isOpen = useSelector((state) => state.sidebar.isOpen); const isOpen = useSelector((state) => state.sidebar.isOpen);
const scrollRef = useRef(null); const scrollRef = useRef(null);
const pdfContainerRef = useRef(null); const pdfContainerRef = useRef(null);
useEffect(() => { useEffect(() => {
dispatch(toggleSidebar(false)); dispatch(toggleSidebar(false));
return () => { return () => {
@@ -335,7 +341,7 @@ function RenderPdf(props) {
}} }}
ref={(node) => { ref={(node) => {
pdfContainerRef.current = node; pdfContainerRef.current = node;
props.drop && props.drop(node); drop && drop(node);
}} }}
id="container" id="container"
> >
@@ -532,6 +538,25 @@ function RenderPdf(props) {
y2={guideline.y2} 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> </div>
</RSC> </RSC>
</> </>
@@ -1,7 +1,6 @@
import React, { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import ModalUi from "../../primitives/ModalUi"; import ModalUi from "../../primitives/ModalUi";
import RecipientList from "./RecipientList"; import RecipientList from "./RecipientList";
import { useDrag } from "react-dnd";
import WidgetList from "./WidgetList"; import WidgetList from "./WidgetList";
import { import {
isMobile, isMobile,
@@ -11,106 +10,54 @@ import {
textWidget, textWidget,
widgets widgets
} from "../../constant/Utils"; } from "../../constant/Utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useWidgetDrag } from "../../hook/useWidgetDrag";
function WidgetComponent(props) { function WidgetComponent(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const signRef = useRef(null); const signRef = useRef(null);
const userInformation = localStorage.getItem("UserInformation"); const userInformation = localStorage.getItem("UserInformation");
const [isSignersModal, setIsSignersModal] = useState(false); const [isSignersModal, setIsSignersModal] = useState(false);
const [, signature] = useDrag({ // Define all draggable widget configurations
type: "BOX", const draggableItems = [
item: { type: "BOX", id: 1, text: "signature" } { id: 1, text: "signature" },
}); { id: 2, text: "stamp" },
const [, stamp] = useDrag({ { id: 3, text: "initials" },
type: "BOX", { id: 4, text: textInputWidget },
item: { type: "BOX", id: 2, text: "stamp" } { id: 6, text: "name" },
}); { id: 7, text: "job title" },
const [, dropdown] = useDrag({ { id: 8, text: "company" },
type: "BOX", { id: 9, text: "email" },
item: { type: "BOX", id: 5, text: "dropdown" } { id: 10, text: "date" },
}); { id: 11, text: textWidget },
const [, checkbox] = useDrag({ { id: 12, text: cellsWidget },
type: "BOX", { id: 13, text: "checkbox" },
item: { type: "BOX", id: 6, text: "checkbox" } { id: 14, text: "dropdown" },
}); { id: 15, text: radioButtonWidget },
const [, textInput] = useDrag({ { id: 16, text: "image" }
type: "BOX", ];
item: { type: "BOX", id: 7, text: textInputWidget }
}); // Create all drag refs in one go
const [, cells] = useDrag({ const widgetRefs = draggableItems.map((item) =>
type: "BOX", useWidgetDrag({ type: "BOX", ...item })
item: { type: "BOX", id: 17, text: cellsWidget } );
});
const [, initials] = useDrag({ // Map your widgets with the generated dragRefs
type: "BOX", const [widgetList, setWidgetList] = useState([]);
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([]);
const handleModal = () => { const handleModal = () => {
setIsSignersModal(!isSignersModal); setIsSignersModal(!isSignersModal);
}; };
useEffect(() => { useEffect(() => {
const widgetRef = [ const updated = widgets.map((obj, index) => ({
signature, ...obj,
stamp, ref: widgetRefs[index]?.dragRef || null
initials, }));
textInput, setWidgetList(updated);
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);
// eslint-disable-next-line // 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 // 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) => (data) =>
![ ![
"dropdown", "dropdown",
@@ -122,7 +69,7 @@ function WidgetComponent(props) {
].includes(data.type) ].includes(data.type)
); );
// allow only (signature, stamp, initials, text, cells) widget when isAllowModification true and user does not have session token // 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) => (data) =>
![ ![
"dropdown", "dropdown",
@@ -137,13 +84,13 @@ function WidgetComponent(props) {
"company" "company"
].includes(data.type) ].includes(data.type)
); );
const selfSignWidgets = widget.filter( const selfSignWidgets = widgetList.filter(
(data) => (data) =>
!["dropdown", radioButtonWidget, textInputWidget].includes(data.type) !["dropdown", radioButtonWidget, textInputWidget].includes(data.type)
); );
//if user select prefill role then allow only date,image,text,checkbox,radio,dropdownAdd commentMore actions //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 //dropdown widget should only be show in template flow
const prefillAllowWidgets = widget.filter((data) => const prefillAllowWidgets = widgetList.filter((data) =>
(props.isPrefillDropdown ? ["dropdown"] : []) (props.isPrefillDropdown ? ["dropdown"] : [])
.concat([radioButtonWidget, textWidget, "date", "image", "checkbox"]) .concat([radioButtonWidget, textWidget, "date", "image", "checkbox"])
.includes(data.type) .includes(data.type)
@@ -161,7 +108,7 @@ function WidgetComponent(props) {
return unlogedInUserWidgets; return unlogedInUserWidgets;
} }
} else if (props?.roleName !== "prefill") { } else if (props?.roleName !== "prefill") {
return widget.filter((data) => ![textWidget].includes(data.type)); return widgetList.filter((data) => ![textWidget].includes(data.type));
} }
}; };
const handleSelectRecipient = () => { 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="w-full h-full flex md:justify-between items-center">
<div className="flex justify-start items-center ml-1 overflow-hidden"> <div className="flex justify-start items-center ml-1 overflow-hidden">
{!isMobile && ( {!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} {widgetName}
</span> </span>
</div> </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> <i className={item.icon}></i>
</div> </div>
</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;
+63 -25
View File
@@ -3430,39 +3430,49 @@ export const updateDateWidgetsRes = (
const contactUser = documentData?.Signers?.find( const contactUser = documentData?.Signers?.find(
(data) => data.objectId === signerId (data) => data.objectId === signerId
); );
const extUser = JSON.stringify([contactUser]); const extUser =
localStorage.getItem("Extand_Class") || JSON.stringify([contactUser]);
let placeHolders = documentData?.Placeholders; let placeHolders = documentData?.Placeholders;
if (isRemovePrefill) { if (isRemovePrefill) {
placeHolders = placeHolders?.filter((x) => x.Role !== "prefill"); placeHolders = placeHolders?.filter((x) => x.Role !== "prefill");
} }
const userDetails = extUser ? JSON.parse(extUser)[0] : contactUser; const userDetails = extUser ? JSON.parse(extUser)[0] : contactUser;
return placeHolders?.map((item) => { return placeHolders?.map((item) => {
if (item?.signerObjId === signerId) { if (item?.signerObjId === signerId || item?.Id === signerId) {
return { // Sort page number placeholders
...item, const sortedPlaceHolder = [...item.placeHolder]
placeHolder: item?.placeHolder?.map((ph) => ({ .sort((a, b) => a.pageNumber - b.pageNumber)
.map((ph) => ({
...ph, ...ph,
pos: ph?.pos?.map((widget) => { // Sort positions within each page
if ( pos: [...ph.pos]
["name", "email", "job title", "company"].includes(widget.type) && .sort((a, b) => a.yPosition - b.yPosition)
!widget.options.defaultValue && .map((widget) => {
!widget.options.response // Update widget values if needed
) { if (
return { ["name", "email", "job title", "company"].includes(
...widget, widget.type
options: { ) &&
...widget.options, !widget.options.defaultValue &&
response: widgetDataValue(widget.type, userDetails) !widget.options.response &&
} userDetails
}; ) {
} return {
return widget; ...widget,
}) options: {
})) ...widget.options,
}; response: widgetDataValue(widget.type, userDetails)
} else { }
return item; };
}
return widget;
})
}));
return { ...item, placeHolder: sortedPlaceHolder };
} }
return item;
}); });
}; };
@@ -4038,3 +4048,31 @@ export const getDefaultDate = (dateStr, format) => {
}; };
//function to get default format //function to get default format
export const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy"; 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 };
};
+142 -54
View File
@@ -8,41 +8,70 @@ function ChangePassword() {
const [currentpassword, setCurrentPassword] = useState(""); const [currentpassword, setCurrentPassword] = useState("");
const [newpassword, setnewpassword] = useState(""); const [newpassword, setnewpassword] = useState("");
const [confirmpassword, setconfirmpassword] = useState(""); const [confirmpassword, setconfirmpassword] = useState("");
const [lengthValid, setLengthValid] = useState(false);
const [caseDigitValid, setCaseDigitValid] = useState(false);
const [specialCharValid, setSpecialCharValid] = useState(false);
const [showNewPassword, setNewShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const toggleNewPasswordVisibility = () => {
setNewShowPassword(!showNewPassword);
};
const toggleConfirmPasswordVisibility = () => {
setShowConfirmPassword(!showConfirmPassword);
};
const handlePasswordChange = (e) => {
const newPassword = e.target.value;
setconfirmpassword(newPassword);
// Check conditions separately
setLengthValid(newPassword.length >= 8);
setCaseDigitValid(
/[a-z]/.test(newPassword) &&
/[A-Z]/.test(newPassword) &&
/\d/.test(newPassword)
);
setSpecialCharValid(/[!@#$%^&*()\-_=+{};:,<.>]/.test(newPassword));
};
const handleSubmit = async (evt) => { const handleSubmit = async (evt) => {
evt.preventDefault(); evt.preventDefault();
try { try {
if (newpassword === confirmpassword) { if (newpassword === confirmpassword) {
Parse.User.logIn(localStorage.getItem("userEmail"), currentpassword) if (lengthValid && caseDigitValid && specialCharValid) {
.then(async (user) => { Parse.User.logIn(localStorage.getItem("userEmail"), currentpassword)
if (user) { .then(async (user) => {
const User = new Parse.User(); if (user) {
const query = new Parse.Query(User); const User = new Parse.User();
await query.get(user.id).then((user) => { const query = new Parse.Query(User);
// Updates the data we want await query.get(user.id).then((user) => {
user.set("password", newpassword); // Updates the data we want
user user.set("password", newpassword);
.save() user
.then(async () => { .save()
let _user = user.toJSON(); .then(async () => {
if (_user) { let _user = user.toJSON();
await Parse.User.become(_user.sessionToken); if (_user) {
localStorage.setItem("accesstoken", _user.sessionToken); await Parse.User.become(_user.sessionToken);
} localStorage.setItem("accesstoken", _user.sessionToken);
alert(t("password-update-alert-1")); }
}) setCurrentPassword("");
.catch((error) => { setnewpassword("");
console.log("err", error); setconfirmpassword("");
alert(t("something-went-wrong-mssg")); alert(t("password-update-alert-1"));
}); })
}); .catch((error) => {
} else { console.log("err", error);
alert(t("password-update-alert-2")); alert(t("something-went-wrong-mssg"));
} });
}) });
.catch((error) => { } else {
alert(t("password-update-alert-3")); alert(t("password-update-alert-2"));
console.error("Error while logging in user", error); }
}); })
.catch((error) => {
alert(t("password-update-alert-3"));
console.error("Error while logging in user", error);
});
}
} else { } else {
alert(t("password-update-alert-4")); alert(t("password-update-alert-4"));
} }
@@ -80,34 +109,93 @@ function ChangePassword() {
<label htmlFor="newpassword" className="text-xs block ml-1"> <label htmlFor="newpassword" className="text-xs block ml-1">
{t("new-password")} {t("new-password")}
</label> </label>
<input <div className="relative">
type="password" <input
name="newpassword" type={showNewPassword ? "text" : "password"}
value={newpassword} name="newpassword"
onChange={(e) => setnewpassword(e.target.value)} value={newpassword}
className="op-input op-input-bordered op-input-sm text-xs w-full" onChange={(e) => setnewpassword(e.target.value)}
placeholder={t("new-password")} className="op-input op-input-bordered op-input-sm text-xs w-full"
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))} placeholder={t("new-password")}
onInput={(e) => e.target.setCustomValidity("")} onInvalid={(e) =>
required e.target.setCustomValidity(t("input-required"))
/> }
onInput={(e) => e.target.setCustomValidity("")}
required
/>
<span
className={`absolute top-[50%] right-[10px] -translate-y-[50%] cursor-pointer text-base-content`}
onClick={toggleNewPasswordVisibility}
>
{showNewPassword ? (
<i className="fa fa-eye-slash" /> // Close eye icon
) : (
<i className="fa fa-eye" /> // Open eye icon
)}
</span>
</div>
</div> </div>
<div> <div>
<label htmlFor="newpassword" className="text-xs block ml-1"> <label htmlFor="confirmpassword" className="text-xs block ml-1">
{t("confirm-password")} {t("confirm-password")}
</label> </label>
<input <div className="relative">
type="password" <input
name="confirmpassword" type={showConfirmPassword ? "text" : "password"}
className="op-input op-input-bordered op-input-sm text-xs w-full" name="confirmpassword"
value={confirmpassword} className="op-input op-input-bordered op-input-sm text-xs w-full"
onChange={(e) => setconfirmpassword(e.target.value)} value={confirmpassword}
placeholder={t("confirm-password")} onChange={handlePasswordChange}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))} placeholder={t("confirm-password")}
onInput={(e) => e.target.setCustomValidity("")} onInvalid={(e) =>
required e.target.setCustomValidity(t("input-required"))
/> }
onInput={(e) => e.target.setCustomValidity("")}
required
/>
<span
className={`absolute top-[50%] right-[10px] -translate-y-[50%] cursor-pointer text-base-content`}
onClick={toggleConfirmPasswordVisibility}
>
{showConfirmPassword ? (
<i className="fa fa-eye-slash" /> // Close eye icon
) : (
<i className="fa fa-eye" /> // Open eye icon
)}
</span>
</div>
</div> </div>
{confirmpassword.length > 0 && (
<div className="mt-1 text-[11px]">
{newpassword.length > 0 && (
<p
className={`${newpassword === confirmpassword ? "text-green-600" : "text-red-600"} text-[11px] mt-1`}
>
{newpassword === confirmpassword ? "✓" : "✗"}{" "}
{t("password-match-length")}
</p>
)}
<p
className={`${lengthValid ? "text-green-600" : "text-red-600"}`}
>
{lengthValid ? "✓" : "✗"} {t("password-length")}
</p>
<p
className={`${
caseDigitValid ? "text-green-600" : "text-red-600"
}`}
>
{caseDigitValid ? "✓" : "✗"} {t("password-case")}
</p>
<p
className={`${
specialCharValid ? "text-green-600" : "text-red-600"
}`}
>
{specialCharValid ? "✓" : "✗"} {t("password-special-char")}
</p>
</div>
)}
<button <button
type="submit" type="submit"
className="op-btn op-btn-primary shadow-md mt-2" className="op-btn op-btn-primary shadow-md mt-2"
+71 -85
View File
@@ -46,8 +46,7 @@ function Form() {
} }
const Forms = (props) => { const Forms = (props) => {
const appName = const appName = "OpenSign™";
"OpenSign™";
const { t } = useTranslation(); const { t } = useTranslation();
const abortController = new AbortController(); const abortController = new AbortController();
const inputFileRef = useRef(null); const inputFileRef = useRef(null);
@@ -70,7 +69,7 @@ const Forms = (props) => {
NotifyOnSignatures: "", NotifyOnSignatures: "",
Bcc: [], Bcc: [],
RedirectUrl: "", RedirectUrl: "",
AllowModifications: false, AllowModifications: false
}); });
const [fileupload, setFileUpload] = useState(""); const [fileupload, setFileUpload] = useState("");
const [selectedFiles, setSelectedFiles] = useState([]); const [selectedFiles, setSelectedFiles] = useState([]);
@@ -101,8 +100,7 @@ const Forms = (props) => {
extUserData?.IsTourEnabled === false extUserData?.IsTourEnabled === false
? "false" ? "false"
: "true"; : "true";
const fileSize = const fileSize = maxFileSize;
maxFileSize;
useEffect(() => { useEffect(() => {
handleReset(); handleReset();
return () => abortController.abort(); return () => abortController.abort();
@@ -114,12 +112,12 @@ const Forms = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const initializeValues = async () => { const initializeValues = async () => {
setFormData((obj) => ({ setFormData((obj) => ({
...obj, ...obj,
NotifyOnSignatures: true, NotifyOnSignatures: true,
SendinOrder: sendinorder, SendinOrder: sendinorder,
IsTourEnabled: istourenabled IsTourEnabled: istourenabled
})); }));
}; };
// `removeFile` is used to reset progress, percentage and remove file if exists // `removeFile` is used to reset progress, percentage and remove file if exists
@@ -278,8 +276,7 @@ const Forms = (props) => {
setfileload(false); setfileload(false);
removeFile(e); removeFile(e);
console.log("err in docx to pdf ", err); console.log("err in docx to pdf ", err);
const error = const error = t("docx-error");
t("docx-error");
if (err?.code === 209) { if (err?.code === 209) {
dispatch(sessionStatus(false)); dispatch(sessionStatus(false));
} else { } else {
@@ -308,25 +305,25 @@ const Forms = (props) => {
const name = generatePdfName(16); const name = generatePdfName(16);
const pdfName = `${name}.pdf`; const pdfName = `${name}.pdf`;
let uploadedUrl = ""; let uploadedUrl = "";
const parseFile = new Parse.File( const parseFile = new Parse.File(
pdfName, pdfName,
[...pdfBytes], [...pdfBytes],
"application/pdf" "application/pdf"
); );
const response = await parseFile.save({ const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => { progress: (progressValue, loaded, total) => {
if (type === "upload" && progressValue !== null) { if (progressValue !== null) {
const percentCompleted = Math.round((loaded * 100) / total); const percentCompleted = Math.round((loaded * 100) / total);
setpercentage(percentCompleted); setpercentage(percentCompleted);
}
}
});
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
uploadedUrl = fileRes.url;
} }
} }
});
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
uploadedUrl = fileRes.url;
}
}
if (uploadedUrl) { if (uploadedUrl) {
const tenantId = localStorage.getItem("TenantId"); const tenantId = localStorage.getItem("TenantId");
const userId = extUserData?.UserId?.objectId; const userId = extUserData?.UserId?.objectId;
@@ -413,11 +410,11 @@ const Forms = (props) => {
object.set("RemindOnceInEvery", remindOnceInEvery); object.set("RemindOnceInEvery", remindOnceInEvery);
object.set("IsTourEnabled", isTourEnabled); object.set("IsTourEnabled", isTourEnabled);
object.set("TimeToCompleteDays", TimeToCompleteDays); object.set("TimeToCompleteDays", TimeToCompleteDays);
object.set("AllowModifications", false); object.set("AllowModifications", false);
object.set("IsEnableOTP", false); object.set("IsEnableOTP", false);
if (formData.NotifyOnSignatures !== undefined) { if (formData.NotifyOnSignatures !== undefined) {
object.set("NotifyOnSignatures", formData.NotifyOnSignatures); object.set("NotifyOnSignatures", formData.NotifyOnSignatures);
} }
if (formData?.RedirectUrl) { if (formData?.RedirectUrl) {
object.set("RedirectUrl", formData.RedirectUrl); object.set("RedirectUrl", formData.RedirectUrl);
} }
@@ -454,9 +451,9 @@ const Forms = (props) => {
setBcc([]); setBcc([]);
setFolder({ ObjectId: "", Name: "" }); setFolder({ ObjectId: "", Name: "" });
const notifySign = const notifySign =
extUserData?.NotifyOnSignatures !== undefined extUserData?.NotifyOnSignatures !== undefined
? extUserData?.NotifyOnSignatures ? extUserData?.NotifyOnSignatures
: true; : true;
setFormData({ setFormData({
Name: "", Name: "",
Description: "", Description: "",
@@ -474,7 +471,7 @@ const Forms = (props) => {
IsEnableOTP: "false", IsEnableOTP: "false",
IsTourEnabled: istourenabled, IsTourEnabled: istourenabled,
RedirectUrl: "", RedirectUrl: "",
AllowModifications: false, AllowModifications: false
}); });
setFileUpload(""); setFileUpload("");
setSelectedFiles([]); setSelectedFiles([]);
@@ -536,9 +533,9 @@ const Forms = (props) => {
setBcc([]); setBcc([]);
setFolder({ ObjectId: "", Name: "" }); setFolder({ ObjectId: "", Name: "" });
const notifySign = const notifySign =
extUserData?.NotifyOnSignatures !== undefined extUserData?.NotifyOnSignatures !== undefined
? extUserData?.NotifyOnSignatures ? extUserData?.NotifyOnSignatures
: true; : true;
let obj = { let obj = {
Name: "", Name: "",
Description: "", Description: "",
@@ -556,7 +553,7 @@ const Forms = (props) => {
IsTourEnabled: istourenabled, IsTourEnabled: istourenabled,
NotifyOnSignatures: notifySign, NotifyOnSignatures: notifySign,
RedirectUrl: "", RedirectUrl: "",
AllowModifications: false, AllowModifications: false
}; };
setFormData(obj); setFormData(obj);
removeFile(); removeFile();
@@ -578,35 +575,27 @@ const Forms = (props) => {
const name = generatePdfName(16); const name = generatePdfName(16);
const pdfFile = await decryptPdf(formData?.file, formData?.password); const pdfFile = await decryptPdf(formData?.file, formData?.password);
setIsDecrypting(false); setIsDecrypting(false);
const res = await getFileAsArrayBuffer(pdfFile); const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res); const flatPdf = await flattenPdf(res);
const parseFile = new Parse.File(name, [...flatPdf], "application/pdf"); const parseFile = new Parse.File(name, [...flatPdf], "application/pdf");
await parseFile.save({ await parseFile.save({
progress: (progressValue, loaded, total, { type }) => { progress: (progressValue, loaded, total) => {
if (type === "upload" && progressValue !== null) { if (progressValue !== null) {
const percentCompleted = Math.round((loaded * 100) / total); const percentCompleted = Math.round((loaded * 100) / total);
setpercentage(percentCompleted); setpercentage(percentCompleted);
}
}
});
// Retrieve the URL of the uploaded file
if (parseFile.url()) {
const fileRes = await getSecureUrl(parseFile.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
removeFile();
const title = generateTitleFromFilename(formData?.file?.name);
setFormData((obj) => ({ ...obj, password: "", Name: title }));
SaveFileSize(size, fileRes?.url, tenantId, userId);
return fileRes.url;
} else {
removeFile();
setFormData((prev) => ({ ...prev, password: "" }));
setIsDecrypting(false);
if (inputFileRef.current) {
inputFileRef.current.value = ""; // Set file input value to empty string
}
} }
}
});
// Retrieve the URL of the uploaded file
if (parseFile.url()) {
const fileRes = await getSecureUrl(parseFile.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
removeFile();
const title = generateTitleFromFilename(formData?.file?.name);
setFormData((obj) => ({ ...obj, password: "", Name: title }));
SaveFileSize(size, fileRes?.url, tenantId, userId);
return fileRes.url;
} else { } else {
removeFile(); removeFile();
setFormData((prev) => ({ ...prev, password: "" })); setFormData((prev) => ({ ...prev, password: "" }));
@@ -615,6 +604,14 @@ const Forms = (props) => {
inputFileRef.current.value = ""; // Set file input value to empty string inputFileRef.current.value = ""; // Set file input value to empty string
} }
} }
} else {
removeFile();
setFormData((prev) => ({ ...prev, password: "" }));
setIsDecrypting(false);
if (inputFileRef.current) {
inputFileRef.current.value = ""; // Set file input value to empty string
}
}
} catch (err) { } catch (err) {
removeFile(); removeFile();
if (err?.code === 209) { if (err?.code === 209) {
@@ -1057,10 +1054,7 @@ const Forms = (props) => {
{isAdvanceOpt && ( {isAdvanceOpt && (
<div <div
style={{ style={{
height: height: props.title === "New Template" ? "100px" : "280px"
props.title === "New Template"
? "100px"
: "280px"
}} }}
className="w-[1px] bg-gray-300 m-auto hidden md:inline-block" className="w-[1px] bg-gray-300 m-auto hidden md:inline-block"
></div> ></div>
@@ -1169,11 +1163,7 @@ const Forms = (props) => {
</Tooltip> </Tooltip>
</label> </label>
<div className="flex flex-col md:flex-row md:gap-4"> <div className="flex flex-col md:flex-row md:gap-4">
<div <div className={`flex items-center gap-2 ml-2 mb-1`}>
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input <input
className="mr-[2px] op-radio op-radio-xs" className="mr-[2px] op-radio op-radio-xs"
type="radio" type="radio"
@@ -1182,11 +1172,7 @@ const Forms = (props) => {
/> />
<div className="text-center">{t("yes")}</div> <div className="text-center">{t("yes")}</div>
</div> </div>
<div <div className={`flex items-center gap-2 ml-2 mb-1`}>
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input <input
className="mr-[2px] op-radio op-radio-xs" className="mr-[2px] op-radio op-radio-xs"
type="radio" type="radio"
+5 -1
View File
@@ -47,10 +47,14 @@ function Login() {
const [image, setImage] = useState(); const [image, setImage] = useState();
const [errMsg, setErrMsg] = useState(); const [errMsg, setErrMsg] = useState();
useEffect(() => { useEffect(() => {
checkUserExt(); handleUserExist();
// eslint-disable-next-line // eslint-disable-next-line
}, []); }, []);
const handleUserExist = async () => {
checkUserExt();
};
const setLocalVar = (user) => { const setLocalVar = (user) => {
localStorage.setItem("accesstoken", user.sessionToken); localStorage.setItem("accesstoken", user.sessionToken);
+38 -21
View File
@@ -3,7 +3,6 @@ import { PDFDocument } from "pdf-lib";
import "../styles/signature.css"; import "../styles/signature.css";
import Parse from "parse"; import Parse from "parse";
import axios from "axios"; import axios from "axios";
import { useDrop } from "react-dnd";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage"; import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import Tour from "../primitives/Tour"; import Tour from "../primitives/Tour";
@@ -49,7 +48,7 @@ import {
widgetDataValue, widgetDataValue,
getOriginalWH, getOriginalWH,
handleCheckResponse, handleCheckResponse,
convertJpegToPng, convertJpegToPng
} from "../constant/Utils"; } from "../constant/Utils";
import Header from "../components/pdf/PdfHeader"; import Header from "../components/pdf/PdfHeader";
import RenderPdf from "../components/pdf/RenderPdf"; import RenderPdf from "../components/pdf/RenderPdf";
@@ -156,16 +155,12 @@ function PdfRequestFiles(
const [assignedWidgetId, setAssignedWidgetId] = useState([]); const [assignedWidgetId, setAssignedWidgetId] = useState([]);
const [showSignPagenumber, setShowSignPagenumber] = useState([]); const [showSignPagenumber, setShowSignPagenumber] = useState([]);
const [owner, setOwner] = useState({}); const [owner, setOwner] = useState({});
const [tenant, setTenant] = useState({});
const [tenantMailTemplate, setTenantMailTemplate] = useState({ const [tenantMailTemplate, setTenantMailTemplate] = useState({
body: "", body: "",
subject: "" subject: ""
}); });
const [isOptionalDetails, setIsOptionalDetails] = useState(false); 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; const isMobile = window.innerWidth < 767;
let isGuestSignFlow = false; let isGuestSignFlow = false;
let sendmail; let sendmail;
@@ -250,6 +245,7 @@ function PdfRequestFiles(
const filterSignTypes = signatureType?.filter( const filterSignTypes = signatureType?.filter(
(x) => x.enabled === true (x) => x.enabled === true
); );
setTenant(tenantDetails || {});
if (tenantDetails?.RequestBody) { if (tenantDetails?.RequestBody) {
setTenantMailTemplate({ setTenantMailTemplate({
body: tenantDetails?.RequestBody, body: tenantDetails?.RequestBody,
@@ -340,7 +336,6 @@ function PdfRequestFiles(
const getCurrentSigner = getSigners?.find( const getCurrentSigner = getSigners?.find(
(data) => data.UserId.objectId === jsonSender?.objectId (data) => data.UserId.objectId === jsonSender?.objectId
); );
currUserId = getCurrentSigner?.objectId currUserId = getCurrentSigner?.objectId
? getCurrentSigner.objectId ? getCurrentSigner.objectId
: contactBookId || : contactBookId ||
@@ -770,12 +765,16 @@ function PdfRequestFiles(
sendmail !== "false" && sendmail !== "false" &&
sendInOrder sendInOrder
) { ) {
const mailBody =
tenantMailTemplate?.body;
const mailSubject =
tenantMailTemplate?.subject;
const requestBody = const requestBody =
updatedDoc.updatedPdfDetails?.[0]?.RequestBody || updatedDoc.updatedPdfDetails?.[0]?.RequestBody ||
tenantMailTemplate?.body; mailBody;
const requestSubject = const requestSubject =
updatedDoc.updatedPdfDetails?.[0]?.RequestSubject || updatedDoc.updatedPdfDetails?.[0]?.RequestSubject ||
tenantMailTemplate?.subject; mailSubject;
if (user) { if (user) {
const expireDate = expiry; const expireDate = expiry;
const newDate = new Date(expireDate); const newDate = new Date(expireDate);
@@ -1099,10 +1098,11 @@ function PdfRequestFiles(
const userId = const userId =
pdfDetails?.[0].Signers?.find((x) => x.objectId === signerObjectId) pdfDetails?.[0].Signers?.find((x) => x.objectId === signerObjectId)
?.UserId?.objectId || jsonSender?.objectId; ?.UserId?.objectId || jsonSender?.objectId;
const params = { const params = {
docId: pdfDetails?.[0].objectId, docId: pdfDetails?.[0].objectId,
reason: reason, reason: reason,
userId: userId userId: userId,
}; };
await axios await axios
.post(`${localStorage.getItem("baseUrl")}functions/declinedoc`, params, { .post(`${localStorage.getItem("baseUrl")}functions/declinedoc`, params, {
@@ -1488,7 +1488,10 @@ function PdfRequestFiles(
const widgetHeight = const widgetHeight =
defaultWidthHeight(dragTypeValue).height * containerScale; defaultWidthHeight(dragTypeValue).height * containerScale;
const extUser = localStorage.getItem("Extand_Class"); 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); const widgetValue = widgetDataValue(dragTypeValue, parseUser);
//adding and updating drop position in array when user drop signature button in div //adding and updating drop position in array when user drop signature button in div
if (item === "onclick") { 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. //`containerRect.top` The distance from the top of the viewport to the top of the element.
const x = offset.x - containerRect.left; const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top; const y = offset.y - containerRect.top;
const getXPosition = signBtnPosition[0] let getXPosition = signBtnPosition[0] ? x - signBtnPosition[0].xPos : x;
? x - signBtnPosition[0].xPos let getYPosition = signBtnPosition[0] ? y - signBtnPosition[0].yPos : y;
: x; // to avoid negative position values (half portion of widget should not be out of pdf container)
const getYPosition = signBtnPosition[0] if (getXPosition < 0) {
? y - signBtnPosition[0].yPos getXPosition = 0;
: y; }
if (getYPosition < 0) {
getYPosition = 0;
}
dropObj = { dropObj = {
xPosition: getXPosition / (containerScale * scale), xPosition: getXPosition / (containerScale * scale),
yPosition: getYPosition / (containerScale * scale), yPosition: getYPosition / (containerScale * scale),
@@ -1572,6 +1579,7 @@ function PdfRequestFiles(
); );
const getPos = currentPagePosition?.pos; const getPos = currentPagePosition?.pos;
const newSignPos = getPos.concat(dropData); const newSignPos = getPos.concat(dropData);
newSignPos.sort((a, b) => a.yPosition - b.yPosition);
let xyPos = { pageNumber: pageNumber, pos: newSignPos }; let xyPos = { pageNumber: pageNumber, pos: newSignPos };
updatePlace.push(xyPos); updatePlace.push(xyPos);
const updatesignerPos = signerPos.map((x) => const updatesignerPos = signerPos.map((x) =>
@@ -1582,9 +1590,17 @@ function PdfRequestFiles(
//else condition to add placeholder widgets on multiple page first time //else condition to add placeholder widgets on multiple page first time
const updatesignerPos = signerPos.map((x) => const updatesignerPos = signerPos.map((x) =>
x.Id === uniqueId && x?.placeHolder 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.Id === uniqueId
? { ...x, placeHolder: [placeHolder] } ? {
...x,
placeHolder: [placeHolder]
}
: x : x
); );
setSignerPos(updatesignerPos); setSignerPos(updatesignerPos);
@@ -2033,7 +2049,6 @@ function PdfRequestFiles(
{containerWH && ( {containerWH && (
<RenderPdf <RenderPdf
setIsPageCopy={setIsPageCopy} setIsPageCopy={setIsPageCopy}
drop={drop}
pageNumber={pageNumber} pageNumber={pageNumber}
pdfOriginalWH={pdfOriginalWH} pdfOriginalWH={pdfOriginalWH}
pdfNewWidth={pdfNewWidth} pdfNewWidth={pdfNewWidth}
@@ -2071,6 +2086,8 @@ function PdfRequestFiles(
setIsReqSignTourDisabled={handleCloseTour} setIsReqSignTourDisabled={handleCloseTour}
currWidgetsDetails={currWidgetsDetails} currWidgetsDetails={currWidgetsDetails}
isShowModal={isShowModal} isShowModal={isShowModal}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
/> />
)} )}
</div> </div>
+26 -9
View File
@@ -3,7 +3,6 @@ import axios from "axios";
import Parse from "parse"; import Parse from "parse";
import "../styles/signature.css"; import "../styles/signature.css";
import { PDFDocument } from "pdf-lib"; import { PDFDocument } from "pdf-lib";
import { useDrop } from "react-dnd";
import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage"; import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import WidgetComponent from "../components/pdf/WidgetComponent"; import WidgetComponent from "../components/pdf/WidgetComponent";
import Tour from "../primitives/Tour"; import Tour from "../primitives/Tour";
@@ -164,11 +163,6 @@ function PlaceHolderSign() {
const [docTitle, setDocTitle] = useState(""); const [docTitle, setDocTitle] = useState("");
const [isEditDoc, setIsEditDoc] = useState(false); const [isEditDoc, setIsEditDoc] = useState(false);
const isMobile = window.innerWidth < 767; 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( const currentUser = localStorage.getItem(
`Parse/${localStorage.getItem("parseAppId")}/currentUser` `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. //`containerRect.top` The distance from the top of the viewport to the top of the element.
const x = offset.x - containerRect.left; const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top; const y = offset.y - containerRect.top;
const getXPosition = signBtnPosition[0] let getXPosition = signBtnPosition[0]
? x - signBtnPosition[0].xPos ? x - signBtnPosition[0].xPos
: x; : x;
const getYPosition = signBtnPosition[0] let getYPosition = signBtnPosition[0]
? y - signBtnPosition[0].yPos ? y - signBtnPosition[0].yPos
: y; : 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 = { dropObj = {
xPosition: getXPosition / (containerScale * scale), xPosition: getXPosition / (containerScale * scale),
yPosition: getYPosition / (containerScale * scale), yPosition: getYPosition / (containerScale * scale),
@@ -1931,6 +1946,7 @@ function PlaceHolderSign() {
return { ...x, ...data }; return { ...x, ...data };
}); });
setPdfDetails(updateDocument); setPdfDetails(updateDocument);
setDocTitle(updateDocument?.[0]?.Name);
try { try {
const Bcc = updateDocument?.[0]?.Bcc?.length const Bcc = updateDocument?.[0]?.Bcc?.length
? { ? {
@@ -2332,7 +2348,6 @@ function PlaceHolderSign() {
numPages={numPages} numPages={numPages}
pageDetails={pageDetails} pageDetails={pageDetails}
placeholder={true} placeholder={true}
drop={drop}
handleDeleteWidget={handleDeleteWidget} handleDeleteWidget={handleDeleteWidget}
handleTabDrag={handleTabDrag} handleTabDrag={handleTabDrag}
handleStop={handleStop} handleStop={handleStop}
@@ -2368,6 +2383,8 @@ function PlaceHolderSign() {
currWidgetsDetails={currWidgetsDetails} currWidgetsDetails={currWidgetsDetails}
setRoleName={setRoleName} setRoleName={setRoleName}
isShowModal={isShowModal} isShowModal={isShowModal}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
/> />
)} )}
</div> </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 Alert from "../primitives/Alert";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Loader from "../primitives/Loader"; import Loader from "../primitives/Loader";
@@ -6,37 +6,24 @@ import Tooltip from "../primitives/Tooltip";
import { import {
getTenantDetails, getTenantDetails,
handleSignatureType, handleSignatureType,
openInNewTab,
signatureTypes, signatureTypes,
usertimezone usertimezone
} from "../constant/Utils"; } from "../constant/Utils";
import Parse from "parse"; import Parse from "parse";
import { Tooltip as ReactTooltip } from "react-tooltip"; 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 TimezoneSelector from "../components/preferences/TimezoneSelector";
import DateFormatSelector from "../components/preferences/DateFormatSelector"; import DateFormatSelector from "../components/preferences/DateFormatSelector";
import FilenameFormatSelector from "../components/preferences/FilenameFormatSelector"; import FilenameFormatSelector from "../components/preferences/FilenameFormatSelector";
import axios from "axios";
import MailTemplateEditor from "../components/preferences/MailTemplateEditor";
const Preferences = () => { const Preferences = () => {
const appName = const appName =
"OpenSign™"; "OpenSign™";
const { t } = useTranslation(); const { t } = useTranslation();
const editorRef = useRef();
const editorRefCom = useRef();
const Extand_Class = localStorage.getItem("Extand_Class"); const Extand_Class = localStorage.getItem("Extand_Class");
const extClass = Extand_Class && JSON.parse(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 [isalert, setIsAlert] = useState({ type: "success", msg: "" });
const [isTopLoader, setIsTopLoader] = useState(false); const [isTopLoader, setIsTopLoader] = useState(false);
const [isLoader, setIsLoader] = useState(false); const [isLoader, setIsLoader] = useState(false);
@@ -56,11 +43,9 @@ const Preferences = () => {
const [dateFormat, setDateFormat] = useState("MM/DD/YYYY"); const [dateFormat, setDateFormat] = useState("MM/DD/YYYY");
const [is12HourTime, setIs12HourTime] = useState(false); const [is12HourTime, setIs12HourTime] = useState(false);
const [isLTVEnabled, setIsLTVEnabled] = useState(false); const [isLTVEnabled, setIsLTVEnabled] = useState(false);
const [isDefaultMail, setIsDefaultMail] = useState({
requestMail: false,
completionMail: false
});
const [fileNameFormat, setFileNameFormat] = useState("DOCNAME"); const [fileNameFormat, setFileNameFormat] = useState("DOCNAME");
const [userInfo, setUserInfo] = useState({});
const [tenantInfo, setTenantInfo] = useState({});
useEffect(() => { useEffect(() => {
fetchSignType(); fetchSignType();
@@ -69,10 +54,10 @@ const Preferences = () => {
const fetchSignType = async () => { const fetchSignType = async () => {
setIsTopLoader(true); setIsTopLoader(true);
const EmailTab = const EmailTab = [
extClass?.[0]?.UserRole === "contracts_Admin" { name: "email", title: t("email"), icon: "fa-light fa-envelope" }
? [{ name: "email", title: t("email"), icon: "fa-light fa-envelope" }] ];
: [];
const arr = [ const arr = [
generaltab, generaltab,
...EmailTab, ...EmailTab,
@@ -85,17 +70,29 @@ const Preferences = () => {
) )
); );
const tenantDetails = await getTenantDetails(user?.objectId); const tenantDetails = await getTenantDetails(user?.objectId);
await tenantEmailTemplate(tenantDetails); setTenantInfo(tenantDetails);
const signatureType = tenantDetails?.SignatureType || []; const signatureType = tenantDetails?.SignatureType || [];
const tenantSignTypes = signatureType?.filter((x) => x.enabled === true); 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) { if (getUser) {
const _getUser = JSON.parse(JSON.stringify(getUser)); const _getUser = JSON.parse(JSON.stringify(getUser));
const notifyOnSignatures = setUserInfo(_getUser);
setIsNotifyOnSignatures(
_getUser?.NotifyOnSignatures !== undefined _getUser?.NotifyOnSignatures !== undefined
? _getUser?.NotifyOnSignatures ? _getUser?.NotifyOnSignatures
: true; : true
setIsNotifyOnSignatures(notifyOnSignatures); );
setTimezone(_getUser?.Timezone || usertimezone); setTimezone(_getUser?.Timezone || usertimezone);
if (tenantSignTypes?.length > 0) { if (tenantSignTypes?.length > 0) {
const signatureType = _getUser?.SignatureType || signatureTypes; const signatureType = _getUser?.SignatureType || signatureTypes;
@@ -108,31 +105,29 @@ const Preferences = () => {
const SignatureType = _getUser?.SignatureType || signatureTypes; const SignatureType = _getUser?.SignatureType || signatureTypes;
setSignatureType(SignatureType); setSignatureType(SignatureType);
} }
const sendinorder = setSendinOrder(
_getUser?.SendinOrder !== undefined ? _getUser?.SendinOrder : true; _getUser?.SendinOrder !== undefined ? _getUser?.SendinOrder : true
setSendinOrder(sendinorder); );
const istourenabled = setIsTourEnabled(
_getUser?.IsTourEnabled !== undefined _getUser?.IsTourEnabled !== undefined ? _getUser?.IsTourEnabled : true
? _getUser?.IsTourEnabled );
: true; setDateFormat(
setIsTourEnabled(istourenabled);
const DateFormat =
_getUser?.DateFormat !== undefined _getUser?.DateFormat !== undefined
? _getUser?.DateFormat ? _getUser?.DateFormat
: "MM/DD/YYYY"; : "MM/DD/YYYY"
setDateFormat(DateFormat); );
const is12Hr = setIs12HourTime(
_getUser?.Is12HourTime !== undefined ? _getUser?.Is12HourTime : false; _getUser?.Is12HourTime !== undefined ? _getUser?.Is12HourTime : false
setIs12HourTime(is12Hr); );
const isLTVEnabled = setIsLTVEnabled(
_getUser?.IsLTVEnabled !== undefined ? _getUser?.IsLTVEnabled : false; _getUser?.IsLTVEnabled !== undefined ? _getUser?.IsLTVEnabled : false
setIsLTVEnabled(isLTVEnabled); );
const downloadFilenameFormat = const downloadFilenameFormat =
_getUser?.DownloadFilenameFormat || "DOCNAME"; _getUser?.DownloadFilenameFormat || "DOCNAME";
setFileNameFormat(downloadFilenameFormat); setFileNameFormat(downloadFilenameFormat);
} }
} catch (err) { } catch (err) {
console.log("err while getting user details", err); console.error("Error while getting user details: ", err);
setErrMsg(t("something-went-wrong-mssg")); setErrMsg(t("something-went-wrong-mssg"));
} finally { } finally {
setIsTopLoader(false); setIsTopLoader(false);
@@ -215,7 +210,7 @@ const Preferences = () => {
} }
} }
} catch (err) { } catch (err) {
console.log("Error updating signature type", err); console.error("Error updating signature type: ", err);
setIsAlert({ type: "danger", msg: err.message }); setIsAlert({ type: "danger", msg: err.message });
} }
@@ -228,178 +223,12 @@ const Preferences = () => {
const handleNotifySignChange = (value) => { const handleNotifySignChange = (value) => {
setIsNotifyOnSignatures(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 handleTourInput = () => setIsTourEnabled(!isTourEnabled);
const handleSendinOrderInput = () => setSendinOrder(!sendinOrder); const handleSendinOrderInput = () => setSendinOrder(!sendinOrder);
const tabName = (ind) => tab.find((t, i) => i === ind)?.name; 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 ( return (
<React.Fragment> <React.Fragment>
{isalert.msg && <Alert type={isalert.type}>{isalert.msg}</Alert>} {isalert.msg && <Alert type={isalert.type}>{isalert.msg}</Alert>}
@@ -793,160 +622,12 @@ const Preferences = () => {
)} )}
{tabName(activeTab) === "email" && ( {tabName(activeTab) === "email" && (
<div className="flex flex-col mb-4"> <div className="flex flex-col mb-4">
<div className="flex flex-col"> <MailTemplateEditor
<h1 className="text-[14px] mb-[0.7rem] font-medium"> info={
{t("request-email")} tenantInfo
</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>
)
} }
<form tenantId={tenantInfo?.objectId}
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>
</div> </div>
)} )}
{tabName(activeTab) === "security" && ( {tabName(activeTab) === "security" && (
+27 -13
View File
@@ -5,7 +5,6 @@ import Parse from "parse";
import Confetti from "react-confetti"; import Confetti from "react-confetti";
import axios from "axios"; import axios from "axios";
import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage"; import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import { useDrop } from "react-dnd";
import EmailComponent from "../components/pdf/EmailComponent"; import EmailComponent from "../components/pdf/EmailComponent";
import WidgetComponent from "../components/pdf/WidgetComponent"; import WidgetComponent from "../components/pdf/WidgetComponent";
import { import {
@@ -69,7 +68,9 @@ import {
import WidgetsValueModal from "../components/pdf/WidgetsValueModal"; import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
import WidgetNameModal from "../components/pdf/WidgetNameModal"; import WidgetNameModal from "../components/pdf/WidgetNameModal";
import CellsSettingModal from "../components/pdf/CellsSettingModal"; 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. //For signYourself inProgress section signer can add sign and complete doc sign.
function SignYourSelf() { function SignYourSelf() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -145,11 +146,6 @@ function SignYourSelf() {
const [widgetsTour, setWidgetsTour] = useState(false); const [widgetsTour, setWidgetsTour] = useState(false);
const [owner, setOwner] = useState({}); 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) => { const index = xyPosition?.findIndex((object) => {
return object.pageNumber === pageNumber; return object.pageNumber === pageNumber;
}); });
@@ -397,15 +393,34 @@ function SignYourSelf() {
.getBoundingClientRect(); .getBoundingClientRect();
//`containerRect.left`, The distance from the left of the viewport to the left side of the element. //`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. //`containerRect.top` The distance from the top of the viewport to the top of the element.
const x = offset.x - containerRect.left; const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top; const y = offset.y - containerRect.top;
const getXPosition = signBtnPosition[0] ? x - signBtnPosition[0].xPos : x; let getXPosition = signBtnPosition[0] ? x - signBtnPosition[0].xPos : x;
const getYPosition = signBtnPosition[0] ? y - signBtnPosition[0].yPos : y; let getYPosition = signBtnPosition[0] ? y - signBtnPosition[0].yPos : y;
const getWidth = widgetTypeExist const getWidth = widgetTypeExist
? calculateInitialWidthHeight(widgetValue).getWidth ? calculateInitialWidthHeight(widgetValue).getWidth
: defaultWidthHeight(dragTypeValue).width; : defaultWidthHeight(dragTypeValue).width;
const getHeight = defaultWidthHeight(dragTypeValue).height; 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 = { dropObj = {
xPosition: getXPosition / (containerScale * scale), xPosition: getXPosition / (containerScale * scale),
yPosition: getYPosition / (containerScale * scale), yPosition: getYPosition / (containerScale * scale),
@@ -454,9 +469,7 @@ function SignYourSelf() {
setFontColor("black"); setFontColor("black");
} }
dispatch(setIsShowModal({ [key]: true })); dispatch(setIsShowModal({ [key]: true }));
// if (dragTypeValue !== "checkbox") {
setCurrWidgetsDetails(dropObj); setCurrWidgetsDetails(dropObj);
// }
}; };
//`handleResend` function is used to resend otp for email verification //`handleResend` function is used to resend otp for email verification
@@ -1388,7 +1401,6 @@ function SignYourSelf() {
pageNumber={pageNumber} pageNumber={pageNumber}
pdfOriginalWH={pdfOriginalWH} pdfOriginalWH={pdfOriginalWH}
pdfNewWidth={pdfNewWidth} pdfNewWidth={pdfNewWidth}
drop={drop}
successEmail={successEmail} successEmail={successEmail}
nodeRef={nodeRef} nodeRef={nodeRef}
handleTabDrag={handleTabDrag} handleTabDrag={handleTabDrag}
@@ -1427,6 +1439,8 @@ function SignYourSelf() {
currWidgetsDetails={currWidgetsDetails} currWidgetsDetails={currWidgetsDetails}
isShowModal={isShowModal} isShowModal={isShowModal}
unSignedWidgetId={unSignedWidgetId} unSignedWidgetId={unSignedWidgetId}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
/> />
)} )}
</div> </div>
+45 -35
View File
@@ -3,7 +3,6 @@ import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import { useParams, useNavigate } from "react-router"; import { useParams, useNavigate } from "react-router";
import axios from "axios"; import axios from "axios";
import "../styles/signature.css"; import "../styles/signature.css";
import { useDrop } from "react-dnd";
import WidgetComponent from "../components/pdf/WidgetComponent"; import WidgetComponent from "../components/pdf/WidgetComponent";
import Tour from "../primitives/Tour"; import Tour from "../primitives/Tour";
import SignerListPlace from "../components/pdf/SignerListPlace"; import SignerListPlace from "../components/pdf/SignerListPlace";
@@ -124,11 +123,6 @@ const TemplatePlaceholder = () => {
const [pdfBase64Url, setPdfBase64Url] = useState(""); const [pdfBase64Url, setPdfBase64Url] = useState("");
const [isUploadPdf, setIsUploadPdf] = useState(false); const [isUploadPdf, setIsUploadPdf] = useState(false);
const isMobile = window.innerWidth < 767; 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 [uniqueId, setUniqueId] = useState("");
const [isModalRole, setIsModalRole] = useState(false); const [isModalRole, setIsModalRole] = useState(false);
const [roleName, setRoleName] = useState(""); const [roleName, setRoleName] = useState("");
@@ -215,28 +209,21 @@ const TemplatePlaceholder = () => {
const filterSignTypes = signatureType?.filter( const filterSignTypes = signatureType?.filter(
(x) => x.enabled === true (x) => x.enabled === true
); );
if (tenantDetails?.RequestBody) { const extUser =
//customize mail state is handle to when user want to customize already set tenant email format then use that format localStorage.getItem("Extand_Class") &&
setCustomizeMail({ JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
subject: tenantDetails?.RequestSubject, const subject = tenantDetails?.RequestSubject ?? "";
body: tenantDetails?.RequestBody const body = tenantDetails?.RequestBody ?? "";
}); //customize mail state is handle to when user want to customize already set tenant email format then use that format
setDefaultMail({ const userSubject =
subject: tenantDetails?.RequestSubject, subject;
body: tenantDetails?.RequestBody const userBody =
}); body;
} else { setCustomizeMail({
const defaultRequestBody = defaultMailBody; subject: userSubject ?? defaultMailSubject,
const defaultSubject = defaultMailSubject; body: userBody ?? defaultMailBody
setCustomizeMail({ });
subject: defaultSubject, setDefaultMail({ subject: userSubject, body: userBody });
body: defaultRequestBody
});
setDefaultMail({
subject: defaultSubject,
body: defaultRequestBody
});
}
return filterSignTypes; return filterSignTypes;
} }
} catch (e) { } catch (e) {
@@ -498,12 +485,33 @@ const TemplatePlaceholder = () => {
.getBoundingClientRect(); .getBoundingClientRect();
const x = offset.x - containerRect.left; const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top; const y = offset.y - containerRect.top;
const getXPosition = signBtnPosition[0] let getXPosition = signBtnPosition[0]
? x - signBtnPosition[0].xPos ? x - signBtnPosition[0].xPos
: x; : x;
const getYPosition = signBtnPosition[0] let getYPosition = signBtnPosition[0]
? y - signBtnPosition[0].yPos ? y - signBtnPosition[0].yPos
: y; : 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 = { dropObj = {
xPosition: getXPosition / (containerScale * scale), xPosition: getXPosition / (containerScale * scale),
yPosition: getYPosition / (containerScale * scale), yPosition: getYPosition / (containerScale * scale),
@@ -754,7 +762,11 @@ const TemplatePlaceholder = () => {
} }
}; };
useEffect(() => { useEffect(() => {
if (pdfDetails?.[0]?.CreatedBy?.objectId !== user?.objectId || isPrefillModal) return; if (
pdfDetails?.[0]?.CreatedBy?.objectId !== user?.objectId ||
isPrefillModal
)
return;
const timer = setTimeout(() => { const timer = setTimeout(() => {
autosavedetails(); autosavedetails();
}, 2000); }, 2000);
@@ -1926,7 +1938,6 @@ const TemplatePlaceholder = () => {
numPages={numPages} numPages={numPages}
pageDetails={pageDetails} pageDetails={pageDetails}
placeholder={true} placeholder={true}
drop={drop}
handleDeleteWidget={handleDeleteWidgetObj} handleDeleteWidget={handleDeleteWidgetObj}
handleTabDrag={handleTabDrag} handleTabDrag={handleTabDrag}
handleStop={handleStop} handleStop={handleStop}
@@ -1963,6 +1974,8 @@ const TemplatePlaceholder = () => {
currWidgetsDetails={currWidgetsDetails} currWidgetsDetails={currWidgetsDetails}
setRoleName={setRoleName} setRoleName={setRoleName}
isShowModal={isShowModal} isShowModal={isShowModal}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
/> />
)} )}
</div> </div>
@@ -2039,9 +2052,6 @@ const TemplatePlaceholder = () => {
initial={true} initial={true}
isTemplateFlow={true} isTemplateFlow={true}
roleName={roleName} roleName={roleName}
Add
commentMore
actions
isPrefillDropdown={true} isPrefillDropdown={true}
/> />
</div> </div>
+7 -15
View File
@@ -1,7 +1,4 @@
import React, { import React, { useState, useEffect } from "react";
useState,
useEffect,
} from "react";
import { Navigate, useNavigate } from "react-router"; import { Navigate, useNavigate } from "react-router";
import Parse from "parse"; import Parse from "parse";
import { SaveFileSize } from "../constant/saveFileSize"; import { SaveFileSize } from "../constant/saveFileSize";
@@ -9,10 +6,7 @@ import dp from "../assets/images/dp.png";
import { sanitizeFileName } from "../utils"; import { sanitizeFileName } from "../utils";
import axios from "axios"; import axios from "axios";
import Tooltip from "../primitives/Tooltip"; import Tooltip from "../primitives/Tooltip";
import { import { getSecureUrl, handleSendOTP } from "../constant/Utils";
getSecureUrl,
handleSendOTP,
} from "../constant/Utils";
import ModalUi from "../primitives/ModalUi"; import ModalUi from "../primitives/ModalUi";
import Loader from "../primitives/Loader"; import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -133,7 +127,7 @@ function UserProfile() {
Name: obj.Name, Name: obj.Name,
JobTitle: jobTitle, JobTitle: jobTitle,
Company: company, Company: company,
Language: obj?.language || "", Language: obj?.language || ""
}; };
await axios.put( await axios.put(
@@ -172,8 +166,8 @@ function UserProfile() {
try { try {
const response = await parseFile.save({ const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => { progress: (progressValue, loaded, total) => {
if (type === "upload" && progressValue !== null) { if (progressValue !== null) {
const percentCompleted = Math.round((loaded * 100) / total); const percentCompleted = Math.round((loaded * 100) / total);
// console.log("percentCompleted ", percentCompleted); // console.log("percentCompleted ", percentCompleted);
setpercentage(percentCompleted); setpercentage(percentCompleted);
@@ -449,7 +443,7 @@ function UserProfile() {
<button <button
type="button" type="button"
onClick={(e) => { onClick={(e) => {
editmode ? handleSubmit(e) : setEditMode(true); editmode ? handleSubmit(e) : setEditMode(true);
}} }}
className="op-btn op-btn-primary md:w-[100px]" className="op-btn op-btn-primary md:w-[100px]"
> >
@@ -460,9 +454,7 @@ function UserProfile() {
onClick={() => onClick={() =>
editmode ? handleCancel() : navigate("/changepassword") editmode ? handleCancel() : navigate("/changepassword")
} }
className={ className={`op-btn ${editmode ? "op-btn-ghost w-[100px]" : "op-btn-secondary"}`}
`op-btn ${editmode ? "op-btn-ghost w-[100px]" : "op-btn-secondary"}`
}
> >
{editmode ? t("cancel") : t("change-password")} {editmode ? t("cancel") : t("change-password")}
</button> </button>
@@ -55,7 +55,7 @@ function CustomModal(props) {
className="op-btn op-btn-primary px-6 ml-[20px] mb-3 mt-1" className="op-btn op-btn-primary px-6 ml-[20px] mb-3 mt-1"
onClick={() => handleExtendBtn()} onClick={() => handleExtendBtn()}
> >
Extend {t("extend")}
</button> </button>
)} )}
{props.isDownloadBtn && ( {props.isDownloadBtn && (
@@ -32,7 +32,8 @@ export const RenderReportCell = ({
startIndex, startIndex,
handleDownload, handleDownload,
handleRemovePrefill, handleRemovePrefill,
reportName reportName,
handleItemClick
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const appName = const appName =
@@ -59,7 +60,11 @@ export const RenderReportCell = ({
); );
case "Reason": case "Reason":
return ( 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?.length > 25
? rowData?.DeclineReason?.slice(0, 25) + "..." ? rowData?.DeclineReason?.slice(0, 25) + "..."
: rowData?.DeclineReason || "-"} : rowData?.DeclineReason || "-"}
@@ -68,7 +73,12 @@ export const RenderReportCell = ({
case "Note": case "Note":
return ( return (
<td key={col} className="p-2 text-center"> <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> </td>
); );
case "Folder": case "Folder":
@@ -97,6 +97,7 @@ const DocumentsReport = (props) => {
const [defaultMail, setDefaultMail] = useState({ body: "", subject: "" }); const [defaultMail, setDefaultMail] = useState({ body: "", subject: "" });
const [currUserId, setCurrUserId] = useState(false); const [currUserId, setCurrUserId] = useState(false);
const [documentDetails, setDocumentDetails] = useState(); const [documentDetails, setDocumentDetails] = useState();
const [objInfoModal, setObjInfoModal] = useState({ title: "", info: "" });
const startIndex = (currentPage - 1) * props.docPerPage; const startIndex = (currentPage - 1) * props.docPerPage;
const { isMoreDocs, setIsNextRecord } = props; const { isMoreDocs, setIsNextRecord } = props;
@@ -202,29 +203,21 @@ const DocumentsReport = (props) => {
const filterSignTypes = signatureType?.filter( const filterSignTypes = signatureType?.filter(
(x) => x.enabled === true (x) => x.enabled === true
); );
//condition to check tenant have some already set any email template const extUser =
if (tenantDetails?.RequestBody) { localStorage.getItem("Extand_Class") &&
//customize mail state is handle to when user want to customize already set tenant email format then use that format JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
setCustomizeMail({ const subject = tenantDetails?.RequestSubject ?? "";
subject: tenantDetails?.RequestSubject, const body = tenantDetails?.RequestBody ?? "";
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 =
setDefaultMail({ subject;
subject: tenantDetails?.RequestSubject, const userBody =
body: tenantDetails?.RequestBody body;
}); setCustomizeMail({
} else { subject: userSubject ?? defaultMailSubject,
const defaultRequestBody = defaultMailBody; body: userBody ?? defaultMailBody
const defaultSubject = defaultMailSubject; });
setCustomizeMail({ setDefaultMail({ subject: userSubject, body: userBody });
subject: defaultSubject,
body: defaultRequestBody
});
setDefaultMail({
subject: defaultSubject,
body: defaultRequestBody
});
}
return filterSignTypes; return filterSignTypes;
} }
} catch (e) { } catch (e) {
@@ -1049,6 +1042,10 @@ const DocumentsReport = (props) => {
); );
} }
}; };
const handleItemClick = (title, info) => {
setObjInfoModal({ title, info });
};
return ( return (
<div className="relative"> <div className="relative">
{Object.keys(actLoader)?.length > 0 && ( {Object.keys(actLoader)?.length > 0 && (
@@ -1173,6 +1170,7 @@ const DocumentsReport = (props) => {
handleDownload={handleDownload} handleDownload={handleDownload}
handleRemovePrefill={handleRemovePrefill} handleRemovePrefill={handleRemovePrefill}
reportName={props.ReportName} reportName={props.ReportName}
handleItemClick={handleItemClick}
/> />
))} ))}
{/* actions */} {/* actions */}
@@ -1852,6 +1850,13 @@ const DocumentsReport = (props) => {
)} )}
</div> </div>
</ModalUi> </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>
</div> </div>
); );
@@ -86,6 +86,7 @@ const TemplatesReport = (props) => {
const [isNewContact, setIsNewContact] = useState({ status: false, id: "" }); const [isNewContact, setIsNewContact] = useState({ status: false, id: "" });
const [isPrefillModal, setIsPrefillModal] = useState({}); const [isPrefillModal, setIsPrefillModal] = useState({});
const [isSubmit, setIsSubmit] = useState(false); const [isSubmit, setIsSubmit] = useState(false);
const [objInfoModal, setObjInfoModal] = useState({ title: "", info: "" });
const startIndex = (currentPage - 1) * props.docPerPage; const startIndex = (currentPage - 1) * props.docPerPage;
const { isMoreDocs, setIsNextRecord } = props; const { isMoreDocs, setIsNextRecord } = props;
const [isMailModal, setIsMailModal] = useState(false); const [isMailModal, setIsMailModal] = useState(false);
@@ -919,29 +920,21 @@ const TemplatesReport = (props) => {
if (tenantDetails && tenantDetails === "user does not exist!") { if (tenantDetails && tenantDetails === "user does not exist!") {
alert(t("user-not-exist")); alert(t("user-not-exist"));
} else if (tenantDetails) { } else if (tenantDetails) {
//condition to check tenant have some already set any email template const extUser =
if (tenantDetails?.RequestBody) { localStorage.getItem("Extand_Class") &&
//customize mail state is handle to when user want to customize already set tenant email format then use that format JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
setCustomizeMail({ const subject = tenantDetails?.RequestSubject ?? "";
subject: tenantDetails?.RequestSubject, const body = tenantDetails?.RequestBody ?? "";
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 =
setDefaultMail({ subject;
subject: tenantDetails?.RequestSubject, const userBody =
body: tenantDetails?.RequestBody body;
}); setCustomizeMail({
} else { subject: userSubject ?? defaultMailSubject,
const defaultRequestBody = defaultMailBody; body: userBody ?? defaultMailBody
const defaultSubject = defaultMailSubject; });
setCustomizeMail({ setDefaultMail({ subject: userSubject, body: userBody });
subject: defaultSubject,
body: defaultRequestBody
});
setDefaultMail({
subject: defaultSubject,
body: defaultRequestBody
});
}
} }
} catch (e) { } catch (e) {
alert(t("user-not-exist")); alert(t("user-not-exist"));
@@ -1011,6 +1004,10 @@ const TemplatesReport = (props) => {
navigate(`/recipientSignPdf/${docId}`); navigate(`/recipientSignPdf/${docId}`);
} }
}; };
const handleItemClick = (title, info) => {
setObjInfoModal({ title, info });
};
return ( return (
<div className="relative"> <div className="relative">
{Object.keys(actLoader)?.length > 0 && ( {Object.keys(actLoader)?.length > 0 && (
@@ -1147,6 +1144,7 @@ const TemplatesReport = (props) => {
handleDownload={handleDownload} handleDownload={handleDownload}
handleRemovePrefill={handleRemovePrefill} handleRemovePrefill={handleRemovePrefill}
reportName={props.ReportName} reportName={props.ReportName}
handleItemClick={handleItemClick}
/> />
))} ))}
<td className="px-2 py-2"> <td className="px-2 py-2">
@@ -1682,6 +1680,13 @@ const TemplatesReport = (props) => {
)} )}
</div> </div>
</ModalUi> </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>
</div> </div>
); );
+1
View File
@@ -16,6 +16,7 @@ export const saveToMySign = async (widget) => {
: `${replaceSpace}__initials`; : `${replaceSpace}__initials`;
const file = base64StringtoFile(base64, fileName); const file = base64StringtoFile(base64, fileName);
const fileUrl = await uploadFile(file, User?.id); const fileUrl = await uploadFile(file, User?.id);
// below code is used to save or update default signature, initials, stamp
const params = { const params = {
id: widget?.defaultSignId, // pass id to update existing signature/initials id: widget?.defaultSignId, // pass id to update existing signature/initials
userId: User.id userId: User.id
-1
View File
@@ -1,7 +1,6 @@
# Use an official Node runtime as the base image # Use an official Node runtime as the base image
FROM node:22.14.0 FROM node:22.14.0
# Install LibreOffice for DOCX to PDF conversions # Install LibreOffice for DOCX to PDF conversions
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y libreoffice \ && apt-get install -y libreoffice \
@@ -11,6 +11,7 @@ export const deleteUserOtp = async (req, res) => {
const extUserQuery = new Parse.Query('contracts_Users'); const extUserQuery = new Parse.Query('contracts_Users');
extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId }); extUserQuery.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
extUserQuery.include('TenantId');
const extUser = await extUserQuery.first({ useMasterKey: true }); const extUser = await extUserQuery.first({ useMasterKey: true });
if (!extUser) return res.status(404).json({ error: 'User not found' }); 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) { export async function sendDeleteOtpEmail(extUser, otp) {
const _extUser = extUser && JSON.parse(JSON.stringify(extUser));
const params = { const params = {
extUserId: extUser.id, extUserId: extUser.id,
from: appName, from: appName,
+2
View File
@@ -60,6 +60,7 @@ import resetPassword from './parsefunction/resetPassword.js';
import saveSignature from './parsefunction/saveSignature.js'; import saveSignature from './parsefunction/saveSignature.js';
import manageSign from './parsefunction/manageSign.js'; import manageSign from './parsefunction/manageSign.js';
import getSignature from './parsefunction/getSignature.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. // 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); Parse.Cloud.afterSave('contracts_Document', DocumentAftersave);
@@ -130,3 +131,4 @@ Parse.Cloud.define('resetpassword', resetPassword);
Parse.Cloud.define('savesignature', saveSignature); Parse.Cloud.define('savesignature', saveSignature);
Parse.Cloud.define('managesign', manageSign); Parse.Cloud.define('managesign', manageSign);
Parse.Cloud.define('getdefaultsignature', getSignature); Parse.Cloud.define('getdefaultsignature', getSignature);
Parse.Cloud.define('updateemailtemplates', updateEmailTemplates);
@@ -49,8 +49,8 @@ async function sendMail(document, publicUrl) {
let signPdf = `${hostUrl}/login/${encodeBase64}`; let signPdf = `${hostUrl}/login/${encodeBase64}`;
const orgName = document.ExtUserPtr.Company ? document.ExtUserPtr.Company : ''; const orgName = document.ExtUserPtr.Company ? document.ExtUserPtr.Company : '';
const senderObj = document?.ExtUserPtr; const senderObj = document?.ExtUserPtr;
const mailBody = document?.ExtUserPtr?.TenantId?.RequestBody || ''; let mailBody = senderObj?.TenantId?.RequestBody || '';
const mailSubject = document?.ExtUserPtr?.TenantId?.RequestSubject || ''; let mailSubject = senderObj?.TenantId?.RequestSubject || '';
let replaceVar; let replaceVar;
if (mailBody && mailSubject) { if (mailBody && mailSubject) {
const replacedRequestBody = mailBody.replace(/"/g, "'"); const replacedRequestBody = mailBody.replace(/"/g, "'");
@@ -126,8 +126,8 @@ async function batchQuery(userId, Documents, Ip, parseConfig, type, publicUrl) {
} }
}); });
} }
const mailBody = x?.ExtUserPtr?.TenantId?.RequestBody || ''; let mailBody = x?.ExtUserPtr?.TenantId?.RequestBody || '';
const mailSubject = x?.ExtUserPtr?.TenantId?.RequestSubject || ''; let mailSubject = x?.ExtUserPtr?.TenantId?.RequestSubject || '';
return { return {
method: 'POST', method: 'POST',
path: '/app/classes/contracts_Document', 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) { export default async function declinedocument(request) {
const docId = request.params.docId; const docId = request.params.docId;
const reason = request.params?.reason || ''; const reason = request.params?.reason || '';
const declineBy = { const userId = request.params.userId;
__type: 'Pointer', const declineBy = { __type: 'Pointer', className: '_User', objectId: userId };
className: '_User', const publicUrl = request.headers.public_url;
objectId: request.params?.userId,
};
if (!docId) { if (!docId) {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'missing parameter docId.'); throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'missing parameter docId.');
} }
try { try {
const docCls = new Parse.Query('contracts_Document'); 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 }); const updateDoc = await docCls.get(docId, { useMasterKey: true });
if (updateDoc) { if (updateDoc) {
const _doc = JSON.parse(JSON.stringify(updateDoc));
const isEnableOTP = updateDoc?.get('IsEnableOTP') || false; const isEnableOTP = updateDoc?.get('IsEnableOTP') || false;
if (!isEnableOTP) { if (!isEnableOTP) {
updateDoc.set('IsDeclined', true); updateDoc.set('IsDeclined', true);
updateDoc.set('DeclineReason', reason); updateDoc.set('DeclineReason', reason);
updateDoc.set('DeclineBy', declineBy); updateDoc.set('DeclineBy', declineBy);
await updateDoc.save(null, { useMasterKey: true }); await updateDoc.save(null, { useMasterKey: true });
sendDeclineMail(_doc, publicUrl, userId, reason);
return 'document declined'; return 'document declined';
} else { } else {
if (!request?.user) { if (!request?.user) {
@@ -30,6 +82,7 @@ export default async function declinedocument(request) {
updateDoc.set('DeclineReason', reason); updateDoc.set('DeclineReason', reason);
updateDoc.set('DeclineBy', declineBy); updateDoc.set('DeclineBy', declineBy);
await updateDoc.save(null, { useMasterKey: true }); await updateDoc.save(null, { useMasterKey: true });
sendDeclineMail(_doc, publicUrl, userId, reason);
return 'document declined'; return 'document declined';
} }
} else { } else {
@@ -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 maxX = width - margin - textWidth; // Ensures text stays inside the border with 30px margin
const OriginIp = docDetails?.OriginIp || ''; const OriginIp = docDetails?.OriginIp || '';
const company = docDetails?.ExtUserPtr?.Company || ''; const company = docDetails?.ExtUserPtr?.Company || '';
const documentHash = docDetails?.DocumentHash || '';
const createdAt = docDetails?.DocSentAt?.iso || docDetails.createdAt; const createdAt = docDetails?.DocSentAt?.iso || docDetails.createdAt;
const createdAtperTimezone = formatDateTime(createdAt, DateFormat, timezone, Is12Hr); const createdAtperTimezone = formatDateTime(createdAt, DateFormat, timezone, Is12Hr);
const IsEnableOTP = docDetails?.IsEnableOTP || false; const IsEnableOTP = docDetails?.IsEnableOTP || false;
@@ -150,9 +151,36 @@ export default async function GenerateCertificate(docDetails) {
color: textValueColor, 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 :', { page.drawText('Organization :', {
x: 30, x: 30,
y: 670, y: organizationY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textKeyColor, color: textKeyColor,
@@ -160,14 +188,14 @@ export default async function GenerateCertificate(docDetails) {
page.drawText(company, { page.drawText(company, {
x: 110, x: 110,
y: 670, y: organizationY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textValueColor, color: textValueColor,
}); });
page.drawText('Created on :', { page.drawText('Created on :', {
x: 30, x: 30,
y: 650, y: createdOnY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textKeyColor, color: textKeyColor,
@@ -175,14 +203,14 @@ export default async function GenerateCertificate(docDetails) {
page.drawText(`${createdAtperTimezone}`, { page.drawText(`${createdAtperTimezone}`, {
x: 97, x: 97,
y: 650, y: createdOnY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textValueColor, color: textValueColor,
}); });
page.drawText('Completed on :', { page.drawText('Completed on :', {
x: 30, x: 30,
y: 630, y: completedOnY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textKeyColor, color: textKeyColor,
@@ -190,14 +218,14 @@ export default async function GenerateCertificate(docDetails) {
page.drawText(`${completedUTCtime}`, { page.drawText(`${completedUTCtime}`, {
x: 115, x: 115,
y: 630, y: completedOnY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textValueColor, color: textValueColor,
}); });
page.drawText('Signers :', { page.drawText('Signers :', {
x: 30, x: 30,
y: 610, y: signersY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textKeyColor, color: textKeyColor,
@@ -205,75 +233,75 @@ export default async function GenerateCertificate(docDetails) {
page.drawText(`${signersCount}`, { page.drawText(`${signersCount}`, {
x: 80, x: 80,
y: 610, y: signersY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textValueColor, color: textValueColor,
}); });
page.drawText('Document originator', { page.drawText('Document originator', {
x: 30, x: 30,
y: 590, y: originatorHeaderY,
size: 17, size: 17,
font: timesRomanFont, font: timesRomanFont,
color: titleColor, color: titleColor,
}); });
page.drawText('Name :', { page.drawText('Name :', {
x: 60, x: 60,
y: 573, y: nameY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textKeyColor, color: textKeyColor,
}); });
page.drawText(ownerName, { page.drawText(ownerName, {
x: 105, x: 105,
y: 573, y: nameY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textValueColor, color: textValueColor,
}); });
page.drawText('Email :', { page.drawText('Email :', {
x: 60, x: 60,
y: 553, y: emailY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textKeyColor, color: textKeyColor,
}); });
page.drawText(ownerEmail, { page.drawText(ownerEmail, {
x: 105, x: 105,
y: 553, y: emailY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textValueColor, color: textValueColor,
}); });
page.drawText('IP address :', { page.drawText('IP address :', {
x: 60, x: 60,
y: 533, y: ipY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textKeyColor, color: textKeyColor,
}); });
page.drawText(`${OriginIp}`, { page.drawText(`${OriginIp}`, {
x: 125, x: 125,
y: 533, y: ipY,
size: text, size: text,
font: timesRomanFont, font: timesRomanFont,
color: textValueColor, color: textValueColor,
}); });
page.drawLine({ page.drawLine({
start: { x: 30, y: 527 }, start: { x: 30, y: ipY - 6 },
end: { x: width - 30, y: 527 }, end: { x: width - 30, y: ipY - 6 },
color: rgb(0.12, 0.12, 0.12), color: rgb(0.12, 0.12, 0.12),
thickness: 0.5, thickness: 0.5,
}); });
let yPosition1 = 512; let yPosition1 = ipY - 21;
let yPosition2 = 498; let yPosition2 = yPosition1 - 14;
let yPosition3 = 478; let yPosition3 = yPosition2 - 20;
let yPosition4 = 458; let yPosition4 = yPosition3 - 20;
let yPosition5 = 438; let yPosition5 = yPosition4 - 20;
let yPosition6 = 418; let yPosition6 = yPosition5 - 20;
let yPosition7 = 398; let yPosition7 = yPosition6 - 20;
let yPosition8 = 363; let yPosition8 = yPosition7 - 35;
auditTrail.slice(0, 3).forEach(async (x, i) => { auditTrail.slice(0, 3).forEach(async (x, i) => {
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : ''; const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : '';
@@ -1,4 +1,5 @@
import fs from 'node:fs'; import fs from 'node:fs';
import { createHash } from 'node:crypto';
import axios from 'axios'; import axios from 'axios';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import { import {
@@ -28,6 +29,10 @@ const headers = {
'X-Parse-Master-Key': masterKEY, 'X-Parse-Master-Key': masterKEY,
}; };
function generateDocumentHash(buffer) {
return createHash('sha256').update(buffer).digest('hex');
}
async function unlinkFile(path) { async function unlinkFile(path) {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
try { try {
@@ -44,11 +49,6 @@ async function uploadFile(pdfName, filepath) {
const filedata = fs.readFileSync(filepath); const filedata = fs.readFileSync(filepath);
let fileUrl; 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'); const fileRes = await parseUploadFile(pdfName, filedata, 'application/pdf');
fileUrl = getSecureUrl(fileRes?.url)?.url; fileUrl = getSecureUrl(fileRes?.url)?.url;
@@ -61,7 +61,7 @@ async function uploadFile(pdfName, filepath) {
} }
// `updateDoc` is used to update signedUrl, AuditTrail, Iscompleted in document // `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 { try {
const UserPtr = { __type: 'Pointer', className: className, objectId: userId }; const UserPtr = { __type: 'Pointer', className: className, objectId: userId };
const obj = { const obj = {
@@ -100,8 +100,16 @@ async function updateDoc(docId, url, userId, ipAddress, data, className, sign) {
isCompleted = true; isCompleted = true;
} }
const body = { SignedUrl: url, AuditTrail: updateAuditTrail, IsCompleted: isCompleted }; const body = { SignedUrl: url, AuditTrail: updateAuditTrail, IsCompleted: isCompleted };
if (documentHash && isCompleted) {
body.DocumentHash = documentHash;
}
const signedRes = await axios.put(`${docUrl}/${docId}`, body, { headers }); 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) { } catch (err) {
console.log('update doc err ', err); console.log('update doc err ', err);
return 'err'; return 'err';
@@ -183,8 +191,8 @@ async function sendCompletedMail(obj) {
if (obj?.isCustomMail) { if (obj?.isCustomMail) {
const tenant = sender?.TenantId; const tenant = sender?.TenantId;
if (tenant) { if (tenant) {
subject = tenant?.CompletionSubject || ''; subject = tenant?.CompletionSubject ? tenant?.CompletionSubject : subject;
body = tenant?.CompletionBody || ''; body = tenant?.CompletionBody ? tenant?.CompletionBody : body;
} else { } else {
const userId = sender?.CreatedBy?.objectId || sender?.UserId?.objectId; const userId = sender?.CreatedBy?.objectId || sender?.UserId?.objectId;
if (userId) { if (userId) {
@@ -198,8 +206,8 @@ async function sendCompletedMail(obj) {
const tenantRes = await tenantQuery.first({ useMasterKey: true }); const tenantRes = await tenantQuery.first({ useMasterKey: true });
if (tenantRes) { if (tenantRes) {
const _tenantRes = JSON.parse(JSON.stringify(tenantRes)); const _tenantRes = JSON.parse(JSON.stringify(tenantRes));
subject = _tenantRes?.CompletionSubject || ''; subject = _tenantRes?.CompletionSubject ? tenant?.CompletionSubject : subject;
body = _tenantRes?.CompletionBody || ''; body = _tenantRes?.CompletionBody ? tenant?.CompletionBody : body;
} }
} catch (err) { } catch (err) {
console.log('error in fetch tenant in signpdf', err.message); console.log('error in fetch tenant in signpdf', err.message);
@@ -427,6 +435,7 @@ async function PDF(req) {
let filePath = `./exports/${name}`; let filePath = `./exports/${name}`;
let signedFilePath = `./exports/signed_${name}`; let signedFilePath = `./exports/signed_${name}`;
let pdfSize = PdfBuffer.length; let pdfSize = PdfBuffer.length;
let documentHash;
if (isCompleted) { if (isCompleted) {
const signersName = _resDoc.Signers?.map(x => x.Name + ' <' + x.Email + '>'); const signersName = _resDoc.Signers?.map(x => x.Name + ' <' + x.Email + '>');
const reason = const reason =
@@ -444,6 +453,7 @@ async function PDF(req) {
//`saveUrl` is used to save signed pdf in exports folder //`saveUrl` is used to save signed pdf in exports folder
fs.writeFileSync(signedFilePath, signedDocs); fs.writeFileSync(signedFilePath, signedDocs);
pdfSize = signedDocs.length; pdfSize = signedDocs.length;
documentHash = generateDocumentHash(signedDocs);
console.log(`✅ PDF digitally signed created: ${signedFilePath} \n`); console.log(`✅ PDF digitally signed created: ${signedFilePath} \n`);
} else { } else {
//`saveUrl` is used to save signed pdf in exports folder //`saveUrl` is used to save signed pdf in exports folder
@@ -464,12 +474,17 @@ async function PDF(req) {
userIP, // client ipAddress, userIP, // client ipAddress,
_resDoc, // auditTrail, signers, etc data _resDoc, // auditTrail, signers, etc data
className, // className based on flow className, // className based on flow
sign // sign base64 sign, // sign base64
isCompleted ? documentHash : undefined
); );
sendNotifyMail(_resDoc, signUser, mailProvider, publicUrl); sendNotifyMail(_resDoc, signUser, mailProvider, publicUrl);
saveFileUsage(pdfSize, data.imageUrl, _resDoc?.CreatedBy?.objectId); saveFileUsage(pdfSize, data.imageUrl, _resDoc?.CreatedBy?.objectId);
if (updatedDoc && updatedDoc.isCompleted) { if (updatedDoc && updatedDoc.isCompleted) {
const hashForDoc = documentHash || updatedDoc?.DocumentHash;
const doc = { ..._resDoc, AuditTrail: updatedDoc.AuditTrail, SignedUrl: data.imageUrl }; const doc = { ..._resDoc, AuditTrail: updatedDoc.AuditTrail, SignedUrl: data.imageUrl };
if (hashForDoc) {
doc.DocumentHash = hashForDoc;
}
sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, `signed_${name}`); sendMailsaveCertifcate(doc, pfx, isCustomMail, mailProvider, `signed_${name}`);
} else { } else {
unlinkFile(pfxname); unlinkFile(pfxname);
@@ -159,7 +159,8 @@ export default async function sendMailGmailProvider(_extRes, template) {
template; template;
if (_extRes) { if (_extRes) {
const refresh_token = _extRes.google_refresh_token; let refresh_token = '';
refresh_token = _extRes?.TenantId?.google_refresh_token;
// generate access token // generate access token
const access_token = await refreshAccessToken(refresh_token); const access_token = await refreshAccessToken(refresh_token);
@@ -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 // Update tenant details
Object.keys(details).forEach(key => { Object.keys(details).forEach(key => {
if (validKeys.includes(key)) { if (validKeys.includes(key)) {
tenant.set(key, details?.[key]); if (details?.[key] !== undefined) {
tenant.set(key, details?.[key]);
} else {
tenant.unset(key);
}
} }
}); });
+2392 -2350
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -18,8 +18,8 @@
"watch": "nodemon index.js" "watch": "nodemon index.js"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.901.0", "@aws-sdk/client-s3": "^3.936.0",
"@aws-sdk/s3-request-presigner": "^3.901.0", "@aws-sdk/s3-request-presigner": "^3.936.0",
"@parse/fs-files-adapter": "^3.0.0", "@parse/fs-files-adapter": "^3.0.0",
"@parse/s3-files-adapter": "^4.2.0", "@parse/s3-files-adapter": "^4.2.0",
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
@@ -27,42 +27,42 @@
"@signpdf/signer-p12": "^3.2.4", "@signpdf/signer-p12": "^3.2.4",
"@signpdf/signpdf": "^3.2.5", "@signpdf/signpdf": "^3.2.5",
"aws-sdk": "^2.1692.0", "aws-sdk": "^2.1692.0",
"axios": "^1.12.2", "axios": "^1.13.2",
"coherentpdf": "^2.5.5", "coherentpdf": "^2.5.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.1.0", "express": "^5.1.0",
"form-data": "^4.0.4", "form-data": "^4.0.5",
"generate-api-key": "^1.0.2", "generate-api-key": "^1.0.2",
"googleapis": "^161.0.0", "googleapis": "^166.0.0",
"libreoffice-convert": "^1.7.0", "libreoffice-convert": "^1.7.0",
"mailgun.js": "^12.1.0", "mailgun.js": "^12.1.1",
"mongodb": "^5.9.2", "mongodb": "^5.9.2",
"multer": "^2.0.2", "multer": "^2.0.2",
"multer-s3": "^3.0.1", "multer-s3": "^3.0.1",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"nodemailer": "^7.0.9", "nodemailer": "^7.0.10",
"p-limit": "^7.1.1", "p-limit": "^7.2.0",
"parse": "^6.1.1", "parse": "^7.0.2",
"parse-dbtool": "^1.2.0", "parse-dbtool": "^1.2.0",
"parse-server": "^8.2.5", "parse-server": "^8.4.0",
"parse-server-api-mail-adapter": "^4.1.0", "parse-server-api-mail-adapter": "^4.1.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"posthog-node": "^5.9.3", "posthog-node": "^5.13.2",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"rate-limiter-flexible": "^8.0.1", "rate-limiter-flexible": "^8.2.1",
"sharp": "^0.34.4", "sharp": "^0.34.5",
"speakeasy": "^2.0.0", "speakeasy": "^2.0.0",
"ws": "^8.18.3" "ws": "^8.18.3"
}, },
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.28.4", "@babel/eslint-parser": "^7.28.5",
"eslint": "^9.37.0", "eslint": "^9.39.1",
"jasmine": "^5.12.0", "jasmine": "^5.12.0",
"mongodb-runner": "^5.10.0", "mongodb-runner": "^5.11.1",
"nodemon": "^3.1.10", "nodemon": "^3.1.11",
"nyc": "^17.1.0", "nyc": "^17.1.0",
"prettier": "^3.6.2" "prettier": "^3.6.2"
}, },
+140 -354
View File
@@ -9,16 +9,17 @@
"version": "1.0.6", "version": "1.0.6",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"husky": "^8.0.0", "husky": "^9.1.7",
"lint-staged": "^15.1.0", "lint-staged": "^16.2.7",
"prettier": "3.1.0" "prettier": "^3.6.2"
} }
}, },
"node_modules/ansi-escapes": { "node_modules/ansi-escapes": {
"version": "7.0.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
"integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"environment": "^1.0.0" "environment": "^1.0.0"
}, },
@@ -30,10 +31,11 @@
} }
}, },
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "6.0.1", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -42,10 +44,11 @@
} }
}, },
"node_modules/ansi-styles": { "node_modules/ansi-styles": {
"version": "6.2.1", "version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -65,23 +68,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"dev": true,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/cli-cursor": { "node_modules/cli-cursor": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"restore-cursor": "^5.0.0" "restore-cursor": "^5.0.0"
}, },
@@ -93,16 +85,17 @@
} }
}, },
"node_modules/cli-truncate": { "node_modules/cli-truncate": {
"version": "4.0.0", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
"integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"slice-ansi": "^5.0.0", "slice-ansi": "^7.1.0",
"string-width": "^7.0.0" "string-width": "^8.0.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=20"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -112,59 +105,32 @@
"version": "2.0.20", "version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/commander": { "node_modules/commander": {
"version": "12.1.0", "version": "14.0.2",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=20"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
} }
}, },
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "10.4.0", "version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/environment": { "node_modules/environment": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
"integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -176,30 +142,8 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"dev": true
},
"node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
"dev": true, "dev": true,
"dependencies": { "license": "MIT"
"cross-spawn": "^7.0.3",
"get-stream": "^8.0.1",
"human-signals": "^5.0.0",
"is-stream": "^3.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^5.1.0",
"onetime": "^6.0.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^3.0.0"
},
"engines": {
"node": ">=16.17"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
@@ -214,10 +158,11 @@
} }
}, },
"node_modules/get-east-asian-width": { "node_modules/get-east-asian-width": {
"version": "1.2.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
"integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -225,49 +170,33 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/get-stream": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
"dev": true,
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
"dev": true,
"engines": {
"node": ">=16.17.0"
}
},
"node_modules/husky": { "node_modules/husky": {
"version": "8.0.3", "version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"dev": true, "dev": true,
"license": "MIT",
"bin": { "bin": {
"husky": "lib/bin.js" "husky": "bin.js"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/typicode" "url": "https://github.com/sponsors/typicode"
} }
}, },
"node_modules/is-fullwidth-code-point": { "node_modules/is-fullwidth-code-point": {
"version": "4.0.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.3.1"
},
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -282,70 +211,39 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/is-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/lilconfig": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
"integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/lint-staged": { "node_modules/lint-staged": {
"version": "15.2.9", "version": "16.2.7",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.9.tgz", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz",
"integrity": "sha512-BZAt8Lk3sEnxw7tfxM7jeZlPRuT4M68O0/CwZhhaw6eeWu0Lz5eERE3m386InivXB64fp/mDID452h48tvKlRQ==", "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"chalk": "~5.3.0", "commander": "^14.0.2",
"commander": "~12.1.0", "listr2": "^9.0.5",
"debug": "~4.3.6", "micromatch": "^4.0.8",
"execa": "~8.0.1", "nano-spawn": "^2.0.0",
"lilconfig": "~3.1.2", "pidtree": "^0.6.0",
"listr2": "~8.2.4", "string-argv": "^0.3.2",
"micromatch": "~4.0.7", "yaml": "^2.8.1"
"pidtree": "~0.6.0",
"string-argv": "~0.3.2",
"yaml": "~2.5.0"
}, },
"bin": { "bin": {
"lint-staged": "bin/lint-staged.js" "lint-staged": "bin/lint-staged.js"
}, },
"engines": { "engines": {
"node": ">=18.12.0" "node": ">=20.17"
}, },
"funding": { "funding": {
"url": "https://opencollective.com/lint-staged" "url": "https://opencollective.com/lint-staged"
} }
}, },
"node_modules/listr2": { "node_modules/listr2": {
"version": "8.2.4", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
"integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"cli-truncate": "^4.0.0", "cli-truncate": "^5.0.0",
"colorette": "^2.0.20", "colorette": "^2.0.20",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"log-update": "^6.1.0", "log-update": "^6.1.0",
@@ -353,7 +251,7 @@
"wrap-ansi": "^9.0.0" "wrap-ansi": "^9.0.0"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=20.0.0"
} }
}, },
"node_modules/log-update": { "node_modules/log-update": {
@@ -361,6 +259,7 @@
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
"integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"ansi-escapes": "^7.0.0", "ansi-escapes": "^7.0.0",
"cli-cursor": "^5.0.0", "cli-cursor": "^5.0.0",
@@ -375,43 +274,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/log-update/node_modules/is-fullwidth-code-point": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
"integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
"dev": true,
"dependencies": {
"get-east-asian-width": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/slice-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
"integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
},
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -425,23 +287,12 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mimic-function": { "node_modules/mimic-function": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -449,63 +300,35 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/ms": { "node_modules/nano-spawn": {
"version": "2.1.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==",
"dev": true
},
"node_modules/npm-run-path": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
"integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
"dev": true, "dev": true,
"dependencies": { "license": "MIT",
"path-key": "^4.0.0"
},
"engines": { "engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0" "node": ">=20.17"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
}
},
"node_modules/npm-run-path/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/onetime": { "node_modules/onetime": {
"version": "6.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"mimic-fn": "^4.0.0" "mimic-function": "^5.0.0"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -531,10 +354,11 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.1.0", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true, "dev": true,
"license": "MIT",
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -550,6 +374,7 @@
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"onetime": "^7.0.0", "onetime": "^7.0.0",
"signal-exit": "^4.1.0" "signal-exit": "^4.1.0"
@@ -561,53 +386,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/restore-cursor/node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dev": true,
"dependencies": {
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rfdc": { "node_modules/rfdc": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true, "dev": true,
"dependencies": { "license": "MIT"
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}
}, },
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true, "dev": true,
"license": "ISC",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
}, },
@@ -616,16 +407,17 @@
} }
}, },
"node_modules/slice-ansi": { "node_modules/slice-ansi": {
"version": "5.0.0", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^6.0.0", "ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^4.0.0" "is-fullwidth-code-point": "^5.0.0"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1" "url": "https://github.com/chalk/slice-ansi?sponsor=1"
@@ -641,27 +433,28 @@
} }
}, },
"node_modules/string-width": { "node_modules/string-width": {
"version": "7.2.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^10.3.0", "get-east-asian-width": "^1.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0" "strip-ansi": "^7.1.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=20"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/strip-ansi": { "node_modules/strip-ansi": {
"version": "7.1.0", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^6.0.1" "ansi-regex": "^6.0.1"
}, },
@@ -672,18 +465,6 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/strip-final-newline": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -696,26 +477,12 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "9.0.0", "version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^6.2.1", "ansi-styles": "^6.2.1",
"string-width": "^7.0.0", "string-width": "^7.0.0",
@@ -728,16 +495,35 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/yaml": { "node_modules/wrap-ansi/node_modules/string-width": {
"version": "2.5.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"
}, },
"engines": { "engines": {
"node": ">= 14" "node": ">= 14.6"
} }
} }
} }
+3 -3
View File
@@ -23,8 +23,8 @@
"author": "OpenSignLabs", "author": "OpenSignLabs",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"husky": "^8.0.0", "husky": "^9.1.7",
"lint-staged": "^15.1.0", "lint-staged": "^16.2.7",
"prettier": "3.1.0" "prettier": "^3.6.2"
} }
} }