Compare commits

...
22 Commits
Author SHA1 Message Date
prafull-opensignlabsandGitHub 8a2329dd60 Merge pull request #2050 from OpenSignLabs/updates-20027001996
v2.33.0
2025-12-08 17:16:35 +05:30
prafull-opensignlabs 75a40fbf43 Delete apps/OpenSignServer/package-lock.json 2025-12-08 11:45:29 +00:00
prafull-opensignlabsandGitHub 334edb3c0c Merge pull request #2042 from OpenSignLabs/updates-19765002150
v2.32.0
2025-11-28 18:54:17 +05:30
prafull-opensignlabs 6465aa7c2c Delete apps/OpenSignServer/package-lock.json 2025-11-28 13:19:46 +00:00
prafull-opensignlabsandGitHub 4593ce7fec Merge pull request #2039 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/npm_and_yarn-42c243e405
build(deps): bump body-parser from 2.2.0 to 2.2.1 in /apps/OpenSignServer in the npm_and_yarn group across 1 directory
2025-11-27 00:09:48 +05:30
dependabot[bot]andGitHub b4ffdf0dc9 build(deps): bump body-parser
Bumps the npm_and_yarn group with 1 update in the /apps/OpenSignServer directory: [body-parser](https://github.com/expressjs/body-parser).


Updates `body-parser` from 2.2.0 to 2.2.1
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-version: 2.2.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 18:22:09 +00:00
AmolandGitHub 3c8b8835c7 Merge pull request #2029 from OpenSignLabs/build_deps
fix: remove type parameter from parse.save method
2025-11-20 20:17:26 +05:30
prafull-opensignlabs 20752161d3 fix: remove type parameter from parse.save method 2025-11-20 20:08:06 +05:30
prafull-opensignlabsandGitHub 002b0d5277 Merge pull request #2027 from OpenSignLabs/build_deps
fix: handle password validation in change password page
2025-11-20 17:13:20 +05:30
prafull-opensignlabsandGitHub 47348b990e Merge pull request #1957 from Miracee97/patch-1
Update README.md
2025-11-20 17:09:04 +05:30
prafull-opensignlabs ac86b6ef61 fix: add password validation in change password page 2025-11-20 17:00:39 +05:30
prafull-opensignlabsandGitHub 7894a6edb8 Merge pull request #2025 from OpenSignLabs/build_deps
build(deps): update dependencies
2025-11-20 16:21:33 +05:30
prafull-opensignlabs dac8957904 build(deps): update dependencies 2025-11-20 16:19:19 +05:30
AmolandGitHub ef686aec6a Refactor Docker workflow for multi-arch builds 2025-11-20 12:19:09 +05:30
AmolandGitHub f31155e0b4 Merge pull request #2022 from OpenSignLabs/updates-19528160086
v2.31.0
2025-11-20 12:17:56 +05:30
AmolandGitHub 78e4857d1a Merge pull request #2023 from OpenSignLabs/all-contributors/add-ehowe
docs: add ehowe as a contributor for code
2025-11-20 12:17:29 +05:30
allcontributors[bot]andGitHub c3144366d9 docs: update .all-contributorsrc 2025-11-20 06:47:02 +00:00
allcontributors[bot]andGitHub f00a7c9e50 docs: update README.md 2025-11-20 06:47:01 +00:00
prafull-opensignlabs 803d25e9d1 Fix: Document title changes are not reflected when the document is sent to signers.
Merge pull request #1498 from nxglabs/staging
2025-11-20 06:44:56 +00:00
prafull-opensignlabsandGitHub ca1b87309f Merge pull request #2011 from OpenSignLabs/updates-18996309218
feat: handle undefine username/password for SMTP
2025-11-01 17:24:58 +05:30
prafull-opensignlabs 0b4548130d feat: handle undefine username/password for SMTP 2025-11-01 11:53:04 +00:00
RaistlinandGitHub a878ca1a96 Update README.md
updated link to api documentation v1.1
2025-09-23 11:33:29 +02:00
82 changed files with 2688 additions and 22157 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 }}
+2 -1
View File
@@ -60,7 +60,7 @@ Welcome to OpenSign, the premier open source docusign alternative - document e-s
- **PDF Template Creation:** OpenSign™ allows you to create and store PDF document templates for repeated use thereby saving you a lot of time & collect e-signatures seamlessly. - **PDF Template Creation:** OpenSign™ allows you to create and store PDF document templates for repeated use thereby saving you a lot of time & collect e-signatures seamlessly.
- **OpenSign™ Drive:** It is a centralised secure vault for your digital documents that makes storing, signing, organizing, sharing & archieving your docs a breeze. - **OpenSign™ Drive:** It is a centralised secure vault for your digital documents that makes storing, signing, organizing, sharing & archieving your docs a breeze.
- **Audit Trails & completion certificate:** Being a security focused solution, OpenSign™ makes it a top priority to save detailed logs for tracking document activities along with time-stamps, IP addresses, email IDs & phone numbers. A completion certificate is generated as soon as document is completed which contains all the document related logs for added safety. - **Audit Trails & completion certificate:** Being a security focused solution, OpenSign™ makes it a top priority to save detailed logs for tracking document activities along with time-stamps, IP addresses, email IDs & phone numbers. A completion certificate is generated as soon as document is completed which contains all the document related logs for added safety.
- **API Support:** OpenSign™ API allows seamless integration into existing systems and software. You can generate an API key from the app and refer the [official API docs](https://docs.opensignlabs.com/docs/API-docs/opensign-api-v-1) to start integrating it in your existing applications. - **API Support:** OpenSign™ API allows seamless integration into existing systems and software. You can generate an API key from the app and refer the [official API docs](https://docs.opensignlabs.com/docs/API-docs/v1.1) to start integrating it in your existing applications.
- **Integrations:** The open source document signing experience becomes even more seamless because of integrations with various Cloud storage systems, CRMs & enterprise platforms. We also have a Zapier integration that allows you to integrate it with virtually any application. - **Integrations:** The open source document signing experience becomes even more seamless because of integrations with various Cloud storage systems, CRMs & enterprise platforms. We also have a Zapier integration that allows you to integrate it with virtually any application.
<img alt="Login page" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/8016edb6-7d6e-4185-ab72-82d2c4a16032" height='200' > <img alt="Login page" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/8016edb6-7d6e-4185-ab72-82d2c4a16032" height='200' >
<img alt="Dash_board" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/6e7b96f7-3154-4c25-bc1e-caf33925cc47" height='200'> <img alt="Dash_board" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/6e7b96f7-3154-4c25-bc1e-caf33925cc47" height='200'>
@@ -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>
@@ -21,9 +21,7 @@ const DisabledButtonExamples = () => {
{/* Option C: Conditional styling */} {/* Option C: Conditional styling */}
<button <button
className={`op-btn op-btn-primary ${ className={`op-btn op-btn-primary ${isDisabled ? "op-btn-vscode-disabled" : ""}`}
isDisabled ? "op-btn-vscode-disabled" : ""
}`}
disabled={isDisabled} disabled={isDisabled}
> >
Conditional Button Conditional Button
+27 -23
View File
@@ -1,26 +1,26 @@
{ {
"name": "open_sign", "name": "open_sign",
"version": "2.30.0", "version": "2.32.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@formkit/auto-animate": "^0.9.0", "@formkit/auto-animate": "^0.9.0",
"@imgly/background-removal": "^1.7.0", "@imgly/background-removal": "^1.7.0",
"@lottiefiles/dotlottie-react": "^0.16.2", "@lottiefiles/dotlottie-react": "^0.17.8",
"@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.11.0",
"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.7.1",
"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.1.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,26 +28,26 @@
"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.10.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",
"react-i18next": "^15.7.4", "react-i18next": "^16.3.5",
"react-konva": "^18.2.12", "react-konva": "^18.2.12",
"react-pdf": "^9.2.1", "react-pdf": "^9.2.1",
"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.10.0",
"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-to-print": "^3.2.0",
"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,34 +96,38 @@
"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.7",
"@vitejs/plugin-react": "^4.7.0", "@vitejs/plugin-react": "^5.1.1",
"@vitest/ui": "^3.2.4", "@vitest/ui": "^4.0.15",
"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.7.4",
"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.17",
"vite": "^7.1.4", "tmp": "^0.2.5",
"vite": "^7.2.6",
"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": "^4.0.15"
},
"overrides": {
"tmp": "$tmp"
}, },
"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",
@@ -166,7 +168,10 @@
"Save as template": "Als Vorlage speichern", "Save as template": "Als Vorlage speichern",
"Fix & resend": "Korrigieren und erneut senden", "Fix & resend": "Korrigieren und erneut senden",
"Kiosk Mode": "Kiosk Modus", "Kiosk Mode": "Kiosk Modus",
"Reset password": "Passwort zurücksetzen" "Reset password": "Passwort zurücksetzen",
"View formdata": "Formulardaten ansehen",
"Download all formdata": "Alle Formulardaten herunterladen",
"API credits": "API-Credits"
}, },
"report-heading": { "report-heading": {
"Sr.No": "Nr.", "Sr.No": "Nr.",
@@ -201,7 +206,8 @@
"Redirect url": "Weiterleitungs-URL", "Redirect url": "Weiterleitungs-URL",
"Created Date": "Erstellungsdatum", "Created Date": "Erstellungsdatum",
"Updated Date": "Aktualisierungsdatum", "Updated Date": "Aktualisierungsdatum",
"Expiry Date": "Ablaufdatum" "Expiry Date": "Ablaufdatum",
"Sent Date": "Sendedatum"
}, },
"report-help": { "report-help": {
"Draft Documents": "Dies sind Dokumente, die Sie begonnen, aber noch nicht zum Versenden fertiggestellt haben.", "Draft Documents": "Dies sind Dokumente, die Sie begonnen, aber noch nicht zum Versenden fertiggestellt haben.",
@@ -440,7 +446,7 @@
"Next to current widget": "Neben dem aktuellen Widget" "Next to current widget": "Neben dem aktuellen Widget"
}, },
"options": "Optionen", "options": "Optionen",
"minimun-check": "Minimale Anzahl", "minimum-check": "Minimale Anzahl",
"maximum-check": "Maximale Anzahl", "maximum-check": "Maximale Anzahl",
"cell-count": "Zellzahl", "cell-count": "Zellzahl",
"default-value": "Standardwert", "default-value": "Standardwert",
@@ -698,7 +704,7 @@
"select-date-format": "Wählen Sie ein Datumsformat", "select-date-format": "Wählen Sie ein Datumsformat",
"quantity-of-credits": "Anzahl der Premium-Credits", "quantity-of-credits": "Anzahl der Premium-Credits",
"remaining-credits": "Verfügbare Premium-Credits:", "remaining-credits": "Verfügbare Premium-Credits:",
"remaining-credits-help": "Verwenden Sie Premium-Credits für API-Dokumentensignaturen, Massenversand oder die Einbettung der OpenSign-Integration auf Ihrer Website. Sie haben {{allowedcredits}} enthaltene Credits und {{addoncredits}} zusätzlich gekaufte Credits übrig.", "remaining-credits-help": "Verwenden Sie Premium-Credits für die API-Dokumentensignatur, den Massenversand oder die Einbettung der OpenSign-Integration auf Ihrer Website. Sie haben noch {{allowedcredits}} inkludierte Credits und {{addoncredits}} zusätzlich gekaufte Credits übrig. Wenn Sie einen Teams- oder Enterprise-Plan nutzen, können diese Credits von allen Benutzern in Ihrem Unternehmen gemeinsam verwendet werden, sofern der Kontoadministrator für Ihr Konto kein Limit festgelegt hat.",
"additional-credits": "Bitte kaufen Sie Premium-Credits hinzu", "additional-credits": "Bitte kaufen Sie Premium-Credits hinzu",
"quota-err-quicksend": "Kontingent erreicht, Sie haben nicht genügend Credits.", "quota-err-quicksend": "Kontingent erreicht, Sie haben nicht genügend Credits.",
"buy-credits": "Premium-Credits kaufen", "buy-credits": "Premium-Credits kaufen",
@@ -1256,5 +1262,51 @@
"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.",
"hide-text-with-asterisks": "Text mit Sternchen ausblenden",
"hide-text-with-asterisks-help": "Der Text im unterzeichneten Dokument wird ausgeblendet, bleibt jedoch für den Dokumentabsender (Eigentümer) über sein {{appName}}-Konto zugänglich.",
"document-id": "Dokument-ID",
"sent-date": "Sendedatum",
"status": "Status",
"signer-form-data-for": "Formulardaten für Unterzeichner: {{signerName}} <{{signerEmail}}>",
"widget-name": "Widget-Name",
"widget-responses": "Widget-Antworten",
"formdata-download-note": "Hinweis: Signaturen, Stempel, Initialen und Bilder werden nicht exportiert.",
"export-all-formdata-error": "Sie können die Formulardaten nicht exportieren, da die Vorlage doppelte Widget-Namen enthält. Bitte erstellen Sie eine neue Vorlage, um Formulardaten exportieren zu können.",
"export-formdata-error": "Sie können die Formulardaten nicht exportieren, da das Dokument doppelte Widget-Namen enthält. Bitte erstellen Sie ein neues Dokument ohne doppelte Widget-Namen, um Formulardaten exportieren zu können.",
"duplicate-widget-name-error": "Dieser Widget-Name ist bereits einem anderen Unterzeichner zugewiesen. Bitte wählen Sie einen anderen Namen.",
"duplicate-template-widget-error": "Sie können diese Vorlage nicht duplizieren, da sie doppelte Widget-Namen enthält.",
"credit-alert": "Kreditwarnung",
"document-form-data": "Dokumenten-Formulardaten",
"view-formdata-paid-title-error": "Die Funktion 'Formulardaten anzeigen' ermöglicht es Ihnen, die von Unterzeichnern ausgefüllten Widget-Daten auszudrucken und herunterzuladen.",
"view-formdata-paid-error": "Hinweis: Die Funktion 'Formulardaten anzeigen' ist nur für Dokumente verfügbar, die ab Version v2.32.0 erstellt wurden und keine doppelten Widget-Namen enthalten.",
"download-all-formdata-paid-title-error": "Die Funktion 'Alle Formulardaten herunterladen' ermöglicht es Ihnen, die von Unterzeichnern ausgefüllten Widget-Daten für alle aus der Vorlage erstellten Dokumente herunterzuladen.",
"download-all-formdata-paid-error": "Hinweis: Die Funktion 'Alle Formulardaten herunterladen' ist nur für Vorlagen verfügbar, die ab Version v2.32.0 erstellt wurden und keine doppelten Widget-Namen enthalten.",
"api-token-not-found": "API-Token nicht gefunden.",
"sub-head-api-credits": "Legen Sie die maximale Anzahl von API-Credits fest, die dieser Benutzer verbrauchen darf.",
"consumed-credits": "Verbrauchte Credits",
"allowed-api-credits": "Zulässige API-Credits",
"allowed-api-credits-help": "Die zulässigen Credits sollten größer oder gleich den verbrauchten Credits sein.",
"Enter-allowed-api-credits": "Zulässige API-Credits eingeben",
"login-to-sandbox": "Im Sandbox-Modus anmelden",
"your-credits-usage": "Ihr Kreditverbrauch",
"remaining": "verbleibend",
"used": "verwendet",
"your-credits-usage-help": "Dies ist die Anzahl der Credits, die mit Ihrem API-Schlüssel verbraucht wurden."
} }
@@ -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",
@@ -166,7 +168,10 @@
"Save as template": "Save as template", "Save as template": "Save as template",
"Fix & resend": "Fix & Resend", "Fix & resend": "Fix & Resend",
"Kiosk Mode": "Kiosk Mode", "Kiosk Mode": "Kiosk Mode",
"Reset password": "Reset password" "Reset password": "Reset password",
"View formdata": "View formdata",
"Download all formdata": "Download all formdata",
"API credits": "API credits"
}, },
"report-heading": { "report-heading": {
"Sr.No": "Sr.No", "Sr.No": "Sr.No",
@@ -201,7 +206,8 @@
"Redirect url": "Redirect url", "Redirect url": "Redirect url",
"Created Date": "Created Date", "Created Date": "Created Date",
"Updated Date": "Updated Date", "Updated Date": "Updated Date",
"Expiry Date": "Expiry Date" "Expiry Date": "Expiry Date",
"Sent Date": "Sent Date"
}, },
"report-help": { "report-help": {
"Draft Documents": "These are documents you have started but have not finalized for sending.", "Draft Documents": "These are documents you have started but have not finalized for sending.",
@@ -441,7 +447,7 @@
"Next to current widget": "Next to current widget" "Next to current widget": "Next to current widget"
}, },
"options": "Options", "options": "Options",
"minimun-check": "Minimun check", "minimum-check": "Minimum check",
"maximum-check": "Maximum check", "maximum-check": "Maximum check",
"cell-count": "Cell count", "cell-count": "Cell count",
"default-value": "Default value", "default-value": "Default value",
@@ -699,7 +705,7 @@
"select-date-format": "Select a date format", "select-date-format": "Select a date format",
"quantity-of-credits": "Quantity of premium credits", "quantity-of-credits": "Quantity of premium credits",
"remaining-credits": "Premium credits available:", "remaining-credits": "Premium credits available:",
"remaining-credits-help": "Use premium credits for API document signing, bulk sending, or embedding OpenSign integration on your website. You have {{allowedcredits}} included credits and {{addoncredits}} additional purchased credits remaining.", "remaining-credits-help": "Use premium credits for API document signing, bulk sending, or embedding OpenSign integration on your website. You have {{allowedcredits}} included credits and {{addoncredits}} additional purchased credits remaining. If you are on Teams or enterprise plan these are the credits that could be shared across all users in your company unless there is a limit set by the account admin for your account.",
"additional-credits": "Please purchase premium credits", "additional-credits": "Please purchase premium credits",
"quota-err-quicksend": "Quota reached, you don't have enough credits.", "quota-err-quicksend": "Quota reached, you don't have enough credits.",
"buy-credits": "Buy premium credits", "buy-credits": "Buy premium credits",
@@ -1257,5 +1263,51 @@
"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.",
"hide-text-with-asterisks": "Hide text with asterisks",
"hide-text-with-asterisks-help": "The text on the signed document will be concealed from view but remain accessible to the document sender (owner) from their {{appName}} account.",
"document-id": "Document ID",
"sent-date": "Sent Date",
"status": "Status",
"signer-form-data-for": "Signer Form data for: {{signerName}} <{{signerEmail}}>",
"widget-name": "Widget Name",
"widget-responses": "Widget Responses",
"formdata-download-note": "Note: Signatures, stamps, initials and images wont be exported.",
"export-all-formdata-error": "You can/t export form data because the template contains duplicate widget names. Please create a fresh template to be able to export form data.",
"export-formdata-error": "You can't export form data because the document contains duplicate widget names. Please create a fresh document without duplicate widget names to be able to export form data.",
"duplicate-widget-name-error": "This widget name is already assigned to another signer. Please choose a different name.",
"duplicate-template-widget-error": "You can't duplicate this template because it contains duplicate widget names.",
"credit-alert": "Credit Alert",
"document-form-data": "Document Form Data",
"view-formdata-paid-title-error": "The View Form Data feature lets you print and download the widget data filled in by signers while signing.",
"view-formdata-paid-error": "Note: 'View Formdata' feature is only available for documents created on or after v2.32.0 that do not contain duplicate widget names.",
"download-all-formdata-paid-title-error": "The Download All Form Data feature lets you download widget data filled in by signers for all documents created from the template.",
"download-all-formdata-paid-error": "Note: 'Download All Formdata' feature is only available for templates created on or after v2.32.0 that do not contain duplicate widget names.",
"api-token-not-found": "API token not found.",
"sub-head-api-credits": " Define the maximum number of API credits this user is allowed to consume.",
"consumed-credits": "Consumed credits",
"allowed-api-credits": "Allowed API credits",
"allowed-api-credits-help": "The allowed credits should be greater than or equal to the consumed credits.",
"Enter-allowed-api-credits": "Enter allowed API credits",
"login-to-sandbox": "Login to sandbox",
"your-credits-usage": "Your credits usage",
"remaining": "remaining",
"used": "used",
"your-credits-usage-help": "This is the number of credits consumed with your API key."
} }
@@ -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",
@@ -166,7 +168,10 @@
"Save as template": "Guardar como plantilla", "Save as template": "Guardar como plantilla",
"Fix & resend": "Corregir y reenviar", "Fix & resend": "Corregir y reenviar",
"Kiosk Mode": "Modo Kiosco", "Kiosk Mode": "Modo Kiosco",
"Reset password": "Restablecer contraseña" "Reset password": "Restablecer contraseña",
"View formdata": "Ver datos del formulario",
"Download all formdata": "Descargar todos los datos del formulario",
"API credits": "Créditos de API"
}, },
"report-heading": { "report-heading": {
"Sr.No": "Nº", "Sr.No": "Nº",
@@ -201,7 +206,8 @@
"Redirect url": "URL de redirección", "Redirect url": "URL de redirección",
"Created Date": "Fecha de creación", "Created Date": "Fecha de creación",
"Updated Date": "Fecha de actualización", "Updated Date": "Fecha de actualización",
"Expiry Date": "Fecha de vencimiento" "Expiry Date": "Fecha de vencimiento",
"Sent Date": "Fecha de envío"
}, },
"report-help": { "report-help": {
"Draft Documents": "Estos son documentos que has iniciado pero no has finalizado para su envío.", "Draft Documents": "Estos son documentos que has iniciado pero no has finalizado para su envío.",
@@ -441,7 +447,7 @@
"Next to current widget": "Junto al widget actual" "Next to current widget": "Junto al widget actual"
}, },
"options": "Opciones", "options": "Opciones",
"minimun-check": "Chequeo mínimo", "minimum-check": "Chequeo mínimo",
"maximum-check": "Chequeo máximo", "maximum-check": "Chequeo máximo",
"cell-count": "recuento de células", "cell-count": "recuento de células",
"default-value": "Valor por defecto", "default-value": "Valor por defecto",
@@ -699,7 +705,7 @@
"select-date-format": "Selecciona un formato de fecha", "select-date-format": "Selecciona un formato de fecha",
"quantity-of-credits": "Cantidad de créditos premium", "quantity-of-credits": "Cantidad de créditos premium",
"remaining-credits": "Créditos premium disponibles:", "remaining-credits": "Créditos premium disponibles:",
"remaining-credits-help": "Usa créditos premium para la API para la firma de documentos, envío en masa, o para incorporar la integración de OpenSign en tu página web. Te quedan {{allowedcredits}} créditos incluidos y {{addoncredits}} créditos comprados adicionalmente.", "remaining-credits-help": "Utilice créditos premium para la firma de documentos mediante API, el envío masivo o para integrar OpenSign en su sitio web. Le quedan {{allowedcredits}} créditos incluidos y {{addoncredits}} créditos adicionales comprados. Si está en un plan Teams o Enterprise, estos créditos pueden compartirse entre todos los usuarios de su empresa, a menos que el administrador de la cuenta haya establecido un límite para su cuenta.",
"additional-credits": "Por favor, compra créditos premium", "additional-credits": "Por favor, compra créditos premium",
"quota-err-quicksend": "Cuota alcanzada, no tienes créditos suficientes.", "quota-err-quicksend": "Cuota alcanzada, no tienes créditos suficientes.",
"buy-credits": "Comprar créditos premium", "buy-credits": "Comprar créditos premium",
@@ -1257,5 +1263,51 @@
"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.",
"hide-text-with-asterisks": "Ocultar texto con asteriscos",
"hide-text-with-asterisks-help": "El texto en el documento firmado se ocultará, pero seguirá siendo accesible para el remitente (propietario) desde su cuenta de {{appName}}.",
"document-id": "ID del documento",
"sent-date": "Fecha de envío",
"status": "Estado",
"signer-form-data-for": "Datos del formulario para el firmante: {{signerName}} <{{signerEmail}}>",
"widget-name": "Nombre del widget",
"widget-responses": "Respuestas del widget",
"formdata-download-note": "Nota: Las firmas, sellos, iniciales e imágenes no se exportarán.",
"export-all-formdata-error": "No puede exportar los datos del formulario porque la plantilla contiene nombres de widgets duplicados. Cree una nueva plantilla para poder exportar los datos.",
"export-formdata-error": "No puede exportar los datos del formulario porque el documento contiene nombres de widgets duplicados. Cree un documento nuevo sin nombres duplicados para poder exportar los datos.",
"duplicate-widget-name-error": "Este nombre de widget ya está asignado a otro firmante. Por favor elija un nombre diferente.",
"duplicate-template-widget-error": "No puede duplicar esta plantilla porque contiene nombres de widgets duplicados.",
"credit-alert": "Alerta de crédito",
"document-form-data": "Datos del formulario del documento",
"view-formdata-paid-title-error": "La función 'Ver datos del formulario' le permite imprimir y descargar los datos de los widgets completados por los firmantes durante la firma.",
"view-formdata-paid-error": "Nota: La función 'Ver datos del formulario' solo está disponible para documentos creados en o después de la versión v2.32.0 que no contengan nombres de widgets duplicados.",
"download-all-formdata-paid-title-error": "La función 'Descargar todos los datos del formulario' le permite descargar los datos de los widgets completados por los firmantes para todos los documentos creados a partir de la plantilla.",
"download-all-formdata-paid-error": "Nota: La función 'Descargar todos los datos del formulario' solo está disponible para plantillas creadas en o después de la versión v2.32.0 que no contengan nombres de widgets duplicados.",
"api-token-not-found": "Token de API no encontrado.",
"sub-head-api-credits": "Defina la cantidad máxima de créditos API que este usuario puede consumir.",
"consumed-credits": "Créditos consumidos",
"allowed-api-credits": "Créditos API permitidos",
"allowed-api-credits-help": "Los créditos permitidos deben ser mayores o iguales a los créditos consumidos.",
"Enter-allowed-api-credits": "Ingrese los créditos API permitidos",
"login-to-sandbox": "Iniciar sesión en sandbox",
"your-credits-usage": "Uso de sus créditos",
"remaining": "restantes",
"used": "usados",
"your-credits-usage-help": "Esta es la cantidad de créditos consumidos con su clave API."
} }
@@ -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",
@@ -174,7 +176,8 @@
"Redirect url": "URL de redirection", "Redirect url": "URL de redirection",
"Created Date": "Date de création", "Created Date": "Date de création",
"Updated Date": "Date de mise à jour", "Updated Date": "Date de mise à jour",
"Expiry Date": "Date d'expiration" "Expiry Date": "Date d'expiration",
"Sent Date": "Date d'envoi"
}, },
"btnLabel": { "btnLabel": {
"sign": "Signer", "sign": "Signer",
@@ -201,7 +204,10 @@
"Save as template": "Enregistrer comme modèle", "Save as template": "Enregistrer comme modèle",
"Fix & resend": "Corriger et renvoyer", "Fix & resend": "Corriger et renvoyer",
"Kiosk Mode": "Mode Kiosque", "Kiosk Mode": "Mode Kiosque",
"Reset password": "Réinitialiser le mot de passe" "Reset password": "Réinitialiser le mot de passe",
"View formdata": "Voir les données du formulaire",
"Download all formdata": "Télécharger toutes les données du formulaire",
"API credits": "Crédits API"
}, },
"report-help": { "report-help": {
"Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.", "Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.",
@@ -440,7 +446,7 @@
"Next to current widget": "À côté du widget actuel" "Next to current widget": "À côté du widget actuel"
}, },
"options": "Possibilités", "options": "Possibilités",
"minimun-check": "Vérification minimale", "minimum-check": "Vérification minimale",
"maximum-check": "Contrôle maximum", "maximum-check": "Contrôle maximum",
"cell-count": "numération cellulaire", "cell-count": "numération cellulaire",
"default-value": "Valeur par défaut", "default-value": "Valeur par défaut",
@@ -699,7 +705,7 @@
"quantity-of-credits": "Quantité de crédits de prime", "quantity-of-credits": "Quantité de crédits de prime",
"remaining-credits": "Crédits premium disponibles :", "remaining-credits": "Crédits premium disponibles :",
"additional-credits": "Veuillez acheter des crédits premium", "additional-credits": "Veuillez acheter des crédits premium",
"remaining-credits-help": "Utilisez des crédits premium pour la signature de documents API, l'envoi groupé ou l'intégration d'OpenSign sur votre site Web. Il vous reste {{allowedcredits}} crédits inclus et {{addoncredits}} crédits achetés supplémentaires.", "remaining-credits-help": "Utilisez des crédits premium pour la signature de documents via l'API, l'envoi groupé ou l'intégration d'OpenSign sur votre site Web. Il vous reste {{allowedcredits}} crédits inclus et {{addoncredits}} crédits supplémentaires achetés. Si vous êtes sur un plan Teams ou Enterprise, ces crédits peuvent être partagés entre tous les utilisateurs de votre entreprise, sauf si l'administrateur du compte a défini une limite pour votre compte.",
"quota-err-quicksend": "Quota atteint, vous n'avez pas assez de crédits.", "quota-err-quicksend": "Quota atteint, vous n'avez pas assez de crédits.",
"buy-credits": "Acheter des crédits premium", "buy-credits": "Acheter des crédits premium",
"rotate-right": "Faire pivoter à droite", "rotate-right": "Faire pivoter à droite",
@@ -1256,5 +1262,51 @@
"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.",
"hide-text-with-asterisks": "Masquer le texte avec des astérisques",
"hide-text-with-asterisks-help": "Le texte sur le document signé sera masqué, mais restera accessible à l'expéditeur du document (propriétaire) depuis son compte {{appName}}.",
"document-id": "ID du document",
"sent-date": "Date d'envoi",
"status": "Statut",
"signer-form-data-for": "Données du formulaire pour le signataire : {{signerName}} <{{signerEmail}}>",
"widget-name": "Nom du widget",
"widget-responses": "Réponses du widget",
"formdata-download-note": "Remarque : Les signatures, tampons, initiales et images ne seront pas exportés.",
"export-all-formdata-error": "Vous ne pouvez pas exporter les données du formulaire car le modèle contient des noms de widgets en double. Veuillez créer un nouveau modèle pour pouvoir exporter les données.",
"export-formdata-error": "Vous ne pouvez pas exporter les données du formulaire car le document contient des noms de widgets en double. Veuillez créer un nouveau document sans doublons pour pouvoir exporter les données.",
"duplicate-widget-name-error": "Ce nom de widget est déjà attribué à un autre signataire. Veuillez choisir un autre nom.",
"duplicate-template-widget-error": "Vous ne pouvez pas dupliquer ce modèle car il contient des noms de widgets en double.",
"credit-alert": "Alerte de crédit",
"document-form-data": "Données du formulaire du document",
"view-formdata-paid-title-error": "La fonction 'Afficher les données du formulaire' vous permet d'imprimer et de télécharger les données des widgets remplies par les signataires lors de la signature.",
"view-formdata-paid-error": "Remarque : La fonction 'Afficher les données du formulaire' est uniquement disponible pour les documents créés à partir de la version v2.32.0 et ne contenant pas de noms de widgets en double.",
"download-all-formdata-paid-title-error": "La fonction 'Télécharger toutes les données du formulaire' vous permet de télécharger les données des widgets remplies par les signataires pour tous les documents créés à partir du modèle.",
"download-all-formdata-paid-error": "Remarque : La fonction 'Télécharger toutes les données du formulaire' est uniquement disponible pour les modèles créés à partir de la version v2.32.0 et ne contenant pas de noms de widgets en double.",
"api-token-not-found": "Jeton API introuvable.",
"sub-head-api-credits": "Définissez le nombre maximal de crédits API que cet utilisateur est autorisé à consommer.",
"consumed-credits": "Crédits consommés",
"allowed-api-credits": "Crédits API autorisés",
"allowed-api-credits-help": "Les crédits autorisés doivent être supérieurs ou égaux aux crédits consommés.",
"Enter-allowed-api-credits": "Saisissez les crédits API autorisés",
"login-to-sandbox": "Se connecter au sandbox",
"your-credits-usage": "Votre utilisation des crédits",
"remaining": "restants",
"used": "utilisés",
"your-credits-usage-help": "Ceci est le nombre de crédits consommés avec votre clé API."
} }
@@ -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": "आपके हस्ताक्षर की आवश्यकता है",
@@ -166,7 +168,10 @@
"Save as template": "टेम्पलेट के रूप में सहेजें", "Save as template": "टेम्पलेट के रूप में सहेजें",
"Fix & resend": "ठीक करें और पुनः भेजें", "Fix & resend": "ठीक करें और पुनः भेजें",
"Kiosk Mode": "कियोस्क मोड", "Kiosk Mode": "कियोस्क मोड",
"Reset password": "पासवर्ड रीसेट करें" "Reset password": "पासवर्ड रीसेट करें",
"View formdata": "फॉर्म डेटा देखें",
"Download all formdata": "सभी फॉर्म डेटा डाउनलोड करें",
"API credits": "API क्रेडिट्स"
}, },
"report-heading": { "report-heading": {
"Sr.No": "क्रमांक", "Sr.No": "क्रमांक",
@@ -201,7 +206,8 @@
"Redirect url": "रीडायरेक्ट URL", "Redirect url": "रीडायरेक्ट URL",
"Created Date": "निर्माण तिथि", "Created Date": "निर्माण तिथि",
"Updated Date": "अद्यतन तिथि", "Updated Date": "अद्यतन तिथि",
"Expiry Date": "समाप्ति तिथि" "Expiry Date": "समाप्ति तिथि",
"Sent Date": "भेजे जाने की तिथि"
}, },
"report-help": { "report-help": {
"Draft Documents": "ये वे दस्तावेज़ हैं जिन्हें आपने शुरू तो किया है लेकिन भेजने के लिए अंतिम रूप नहीं दिया है।", "Draft Documents": "ये वे दस्तावेज़ हैं जिन्हें आपने शुरू तो किया है लेकिन भेजने के लिए अंतिम रूप नहीं दिया है।",
@@ -440,7 +446,7 @@
"Next to current widget": "वर्तमान विजेट के आगे" "Next to current widget": "वर्तमान विजेट के आगे"
}, },
"options": "विकल्प", "options": "विकल्प",
"minimun-check": "न्यूनतम जाँच", "minimum-check": "न्यूनतम जाँच",
"maximum-check": "अधिकतम जाँच", "maximum-check": "अधिकतम जाँच",
"cell-count": "सेल्स संख्या", "cell-count": "सेल्स संख्या",
"default-value": "डिफ़ॉल्ट मान", "default-value": "डिफ़ॉल्ट मान",
@@ -698,7 +704,7 @@
"select-date-format": "एक दिनांक प्रारूप चुनें", "select-date-format": "एक दिनांक प्रारूप चुनें",
"quantity-of-credits": "प्रीमियम क्रेडिट की मात्रा", "quantity-of-credits": "प्रीमियम क्रेडिट की मात्रा",
"remaining-credits": "प्रीमियम क्रेडिट उपलब्ध:", "remaining-credits": "प्रीमियम क्रेडिट उपलब्ध:",
"remaining-credits-help": "अपनी वेबसाइट पर एपीआई दस्तावेज़ हस्ताक्षर, थोक भेजने या OpenSign एकीकरण को एम्बेड करने के लिए प्रीमियम क्रेडिट का उपयोग करें। आपके पास {{allowedcredits}} शामिल क्रेडिट और {{addoncredits}} अतिरिक्त खरीदे ए क्रेडिट शेष हैं।", "remaining-credits-help": "API दस्तावेज़ साइनिंग, बल्क सेंडिंग या अपनी वेबसाइट पर OpenSign इंटीग्रेशन एम्बेड करने के लिए प्रीमियम क्रेडिट का उपयोग करें। आपके पास {{allowedcredits}} शामिल क्रेडिट और {{addoncredits}} अतिरिक्त खरीदे हुए क्रेडिट शेष हैं। यदि आप Teams या Enterprise प्लान पर हैं, तो ये क्रेडिट आपकी कंपनी के सभी उपयोगकर्ताओं के बीच साझा किए जा सकते हैं, जब तक कि आपके खाते के लिए अकाउंट एडमिन ने कोई सीमा निर्धारित न की हो।",
"additional-credits": "कृपया प्रीमियम क्रेडिट खरीदें", "additional-credits": "कृपया प्रीमियम क्रेडिट खरीदें",
"quota-err-quicksend": "कोटा पहुँच गया, आपके पास पर्याप्त क्रेडिट नहीं हैं।", "quota-err-quicksend": "कोटा पहुँच गया, आपके पास पर्याप्त क्रेडिट नहीं हैं।",
"buy-credits": "प्रीमियम क्रेडिट खरीदें", "buy-credits": "प्रीमियम क्रेडिट खरीदें",
@@ -1256,5 +1262,51 @@
"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": "यदि कोई व्यक्तिगत उपयोगकर्ता-स्तरीय सेटिंग्स कॉन्फ़िगर नहीं की गई हैं, तो ये वैश्विक सेटिंग्स स्वचालित रूप से लागू होंगी।",
"hide-text-with-asterisks": "टेक्स्ट को तारांकन (*) के साथ छुपाएँ",
"hide-text-with-asterisks-help": "हस्ताक्षरित दस्तावेज़ पर टेक्स्ट दृश्य से छिपा रहेगा, लेकिन दस्तावेज़ भेजने वाले (मालिक) के लिए उनके {{appName}} खाते से सुलभ रहेगा।",
"document-id": "दस्तावेज़ ID",
"sent-date": "भेजे जाने की तिथि",
"status": "स्थिति",
"signer-form-data-for": "हस्ताक्षरकर्ता के लिए फ़ॉर्म डेटा: {{signerName}} <{{signerEmail}}>",
"widget-name": "विजेट नाम",
"widget-responses": "विजेट प्रतिक्रियाएँ",
"formdata-download-note": "नोट: हस्ताक्षर, स्टैम्प, प्रारंभिक अक्षर और छवियाँ निर्यात नहीं की जाएँगी।",
"export-all-formdata-error": "आप फ़ॉर्म डेटा निर्यात नहीं कर सकते क्योंकि टेम्पलेट में डुप्लिकेट विजेट नाम हैं। कृपया नया टेम्पलेट बनाएं ताकि आप फ़ॉर्म डेटा निर्यात कर सकें।",
"export-formdata-error": "आप फ़ॉर्म डेटा निर्यात नहीं कर सकते क्योंकि दस्तावेज़ में डुप्लिकेट विजेट नाम हैं। कृपया नया दस्तावेज़ बनाएं जिसमें डुप्लिकेट विजेट नाम न हों।",
"duplicate-widget-name-error": "यह विजेट नाम पहले से ही किसी अन्य साइनर को सौंपा गया है। कृपया कोई दूसरा नाम चुनें।",
"duplicate-template-widget-error": "आप इस टेम्पलेट को डुप्लिकेट नहीं कर सकते क्योंकि इसमें डुप्लिकेट विजेट नाम हैं।",
"credit-alert": "क्रेडिट अलर्ट",
"document-form-data": "दस्तावेज़ फ़ॉर्म डेटा",
"view-formdata-paid-title-error": "'फ़ॉर्म डेटा देखें' सुविधा आपको हस्ताक्षर करते समय साइनरों द्वारा भरे गए विजेट डेटा को प्रिंट और डाउनलोड करने की सुविधा देती है.",
"view-formdata-paid-error": "नोट: 'फ़ॉर्म डेटा देखें' सुविधा केवल उन दस्तावेज़ों के लिए उपलब्ध है जो v2.32.0 या उसके बाद बनाए गए हों और जिनमें डुप्लिकेट विजेट नाम न हों.",
"download-all-formdata-paid-title-error": "'सभी फ़ॉर्म डेटा डाउनलोड करें' सुविधा आपको टेम्पलेट से बनाए गए सभी दस्तावेज़ों के लिए साइनरों द्वारा भरे गए विजेट डेटा को डाउनलोड करने देती है.",
"download-all-formdata-paid-error": "नोट: 'सभी फ़ॉर्म डेटा डाउनलोड करें' सुविधा केवल उन टेम्पलेट्स के लिए उपलब्ध है जो v2.32.0 या उसके बाद बनाए गए हों और जिनमें डुप्लिकेट विजेट नाम न हों.",
"api-token-not-found": "API टोकन नहीं मिला।",
"sub-head-api-credits": "इस उपयोगकर्ता द्वारा उपभोग किए जाने वाले API क्रेडिट की अधिकतम संख्या निर्धारित करें।",
"consumed-credits": "उपयोग किए गए क्रेडिट",
"allowed-api-credits": "अनुमत API क्रेडिट",
"allowed-api-credits-help": "अनुमत क्रेडिट उपयोग किए गए क्रेडिट के बराबर या उससे अधिक होने चाहिए।",
"Enter-allowed-api-credits": "अनुमत API क्रेडिट दर्ज करें",
"login-to-sandbox": "सैंडबॉक्स में लॉगिन करें",
"your-credits-usage": "आपके क्रेडिट उपयोग",
"remaining": "शेष",
"used": "उपयोग किए गए",
"your-credits-usage-help": "यह आपके API कुंजी द्वारा उपभोग किए गए क्रेडिट की संख्या है।"
} }
@@ -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",
@@ -166,7 +168,10 @@
"Save as template": "Salva come modello", "Save as template": "Salva come modello",
"Fix & resend": "Correggi e reinvia", "Fix & resend": "Correggi e reinvia",
"Kiosk Mode": "Modalità Kiosk", "Kiosk Mode": "Modalità Kiosk",
"Reset password": "Reimposta password" "Reset password": "Reimposta password",
"View formdata": "Visualizza i dati del modulo",
"Download all formdata": "Scarica tutti i dati del modulo",
"API credits": "Crediti API"
}, },
"report-heading": { "report-heading": {
"Sr.No": "Nr.", "Sr.No": "Nr.",
@@ -201,7 +206,8 @@
"Redirect url": "URL di reindirizzamento", "Redirect url": "URL di reindirizzamento",
"Created Date": "Data di creazione", "Created Date": "Data di creazione",
"Updated Date": "Data di aggiornamento", "Updated Date": "Data di aggiornamento",
"Expiry Date": "Data di scadenza" "Expiry Date": "Data di scadenza",
"Sent Date": "Data di invio"
}, },
"report-help": { "report-help": {
"Draft Documents": "Questi sono documenti che hai iniziato ma non hai ancora finalizzato per l'invio.", "Draft Documents": "Questi sono documenti che hai iniziato ma non hai ancora finalizzato per l'invio.",
@@ -263,7 +269,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",
@@ -440,7 +446,7 @@
"Next to current widget": "Accanto al widget corrente" "Next to current widget": "Accanto al widget corrente"
}, },
"options": "Opzioni", "options": "Opzioni",
"minimun-check": "Controllo minimo", "minimum-check": "Controllo minimo",
"maximum-check": "Controllo massimo", "maximum-check": "Controllo massimo",
"cell-count": "conteggio delle cellule", "cell-count": "conteggio delle cellule",
"default-value": "Valore predefinita", "default-value": "Valore predefinita",
@@ -698,7 +704,7 @@
"select-date-format": "Seleziona un formato data", "select-date-format": "Seleziona un formato data",
"quantity-of-credits": "Quantità di crediti premium", "quantity-of-credits": "Quantità di crediti premium",
"remaining-credits": "Crediti premium disponibili:", "remaining-credits": "Crediti premium disponibili:",
"remaining-credits-help": "Usa i crediti premium per firmare documenti API, invii in massa o per integrare OpenSign nel tuo sito web. Hai a disposizione {{allowedcredits}} crediti inclusi e {{addoncredits}} crediti acquistati aggiuntivi rimanenti.", "remaining-credits-help": "Usa i crediti premium per la firma di documenti tramite API, l'invio massivo o per integrare OpenSign nel tuo sito web. Hai ancora {{allowedcredits}} crediti inclusi e {{addoncredits}} crediti aggiuntivi acquistati. Se sei in un piano Teams o Enterprise, questi crediti possono essere condivisi tra tutti gli utenti della tua azienda, a meno che l'amministratore dell'account non abbia impostato un limite specifico per il tuo account.",
"additional-credits": "Acquista crediti premium", "additional-credits": "Acquista crediti premium",
"quota-err-quicksend": "Quota raggiunta, non hai abbastanza crediti.", "quota-err-quicksend": "Quota raggiunta, non hai abbastanza crediti.",
"buy-credits": "Acquista Crediti Premium", "buy-credits": "Acquista Crediti Premium",
@@ -1256,5 +1262,51 @@
"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.",
"hide-text-with-asterisks": "Nascondi il testo con asterischi",
"hide-text-with-asterisks-help": "Il testo nel documento firmato sarà nascosto dalla visualizzazione ma rimarrà accessibile al mittente (proprietario) tramite il suo account {{appName}}.",
"document-id": "ID documento",
"sent-date": "Data di invio",
"status": "Stato",
"signer-form-data-for": "Dati del modulo per il firmatario: {{signerName}} <{{signerEmail}}>",
"widget-name": "Nome del widget",
"widget-responses": "Risposte del widget",
"formdata-download-note": "Nota: Firme, timbri, iniziali e immagini non verranno esportati.",
"export-all-formdata-error": "Non è possibile esportare i dati del modulo perché il modello contiene nomi di widget duplicati. Creare un nuovo modello per poter esportare i dati.",
"export-formdata-error": "Non è possibile esportare i dati del modulo perché il documento contiene nomi di widget duplicati. Creare un nuovo documento senza nomi duplicati per poter esportare i dati.",
"duplicate-widget-name-error": "Questo nome di widget è già assegnato a un altro firmatario. Scegli un nome diverso.",
"duplicate-template-widget-error": "Non è possibile duplicare questo modello perché contiene nomi di widget duplicati.",
"credit-alert": "Avviso di credito",
"document-form-data": "Dati del modulo del documento",
"view-formdata-paid-title-error": "La funzione 'Visualizza dati del modulo' consente di stampare e scaricare i dati dei widget compilati dai firmatari durante la firma.",
"view-formdata-paid-error": "Nota: La funzione 'Visualizza dati del modulo' è disponibile solo per documenti creati dalla versione v2.32.0 in poi che non contengono nomi di widget duplicati.",
"download-all-formdata-paid-title-error": "La funzione 'Scarica tutti i dati del modulo' consente di scaricare i dati dei widget compilati dai firmatari per tutti i documenti creati dal modello.",
"download-all-formdata-paid-error": "Nota: La funzione 'Scarica tutti i dati del modulo' è disponibile solo per modelli creati dalla versione v2.32.0 in poi che non contengano nomi di widget duplicati.",
"api-token-not-found": "Token API non trovato.",
"sub-head-api-credits": "Definisci il numero massimo di crediti API che questo utente può consumare.",
"consumed-credits": "Crediti consumati",
"allowed-api-credits": "Crediti API consentiti",
"allowed-api-credits-help": "I crediti consentiti devono essere maggiori o uguali ai crediti consumati.",
"Enter-allowed-api-credits": "Inserisci i crediti API consentiti",
"login-to-sandbox": "Accedi alla sandbox",
"your-credits-usage": "Utilizzo dei tuoi crediti",
"remaining": "rimanenti",
"used": "usati",
"your-credits-usage-help": "Questo è il numero di crediti consumati con la tua chiave API."
} }
Binary file not shown.
-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;
-1
View File
@@ -225,7 +225,6 @@ const AddUser = (props) => {
className="block text-xs font-semibold" className="block text-xs font-semibold"
> >
{t("phone")} {t("phone")}
{/* <span className="text-[red] text-[13px]"> *</span> */}
</label> </label>
<input <input
type="text" type="text"
+5 -2
View File
@@ -25,13 +25,16 @@ const BulkSendUi = (props) => {
//function to check at least one signature field exist //function to check at least one signature field exist
const signatureExist = async () => { const signatureExist = async () => {
const isPrefill = props?.Placeholders.some((x) => x?.Role === "prefill"); const isPrefill =
props?.Placeholders?.some((x) => x?.Role === "prefill") || false;
if (isPrefill) { if (isPrefill) {
setIsPrefillExist(isPrefill); setIsPrefillExist(isPrefill);
} }
setIsDisableBulkSend(false); setIsDisableBulkSend(false);
const getPlaceholder = props?.Placeholders; const getPlaceholder = props?.Placeholders;
const removePrefill = getPlaceholder.filter((x) => x?.Role !== "prefill"); const removePrefill = getPlaceholder?.filter(
(x) => x?.Role !== "prefill"
);
const checkIsSignatureExistt = removePrefill?.every((placeholderObj) => const checkIsSignatureExistt = removePrefill?.every((placeholderObj) =>
placeholderObj?.placeHolder?.some((holder) => placeholderObj?.placeHolder?.some((holder) =>
holder?.pos?.some((posItem) => posItem?.type === "signature") holder?.pos?.some((posItem) => posItem?.type === "signature")
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import ModalUi from "../primitives/ModalUi"; import ModalUi from "../primitives/ModalUi";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -7,7 +7,6 @@ const ColumnSelector = ({
allColumns = [], allColumns = [],
visibleColumns = [], visibleColumns = [],
columnLabels = {}, columnLabels = {},
defaultColumns = [],
onApply, onApply,
onClose onClose
}) => { }) => {
@@ -42,7 +41,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 -4
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();
@@ -60,7 +60,6 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
setAppLogo(logo); setAppLogo(logo);
} }
} }
const handleLogout = async () => { const handleLogout = async () => {
setIsOpen(false); setIsOpen(false);
setIsLoggingOut(true); setIsLoggingOut(true);
@@ -141,7 +140,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>
); );
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import ModalUi from "../../primitives/ModalUi"; import ModalUi from "../../primitives/ModalUi";
import { fontsizeArr, fontColorArr } from "../../constant/Utils"; import { fontsizeArr, fontColorArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -7,7 +7,7 @@ export default function CellsSettingModal({
isOpen, isOpen,
handleClose, handleClose,
defaultData, defaultData,
handleSave handleSave,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [name, setName] = useState(""); const [name, setName] = useState("");
@@ -31,13 +31,16 @@ export default function CellsSettingModal({
name, name,
cellCount: parseInt(cellCount, 10), cellCount: parseInt(cellCount, 10),
fontSize, fontSize,
fontColor fontColor,
}); });
}; };
return ( return (
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("widget-info")}> <ModalUi isOpen={isOpen} handleClose={handleClose} title={t("widget-info")}>
<form onSubmit={onSubmit} className="p-[20px] text-base-content flex flex-col gap-3"> <form
onSubmit={onSubmit}
className="p-[20px] text-base-content flex flex-col gap-3"
>
<div> <div>
<label htmlFor="name" className="text-[13px]"> <label htmlFor="name" className="text-[13px]">
{t("name")} <span className="text-[red]">*</span> {t("name")} <span className="text-[red]">*</span>
@@ -91,7 +94,10 @@ export default function CellsSettingModal({
</option> </option>
))} ))}
</select> </select>
<span className="w-5 h-[19px] ml-1" style={{ background: fontColor }} /> <span
className="w-5 h-[19px] ml-1"
style={{ background: fontColor }}
/>
</div> </div>
</div> </div>
<div className="h-[1px] w-full bg-[#b7b3b3] my-[16px]"></div> <div className="h-[1px] w-full bg-[#b7b3b3] my-[16px]"></div>
@@ -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,24 +14,21 @@ 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";
import "../../styles/opensigndrive.css"; 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 } 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 =
@@ -221,16 +216,17 @@ function Placeholder(props) {
//placeholder, template flow //placeholder, template flow
else if (props.isPlaceholder && !props.isDragging) { else if (props.isPlaceholder && !props.isDragging) {
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails(props.pos); props.setCurrWidgetsDetails && props.setCurrWidgetsDetails(props.pos);
if ( if (props?.data?.Role !== "prefill") {
props.pos.key === props?.currWidgetsDetails?.key && //this condition is used open signers attach modal but it should only open when widgets already selected
props?.data?.Role !== "prefill" if (props?.currWidgetsDetails?.key === props.pos?.key) {
) { props.handleLinkUser(props.data.Id);
props.handleLinkUser(props.data.Id); }
props.setUniqueId(props.data.Id); props.setUniqueId(props.data.Id);
const checkIndex = props.xyPosition.findIndex( const checkIndex = props.xyPosition.findIndex(
(data) => data.Id === props.data.Id (data) => data.Id === props.data.Id
); );
props.setIsSelectId(checkIndex || 0); props.setIsSelectId(checkIndex || 0);
props?.setRoleName("");
} else if (props?.data?.Role === "prefill") { } else if (props?.data?.Role === "prefill") {
dispatch(setIsShowModal({ [props.pos.key]: true })); dispatch(setIsShowModal({ [props.pos.key]: true }));
props.setUniqueId(props?.data?.Id); props.setUniqueId(props?.data?.Id);
@@ -306,31 +302,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 +567,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,
@@ -602,7 +575,7 @@ function Placeholder(props) {
props.containerWH props.containerWH
); );
const fontSize = (size || 12) * containerScale * props.scale; const fontSize = (size || 12) * containerScale * props.scale;
//isMinHeight to set text box minimum height // isMinHeight to set text box minimum height
if (isMinHeight) { if (isMinHeight) {
return fontSize * 1.5 + "px"; return fontSize * 1.5 + "px";
} else { } else {
@@ -624,22 +597,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";
@@ -690,10 +647,7 @@ function Placeholder(props) {
}; };
const handleClearDate = () => { const handleClearDate = () => {
setIsToday(false); setIsToday(false);
setSelectDate((prev) => ({ setSelectDate((prev) => ({ ...prev, date: "" }));
...prev,
date: ""
}));
}; };
return ( return (
<> <>
@@ -705,18 +659,24 @@ function Placeholder(props) {
onClick={() => { onClick={() => {
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails &&
props.setCurrWidgetsDetails(props.pos); props.setCurrWidgetsDetails(props.pos);
props?.setRoleName("prefill");
props.setUniqueId(props?.data?.Id);
}} }}
style={{ style={{
fontFamily: "Arial, sans-serif", fontFamily: "Arial, sans-serif",
position: "absolute", position: "absolute",
left: xPos(props.pos, props.isSignYourself), left: xPos(props.pos, props.isSignYourself),
top: yPos(props.pos, props.isSignYourself), top: yPos(props.pos, props.isSignYourself),
...(props.pos?.Width ? { width: props.pos?.Width - 5 } : {}),
...(props.pos?.Height ? { height: props.pos?.Height } : {}),
fontSize: fontSize, fontSize: fontSize,
color: fontColor, color: fontColor,
zIndex: 99 zIndex: 99
}} }}
> >
{props.pos?.options?.response} {
props.pos?.options?.response
}
</span> </span>
) : ( ) : (
<Rnd <Rnd
@@ -773,7 +733,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);
@@ -1034,10 +998,7 @@ function Placeholder(props) {
type="checkbox" type="checkbox"
className="op-checkbox op-checkbox-xs" className="op-checkbox op-checkbox-xs"
onClick={() => { onClick={() => {
setSelectDate((prev) => ({ setSelectDate((prev) => ({ ...prev, date: "" }));
...prev,
date: ""
}));
setIsToday(!isToday); setIsToday(!isToday);
}} }}
/> />
@@ -1146,9 +1107,7 @@ function Placeholder(props) {
<button <button
type="button" type="button"
className="op-btn op-btn-primary" className="op-btn op-btn-primary"
onClick={() => { onClick={() => saveDateSetting(selectDate)}
saveDateSetting(selectDate);
}}
> >
{t("save")} {t("save")}
</button> </button>
@@ -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
@@ -42,10 +42,10 @@ function PlaceholderType(props) {
// prefer the latest response value over any default value // prefer the latest response value over any default value
const widgetData = const widgetData =
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? ""; props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
const widgetTypeTranslation = t(`widgets-name.${props?.pos?.type}`); // const widgetTypeTranslation = t(`widgets-name.${props?.pos?.type}`);
const [widgetValue, setwidgetValue] = useState(); const [widgetValue, setwidgetValue] = useState();
const [selectedCheckbox, setSelectedCheckbox] = useState([]); const [selectedCheckbox, setSelectedCheckbox] = useState([]);
const [hint, setHint] = useState(""); // const [hint, setHint] = useState("");
const [imgUrl, setImgUrl] = useState(""); const [imgUrl, setImgUrl] = useState("");
const fontSize = props.calculateFont(props.pos.options?.fontSize); const fontSize = props.calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black"; const fontColor = props.pos.options?.fontColor || "black";
@@ -72,11 +72,11 @@ function PlaceholderType(props) {
// keep displayed value in sync with the stored response // keep displayed value in sync with the stored response
setwidgetValue(widgetData); setwidgetValue(widgetData);
} }
if (props.pos?.options?.hint) { // if (props.pos?.options?.hint) {
setHint(props.pos?.options.hint); // setHint(props.pos?.options.hint);
} else if (props.pos?.options?.validation?.type) { // } else if (props.pos?.options?.validation?.type) {
checkRegularExpress(props.pos?.options?.validation?.type, setHint); // checkRegularExpress(props.pos?.options?.validation?.type, setHint);
} // }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.pos, widgetData, type]); }, [props.pos, widgetData, type]);
@@ -130,6 +130,14 @@ function PlaceholderType(props) {
loadImage(); loadImage();
}, [props.pos.SignUrl]); }, [props.pos.SignUrl]);
const formatWidgetName = () => {
const widgetName = props?.pos?.options?.name;
const name = widgetName ? widgetName?.split(`-`) : ["-", "-", "-"];
const lastWord = name.length > 1 ? `-${name[name.length - 1]}` : "";
const title =
props?.pos?.type === name[0] ? `${name[0]}${lastWord}` : widgetName;
return props?.pos?.options?.hint || title;
};
switch (type) { switch (type) {
case "signature": case "signature":
return props.pos.SignUrl ? ( return props.pos.SignUrl ? (
@@ -150,7 +158,7 @@ function PlaceholderType(props) {
}} }}
className="font-medium" className="font-medium"
> >
{hint || widgetTypeTranslation} {formatWidgetName()}
</div> </div>
)} )}
</div> </div>
@@ -174,7 +182,7 @@ function PlaceholderType(props) {
}} }}
className="font-medium" className="font-medium"
> >
{hint || widgetTypeTranslation} {formatWidgetName()}
</div> </div>
)} )}
</div> </div>
@@ -221,9 +229,11 @@ function PlaceholderType(props) {
case textInputWidget: case textInputWidget:
return props.isSignYourself || iswidgetEnable ? ( return props.isSignYourself || iswidgetEnable ? (
<textarea <textarea
placeholder={hint || t("widgets-name.text")} placeholder={formatWidgetName()}
rows={1} rows={1}
value={widgetValue} value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`} className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{ style={{
fontSize: fontSize, fontSize: fontSize,
@@ -237,7 +247,7 @@ function PlaceholderType(props) {
/> />
) : ( ) : (
<div style={textWidgetStyle} className="select-none-cls"> <div style={textWidgetStyle} className="select-none-cls">
<span>{hint || widgetTypeTranslation}</span> <span className="ml-0.5">{formatWidgetName()}</span>
</div> </div>
); );
case cellsWidget: { case cellsWidget: {
@@ -256,12 +266,14 @@ function PlaceholderType(props) {
isEnabled={iswidgetEnable} isEnabled={iswidgetEnable}
count={count} count={count}
height={height} height={height}
value={cells.join("")} value={
cells.join("")
}
editable={isEditable} editable={isEditable}
resizable={props?.isAllowModify} resizable={props?.isAllowModify}
fontSize={fontSize} fontSize={fontSize}
fontColor={fontColor} fontColor={fontColor}
hint={hint} hint={formatWidgetName()}
onCellCountChange={handleCellResize} onCellCountChange={handleCellResize}
/> />
); );
@@ -295,7 +307,7 @@ function PlaceholderType(props) {
}} }}
className="font-medium text-center" className="font-medium text-center"
> >
{hint || widgetTypeTranslation} {formatWidgetName()}
</div> </div>
)} )}
</div> </div>
@@ -304,9 +316,11 @@ function PlaceholderType(props) {
return iswidgetEnable ? ( return iswidgetEnable ? (
<textarea <textarea
readOnly readOnly
placeholder={hint || widgetTypeTranslation} placeholder={formatWidgetName()}
rows={1} rows={1}
value={widgetValue} value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`} className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{ style={{
fontSize: fontSize, fontSize: fontSize,
@@ -319,16 +333,18 @@ function PlaceholderType(props) {
/> />
) : ( ) : (
<div className="flex h-full select-none-cls" style={textWidgetStyle}> <div className="flex h-full select-none-cls" style={textWidgetStyle}>
<span> {props.pos?.options?.hint || widgetTypeTranslation}</span> <span className="ml-0.5">{formatWidgetName()}</span>
</div> </div>
); );
case "company": case "company":
return iswidgetEnable ? ( return iswidgetEnable ? (
<textarea <textarea
readOnly readOnly
placeholder={hint || widgetTypeTranslation} placeholder={formatWidgetName()}
rows={1} rows={1}
value={widgetValue} value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`} className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{ style={{
fontSize: fontSize, fontSize: fontSize,
@@ -341,16 +357,18 @@ function PlaceholderType(props) {
/> />
) : ( ) : (
<div style={textWidgetStyle} className="select-none-cls"> <div style={textWidgetStyle} className="select-none-cls">
<span>{hint || widgetTypeTranslation}</span> <span className="ml-0.5">{formatWidgetName()}</span>
</div> </div>
); );
case "job title": case "job title":
return iswidgetEnable ? ( return iswidgetEnable ? (
<textarea <textarea
readOnly readOnly
placeholder={hint || widgetTypeTranslation} placeholder={formatWidgetName()}
rows={1} rows={1}
value={widgetValue} value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`} className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{ style={{
fontSize: fontSize, fontSize: fontSize,
@@ -363,7 +381,7 @@ function PlaceholderType(props) {
/> />
) : ( ) : (
<div style={textWidgetStyle} className="select-none-cls"> <div style={textWidgetStyle} className="select-none-cls">
<span>{hint || widgetTypeTranslation}</span> <span className="ml-0.5">{formatWidgetName()}</span>
</div> </div>
); );
case "date": case "date":
@@ -444,7 +462,7 @@ function PlaceholderType(props) {
}} }}
className="font-medium text-center" className="font-medium text-center"
> >
{hint || widgetTypeTranslation} {formatWidgetName()}
</div> </div>
)} )}
</div> </div>
@@ -453,9 +471,11 @@ function PlaceholderType(props) {
return iswidgetEnable ? ( return iswidgetEnable ? (
<textarea <textarea
readOnly readOnly
placeholder={hint || widgetTypeTranslation} placeholder={formatWidgetName()}
rows={1} rows={1}
value={widgetValue} value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`} className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{ style={{
fontSize: fontSize, fontSize: fontSize,
@@ -468,7 +488,7 @@ function PlaceholderType(props) {
/> />
) : ( ) : (
<div style={textWidgetStyle} className="select-none-cls"> <div style={textWidgetStyle} className="select-none-cls">
<span>{hint || widgetTypeTranslation}</span> <span className="ml-0.5">{formatWidgetName()}</span>
</div> </div>
); );
case radioButtonWidget: case radioButtonWidget:
@@ -513,7 +533,9 @@ function PlaceholderType(props) {
readOnly readOnly
placeholder={t("widgets-name.text")} placeholder={t("widgets-name.text")}
rows={1} rows={1}
value={widgetValue} value={
widgetValue
}
className={textWidgetCls} className={textWidgetCls}
style={{ style={{
fontFamily: "Arial, sans-serif", fontFamily: "Arial, sans-serif",
@@ -546,7 +568,7 @@ function PlaceholderType(props) {
}} }}
className="font-medium" className="font-medium"
> >
{hint || widgetTypeTranslation} {formatWidgetName()}
</div> </div>
)} )}
</div> </div>
+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,4 +1,3 @@
import React from "react";
import ModalUi from "../../primitives/ModalUi"; import ModalUi from "../../primitives/ModalUi";
import { fontColorArr, fontsizeArr } from "../../constant/Utils"; import { fontColorArr, fontsizeArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -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,
@@ -12,105 +11,53 @@ import {
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 = () => {
@@ -8,12 +8,16 @@ import {
cellsWidget, cellsWidget,
textWidget textWidget
} from "../../constant/Utils"; } from "../../constant/Utils";
import {
widgetNamesArr
} from "../../utils";
import { fontColorArr, fontsizeArr } from "../../constant/Utils"; import { fontColorArr, fontsizeArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const WidgetNameModal = (props) => { const WidgetNameModal = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const signTypes = props?.signatureType || signatureTypes; const signTypes = props?.signatureType || signatureTypes;
const [lastSubmittedName, setLastSubmittedName] = useState("");
const [formdata, setFormdata] = useState({ const [formdata, setFormdata] = useState({
name: "", name: "",
defaultValue: "", defaultValue: "",
@@ -46,7 +50,7 @@ const WidgetNameModal = (props) => {
[type] [type]
); );
const handleHint = () => { const handleHintPlaceholder = () => {
const type = props.defaultdata?.type; const type = props.defaultdata?.type;
if (type === "signature") { if (type === "signature") {
@@ -67,7 +71,7 @@ const WidgetNameModal = (props) => {
name: props.defaultdata?.options?.name || "", name: props.defaultdata?.options?.name || "",
defaultValue: props.defaultdata?.options?.defaultValue || "", defaultValue: props.defaultdata?.options?.defaultValue || "",
status: props.defaultdata?.options?.status || "required", status: props.defaultdata?.options?.status || "required",
hint: props.defaultdata?.options?.hint || handleHint(), hint: props.defaultdata?.options?.hint || "",
textvalidate: textvalidate:
props.defaultdata?.options?.validation?.type === "regex" props.defaultdata?.options?.validation?.type === "regex"
? props.defaultdata?.options?.validation?.pattern ? props.defaultdata?.options?.validation?.pattern
@@ -75,12 +79,14 @@ const WidgetNameModal = (props) => {
isReadOnly: props.defaultdata?.options?.isReadOnly || false, isReadOnly: props.defaultdata?.options?.isReadOnly || false,
cellCount: props.defaultdata?.options?.cellCount || 5, cellCount: props.defaultdata?.options?.cellCount || 5,
}); });
setLastSubmittedName(props.defaultdata?.options?.name || "");
} else { } else {
setFormdata({ setFormdata({
...formdata, ...formdata,
name: props.defaultdata?.options?.name || "", name: props.defaultdata?.options?.name || "",
cellCount: props.defaultdata?.options?.cellCount || 5, cellCount: props.defaultdata?.options?.cellCount || 5,
}); });
setLastSubmittedName(props.defaultdata?.options?.name || "");
} }
if (signTypes.length > 0) { if (signTypes.length > 0) {
@@ -93,6 +99,19 @@ const WidgetNameModal = (props) => {
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
if (props.handleData) { if (props.handleData) {
const widgetNames = widgetNamesArr(
props.widgetsSource,
props?.activeSignerId
);
if (lastSubmittedName && lastSubmittedName !== formdata.name) {
const widgetNameExist = widgetNames?.find(
(widget) => widget === formdata.name
);
if (widgetNameExist) {
alert(t("duplicate-widget-name-error"));
return;
}
}
if (isSignOrInitials) { if (isSignOrInitials) {
const enabledSignTypes = signatureType?.filter((x) => x.enabled); const enabledSignTypes = signatureType?.filter((x) => x.enabled);
const isDefaultSignTypeOnly = const isDefaultSignTypeOnly =
@@ -292,7 +311,7 @@ const WidgetNameModal = (props) => {
{!props?.isSelfSign && {!props?.isSelfSign &&
!isSignOrInitials && !isSignOrInitials &&
props?.roleName !== "prefill" && ( props?.roleName !== "prefill" && (
<div className="mb-[0.75rem]"> <div className={showFontControls ? "mb-[0.5rem]" : "mb-[0.75rem]"}>
<div className="flex flex-row gap-[10px] mb-[0.5rem]"> <div className="flex flex-row gap-[10px] mb-[0.5rem]">
{statusArr.map((data, ind) => { {statusArr.map((data, ind) => {
return ( return (
@@ -324,30 +343,29 @@ const WidgetNameModal = (props) => {
{[ {[
textInputWidget, textInputWidget,
cellsWidget, cellsWidget,
].includes(props.defaultdata?.type) && ].includes(props.defaultdata?.type) && (
props?.roleName !== "prefill" && ( <div className="flex items-center">
<div className="flex items-center"> <input
<input id="isReadOnly"
id="isReadOnly" name="isReadOnly"
name="isReadOnly" type="checkbox"
type="checkbox" checked={formdata.isReadOnly}
checked={formdata.isReadOnly} className="op-checkbox op-checkbox-xs"
className="op-checkbox op-checkbox-xs" onChange={() =>
onChange={() => setFormdata((prev) => ({
setFormdata((prev) => ({ ...formdata,
...formdata, isReadOnly: !prev.isReadOnly
isReadOnly: !prev.isReadOnly }))
})) }
} />
/> <label
<label className="ml-1.5 mb-0 capitalize text-[13px]"
className="ml-1.5 mb-0 capitalize text-[13px]" htmlFor="isreadonly"
htmlFor="isreadonly" >
> {t("read-only")}
{t("read-only")} </label>
</label> </div>
</div> )}
)}
</div> </div>
)} )}
{isSignOrInitials && ( {isSignOrInitials && (
@@ -387,6 +405,7 @@ const WidgetNameModal = (props) => {
maxLength={40} maxLength={40}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
name="hint" name="hint"
placeholder={handleHintPlaceholder()}
value={formdata.hint} value={formdata.hint}
onChange={(e) => handleChange(e)} onChange={(e) => handleChange(e)}
/> />
@@ -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>
);
}
@@ -1718,7 +1718,7 @@ function WidgetsValueModal(props) {
// focusNextWidget(currWidgetsDetails?.key, xyPosition) // focusNextWidget(currWidgetsDetails?.key, xyPosition)
//function too use on click on next/finish button then update modal UI according to current widgets //function too use on click on next/finish button then update modal UI according to current widgets
const handleClickOnNext = (isFinishDoc) => { const handleClickOnNext = async (isFinishDoc) => {
if ( if (
["signature", "stamp", "image", "initials"].includes( ["signature", "stamp", "image", "initials"].includes(
currWidgetsDetails?.type currWidgetsDetails?.type
@@ -1777,6 +1777,7 @@ function WidgetsValueModal(props) {
dispatch(setIsShowModal({ [nextWidgetDetails?.key]: true })); dispatch(setIsShowModal({ [nextWidgetDetails?.key]: true }));
props.setCurrWidgetsDetails(nextWidgetDetails); props.setCurrWidgetsDetails(nextWidgetDetails);
} }
}; };
//function is used to disable/enable save button //function is used to disable/enable save button
const handleDisable = () => { const handleDisable = () => {
@@ -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;
+183 -141
View File
@@ -116,6 +116,19 @@ export const getSecureUrl = async (url) => {
} }
}; };
// `generateId` generates a random alphanumeric ID of specified length.
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;
}
/** /**
* Removes a trailing path segment from a URL string, if present. * Removes a trailing path segment from a URL string, if present.
* *
@@ -447,14 +460,15 @@ export const addWidgetOptions = (
widgetValue widgetValue
) => { ) => {
let defaultOpt; let defaultOpt;
const id = generateId(6);
if (placeholder) { if (placeholder) {
const countSameWidget = placeholder?.reduce((count, page) => { const countSameWidget = placeholder?.reduce((count, page) => {
return count + page.pos.filter((item) => item.type === type).length; return count + page.pos.filter((item) => item.type === type).length;
}, 0); }, 0);
const count = countSameWidget + 1; const count = countSameWidget + 1;
defaultOpt = { name: `${type}-${count}`, status: "required" }; defaultOpt = { name: `${type}-${id}-${count}`, status: "required" };
} else { } else {
defaultOpt = { name: `${type}-1`, status: "required" }; defaultOpt = { name: `${type}-${id}-1`, status: "required" };
} }
switch (type) { switch (type) {
case "signature": case "signature":
@@ -464,7 +478,10 @@ export const addWidgetOptions = (
case "checkbox": case "checkbox":
return { ...defaultOpt, isReadOnly: false, isHideLabel: false }; return { ...defaultOpt, isReadOnly: false, isHideLabel: false };
case textInputWidget: case textInputWidget:
return { ...defaultOpt, isReadOnly: false }; return {
...defaultOpt,
isReadOnly: false
};
case cellsWidget: case cellsWidget:
return { return {
...defaultOpt, ...defaultOpt,
@@ -476,11 +493,20 @@ export const addWidgetOptions = (
case "initials": case "initials":
return defaultOpt; return defaultOpt;
case "name": case "name":
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" }; return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "company": case "company":
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" }; return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "job title": case "job title":
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" }; return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "date": { case "date": {
const dateFormat = signer?.DateFormat const dateFormat = signer?.DateFormat
? selectFormat(signer?.DateFormat) ? selectFormat(signer?.DateFormat)
@@ -505,8 +531,7 @@ export const addWidgetOptions = (
return { return {
...defaultOpt, ...defaultOpt,
values: [], values: [],
isReadOnly: false, isReadOnly: false
isHideLabel: false
}; };
case textWidget: case textWidget:
return defaultOpt; return defaultOpt;
@@ -523,14 +548,15 @@ export const addWidgetSelfsignOptions = (
) => { ) => {
let defaultOpt; let defaultOpt;
//condition to handle widgets name field //condition to handle widgets name field
const id = generateId(6);
if (placeholder) { if (placeholder) {
const countSameWidget = placeholder?.reduce((count, page) => { const countSameWidget = placeholder?.reduce((count, page) => {
return count + page.pos.filter((item) => item.type === type).length; return count + page.pos.filter((item) => item.type === type).length;
}, 0); }, 0);
const count = countSameWidget + 1; const count = countSameWidget + 1;
defaultOpt = { name: `${type}-${count}`, status: "required" }; defaultOpt = { name: `${type}-${id}-${count}`, status: "required" };
} else { } else {
defaultOpt = { name: `${type}-1`, status: "required" }; defaultOpt = { name: `${type}-${id}-1`, status: "required" };
} }
switch (type) { switch (type) {
case "signature": case "signature":
@@ -2980,34 +3006,34 @@ export const saveLanguageInLocal = (i18n) => {
const detectedLanguage = i18n.language || "en"; const detectedLanguage = i18n.language || "en";
localStorage.setItem("i18nextLng", detectedLanguage); localStorage.setItem("i18nextLng", detectedLanguage);
}; };
//function to get default signatur eof current user from `contracts_Signature` class
// function to get default signature of current user from `contracts_Signature` class
export const getDefaultSignature = async (objectId) => { export const getDefaultSignature = async (objectId) => {
try { try {
const query = new Parse.Query("contracts_Signature"); if (objectId) {
query.equalTo("UserId", { const result = await Parse.Cloud.run("getdefaultsignature", {
__type: "Pointer", userId: objectId
className: "_User", });
objectId: objectId if (result) {
}); const res = JSON.parse(JSON.stringify(result));
const defaultSignature = res?.ImageURL
? await getBase64FromUrl(res?.ImageURL, true)
: "";
const defaultInitial = res?.Initials
? await getBase64FromUrl(res?.Initials, true)
: "";
const result = await query.first(); return {
if (result) { status: "success",
const res = JSON.parse(JSON.stringify(result)); res: {
const defaultSignature = res?.ImageURL id: result?.id,
? await getBase64FromUrl(res?.ImageURL, true) defaultSignature: defaultSignature,
: ""; defaultInitial: defaultInitial
const defaultInitial = res?.Initials }
? await getBase64FromUrl(res?.Initials, true) };
: ""; }
} else {
return { return { status: "error" };
status: "success",
res: {
id: result?.id,
defaultSignature: defaultSignature,
defaultInitial: defaultInitial
}
};
} }
} catch (err) { } catch (err) {
console.log( console.log(
@@ -3430,39 +3456,52 @@ 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 placeHolder = Array.isArray(item.placeHolder)
placeHolder: item?.placeHolder?.map((ph) => ({ ? [...item.placeHolder]
: [];
const sortedPlaceHolder = placeHolder
.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;
}); });
}; };
@@ -3862,25 +3901,17 @@ export const sendEmailToSigners = async (
} else { } else {
data = { SendMail: true }; data = { SendMail: true };
} }
try { const docUrl = `${localStorage.getItem("baseUrl")}classes/contracts_Document`;
await axios.put( await axios.put(`${docUrl}/${pdfDetails[0]?.objectId}`, data, {
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${ headers: {
pdfDetails[0]?.objectId "Content-Type": "application/json",
}`, "X-Parse-Application-Id": localStorage.getItem("parseAppId"),
data, "X-Parse-Session-Token": sessiontoken
{ }
headers: { });
"Content-Type": "application/json", } catch (error) {
"X-Parse-Application-Id": localStorage.getItem("parseAppId"), const err = error?.response?.data?.error || error?.message;
"X-Parse-Session-Token": sessiontoken console.error("Error while updating doc: ", err);
}
}
);
} catch (err) {
console.log("axois err ", err);
}
} catch (e) {
console.log("error", e);
} }
} }
return { status: "success" }; return { status: "success" };
@@ -3953,74 +3984,68 @@ export const getSignerEmail = (data, signers) => {
//function is used to delete widgets //function is used to delete widgets
export const handleDeleteWidget = (key, Id, pageNumber, signerPos) => { export const handleDeleteWidget = (key, Id, pageNumber, signerPos) => {
const updateData = []; // Find the signer by ID
const signer = signerPos.find((item) => item.Id === Id);
if (!signer || !signer.placeHolder) return signerPos;
const filterSignerPos = signerPos.filter((data) => data.Id === Id); const placeHolder = signer.placeHolder;
if (filterSignerPos.length > 0) { // Find the page inside placeHolder
const getPlaceHolder = filterSignerPos[0].placeHolder; const currentPage = placeHolder.find((p) => p.pageNumber === pageNumber);
if (!currentPage) return signerPos;
const getPageNumer = getPlaceHolder.filter( // Remove widget from the selected page
(data) => data.pageNumber === pageNumber const updatedPos = currentPage.pos.filter((w) => w.key !== key);
// -------------------------------------------------------
// CASE A → After deletion, page still has widgets then keep that widgets
// -------------------------------------------------------
if (updatedPos.length > 0) {
const updatedPlaceHolder = placeHolder.map((p) =>
p.pageNumber === pageNumber ? { ...p, pos: updatedPos } : p
); );
if (getPageNumer.length > 0) { return signerPos.map((s) =>
const getXYdata = getPageNumer[0].pos.filter((data) => data.key !== key); s.Id === Id ? { ...s, placeHolder: updatedPlaceHolder } : s
);
if (getXYdata.length > 0) {
updateData.push(getXYdata);
const newUpdatePos = getPlaceHolder.map((obj) => {
if (obj.pageNumber === pageNumber) {
return { ...obj, pos: updateData[0] };
}
return obj;
});
const newUpdateSigner = signerPos.map((obj) => {
if (obj.Id === Id) {
return { ...obj, placeHolder: newUpdatePos };
}
return obj;
});
return newUpdateSigner;
} else {
const getRemainPage = filterSignerPos[0].placeHolder.filter(
(data) => data.pageNumber !== pageNumber
);
//condition to check placeholder length is greater than 1 do not need to remove whole placeholder
//array only resove particular widgets
if (getRemainPage && getRemainPage.length > 0) {
const newUpdatePos = filterSignerPos.map((obj) => {
if (obj.Id === Id) {
return { ...obj, placeHolder: getRemainPage };
}
return obj;
});
let signerupdate = [];
signerupdate = signerPos.filter((data) => data.Id !== Id);
signerupdate.push(newUpdatePos[0]);
return signerupdate;
} else {
const updatedData = signerPos
.map((item) => {
if (item.Id === Id && item?.Role === "prefill") {
return null; // mark this item to remove
}
if (item.Id === Id) {
const updatedItem = { ...item };
delete updatedItem.placeHolder;
return updatedItem;
}
return item;
})
.filter(Boolean); // remove nulls (deleted items)
return updatedData;
}
}
}
} }
// -------------------------------------------------------
// CASE B → current page becomes empty → remove current page entirely and keep another pages which have widgets
// -------------------------------------------------------
const remainPages = placeHolder.filter((p) => p.pageNumber !== pageNumber);
//`remainPages` keep another pages's data which have widgets
if (remainPages.length > 0) {
const index = signerPos.findIndex((s) => s.Id === Id);
if (index === -1) return signerPos;
const newSignerPos = [...signerPos];
newSignerPos[index] = {
...newSignerPos[index],
placeHolder: remainPages
};
return newSignerPos;
}
// -------------------------------------------------------
// CASE C → No pages have widgets after delete widgets
// Remove placeHolder field for particular signers, EXCEPT prefill role
// -------------------------------------------------------
const updatedData = signerPos
.map((s) => {
if (s.Id === Id && s.Role === "prefill") return null; // remove whole signer
if (s.Id === Id) {
const updated = { ...s };
delete updated.placeHolder;
return updated;
}
return s;
})
.filter(Boolean); // remove nulls (deleted items)
return updatedData;
}; };
//function to convert formatted date to new Date() format //function to convert formatted date to new Date() format
export const getDefaultDate = (dateStr, format) => { export const getDefaultDate = (dateStr, format) => {
@@ -4038,3 +4063,20 @@ 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";
//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 };
};
+20
View File
@@ -533,3 +533,23 @@ export default function reportJson(id) {
return null; return null;
} }
} }
export const usersActions = [
{
btnId: "4741",
hoverLabel: "Delete",
btnColor: "op-btn-secondary",
btnIcon: "fa-light fa-trash",
redirectUrl: "",
action: "delete",
restrictAdmin: true
},
{
btnId: "1910",
hoverLabel: "Reset password",
btnIcon: "fa-light fa-key",
redirectUrl: "",
action: "resetpassword",
restrictAdmin: true
}
];
+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"
+15 -11
View File
@@ -314,8 +314,8 @@ const Forms = (props) => {
"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);
} }
@@ -342,6 +342,7 @@ const Forms = (props) => {
setSelectedFiles([]); setSelectedFiles([]);
} }
} catch (error) { } catch (error) {
console.error("error ", error);
if (error?.code === 209) { if (error?.code === 209) {
dispatch(sessionStatus(false)); dispatch(sessionStatus(false));
} else { } else {
@@ -388,13 +389,16 @@ const Forms = (props) => {
object.set("Description", formData?.Description); object.set("Description", formData?.Description);
object.set("Note", formData?.Note); object.set("Note", formData?.Note);
if (props.title === "Request Signatures") { if (props.title === "Request Signatures") {
if ( if (
extUserData?.TenantId?.RequestBody && extUserData?.TenantId?.RequestBody &&
extUserData?.TenantId?.RequestSubject extUserData?.TenantId?.RequestSubject
) { ) {
object.set("RequestBody", extUserData?.TenantId?.RequestBody); object.set("RequestBody", extUserData?.TenantId?.RequestBody);
object.set("RequestSubject", extUserData?.TenantId?.RequestSubject); object.set(
} "RequestSubject",
extUserData?.TenantId?.RequestSubject
);
}
} }
if (props.title !== "Sign Yourself") { if (props.title !== "Sign Yourself") {
const isChecked = formData.SendinOrder === "false" ? false : true; const isChecked = formData.SendinOrder === "false" ? false : true;
@@ -582,8 +586,8 @@ const Forms = (props) => {
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);
} }
+1 -1
View File
@@ -463,7 +463,7 @@ function GuestLogin() {
e.preventDefault(); e.preventDefault();
setIsOptionalDetails(!isOptionalDetails); setIsOptionalDetails(!isOptionalDetails);
}} }}
className="op-link op-link-secondary max-w-fit text-xs" className="text-base-content/60 no-underline hover:underline focus:outline-none max-w-fit text-xs"
> >
{isOptionalDetails {isOptionalDetails
? t("hide-optional-details") ? t("hide-optional-details")
+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);
+20 -42
View File
@@ -46,10 +46,9 @@ const ManageSign = () => {
objectId: User.id objectId: User.id
}; };
try { try {
const signCls = "contracts_Signature"; const signRes = await Parse.Cloud.run("getdefaultsignature", {
const signQuery = new Parse.Query(signCls); userId: User.id
signQuery.equalTo("UserId", userId); });
const signRes = await signQuery.first();
if (signRes) { if (signRes) {
const res = signRes.toJSON(); const res = signRes.toJSON();
setId(res.objectId); setId(res.objectId);
@@ -226,44 +225,23 @@ const ManageSign = () => {
}; };
const saveEntry = async (obj) => { const saveEntry = async (obj) => {
const signCls = "contracts_Signature"; try {
const User = Parse?.User?.current()?.id; const User = Parse?.User?.current()?.id;
const userId = { __type: "Pointer", className: "_User", objectId: User }; const res = await Parse.Cloud.run("managesign", {
if (id) { signature: obj.url,
try { userId: User,
const updateSign = new Parse.Object(signCls); initials: obj.initialsUrl,
updateSign.id = id; id: id,
updateSign.set("Initials", obj.initialsUrl ? obj.initialsUrl : ""); title: obj.name
updateSign.set("ImageURL", obj.url ? obj.url : ""); });
updateSign.set("SignatureName", obj.name); setIsAlert({ type: "success", message: t("signature-saved-alert") });
updateSign.set("UserId", userId); return res;
const res = await updateSign.save(); } catch (err) {
setIsAlert({ type: "success", message: t("signature-saved-alert") }); console.log(err);
return res; setIsAlert({ type: "danger", message: `${err.message}` });
} catch (err) { } finally {
console.log(err); setIsLoader(false);
setIsAlert({ type: "danger", message: `${err.message}` }); setTimeout(() => setIsAlert({}), 2000);
} finally {
setIsLoader(false);
setTimeout(() => setIsAlert({}), 2000);
}
} else {
try {
const updateSign = new Parse.Object(signCls);
updateSign.set("Initials", obj.initialsUrl ? obj.initialsUrl : "");
updateSign.set("ImageURL", obj.url);
updateSign.set("SignatureName", obj.name);
updateSign.set("UserId", userId);
const res = await updateSign.save();
setIsAlert({ type: "success", message: t("signature-saved-alert") });
return res;
} catch (err) {
console.log(err);
setIsAlert({ type: "success", message: `${err.message}` });
} finally {
setIsLoader(false);
setTimeout(() => setIsAlert({}), 2000);
}
} }
}; };
+68 -22
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 ||
@@ -414,6 +409,35 @@ function PdfRequestFiles(
} else { } else {
const obj = documentData?.[0]; const obj = documentData?.[0];
setSendInOrder(obj?.SendinOrder || false); setSendInOrder(obj?.SendinOrder || false);
if (
obj?.Signers?.length &&
obj?.Placeholders?.length &&
currUserId !== "undefined"
) {
const params = {
event: "viewed",
contactId: currUserId,
body: {
objectId: documentData?.[0].objectId,
}
};
try {
await axios.post(
`${localStorage.getItem("baseUrl")}functions/triggerevent`,
params,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id":
localStorage.getItem("parseAppId"),
sessiontoken: localStorage.getItem("accesstoken")
}
}
);
} catch (err) {
console.log("Err ", err);
}
}
} }
let signers = []; let signers = [];
@@ -734,7 +758,7 @@ function PdfRequestFiles(
docId, docId,
contactId, contactId,
objectId, objectId,
widgets widgets,
); );
if (resSign && resSign.status === "success") { if (resSign && resSign.status === "success") {
dispatch(setTypedSignFont("Fasthand")); dispatch(setTypedSignFont("Fasthand"));
@@ -770,12 +794,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 +1127,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 +1517,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 +1562,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 +1608,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 +1619,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 +2078,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 +2115,8 @@ function PdfRequestFiles(
setIsReqSignTourDisabled={handleCloseTour} setIsReqSignTourDisabled={handleCloseTour}
currWidgetsDetails={currWidgetsDetails} currWidgetsDetails={currWidgetsDetails}
isShowModal={isShowModal} isShowModal={isShowModal}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
/> />
)} )}
</div> </div>
+32 -12
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),
@@ -1602,7 +1617,7 @@ function PlaceHolderSign() {
{}, {},
fontSize: textSize || 12, fontSize: textSize || 12,
fontColor: textColor || "black", fontColor: textColor || "black",
isReadOnly: defaultdata?.isReadOnly || false isReadOnly: defaultdata?.isReadOnly || false,
} }
}; };
} else if (position.type === cellsWidget) { } else if (position.type === cellsWidget) {
@@ -1622,7 +1637,7 @@ function PlaceHolderSign() {
{}, {},
fontSize: textSize || 12, fontSize: textSize || 12,
fontColor: textColor || "black", fontColor: textColor || "black",
isReadOnly: defaultdata?.isReadOnly || false isReadOnly: defaultdata?.isReadOnly || false,
} }
}; };
} }
@@ -1645,7 +1660,7 @@ function PlaceHolderSign() {
defaultValue: defaultdata.defaultValue, defaultValue: defaultdata.defaultValue,
hint: defaultdata?.hint || "", hint: defaultdata?.hint || "",
fontSize: textSize || 12, fontSize: textSize || 12,
fontColor: textColor || "black" fontColor: textColor || "black",
} }
}; };
} }
@@ -1728,11 +1743,14 @@ function PlaceHolderSign() {
} }
}; };
const handleLinkUser = (id) => { const handleLinkUser = (id) => {
setRoleName("");
setIsAddUser({ [id]: true }); setIsAddUser({ [id]: true });
}; };
//`handleAddUser` function to use add new user //`handleAddUser` function to use add new user
const handleAddUser = (data, signerObjId) => { const handleAddUser = (data, signerObjId) => {
const id = signerObjId ? signerObjId : uniqueId; const id = signerObjId ? signerObjId : uniqueId;
setRoleName("");
setCurrWidgetsDetails({});
if (isAddSigner) { if (isAddSigner) {
handleAddNewRecipients(data); handleAddNewRecipients(data);
} else { } else {
@@ -1931,6 +1949,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 +2351,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 +2386,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 -14
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();
@@ -143,13 +144,7 @@ function SignYourSelf() {
const [isUploadPdf, setIsUploadPdf] = useState(false); const [isUploadPdf, setIsUploadPdf] = useState(false);
const [unSignedWidgetId, setUnSignedWidgetId] = useState(""); const [unSignedWidgetId, setUnSignedWidgetId] = useState("");
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 +392,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 +468,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 +1400,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 +1438,8 @@ function SignYourSelf() {
currWidgetsDetails={currWidgetsDetails} currWidgetsDetails={currWidgetsDetails}
isShowModal={isShowModal} isShowModal={isShowModal}
unSignedWidgetId={unSignedWidgetId} unSignedWidgetId={unSignedWidgetId}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
/> />
)} )}
</div> </div>
+50 -36
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("");
@@ -166,6 +160,7 @@ const TemplatePlaceholder = () => {
const [documentDetails, setDocumentDetails] = useState(); const [documentDetails, setDocumentDetails] = useState();
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [isUseTemplate, setIsUseTemplate] = useState(false); const [isUseTemplate, setIsUseTemplate] = useState(false);
const [prevRole, setPrevRole] = useState("");
const currentUser = localStorage.getItem( const currentUser = localStorage.getItem(
`Parse/${localStorage.getItem("parseAppId")}/currentUser` `Parse/${localStorage.getItem("parseAppId")}/currentUser`
); );
@@ -215,28 +210,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 +486,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 +763,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);
@@ -1105,6 +1118,7 @@ const TemplatePlaceholder = () => {
// `handleAddSigner` is used to open Add Role Modal // `handleAddSigner` is used to open Add Role Modal
const handleAddSigner = () => { const handleAddSigner = () => {
setIsModalRole(true); setIsModalRole(true);
setPrevRole(roleName || "");
setRoleName(""); setRoleName("");
}; };
@@ -1132,6 +1146,7 @@ const TemplatePlaceholder = () => {
setSignerPos((prev) => [...prev, signerPosObj]); setSignerPos((prev) => [...prev, signerPosObj]);
setIsModalRole(false); setIsModalRole(false);
setRoleName(""); setRoleName("");
setCurrWidgetsDetails({});
setUniqueId(Id); setUniqueId(Id);
setBlockColor(color[index]); setBlockColor(color[index]);
setIsSelectId(index); setIsSelectId(index);
@@ -1293,6 +1308,7 @@ const TemplatePlaceholder = () => {
}; };
const handleCloseRoleModal = () => { const handleCloseRoleModal = () => {
setRoleName(prevRole || "");
setIsModalRole(false); setIsModalRole(false);
}; };
const handleSaveWidgetsOptions = ( const handleSaveWidgetsOptions = (
@@ -1510,7 +1526,7 @@ const TemplatePlaceholder = () => {
defaultValue: defaultdata.defaultValue, defaultValue: defaultdata.defaultValue,
hint: defaultdata?.hint || "", hint: defaultdata?.hint || "",
fontSize: textSize || 12, fontSize: textSize || 12,
fontColor: textColor || "black" fontColor: textColor || "black",
} }
}; };
} }
@@ -1926,7 +1942,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 +1978,8 @@ const TemplatePlaceholder = () => {
currWidgetsDetails={currWidgetsDetails} currWidgetsDetails={currWidgetsDetails}
setRoleName={setRoleName} setRoleName={setRoleName}
isShowModal={isShowModal} isShowModal={isShowModal}
signBtnPosition={signBtnPosition}
addPositionOfSignature={addPositionOfSignature}
/> />
)} )}
</div> </div>
@@ -2039,9 +2056,6 @@ const TemplatePlaceholder = () => {
initial={true} initial={true}
isTemplateFlow={true} isTemplateFlow={true}
roleName={roleName} roleName={roleName}
Add
commentMore
actions
isPrefillDropdown={true} isPrefillDropdown={true}
/> />
</div> </div>
+28 -50
View File
@@ -13,26 +13,8 @@ import {
import DeleteUserModal from "../primitives/DeleteUserModal"; import DeleteUserModal from "../primitives/DeleteUserModal";
import axios from "axios"; import axios from "axios";
import PasswordResetModal from "../primitives/PasswordResetModal"; import PasswordResetModal from "../primitives/PasswordResetModal";
import { usersActions } from "../json/ReportJson";
const actions = [
{
btnId: "4741",
hoverLabel: "Delete",
btnColor: "op-btn-secondary",
btnIcon: "fa-light fa-trash",
redirectUrl: "",
action: "delete",
restrictAdmin: true
},
{
btnId: "1910",
hoverLabel: "Reset password",
btnIcon: "fa-light fa-key",
redirectUrl: "",
action: "resetpassword",
restrictAdmin: true
},
];
const heading = ["Sr.No", "Name", "Email", "Phone", "Role", "Team", "Active"]; const heading = ["Sr.No", "Name", "Email", "Phone", "Role", "Team", "Active"];
const UserList = () => { const UserList = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -342,7 +324,7 @@ const UserList = () => {
{t(`report-heading.${item}`)} {t(`report-heading.${item}`)}
</th> </th>
))} ))}
{actions?.length > 0 && ( {usersActions?.length > 0 && (
<th className="p-2 text-transparent pointer-events-none"> <th className="p-2 text-transparent pointer-events-none">
{t("action")} {t("action")}
</th> </th>
@@ -375,10 +357,14 @@ const UserList = () => {
</td> </td>
{handleActiveToggleVisibility(item) ? ( {handleActiveToggleVisibility(item) ? (
<td className="px-4 py-2 font-semibold"> <td className="px-4 py-2 font-semibold">
<label className="cursor-pointer relative block items-center mb-0"> <label
htmlFor={`isdisabled-${item.objectId}`}
className="cursor-pointer relative block items-center mb-0"
>
<input <input
id={`isdisabled-${item.objectId}`}
type="checkbox" type="checkbox"
className="op-toggle transition-all op-toggle-secondary" className="op-toggle checked:[--tglbg:#3368ff] transition-all checked:text-white"
checked={item?.IsDisabled !== true} checked={item?.IsDisabled !== true}
onChange={() => handleToggleBtn(item)} onChange={() => handleToggleBtn(item)}
/> />
@@ -425,8 +411,8 @@ const UserList = () => {
{isAdmin && ( {isAdmin && (
<td className="px-3 py-2"> <td className="px-3 py-2">
<div className="text-base-content min-w-max flex flex-row gap-x-2 gap-y-1 justify-start items-center"> <div className="text-base-content min-w-max flex flex-row gap-x-2 gap-y-1 justify-start items-center">
{actions?.length > 0 && {usersActions?.length > 0 &&
actions.map((act, index) => ( usersActions?.map((act, index) => (
<React.Fragment key={index}> <React.Fragment key={index}>
{handleBtnVisibility(act, item) && ( {handleBtnVisibility(act, item) && (
<div <div
@@ -454,37 +440,29 @@ const UserList = () => {
)} )}
</div> </div>
)} )}
<DeleteUserModal
title={t("delete-account")}
deleting={deleting}
userEmail={item?.Email}
isOpen={
isActModal[
"delete_" + item.objectId
]
}
onConfirm={() =>
handleDeleteAccount(item)
}
deleteRes={deleteUserRes}
handleClose={handleCloseModal}
/>
<PasswordResetModal
isOpen={
isActModal[
"resetpassword_" + item.objectId
]
}
userId={item?.UserId?.objectId}
onClose={handleCloseModal}
onSubmit={submitPassword}
showAlert={showAlert}
/>
</React.Fragment> </React.Fragment>
))} ))}
</div> </div>
</td> </td>
)} )}
<DeleteUserModal
title={t("delete-account")}
deleting={deleting}
userEmail={item?.Email}
isOpen={isActModal["delete_" + item.objectId]}
onConfirm={() => handleDeleteAccount(item)}
deleteRes={deleteUserRes}
handleClose={handleCloseModal}
/>
<PasswordResetModal
isOpen={
isActModal["resetpassword_" + item.objectId]
}
userId={item?.UserId?.objectId}
onClose={handleCloseModal}
onSubmit={submitPassword}
showAlert={showAlert}
/>
</tr> </tr>
))} ))}
</tbody> </tbody>
+2 -2
View File
@@ -172,8 +172,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);
+2 -2
View File
@@ -139,7 +139,7 @@ const AddContact = (props) => {
)} )}
<div className="w-full mx-auto p-[8px]"> <div className="w-full mx-auto p-[8px]">
{!props?.isDisableTitle && ( {!props?.isDisableTitle && (
<div className="text-[14px] font-[700]">{t("add-contact")}</div> <div className="text-[14px] font-[700] text-base-content mb-1">{t("add-contact")}</div>
)} )}
{isUserExist && props?.isAddYourSelfCheckbox && ( {isUserExist && props?.isAddYourSelfCheckbox && (
<div className="mb-[0.75rem] flex items-center mt-1"> <div className="mb-[0.75rem] flex items-center mt-1">
@@ -248,7 +248,7 @@ const AddContact = (props) => {
e.preventDefault(); e.preventDefault();
setIsOptionalDetails(!isOptionalDetails); setIsOptionalDetails(!isOptionalDetails);
}} }}
className="op-link op-link-secondary no-underline hover:underline" className="text-base-content/60 no-underline hover:underline focus:outline-none"
> >
{isOptionalDetails {isOptionalDetails
? t("hide-optional-details") ? t("hide-optional-details")
@@ -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":
+1 -1
View File
@@ -158,7 +158,7 @@ const SignerCell = ({ reportName, item, handleRemovePrefill }) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{item.Placeholders.map( {item?.Placeholders?.map(
(x, i) => (x, i) =>
x.Role !== "prefill" && ( x.Role !== "prefill" && (
<tr key={i} className="text-sm font-medium"> <tr key={i} className="text-sm font-medium">
@@ -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);
@@ -237,8 +238,8 @@ const TemplatesReport = (props) => {
navigate(`/${act.redirectUrl}/${item.objectId}`); navigate(`/${act.redirectUrl}/${item.objectId}`);
} else { } else {
// handle Use template // handle Use template
const placeholder = item?.Placeholders; const placeholder = item?.Placeholders || [];
const isRoleExist = placeholder.filter((x) => x.Role !== "prefill"); const isRoleExist = placeholder?.filter((x) => x.Role !== "prefill");
//condition to check atleast one role is present for use template //condition to check atleast one role is present for use template
if (isRoleExist && isRoleExist?.length > 0) { if (isRoleExist && isRoleExist?.length > 0) {
const checkIsSignatureExist = isRoleExist?.every((placeholderObj) => const checkIsSignatureExist = isRoleExist?.every((placeholderObj) =>
@@ -345,9 +346,13 @@ const TemplatesReport = (props) => {
setIsShareWith({ [item.objectId]: true }); setIsShareWith({ [item.objectId]: true });
} }
else if (act.action === "duplicate") { else if (act.action === "duplicate") {
const isPrefill = item?.Placeholders?.some((p) => p?.Role === "prefill"); const hasDuplicate = utils.hasDuplicateWidgetNames(item?.Placeholders);
setError(isPrefill ? t("duplicate-template-error") : ""); if (hasDuplicate) {
setIsModal({ [`duplicate_${item.objectId}`]: true }); setError(t("duplicate-template-widget-error"));
setIsModal({ [`duplicate_${item.objectId}`]: true });
} else {
setIsModal({ [`duplicate_${item.objectId}`]: true });
}
} else if (act.action === "rename") { } else if (act.action === "rename") {
setIsModal({ [`rename_${item.objectId}`]: true }); setIsModal({ [`rename_${item.objectId}`]: true });
} else if (act.action === "edit") { } else if (act.action === "edit") {
@@ -726,7 +731,7 @@ const TemplatesReport = (props) => {
// `handleBulkSend` is used to open modal as well as fetch template // `handleBulkSend` is used to open modal as well as fetch template
// and show Ui on the basis template response handleBulkSendTemplate // and show Ui on the basis template response handleBulkSendTemplate
const handleBulkSend = async (template) => { const handleBulkSend = async (template) => {
const isPrefillExist = template?.Placeholders.some( const isPrefillExist = template?.Placeholders?.some(
(x) => x.Role === "prefill" (x) => x.Role === "prefill"
); );
if (isPrefillExist) { if (isPrefillExist) {
@@ -755,6 +760,7 @@ const TemplatesReport = (props) => {
} }
}; };
// `handleShareWith` is used to save teams in sharedWith field // `handleShareWith` is used to save teams in sharedWith field
const handleShareWith = async (e, template) => { const handleShareWith = async (e, template) => {
e.preventDefault(); e.preventDefault();
@@ -919,29 +925,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 +1009,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 +1149,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 +1685,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>
); );
+44 -9
View File
@@ -17,21 +17,20 @@ export const saveToMySign = async (widget) => {
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 // below code is used to save or update default signature, initials, stamp
const signCls = new Parse.Object("contracts_Signature"); const params = {
if (widget?.defaultSignId) { id: widget?.defaultSignId, // pass id to update existing signature/initials
signCls.id = widget.defaultSignId; userId: User.id
} };
if (widget?.type === "initials") { if (widget?.type === "initials") {
signCls.set("Initials", fileUrl); params.initials = fileUrl; // save initials image url
} else if (widget?.type === "signature") { } else if (widget?.type === "signature") {
signCls.set("ImageURL", fileUrl); params.signature = fileUrl; // save signature image url
} }
signCls.set("UserId", Parse.User.createWithoutData(User.id)); const signRes = await Parse.Cloud.run("savesignature", params);
const signRes = await signCls.save();
return { base64File: base64, id: signRes?.id }; return { base64File: base64, id: signRes?.id };
} catch (err) { } catch (err) {
console.log("Err while saving signature", err); console.log("Err while saving signature", err);
return url; return err;
} }
} }
}; };
@@ -42,6 +41,42 @@ export const hasSignatureWidget = (s) =>
(ph?.pos ?? []).some((p) => p?.type === "signature") (ph?.pos ?? []).some((p) => p?.type === "signature")
); );
export const widgetNamesArr = (placeholders, signerId) => {
const widgetNames =
placeholders?.length > 0
? placeholders
?.filter((x) => x.Id !== signerId) // exclude current signer
?.flatMap((item) => item.placeHolder || []) // go inside placeHolder[]
?.flatMap((ph) => ph.pos || []) // go inside pos[]
?.map((pos) => pos.options?.name) // pick the name
.filter(Boolean)
: [];
return widgetNames;
};
// `hasDuplicateWidgetNames` checks for duplicate widget names across different placeholder objects
export const hasDuplicateWidgetNames = (placeholders = []) => {
const seen = new Set(); // names we've already seen in previous objs
for (const obj of placeholders || []) {
const widgetNames =
obj?.placeHolder
?.flatMap((ph) => ph.pos || [])
?.map((pos) => pos.options?.name)
.filter(Boolean) || [];
const uniqueNames = [...new Set(widgetNames)]; // ignore duplicates inside same obj
const isDuplicate = uniqueNames.some((name) => seen.has(name));
if (isDuplicate) return true; // early exit on first cross-obj duplicate
uniqueNames.forEach((name) => seen.add(name));
}
return false;
};
/** /**
* *
* @param {Array<object>} pages * @param {Array<object>} pages
-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,
@@ -13,10 +13,13 @@ async function convertLibre(input, ext, opts) {
} }
}); });
} }
// -------------------- Concurrency limiter (no dependency) --------------------
const MAX_CONCURRENCY = Number(process.env.DOCX2PDF_CONCURRENCY || 2); // 1 for tiny droplets // -------------------- Concurrency limiter --------------------
// CRITICAL FIX: Reduced to 1 for CPU-intensive LibreOffice conversions
const MAX_CONCURRENCY = Number(process.env.DOCX2PDF_CONCURRENCY || 1);
let active = 0; let active = 0;
const queue = []; const queue = [];
function runWithLimit(task) { function runWithLimit(task) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const run = () => { const run = () => {
@@ -34,7 +37,7 @@ function runWithLimit(task) {
}); });
} }
// -------------------- Timeout helper (async version = no TS hint) -------------------- // -------------------- Timeout helper --------------------
/** /**
* @template T * @template T
* @param {Promise<T>} promise * @param {Promise<T>} promise
@@ -58,6 +61,7 @@ export async function withTimeout(promise, ms, label = 'operation') {
const storage = multer.memoryStorage(); const storage = multer.memoryStorage();
export const upload = multer({ export const upload = multer({
storage, storage,
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB hard limit at multer level
fileFilter: (req, file, cb) => { fileFilter: (req, file, cb) => {
const okExt = /\.docx$/i.test(file.originalname || ''); const okExt = /\.docx$/i.test(file.originalname || '');
const okMime = const okMime =
@@ -109,14 +113,30 @@ export default async function docxtopdf(req, res) {
return res.status(403).json({ error: 'Tenant not found for user.' }); return res.status(403).json({ error: 'Tenant not found for user.' });
} }
// ---- DOCX -> PDF (buffer -> buffer), with concurrency + timeout ---- // ---- DOCX -> PDF conversion with concurrency control and timeout ----
const fileName = `${generatePdfName(16)}.pdf`; const fileName = `${generatePdfName(16)}.pdf`;
const pdfBuffer = await runWithLimit(() => // FIX: Increased timeout to 90s for large files, added nice priority
withTimeout(convertLibre(req.file.buffer, '.pdf', undefined), 60_000, 'DOCX->PDF') const pdfBuffer = await runWithLimit(async () => {
); // Log for monitoring
console.log(`[DOCX2PDF] Starting conversion, active: ${active}, queued: ${queue.length}`);
// ---- Upload PDF (no disk IO) ---- const startTime = Date.now();
try {
const result = await withTimeout(
convertLibre(req.file.buffer, '.pdf', undefined),
90_000, // Increased from 60s to 90s
'DOCX->PDF'
);
console.log(`[DOCX2PDF] Completed in ${Date.now() - startTime}ms`);
return result;
} catch (error) {
console.error(`[DOCX2PDF] Failed after ${Date.now() - startTime}ms:`, error.message);
throw error;
}
});
// ---- Upload PDF ----
const activeFileAdapter = resUser.data.results[0]?.TenantId?.ActiveFileAdapter; const activeFileAdapter = resUser.data.results[0]?.TenantId?.ActiveFileAdapter;
let fileUrl; let fileUrl;
if (activeFileAdapter) { if (activeFileAdapter) {
@@ -140,12 +160,23 @@ export default async function docxtopdf(req, res) {
return res.status(200).json({ message: 'success.', url: fileUrl }); return res.status(200).json({ message: 'success.', url: fileUrl });
} catch (err) { } catch (err) {
const msg = // More specific error messages
let msg =
err?.response?.data?.error || err?.response?.data || err?.message || 'Something went wrong.'; err?.response?.data?.error || err?.response?.data || err?.message || 'Something went wrong.';
console.log(`Error in docxtopdf: ${msg}`);
// Friendly message to the client // Friendly message to the client
const message = const message =
'We are currently experiencing some issues with processing DOCX files. Please upload the PDF version or contact us on support@opensignlabs.com'; 'We are currently experiencing some issues with processing DOCX files. Please upload the PDF version or contact us on support@opensignlabs.com';
if (msg.includes('timed out')) {
msg =
'Document conversion is taking too long. Please try a smaller file or contact support@opensignlabs.com';
} else if (msg.includes('too large') || msg.includes('size')) {
msg =
'File is too large to process. Please reduce the file size or contact support@opensignlabs.com';
} else {
msg = message;
}
console.error(`[DOCX2PDF] Error: ${msg}`);
return res.status(400).json({ error: message }); return res.status(400).json({ error: message });
} }
} }
+10
View File
@@ -57,6 +57,11 @@ import addUser from './parsefunction/addUser.js';
import filterDocs from './parsefunction/filterDocs.js'; import filterDocs from './parsefunction/filterDocs.js';
import sendDeleteUserMail from './parsefunction/sendDeleteUserMail.js'; import sendDeleteUserMail from './parsefunction/sendDeleteUserMail.js';
import resetPassword from './parsefunction/resetPassword.js'; import resetPassword from './parsefunction/resetPassword.js';
import saveSignature from './parsefunction/saveSignature.js';
import manageSign from './parsefunction/manageSign.js';
import getSignature from './parsefunction/getSignature.js';
import updateEmailTemplates from './parsefunction/updateEmailTemplates.js';
import triggerEvent from './parsefunction/triggerEvent.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);
@@ -124,3 +129,8 @@ Parse.Cloud.define('adduser', addUser);
Parse.Cloud.define('filterdocs', filterDocs); Parse.Cloud.define('filterdocs', filterDocs);
Parse.Cloud.define('senddeleterequest', sendDeleteUserMail); Parse.Cloud.define('senddeleterequest', sendDeleteUserMail);
Parse.Cloud.define('resetpassword', resetPassword); Parse.Cloud.define('resetpassword', resetPassword);
Parse.Cloud.define('savesignature', saveSignature);
Parse.Cloud.define('managesign', manageSign);
Parse.Cloud.define('getdefaultsignature', getSignature);
Parse.Cloud.define('updateemailtemplates', updateEmailTemplates);
Parse.Cloud.define('triggerevent', triggerEvent);
@@ -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',
@@ -192,7 +192,6 @@ async function batchQuery(userId, Documents, Ip, parseConfig, type, publicUrl) {
}, },
}; };
}); });
// console.log('requests ', requests);
if (requests?.length > 0) { if (requests?.length > 0) {
const newrequests = [requests?.[0]]; const newrequests = [requests?.[0]];
const response = await axios.post('batch', { requests: newrequests }, parseConfig); const response = await axios.post('batch', { requests: newrequests }, parseConfig);
@@ -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 {
@@ -0,0 +1,18 @@
export default async function getSignature(request) {
const { userId } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
}
if (userId !== request.user?.id) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot save signature for the current user.');
}
try {
const query = new Parse.Query('contracts_Signature');
query.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
const result = await query.first({ useMasterKey: true });
return result;
} catch (err) {
console.error('Error fetching signature:', err);
throw err;
}
}
@@ -0,0 +1,28 @@
export default async function manageSign(request) {
const { signature, userId, initials, id, title } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
}
if (userId !== request.user?.id) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot save signature for the current user.');
}
const userPtr = { __type: 'Pointer', className: '_User', objectId: userId };
try {
const signatureCls = new Parse.Object('contracts_Signature');
if (id) {
signatureCls.id = id;
}
signatureCls.set('Initials', initials ? initials : '');
signatureCls.set('ImageURL', signature ? signature : '');
signatureCls.set('SignatureName', title ? title : '');
if (userPtr) {
signatureCls.set('UserId', userPtr);
}
const signRes = await signatureCls.save(null, { useMasterKey: true });
return signRes;
} catch (err) {
console.error('Error saving signature:', err);
throw err;
}
}
@@ -41,6 +41,7 @@ export default async function GenerateCertificate(docDetails) {
const maxX = width - margin - textWidth; // Ensures text stays inside the border with 30px margin const 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);
@@ -61,6 +61,7 @@ export default async function saveAsTemplate(request) {
status: 'required', status: 'required',
response: '', response: '',
...(p?.options?.defaultValue ? { defaultValue: '' } : {}), ...(p?.options?.defaultValue ? { defaultValue: '' } : {}),
...(p?.options?.isReadOnly ? { isReadOnly: false } : {}),
}, },
})), })),
})); }));
@@ -89,7 +90,14 @@ export default async function saveAsTemplate(request) {
pos: (page.pos || []).map(widget => { pos: (page.pos || []).map(widget => {
// if there is a defaultValue in options, zero it out // if there is a defaultValue in options, zero it out
if (widget.options && widget.options.defaultValue !== undefined) { if (widget.options && widget.options.defaultValue !== undefined) {
return { ...widget, options: { ...widget.options, defaultValue: '' } }; // reset only the value return {
...widget,
options: {
...widget.options,
defaultValue: '',
...(widget?.options?.isReadOnly ? { isReadOnly: false } : {}),
},
}; // reset only the value
} }
// otherwise, return the widget unchanged // otherwise, return the widget unchanged
return widget; return widget;
@@ -0,0 +1,33 @@
export default async function saveSignature(request) {
const { signature, userId, initials, id, title } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
}
if (userId !== request.user?.id) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot save signature for the current user.');
}
const userPtr = { __type: 'Pointer', className: '_User', objectId: userId };
try {
const signatureCls = new Parse.Object('contracts_Signature');
if (id) {
signatureCls.id = id;
}
if (initials) {
signatureCls.set('Initials', initials);
}
if (signature) {
signatureCls.set('ImageURL', signature);
}
if (title) {
signatureCls.set('SignatureName', title);
}
if (userPtr) {
signatureCls.set('UserId', userPtr);
}
const signRes = await signatureCls.save(null, { useMasterKey: true });
return signRes;
} catch (err) {
throw err;
}
}
@@ -159,7 +159,8 @@ export default async function sendMailGmailProvider(_extRes, template) {
template; 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);
@@ -12,15 +12,23 @@ async function sendMailProvider(req, plan, monthchange) {
let mailgunClient; let mailgunClient;
let mailgunDomain; let mailgunDomain;
if (smtpenable) { if (smtpenable) {
transporterSMTP = createTransport({ let transporterConfig = {
host: process.env.SMTP_HOST, host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT || 465, port: process.env.SMTP_PORT || 465,
secure: smtpsecure, secure: smtpsecure,
auth: { };
// ✅ Add auth only if BOTH username & password exist
const smtpUser = process.env.SMTP_USERNAME;
const smtpPass = process.env.SMTP_PASS;
if (smtpUser && smtpPass) {
transporterConfig.auth = {
user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL, user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL,
pass: process.env.SMTP_PASS, pass: smtpPass,
}, };
}); }
transporterSMTP = createTransport(transporterConfig);
} else { } else {
if (mailgunApiKey) { if (mailgunApiKey) {
const mailgun = new Mailgun(formData); const mailgun = new Mailgun(formData);
@@ -185,7 +193,7 @@ async function sendMailProvider(req, plan, monthchange) {
} }
} }
} catch (err) { } catch (err) {
console.log('err in sendmailv3', err); console.log(`Error in sendmailv3: ${err}`);
if (fs.existsSync(testPdf)) { if (fs.existsSync(testPdf)) {
try { try {
fs.unlinkSync(testPdf); fs.unlinkSync(testPdf);
@@ -236,7 +244,7 @@ async function sendMailProvider(req, plan, monthchange) {
} }
} }
} catch (err) { } catch (err) {
console.log('err in sendmailv3', err); console.log(`Error in sendmailv3: ${err}`);
if (err) { if (err) {
return { status: 'error' }; return { status: 'error' };
} }
@@ -0,0 +1,64 @@
import axios from 'axios';
import { cloudServerUrl, serverAppId } from '../../Utils.js';
export default async function triggerEvent(request) {
const event = request.params.event;
const body = request.params.body;
const docId = body.objectId;
const contactId = request.params.contactId;
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const appId = serverAppId;
try {
const docQuery = new Parse.Query('contracts_Document');
docQuery.select(['Name', 'IsEnableOTP', 'SignedUrl', 'AuditTrail']);
const docRes = await docQuery.get(docId, { useMasterKey: true });
const _docRes = docRes && docRes?.toJSON();
const isEnableOTP = docRes?.get('IsEnableOTP') || false;
let userId;
if (isEnableOTP) {
const userRes = await axios.get(serverUrl + '/users/me', {
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': request.headers['sessiontoken'],
},
});
userId = userRes.data && userRes.data.objectId;
if (!userId) {
return { message: 'User not found!' };
}
}
if (event === 'viewed' && contactId) {
const auditTrail = Array.isArray(_docRes.AuditTrail) ? _docRes.AuditTrail : [];
const isUserExist = auditTrail.some(x => x?.UserPtr?.objectId === contactId && x?.ViewedOn);
if (!isUserExist) {
const contactPtr = {
__type: 'Pointer',
className: 'contracts_Contactbook',
objectId: contactId,
};
const date = new Date().toISOString();
const newEntry = {
UserPtr: contactPtr,
SignedUrl: _docRes?.SignedUrl || '',
Activity: 'Viewed',
ipAddress: request.headers['x-real-ip'],
ViewedOn: date,
};
// update Audit trail entry
const updateDoc = new Parse.Object('contracts_Document');
updateDoc.id = docRes.id;
updateDoc.set('AuditTrail', [...auditTrail, newEntry]);
await updateDoc.save(null, { useMasterKey: true });
}
}
return { message: 'event called!' };
} catch (err) {
console.log(`Err in triggerEvent: ${err}`);
return { message: 'Something went wrong!' };
}
}
@@ -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);
}
} }
}); });
+17 -8
View File
@@ -17,7 +17,7 @@ import { exec } from 'child_process';
import { createTransport } from 'nodemailer'; import { createTransport } from 'nodemailer';
import { appName, cloudServerUrl, serverAppId, smtpenable, smtpsecure, useLocal } from './Utils.js'; import { appName, cloudServerUrl, serverAppId, smtpenable, smtpsecure, useLocal } from './Utils.js';
import { SSOAuth } from './auth/authadapter.js'; import { SSOAuth } from './auth/authadapter.js';
import createContactIndex from './migrationdb/createContactIndex.js'; import runDbMigrations from './migrationdb/index.js';
import { validateSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js'; import { validateSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
import maintenance_mode_message from 'aws-sdk/lib/maintenance_mode_message.js'; import maintenance_mode_message from 'aws-sdk/lib/maintenance_mode_message.js';
let fsAdapter; let fsAdapter;
@@ -61,20 +61,28 @@ let mailgunDomain;
let isMailAdapter = false; let isMailAdapter = false;
if (smtpenable) { if (smtpenable) {
try { try {
transporterMail = createTransport({ let transporterConfig = {
host: process.env.SMTP_HOST, host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT || 465, port: process.env.SMTP_PORT || 465,
secure: smtpsecure, secure: smtpsecure,
auth: { };
// ✅ Add auth only if BOTH username & password exist
const smtpUser = process.env.SMTP_USERNAME;
const smtpPass = process.env.SMTP_PASS;
if (smtpUser && smtpPass) {
transporterConfig.auth = {
user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL, user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL,
pass: process.env.SMTP_PASS, pass: smtpPass,
}, };
}); }
transporterMail = createTransport(transporterConfig);
await transporterMail.verify(); await transporterMail.verify();
isMailAdapter = true; isMailAdapter = true;
} catch (err) { } catch (err) {
isMailAdapter = false; isMailAdapter = false;
console.log('Please provide valid SMTP credentials'); console.log(`Please provide valid SMTP credentials: ${err}`);
} }
} else if (process.env.MAILGUN_API_KEY) { } else if (process.env.MAILGUN_API_KEY) {
try { try {
@@ -111,6 +119,7 @@ export const config = {
allowClientClassCreation: false, allowClientClassCreation: false,
allowExpiredAuthDataToken: false, allowExpiredAuthDataToken: false,
enableInsecureAuthAdapters: false, enableInsecureAuthAdapters: false,
databaseOptions: { allowPublicExplain: false },
encodeParseObjectInCloudFunction: true, encodeParseObjectInCloudFunction: true,
...(isMailAdapter === true ...(isMailAdapter === true
? { ? {
@@ -232,7 +241,7 @@ if (!process.env.TESTING) {
console.log('opensign-server running on port ' + port + '.'); console.log('opensign-server running on port ' + port + '.');
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
// console.log('isWindows', isWindows); // console.log('isWindows', isWindows);
createContactIndex(); runDbMigrations();
const migrate = isWindows const migrate = isWindows
? `set APPLICATION_ID=${serverAppId}&& set SERVER_URL=${cloudServerUrl}&& set MASTER_KEY=${process.env.MASTER_KEY}&& npx parse-dbtool migrate` ? `set APPLICATION_ID=${serverAppId}&& set SERVER_URL=${cloudServerUrl}&& set MASTER_KEY=${process.env.MASTER_KEY}&& npx parse-dbtool migrate`
: `APPLICATION_ID=${serverAppId} SERVER_URL=${cloudServerUrl} MASTER_KEY=${process.env.MASTER_KEY} npx parse-dbtool migrate`; : `APPLICATION_ID=${serverAppId} SERVER_URL=${cloudServerUrl} MASTER_KEY=${process.env.MASTER_KEY} npx parse-dbtool migrate`;
@@ -18,8 +18,7 @@ export default async function createContactIndex() {
const migrationExists = await migrationCollection.findOne({ name: migrationName }); const migrationExists = await migrationCollection.findOne({ name: migrationName });
if (migrationExists) { if (migrationExists) {
console.log(' INFO No migrations were executed, database schema was already up to date.'); console.log(' INFO The unqiue index for contracts_Contactbook is already present.');
console.log(' SUCCESS Successfully ran indexed migrations directly on db.');
return; return;
} }
@@ -61,10 +60,9 @@ export default async function createContactIndex() {
// Insert the document // Insert the document
await migrationdb.insertOne(schemaDocument); await migrationdb.insertOne(schemaDocument);
console.log(' Unique index created successfully.'); console.log(' SUCCESS The unqiue index for contracts_Contactbook is already created.');
console.log(' SUCCESS Successfully ran indexed migrations directly on db.');
} catch (error) { } catch (error) {
console.log(' ERROR running indexed migration:', error); console.log(' ERROR Running unqiue index for contracts_Contactbook migration:', error);
} finally { } finally {
await client.close(); await client.close();
} }
@@ -0,0 +1,59 @@
import dotenv from 'dotenv';
import { MongoClient } from 'mongodb';
import { generateId } from '../Utils.js';
dotenv.config({ quiet: true });
export default async function createDocumentIndex() {
// Provide the complete MongoDB connection URL with the database name
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/dev'; // Replace with your MongoDB URI
const client = new MongoClient(uri);
try {
await client.connect();
const database = client.db();
const migrationCollection = database.collection('Migrationdb');
const docMigration = 'documentIndex_1';
// Check if the migration has already been executed
const docMigrationExists = await migrationCollection.findOne({ name: docMigration });
if (docMigrationExists) {
console.log(' INFO The completed report index for contracts_document is already present.');
return;
}
const docCollection = database.collection('contracts_Document');
await docCollection.createIndex(
{ _p_CreatedBy: 1, _updated_at: -1 },
{
name: 'idx_docs_by_creator_recent_completed',
partialFilterExpression: { IsCompleted: true },
}
);
await docCollection.createIndex(
{ Signers: 1, _updated_at: -1 },
{
name: 'idx_docs_by_signer_recent_completed',
partialFilterExpression: { IsCompleted: true },
}
);
// Save the migration record in the migrationdb collection
await migrationCollection.insertOne({
_id: generateId(10),
name: docMigration,
_created_at: new Date(),
_updated_at: new Date(),
executedAt: new Date(),
details: 'Created unique index on CreatedBy, IsImported, Email',
});
console.log(' SUCCESS The completed report index for contracts_document is created.');
} catch (error) {
console.log(' ERROR Running completed report index for contracts_document migration:', error);
} finally {
await client.close();
}
}
+7
View File
@@ -0,0 +1,7 @@
import createContactIndex from './createContactIndex.js';
import createDocumentIndex from './createDocumentIndex.js';
export default async function runDbMigrations() {
await createContactIndex();
await createDocumentIndex();
}
-20215
View File
File diff suppressed because it is too large Load Diff
+26 -24
View File
@@ -1,6 +1,6 @@
{ {
"name": "open_sign_server", "name": "open_sign_server",
"version": "2.30.0", "version": "2.32.0",
"description": "opensign server using the parse-server module", "description": "opensign server using the parse-server module",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -18,58 +18,60 @@
"watch": "nodemon index.js" "watch": "nodemon index.js"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.901.0", "@aws-sdk/client-s3": "^3.943.0",
"@aws-sdk/s3-request-presigner": "^3.901.0", "@aws-sdk/s3-request-presigner": "^3.943.0",
"@parse/fs-files-adapter": "^3.0.0", "@parse/fs-files-adapter": "^3.0.0",
"@parse/push-adapter": "^8.0.2",
"@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",
"@signpdf/placeholder-pdf-lib": "^3.2.6", "@signpdf/placeholder-pdf-lib": "^3.2.6",
"@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.2.1",
"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": "^167.0.0",
"libreoffice-convert": "^1.7.0", "libreoffice-convert": "^1.7.0",
"mailgun.js": "^12.1.0", "mailgun.js": "^12.2.0",
"mongodb": "^6.20.0", "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.3",
"nodemailer": "^7.0.9", "nodemailer": "^7.0.11",
"p-limit": "^7.1.1", "p-limit": "^7.2.0",
"parse": "^6.1.1", "parse": "^7.1.2",
"parse-dbtool": "^1.2.0", "parse-dbtool": "^1.2.0",
"parse-server": "^8.2.5", "parse-server": "^8.5.0",
"parse-server-api-mail-adapter": "^4.1.0", "parse-server-api-mail-adapter": "^5.0.2",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"posthog-node": "^5.9.3", "posthog-node": "^5.14.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"rate-limiter-flexible": "^8.0.1", "rate-limiter-flexible": "^8.3.0",
"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.7.4"
}, },
"overrides": { "overrides": {
"ws": "$ws", "ws": "$ws",
"parse": "$parse", "parse": "$parse",
"form-data": "$form-data" "form-data": "$form-data",
"@parse/push-adapter": "$@parse/push-adapter"
}, },
"engines": { "engines": {
"node": "18 || 20 || 22" "node": "18 || 20 || 22"
+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"
} }
} }