Compare commits

..
13 Commits
Author SHA1 Message Date
prafull-opensignlabsandGitHub bce0e4971e Merge pull request #2059 from OpenSignLabs/updates-20428403989
v2.34.0
2025-12-22 15:27:36 +05:30
prafull-opensignlabs 4fb8aa9a16 Delete apps/OpenSign/package-lock.json 2025-12-22 09:56:25 +00:00
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-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-opensignlabsandGitHub 7894a6edb8 Merge pull request #2025 from OpenSignLabs/build_deps
build(deps): update dependencies
2025-11-20 16:21:33 +05:30
RaistlinandGitHub a878ca1a96 Update README.md
updated link to api documentation v1.1
2025-09-23 11:33:29 +02:00
82 changed files with 6238 additions and 27456 deletions
+1 -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.
- **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.
- **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.
<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'>
@@ -21,9 +21,7 @@ const DisabledButtonExamples = () => {
{/* Option C: Conditional styling */}
<button
className={`op-btn op-btn-primary ${
isDisabled ? "op-btn-vscode-disabled" : ""
}`}
className={`op-btn op-btn-primary ${isDisabled ? "op-btn-vscode-disabled" : ""}`}
disabled={isDisabled}
>
Conditional Button
-17180
View File
File diff suppressed because it is too large Load Diff
+32 -28
View File
@@ -1,57 +1,57 @@
{
"name": "open_sign",
"version": "2.30.0",
"version": "2.34.0",
"private": true,
"dependencies": {
"@formkit/auto-animate": "^0.9.0",
"@imgly/background-removal": "^1.7.0",
"@lottiefiles/dotlottie-react": "^0.16.2",
"@lottiefiles/dotlottie-react": "^0.17.10",
"@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.2.1",
"@reduxjs/toolkit": "^2.10.1",
"@reactour/tour": "^3.8.0",
"@reduxjs/toolkit": "^2.11.2",
"axios": "^1.13.2",
"body-scroll-lock": "^4.0.0-beta.0",
"date-fns-tz": "^3.2.0",
"file-saver": "^2.0.5",
"i18next": "^25.6.3",
"i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"parse": "^7.0.2",
"parse": "^8.0.0",
"pdf-lib": "^1.17.1",
"pkijs": "^3.3.3",
"print-js": "^1.6.0",
"prismjs": "^1.30.0",
"quill-html-edit-button": "^3.0.0",
"radix-ui": "^1.4.3",
"react": "^18.3.1",
"react": "^19.2.3",
"react-bootstrap": "^2.10.10",
"react-confetti": "^6.4.0",
"react-datepicker": "^8.9.0",
"react-datepicker": "^9.0.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dnd-multi-backend": "^9.0.0",
"react-dom": "^18.3.1",
"react-dom": "^19.2.3",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
"react-i18next": "^15.7.4",
"react-konva": "^18.2.12",
"react-pdf": "^9.2.1",
"react-i18next": "^16.5.0",
"react-konva": "^19.2.1",
"react-pdf": "^10.2.0",
"react-quill-new": "^3.6.0",
"react-redux": "^9.2.0",
"react-rnd": "^10.5.2",
"react-router": "^7.9.6",
"react-router": "^7.10.1",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.2",
"react-signature-canvas": "^1.1.0-alpha.2",
"react-timezone-select": "^3.2.8",
"react-to-print": "^3.2.0",
"react-tooltip": "^5.30.0",
"reactour": "^1.19.4",
"redux": "^5.0.1",
"regex-parser": "^2.3.1",
"serve": "^14.2.5",
"styled-components": "^5.3.11",
"web-vitals": "^5.1.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
},
@@ -98,33 +98,37 @@
"@babel/preset-react": "^7.28.5",
"@babel/runtime-corejs2": "^7.28.3",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.6",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.22",
"@types/react": "^19.2.7",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/ui": "^4.0.15",
"autoprefixer": "^10.4.23",
"babel-loader": "^10.0.0",
"commitizen": "^4.3.1",
"concurrently": "^9.2.1",
"css-loader": "^7.1.2",
"daisyui": "^4.12.24",
"eslint": "^9.39.1",
"eslint": "^9.39.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^27.2.0",
"jsdom": "^27.3.0",
"lint-staged": "^16.2.7",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"pretty-quick": "^4.2.2",
"rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.18",
"vite": "^7.2.4",
"tailwindcss": "^3.4.19",
"tmp": "^0.2.5",
"vite": "^7.3.0",
"vite-plugin-svgr": "^4.5.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
"vite-tsconfig-paths": "^6.0.1",
"vitest": "^4.0.15"
},
"overrides": {
"tmp": "$tmp"
},
"engines": {
"node": "18 || 20 || 22"
}
}
}
@@ -168,7 +168,10 @@
"Save as template": "Als Vorlage speichern",
"Fix & resend": "Korrigieren und erneut senden",
"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": {
"Sr.No": "Nr.",
@@ -203,7 +206,8 @@
"Redirect url": "Weiterleitungs-URL",
"Created Date": "Erstellungsdatum",
"Updated Date": "Aktualisierungsdatum",
"Expiry Date": "Ablaufdatum"
"Expiry Date": "Ablaufdatum",
"Sent Date": "Sendedatum"
},
"report-help": {
"Draft Documents": "Dies sind Dokumente, die Sie begonnen, aber noch nicht zum Versenden fertiggestellt haben.",
@@ -266,7 +270,7 @@
"signature": "Unterschrift",
"upload-image": "Bild hochladen",
"clear": "Löschen",
"upload-signature/Image": "Bitte Unterschrift/Bild hochladen",
"upload-signature/Image": "Bitte Unterschrift/Bild hochladen/Stempel",
"initials": "Initialen",
"API": "API",
"api-token": "API-Token",
@@ -442,7 +446,7 @@
"Next to current widget": "Neben dem aktuellen Widget"
},
"options": "Optionen",
"minimun-check": "Minimale Anzahl",
"minimum-check": "Minimale Anzahl",
"maximum-check": "Maximale Anzahl",
"cell-count": "Zellzahl",
"default-value": "Standardwert",
@@ -584,7 +588,7 @@
"provide-your-details": "Bitte geben Sie Ihre Daten an",
"provide-email": "Bitte E-Mail angeben.",
"additional-info": "Zusätzliche Informationen",
"signature-saved-alert": "Unterschrift erfolgreich gespeichert.",
"signature-saved-alert": "erfolgreich gespeichert.",
"saved-successfully": "Erfolgreich gespeichert.",
"fill-field": "Bitte dieses Feld ausfüllen",
"error-template": "Fehler: Vorlage nicht gefunden!",
@@ -700,7 +704,7 @@
"select-date-format": "Wählen Sie ein Datumsformat",
"quantity-of-credits": "Anzahl der 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",
"quota-err-quicksend": "Kontingent erreicht, Sie haben nicht genügend Credits.",
"buy-credits": "Premium-Credits kaufen",
@@ -1274,5 +1278,55 @@
"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."
"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.",
"api-credits-updated": "API-Credits wurden erfolgreich zugewiesen.",
"min-date": "Mindestdatum",
"max-date": "Höchstdatum",
"invalid-min-date": "Ungültiges Mindestdatum.",
"min-less-max": "Das Mindestdatum muss kleiner oder gleich dem Höchstdatum sein.",
"invalid-max-date": "Ungültiges Höchstdatum.",
"max-less-min": "Das Höchstdatum muss größer oder gleich dem Mindestdatum sein.",
"date-range": "Datumsbereich",
"date-range-help": "Legen Sie ein Mindest- und/oder Höchstdatum fest, um die Auswahl einzuschränken. Das gewählte Datum muss innerhalb dieses Bereichs liegen.",
"option-disabled-by-owner": "Der Dokumentersteller hat diese Option deaktiviert",
"pen-colors": "Stiftfarben",
"pen-colors-help": "Wählen Sie aus, welche Stiftfarben für Signatur- und Initialen-Widgets verfügbar sind. Aktivieren Sie ein Kontrollkästchen, um eine Farbe anzuzeigen, und deaktivieren Sie es, um sie auszublenden.",
"security-key": "Sicherheitsschlüssel",
"remove-limit": "Limit entfernen",
"removed-api-credits": "Das API-Credit-Limit wurde entfernt.",
"enable-authentication": "Authentifizierung aktivieren",
"working": "In Bearbeitung",
"test-security-key": "Sicherheitsschlüssel testen",
"generate": "Generieren",
"regenerate": "Neu generieren"
}
@@ -168,7 +168,10 @@
"Save as template": "Save as template",
"Fix & resend": "Fix & Resend",
"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": {
"Sr.No": "Sr.No",
@@ -203,7 +206,8 @@
"Redirect url": "Redirect url",
"Created Date": "Created Date",
"Updated Date": "Updated Date",
"Expiry Date": "Expiry Date"
"Expiry Date": "Expiry Date",
"Sent Date": "Sent Date"
},
"report-help": {
"Draft Documents": "These are documents you have started but have not finalized for sending.",
@@ -266,7 +270,7 @@
"signature": "Signature",
"upload-image": " Upload image",
"clear": "Clear",
"upload-signature/Image": "Please upload signature/Image",
"upload-signature/Image": "Please upload signature/Image/stamp",
"initials": "Initials",
"API": "API",
"api-token": "API token",
@@ -443,7 +447,7 @@
"Next to current widget": "Next to current widget"
},
"options": "Options",
"minimun-check": "Minimun check",
"minimum-check": "Minimum check",
"maximum-check": "Maximum check",
"cell-count": "Cell count",
"default-value": "Default value",
@@ -585,7 +589,7 @@
"provide-your-details": "Provide your details",
"provide-email": "Please provide email.",
"additional-info": "Additional info",
"signature-saved-alert": "Signature saved successfully.",
"signature-saved-alert": "Saved successfully.",
"saved-successfully": "Saved successfully.",
"fill-field": "Please fill out this field",
"error-template": "Error: Template not found!",
@@ -701,7 +705,7 @@
"select-date-format": "Select a date format",
"quantity-of-credits": "Quantity of premium credits",
"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",
"quota-err-quicksend": "Quota reached, you don't have enough credits.",
"buy-credits": "Buy premium credits",
@@ -1275,5 +1279,55 @@
"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."
"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.",
"api-credits-updated": "API credits have been allocated successfully.",
"min-date": "Min Date",
"max-date": "Max Date",
"invalid-min-date": "Invalid min date.",
"min-less-max": "Min date must be less than or equal to max date.",
"invalid-max-date": "Invalid max date.",
"max-less-min": "Max date must be greater than or equal to min date.",
"date-range": "Date range",
"date-range-help": "Set Min and/or Max to restrict selection. Chosen date must fall within the range.",
"option-disabled-by-owner": "Document creator has disabled this option",
"pen-colors": "Pen colors",
"pen-colors-help": "Choose which pen colors are available for Signature and Initials widgets. Check to show a color, uncheck to hide it.",
"security-key": "Security key",
"remove-limit": "Remove limit",
"removed-api-credits": "The API credit limit has been removed.",
"enable-authentication": "Enable authentication",
"working": "Working",
"test-security-key": "Test security key",
"generate": "Generate",
"regenerate": "Regenerate"
}
@@ -168,7 +168,10 @@
"Save as template": "Guardar como plantilla",
"Fix & resend": "Corregir y reenviar",
"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": {
"Sr.No": "Nº",
@@ -203,7 +206,8 @@
"Redirect url": "URL de redirección",
"Created Date": "Fecha de creació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": {
"Draft Documents": "Estos son documentos que has iniciado pero no has finalizado para su envío.",
@@ -266,7 +270,7 @@
"signature": "Firma",
"upload-image": " Subir imagen",
"clear": "Limpiar",
"upload-signature/Image": "Por favor, sube la firma/imagen",
"upload-signature/Image": "Por favor, sube la firma/imagen/estampilla",
"initials": "Iniciales",
"API": "API",
"api-token": "Token API",
@@ -443,7 +447,7 @@
"Next to current widget": "Junto al widget actual"
},
"options": "Opciones",
"minimun-check": "Chequeo mínimo",
"minimum-check": "Chequeo mínimo",
"maximum-check": "Chequeo máximo",
"cell-count": "recuento de células",
"default-value": "Valor por defecto",
@@ -585,7 +589,7 @@
"provide-your-details": "Proporciona tus detalles",
"provide-email": "Por favor, proporciona un correo.",
"additional-info": "Información adicional",
"signature-saved-alert": "Firma guardada exitosamente.",
"signature-saved-alert": "guardada exitosamente.",
"saved-successfully": "Guardado con éxito.",
"fill-field": "Por favor, rellena este campo",
"error-template": "Error: ¡Plantilla no encontrada!",
@@ -701,7 +705,7 @@
"select-date-format": "Selecciona un formato de fecha",
"quantity-of-credits": "Cantidad de créditos premium",
"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",
"quota-err-quicksend": "Cuota alcanzada, no tienes créditos suficientes.",
"buy-credits": "Comprar créditos premium",
@@ -1275,5 +1279,55 @@
"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."
"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.",
"api-credits-updated": "Los créditos de API se han asignado correctamente.",
"min-date": "Fecha mínima",
"max-date": "Fecha máxima",
"invalid-min-date": "Fecha mínima no válida.",
"min-less-max": "La fecha mínima debe ser menor o igual a la fecha máxima.",
"invalid-max-date": "Fecha máxima no válida.",
"max-less-min": "La fecha máxima debe ser mayor o igual a la fecha mínima.",
"date-range": "Rango de fechas",
"date-range-help": "Establezca una fecha mínima y/o máxima para restringir la selección. La fecha elegida debe estar dentro del rango.",
"option-disabled-by-owner": "El creador del documento ha deshabilitado esta opción",
"pen-colors": "Colores del bolígrafo",
"pen-colors-help": "Elija qué colores de bolígrafo están disponibles para los widgets de Firma e Iniciales. Marque una casilla para mostrar un color y desmárquela para ocultarlo.",
"security-key": "Clave de seguridad",
"remove-limit": "Eliminar límite",
"removed-api-credits": "El límite de créditos API ha sido eliminado.",
"enable-authentication": "Habilitar autenticación",
"working": "En funcionamiento",
"test-security-key": "Probar clave de seguridad",
"generate": "Generar",
"regenerate": "Regenerar"
}
@@ -176,7 +176,8 @@
"Redirect url": "URL de redirection",
"Created Date": "Date de création",
"Updated Date": "Date de mise à jour",
"Expiry Date": "Date d'expiration"
"Expiry Date": "Date d'expiration",
"Sent Date": "Date d'envoi"
},
"btnLabel": {
"sign": "Signer",
@@ -203,7 +204,10 @@
"Save as template": "Enregistrer comme modèle",
"Fix & resend": "Corriger et renvoyer",
"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": {
"Draft Documents": "Il s'agit de documents que vous avez commencés mais que vous n'avez pas finalisés pour envoi.",
@@ -266,7 +270,7 @@
"signature": "Signature",
"upload-image": "Télécharger une image",
"clear": "Vider",
"upload-signature/Image": "télécharger-signature/Image",
"upload-signature/Image": "télécharger-signature/Image/timbre",
"initials": "Initiales",
"API": "API",
"api-token": "Jeton API",
@@ -442,7 +446,7 @@
"Next to current widget": "À côté du widget actuel"
},
"options": "Possibilités",
"minimun-check": "Vérification minimale",
"minimum-check": "Vérification minimale",
"maximum-check": "Contrôle maximum",
"cell-count": "numération cellulaire",
"default-value": "Valeur par défaut",
@@ -584,7 +588,7 @@
"provide-your-details": "Fournissez vos coordonnées",
"provide-email": "Veuillez fournir un e-mail.",
"additional-info": "Information additionnelle",
"signature-saved-alert": "Signature enregistrée avec succès.",
"signature-saved-alert": "enregistrée avec succès.",
"saved-successfully": "Enregistré avec succès.",
"fill-field": "Veuillez remplir ce champ",
"error-template": "Erreur : modèle introuvable ! ",
@@ -701,7 +705,7 @@
"quantity-of-credits": "Quantité de crédits de prime",
"remaining-credits": "Crédits premium disponibles :",
"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.",
"buy-credits": "Acheter des crédits premium",
"rotate-right": "Faire pivoter à droite",
@@ -1274,5 +1278,55 @@
"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."
"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.",
"api-credits-updated": "Les crédits API ont été attribués avec succès.",
"min-date": "Date minimale",
"max-date": "Date maximale",
"invalid-min-date": "Date minimale invalide.",
"min-less-max": "La date minimale doit être inférieure ou égale à la date maximale.",
"invalid-max-date": "Date maximale invalide.",
"max-less-min": "La date maximale doit être supérieure ou égale à la date minimale.",
"date-range": "Plage de dates",
"date-range-help": "Définissez une date minimale et/ou maximale pour restreindre la sélection. La date choisie doit se situer dans cette plage.",
"option-disabled-by-owner": "Le créateur du document a désactivé cette option",
"pen-colors": "Couleurs du stylo",
"pen-colors-help": "Choisissez quelles couleurs de stylo sont disponibles pour les widgets Signature et Initiales. Cochez pour afficher une couleur, décochez pour la masquer.",
"security-key": "Clé de sécurité",
"remove-limit": "Supprimer la limite",
"removed-api-credits": "La limite de crédits API a été supprimée.",
"enable-authentication": "Activer l'authentification",
"working": "En cours",
"test-security-key": "Tester la clé de sécurité",
"generate": "Générer",
"regenerate": "Régénérer"
}
@@ -168,7 +168,10 @@
"Save as template": "टेम्पलेट के रूप में सहेजें",
"Fix & resend": "ठीक करें और पुनः भेजें",
"Kiosk Mode": "कियोस्क मोड",
"Reset password": "पासवर्ड रीसेट करें"
"Reset password": "पासवर्ड रीसेट करें",
"View formdata": "फॉर्म डेटा देखें",
"Download all formdata": "सभी फॉर्म डेटा डाउनलोड करें",
"API credits": "API क्रेडिट्स"
},
"report-heading": {
"Sr.No": "क्रमांक",
@@ -203,7 +206,8 @@
"Redirect url": "रीडायरेक्ट URL",
"Created Date": "निर्माण तिथि",
"Updated Date": "अद्यतन तिथि",
"Expiry Date": "समाप्ति तिथि"
"Expiry Date": "समाप्ति तिथि",
"Sent Date": "भेजे जाने की तिथि"
},
"report-help": {
"Draft Documents": "ये वे दस्तावेज़ हैं जिन्हें आपने शुरू तो किया है लेकिन भेजने के लिए अंतिम रूप नहीं दिया है।",
@@ -266,7 +270,7 @@
"signature": "हस्ताक्षर",
"upload-image": " छवि अपलोड करें",
"clear": "स्पष्ट",
"upload-signature/Image": "कृपया हस्ताक्षर/छवि अपलोड करें",
"upload-signature/Image": "कृपया हस्ताक्षर/छवि अपलोड करें/मोहर",
"initials": "हस्ताक्षर",
"API": "एपीआई",
"api-token": "एपीआई टोकन",
@@ -442,7 +446,7 @@
"Next to current widget": "वर्तमान विजेट के आगे"
},
"options": "विकल्प",
"minimun-check": "न्यूनतम जाँच",
"minimum-check": "न्यूनतम जाँच",
"maximum-check": "अधिकतम जाँच",
"cell-count": "सेल्स संख्या",
"default-value": "डिफ़ॉल्ट मान",
@@ -584,7 +588,7 @@
"provide-your-details": "अपना विवरण प्रदान करें",
"provide-email": "कृपया ईमेल प्रदान करें।",
"additional-info": "अतिरिक्त जानकारी",
"signature-saved-alert": "हस्ताक्षर सफलतापूर्वक सहेजा गया।",
"signature-saved-alert": "सफलतापूर्वक सहेजा गया।",
"saved-successfully": "सफलतापूर्वक सहेजा गया।",
"fill-field": "कृपया इस फ़ील्ड को भरें",
"error-template": "त्रुटि: टेम्पलेट नहीं मिला!",
@@ -700,7 +704,7 @@
"select-date-format": "एक दिनांक प्रारूप चुनें",
"quantity-of-credits": "प्रीमियम क्रेडिट की मात्रा",
"remaining-credits": "प्रीमियम क्रेडिट उपलब्ध:",
"remaining-credits-help": "अपनी वेबसाइट पर एपीआई दस्तावेज़ हस्ताक्षर, थोक भेजने या OpenSign एकीकरण को एम्बेड करने के लिए प्रीमियम क्रेडिट का उपयोग करें। आपके पास {{allowedcredits}} शामिल क्रेडिट और {{addoncredits}} अतिरिक्त खरीदे ए क्रेडिट शेष हैं।",
"remaining-credits-help": "API दस्तावेज़ साइनिंग, बल्क सेंडिंग या अपनी वेबसाइट पर OpenSign इंटीग्रेशन एम्बेड करने के लिए प्रीमियम क्रेडिट का उपयोग करें। आपके पास {{allowedcredits}} शामिल क्रेडिट और {{addoncredits}} अतिरिक्त खरीदे हुए क्रेडिट शेष हैं। यदि आप Teams या Enterprise प्लान पर हैं, तो ये क्रेडिट आपकी कंपनी के सभी उपयोगकर्ताओं के बीच साझा किए जा सकते हैं, जब तक कि आपके खाते के लिए अकाउंट एडमिन ने कोई सीमा निर्धारित न की हो।",
"additional-credits": "कृपया प्रीमियम क्रेडिट खरीदें",
"quota-err-quicksend": "कोटा पहुँच गया, आपके पास पर्याप्त क्रेडिट नहीं हैं।",
"buy-credits": "प्रीमियम क्रेडिट खरीदें",
@@ -1274,5 +1278,55 @@
"global-smtp-settings-help-p2": "सभी हस्ताक्षर अनुरोध ईमेल की बेहतर इनबॉक्स डिलीवरी सुनिश्चित करने के लिए, आप अपना पसंदीदा ईमेल प्रदाता जोड़ सकते हैं। निम्न विकल्पों में से एक चुनें:",
"global-connect-to-gmail-help": "अपनी संस्था की इनबॉक्स से सीधे हस्ताक्षर अनुरोध ईमेल भेजने के लिए Gmail खाते का उपयोग करें। यह डिलीवरी दर, प्रतिष्ठा और विश्वसनीयता में सुधार करता है।",
"global-connect-to-smtp-help": "अपने डोमेन के माध्यम से ईमेल भेजने के लिए अपना स्वयं का SMTP सर्वर उपयोग करें। यह आपकी ईमेल संरचना पर पूर्ण नियंत्रण प्रदान करता है, डिलीवरी को बेहतर बनाता है और ब्रांड की निरंतरता बनाए रखता है।",
"global-smtp-settings-help-p3": "यदि कोई व्यक्तिगत उपयोगकर्ता-स्तरीय सेटिंग्स कॉन्फ़िगर नहीं की गई हैं, तो ये वैश्विक सेटिंग्स स्वचालित रूप से लागू होंगी।"
"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 कुंजी द्वारा उपभोग किए गए क्रेडिट की संख्या है।",
"api-credits-updated": "API क्रेडिट सफलतापूर्वक आवंटित कर दिए गए हैं।",
"min-date": "न्यूनतम तिथि",
"max-date": "अधिकतम तिथि",
"invalid-min-date": "अमान्य न्यूनतम तिथि।",
"min-less-max": "न्यूनतम तिथि अधिकतम तिथि से कम या उसके बराबर होनी चाहिए।",
"invalid-max-date": "अमान्य अधिकतम तिथि।",
"max-less-min": "अधिकतम तिथि न्यूनतम तिथि से अधिक या उसके बराबर होनी चाहिए।",
"date-range": "तिथि सीमा",
"date-range-help": "चयन को सीमित करने के लिए न्यूनतम और/या अधिकतम तिथि सेट करें। चयनित तिथि इस सीमा के भीतर होनी चाहिए।",
"option-disabled-by-owner": "दस्तावेज़ निर्माता ने इस विकल्प को अक्षम कर दिया है",
"pen-colors": "पेन के रंग",
"pen-colors-help": "हस्ताक्षर और प्रारंभिक विजेट्स के लिए उपलब्ध पेन रंग चुनें। किसी रंग को दिखाने के लिए चेक करें और छिपाने के लिए अनचेक करें।",
"security-key": "सुरक्षा कुंजी",
"remove-limit": "सीमा हटाएँ",
"removed-api-credits": "API क्रेडिट सीमा हटा दी गई है।",
"enable-authentication": "प्रमाणीकरण सक्षम करें",
"working": "कार्यरत",
"test-security-key": "सुरक्षा कुंजी का परीक्षण करें",
"generate": "जनरेट करें",
"regenerate": "पुनः जनरेट करें"
}
@@ -168,7 +168,10 @@
"Save as template": "Salva come modello",
"Fix & resend": "Correggi e reinvia",
"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": {
"Sr.No": "Nr.",
@@ -203,7 +206,8 @@
"Redirect url": "URL di reindirizzamento",
"Created Date": "Data di creazione",
"Updated Date": "Data di aggiornamento",
"Expiry Date": "Data di scadenza"
"Expiry Date": "Data di scadenza",
"Sent Date": "Data di invio"
},
"report-help": {
"Draft Documents": "Questi sono documenti che hai iniziato ma non hai ancora finalizzato per l'invio.",
@@ -266,7 +270,7 @@
"signature": "Firma",
"upload-image": "Carica immagine",
"clear": "Cancella",
"upload-signature/Image": "Carica firma/immagine",
"upload-signature/Image": "Carica firma/immagine/timbro",
"initials": "Iniziali",
"API": "API",
"api-token": "Token API",
@@ -442,7 +446,7 @@
"Next to current widget": "Accanto al widget corrente"
},
"options": "Opzioni",
"minimun-check": "Controllo minimo",
"minimum-check": "Controllo minimo",
"maximum-check": "Controllo massimo",
"cell-count": "conteggio delle cellule",
"default-value": "Valore predefinita",
@@ -584,7 +588,7 @@
"provide-your-details": "Fornisci i tuoi dettagli",
"provide-email": "Fornisci l'email.",
"additional-info": "Informazioni aggiuntive",
"signature-saved-alert": "Firma salvata con successo.",
"signature-saved-alert": "salvata con successo.",
"saved-successfully": "Salvato con successo.",
"fill-field": "Compila questo campo",
"error-template": "Errore: Modello non trovato!",
@@ -700,7 +704,7 @@
"select-date-format": "Seleziona un formato data",
"quantity-of-credits": "Quantità di crediti premium",
"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",
"quota-err-quicksend": "Quota raggiunta, non hai abbastanza crediti.",
"buy-credits": "Acquista Crediti Premium",
@@ -1274,5 +1278,55 @@
"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."
"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.",
"api-credits-updated": "I crediti API sono stati assegnati con successo.",
"min-date": "Data minima",
"max-date": "Data massima",
"invalid-min-date": "Data minima non valida.",
"min-less-max": "La data minima deve essere minore o uguale alla data massima.",
"invalid-max-date": "Data massima non valida.",
"max-less-min": "La data massima deve essere maggiore o uguale alla data minima.",
"date-range": "Intervallo di date",
"date-range-help": "Imposta una data minima e/o massima per limitare la selezione. La data scelta deve rientrare nell'intervallo.",
"option-disabled-by-owner": "Il creatore del documento ha disabilitato questa opzione.",
"pen-colors": "Colori della penna",
"pen-colors-help": "Scegli quali colori della penna sono disponibili per i widget Firma e Iniziali. Seleziona una casella per mostrare un colore e deselezionala per nasconderlo.",
"security-key": "Chiave di sicurezza",
"remove-limit": "Rimuovi limite",
"removed-api-credits": "Il limite dei crediti API è stato rimosso.",
"enable-authentication": "Abilita autenticazione",
"working": "In funzione",
"test-security-key": "Testa la chiave di sicurezza",
"generate": "Genera",
"regenerate": "Rigenera"
}
Binary file not shown.
-1
View File
@@ -225,7 +225,6 @@ const AddUser = (props) => {
className="block text-xs font-semibold"
>
{t("phone")}
{/* <span className="text-[red] text-[13px]"> *</span> */}
</label>
<input
type="text"
+10 -8
View File
@@ -25,13 +25,16 @@ const BulkSendUi = (props) => {
//function to check at least one signature field exist
const signatureExist = async () => {
const isPrefill = props?.Placeholders.some((x) => x?.Role === "prefill");
const isPrefill =
props?.Placeholders?.some((x) => x?.Role === "prefill") || false;
if (isPrefill) {
setIsPrefillExist(isPrefill);
}
setIsDisableBulkSend(false);
const getPlaceholder = props?.Placeholders;
const removePrefill = getPlaceholder.filter((x) => x?.Role !== "prefill");
const removePrefill = getPlaceholder?.filter(
(x) => x?.Role !== "prefill"
);
const checkIsSignatureExistt = removePrefill?.every((placeholderObj) =>
placeholderObj?.placeHolder?.some((holder) =>
holder?.pos?.some((posItem) => posItem?.type === "signature")
@@ -187,9 +190,7 @@ const BulkSendUi = (props) => {
const batchQuery = async (Documents) => {
const token =
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
const functionsUrl = `${localStorage.getItem(
"baseUrl"
)}functions/batchdocuments`;
const functionsUrl = `${localStorage.getItem("baseUrl")}functions/batchdocuments`;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
@@ -198,13 +199,14 @@ const BulkSendUi = (props) => {
const params = { Documents: JSON.stringify(Documents) };
try {
const res = await axios.post(functionsUrl, params, { headers: headers });
// console.log("res ", res);
if (res.data && res.data.result) {
props.handleClose("success", Documents?.length);
}
} catch (err) {
console.log("Err ", err);
props.handleClose("error", 0);
const message =
err?.response?.data?.error || err?.message || "something went wrong.";
console.error("Error sending documents:", message);
props.handleClose("error", 0, message);
} finally {
setIsSubmit(false);
}
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import ModalUi from "../primitives/ModalUi";
import { useTranslation } from "react-i18next";
@@ -7,7 +7,6 @@ const ColumnSelector = ({
allColumns = [],
visibleColumns = [],
columnLabels = {},
defaultColumns = [],
onApply,
onClose
}) => {
+1 -2
View File
@@ -60,7 +60,6 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
setAppLogo(logo);
}
}
const handleLogout = async () => {
setIsOpen(false);
setIsLoggingOut(true);
@@ -198,7 +197,7 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
</div>
<ul
tabIndex={0}
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
className={`mt-4 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
isOpen ? "" : "hidden"
}`}
>
+3 -4
View File
@@ -1,4 +1,3 @@
import { Helmet } from "react-helmet";
import { useLocation, matchPath } from "react-router";
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
@@ -70,13 +69,13 @@ export default function Title() {
[t, prefix, appName]
);
const manifestUrl = useManifestUrl(appName, logo);
return (
<Helmet>
<>
<title>{title}</title>
<meta name="description" content={title} />
{logo && <link rel="icon" type="image/png" href={logo} />}
<link rel="manifest" href={manifestUrl} />
</Helmet>
</>
);
}
@@ -56,9 +56,7 @@ function DriveBody(props) {
setRename("");
const trimmedValue = renameValue.trim();
if (trimmedValue.length > 0) {
const updateName = {
Name: renameValue
};
const updateName = { Name: renameValue };
const docId = data.objectId;
const docData = props.pdfData;
const updatedData = docData.map((item) => {
@@ -84,7 +82,7 @@ function DriveBody(props) {
}
);
} catch (err) {
console.log("Err ", err);
console.error("Error in rename doc", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
@@ -160,9 +158,7 @@ function DriveBody(props) {
const handleDeleteDocument = async (docData) => {
setIsDeleteDoc({});
const docId = docData.objectId;
const data = {
IsArchive: true
};
const data = { IsArchive: true };
await axios
.put(
@@ -184,7 +180,7 @@ function DriveBody(props) {
}
})
.catch((err) => {
console.log("Err ", err);
console.error("Err in delete doc", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
@@ -218,16 +214,12 @@ function DriveBody(props) {
}
};
} else {
updateData = {
Folder: { __op: "Delete" }
};
updateData = { Folder: { __op: "Delete" } };
}
await axios
.put(
`${localStorage.getItem(
"baseUrl"
)}classes/contracts_Document/${updateDocId}`,
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${updateDocId}`,
updateData,
{
headers: {
@@ -248,7 +240,7 @@ function DriveBody(props) {
}
})
.catch((err) => {
console.log("err", err);
console.error("err in move folder", err);
});
setIsOpenMoveModal(false);
@@ -296,7 +288,7 @@ function DriveBody(props) {
props.setPdfData(updatedData);
}
} catch (err) {
console.log("Err ", err);
console.error("Err in delete folder", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import ModalUi from "../../primitives/ModalUi";
import { fontsizeArr, fontColorArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
@@ -7,7 +7,7 @@ export default function CellsSettingModal({
isOpen,
handleClose,
defaultData,
handleSave
handleSave,
}) {
const { t } = useTranslation();
const [name, setName] = useState("");
@@ -31,13 +31,16 @@ export default function CellsSettingModal({
name,
cellCount: parseInt(cellCount, 10),
fontSize,
fontColor
fontColor,
});
};
return (
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("widget-info")}>
<form onSubmit={onSubmit} className="p-[20px] text-base-content flex flex-col gap-3">
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("widget-info")}>
<form
onSubmit={onSubmit}
className="p-[20px] text-base-content flex flex-col gap-3"
>
<div>
<label htmlFor="name" className="text-[13px]">
{t("name")} <span className="text-[red]">*</span>
@@ -91,7 +94,10 @@ export default function CellsSettingModal({
</option>
))}
</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 className="h-[1px] w-full bg-[#b7b3b3] my-[16px]"></div>
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
function DefaultSignature(props) {
@@ -105,9 +105,7 @@ function DefaultSignature(props) {
</button>
{!props.isDefault && (
<div className="absolute bg-black/70 text-white w-full h-full flex items-center justify-center text-[11px] cursor-default">
<span className="-rotate-45">
Document creator has disabled this option
</span>
<span className="-rotate-45">{t("option-disabled-by-owner")}</span>
</div>
)}
</div>
@@ -54,6 +54,10 @@ const EditTemplate = ({
AllowModifications: template?.AllowModifications || false,
TimeToCompleteDays: template?.TimeToCompleteDays || 15,
});
const pensList = ["blue", "red", "black"];
const [selectedColors, setSelectedColors] = useState(
template?.PenColors || pensList
);
const [isUpdate, setIsUpdate] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [uploadPdf, setUploadPdf] = useState({
@@ -211,6 +215,7 @@ const EditTemplate = ({
IsEnableOTP: IsEnableOTP,
IsTourEnabled: isTourEnabled,
AllowModifications: allowModify,
PenColors: selectedColors,
...reminderDate
};
onSuccess(data);
@@ -245,6 +250,19 @@ const EditTemplate = ({
handleClose();
};
const handleColorsChange = (color) => {
setSelectedColors((prev) => {
// If user tries to uncheck the last remaining color → block it
if (prev.length === 1 && prev.includes(color)) {
return prev;
}
// Normal toggle behavior
return prev.includes(color)
? prev.filter((c) => c !== color) // remove
: [...prev, color]; // add
});
};
return (
<ModalUi
isOpen
@@ -485,6 +503,41 @@ const EditTemplate = ({
</div>
</div>
</div>
<div className="text-xs mt-3 mb-4">
<label htmlFor="penColors">
{t("pen-colors")}
<a data-tooltip-id="pen-colors-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<Tooltip id="pen-colors-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("pen-colors")}</p>
<div>{t("pen-colors-help")}</div>
</div>
</Tooltip>
</label>
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
{pensList.map((color) => (
<div
key={color}
className="flex flex-row gap-[5px] items-center"
>
<input
className="mr-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
name="penColors"
checked={selectedColors.includes(color)}
onChange={() => handleColorsChange(color)}
/>
<div className="hover:underline underline-offset-2 cursor-default capitalize">
{color}
</div>
</div>
))}
</div>
</div>
<div className="text-xs mt-3">
<SignersInput
label={t("Bcc")}
@@ -27,7 +27,7 @@ import moment from "moment";
import "../../styles/opensigndrive.css";
import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { setIsShowModal } from "../../redux/reducers/widgetSlice";
import { themeColor } from "../../constant/const";
import { useGuidelinesContext } from "../../context/GuidelinesContext";
@@ -136,7 +136,7 @@ function Placeholder(props) {
data?.format,
props.fontSize || props.pos?.options?.fontSize || 12,
props.fontColor || props.pos?.options?.fontColor || "black",
formdata
formdata,
);
};
@@ -216,16 +216,17 @@ function Placeholder(props) {
//placeholder, template flow
else if (props.isPlaceholder && !props.isDragging) {
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails(props.pos);
if (
props.pos.key === props?.currWidgetsDetails?.key &&
props?.data?.Role !== "prefill"
) {
props.handleLinkUser(props.data.Id);
if (props?.data?.Role !== "prefill") {
//this condition is used open signers attach modal but it should only open when widgets already selected
if (props?.currWidgetsDetails?.key === props.pos?.key) {
props.handleLinkUser(props.data.Id);
}
props.setUniqueId(props.data.Id);
const checkIndex = props.xyPosition.findIndex(
(data) => data.Id === props.data.Id
);
props.setIsSelectId(checkIndex || 0);
props?.setRoleName("");
} else if (props?.data?.Role === "prefill") {
dispatch(setIsShowModal({ [props.pos.key]: true }));
props.setUniqueId(props?.data?.Id);
@@ -566,7 +567,7 @@ function Placeholder(props) {
}
}
};
//function to calculate font size
// function to calculate font size
const calculateFont = (size, isMinHeight) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
@@ -574,7 +575,7 @@ function Placeholder(props) {
props.containerWH
);
const fontSize = (size || 12) * containerScale * props.scale;
//isMinHeight to set text box minimum height
// isMinHeight to set text box minimum height
if (isMinHeight) {
return fontSize * 1.5 + "px";
} else {
@@ -619,12 +620,12 @@ function Placeholder(props) {
};
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
className="border-gray-400 rounded-[50px] border-[1px] px-3 md:ml-2 text-xs py-2 focus:outline-none hover:border-base-content "
className="border-gray-400 rounded-[50px] border-[1px] px-3 text-xs py-2 focus:outline-none hover:border-base-content "
onClick={onClick}
ref={ref}
>
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
<i className={`${value ? "ml-[5px]" : ""} fa-light fa-calendar `}></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
@@ -646,10 +647,7 @@ function Placeholder(props) {
};
const handleClearDate = () => {
setIsToday(false);
setSelectDate((prev) => ({
...prev,
date: ""
}));
setSelectDate((prev) => ({ ...prev, date: "" }));
};
return (
<>
@@ -657,23 +655,39 @@ function Placeholder(props) {
{props.pos?.options?.response &&
props.pos.key !== props?.currWidgetsDetails?.key &&
props.pos.type === textWidget ? (
<span
<textarea
readOnly
onClick={() => {
if (props?.ispublicTemplate) return;
props.setCurrWidgetsDetails &&
props.setCurrWidgetsDetails(props.pos);
props?.setRoleName && props?.setRoleName("prefill");
props.setUniqueId && props.setUniqueId(props?.data?.Id);
}}
style={{
fontFamily: "Arial, sans-serif",
position: "absolute",
left: xPos(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,
color: fontColor,
zIndex: 99
zIndex: 99,
cursor: getCursor(),
resize: "none",
background: "transparent",
width: props.posWidth(props.pos, props.isSignYourself),
whiteSpace: "pre-wrap",
overflow: "hidden",
outline: "none",
border: "none"
}}
>
{props.pos?.options?.response}
</span>
{
props.pos?.options?.response
}
</textarea>
) : (
<Rnd
id={props.pos.key}
@@ -733,7 +747,7 @@ function Placeholder(props) {
props?.data,
props?.isNeedSign,
props?.uniqueId
) //handle block color of widget for
)
}}
onDrag={(_, d) => {
props?.handleTabDrag?.(props.pos.key);
@@ -934,7 +948,7 @@ function Placeholder(props) {
<span>{t("set-date")} :</span>
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start md:ml-2 ">
<div className="flex justify-start md:ml-2">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
@@ -988,16 +1002,13 @@ function Placeholder(props) {
}
portalId="root-portal"
/>
<label className=" flex items-center gap-1 cursor-pointer">
<label className="flex items-center gap-1 cursor-pointer mb-0">
<input
checked={isToday}
type="checkbox"
className="op-checkbox op-checkbox-xs"
onClick={() => {
setSelectDate((prev) => ({
...prev,
date: ""
}));
setSelectDate((prev) => ({ ...prev, date: "" }));
setIsToday(!isToday);
}}
/>
@@ -1005,7 +1016,7 @@ function Placeholder(props) {
</label>
<span
onClick={() => handleClearDate()}
className="underline text-blue-500 cursor-pointer md:ml-2"
className="underline text-blue-500 cursor-pointer ml-2"
>
{t("clear")}
</span>
@@ -1106,9 +1117,7 @@ function Placeholder(props) {
<button
type="button"
className="op-btn op-btn-primary"
onClick={() => {
saveDateSetting(selectDate);
}}
onClick={() => saveDateSetting(selectDate)}
>
{t("save")}
</button>
@@ -1,5 +1,11 @@
import { useEffect, useMemo, useState, forwardRef } from "react";
import {
useEffect,
useState,
forwardRef,
useRef
} from "react";
import {
onChangeInput,
getMonth,
getYear,
radioButtonWidget,
@@ -9,9 +15,7 @@ import {
months,
years,
selectCheckbox,
checkRegularExpress,
isBase64,
getSignerPages
isBase64
} from "../../constant/Utils";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
@@ -28,6 +32,7 @@ const widgetCls =
function PlaceholderType(props) {
const selectWidgetCls = `w-full h-full absolute left-0 top-0 focus:outline-none text-base-content`;
const { t } = useTranslation();
const textRef = useRef();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const prefillImgLoad = useSelector((state) => state.widget.prefillImgLoad);
const type = props?.pos?.type;
@@ -42,10 +47,8 @@ function PlaceholderType(props) {
// prefer the latest response value over any default value
const widgetData =
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
const widgetTypeTranslation = t(`widgets-name.${props?.pos?.type}`);
const [widgetValue, setwidgetValue] = useState();
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
const [hint, setHint] = useState("");
const [imgUrl, setImgUrl] = useState("");
const fontSize = props.calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
@@ -72,11 +75,6 @@ function PlaceholderType(props) {
// keep displayed value in sync with the stored response
setwidgetValue(widgetData);
}
if (props.pos?.options?.hint) {
setHint(props.pos?.options.hint);
} else if (props.pos?.options?.validation?.type) {
checkRegularExpress(props.pos?.options?.validation?.type, setHint);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.pos, widgetData, type]);
@@ -130,6 +128,47 @@ function PlaceholderType(props) {
loadImage();
}, [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;
};
//useEffect is used to increase auto height of text/textInput widget when user using multiline option and enter value in next line
useEffect(() => {
if (!textRef.current) return;
if (
(type === "text" || type === textInputWidget) &&
textRef.current &&
widgetValue
) {
const el = textRef.current;
// Count actual number of lines
const lines = widgetValue?.split("\n")?.length || props.pos?.Height;
// Get line height from computed style
const lineHeight = parseInt(window.getComputedStyle(el).lineHeight);
const textWidgetHeight = lines * lineHeight;
// Set widget box height (your logic)
onChangeInput(
null,
props.pos,
props?.xyPosition,
props.index,
props?.setXyPosition,
props.data && props.data.Id,
null,
null,
null,
null,
null,
textWidgetHeight
);
}
}, [widgetValue]);
switch (type) {
case "signature":
return props.pos.SignUrl ? (
@@ -150,7 +189,7 @@ function PlaceholderType(props) {
}}
className="font-medium"
>
{hint || widgetTypeTranslation}
{formatWidgetName()}
</div>
)}
</div>
@@ -174,7 +213,7 @@ function PlaceholderType(props) {
}}
className="font-medium"
>
{hint || widgetTypeTranslation}
{formatWidgetName()}
</div>
)}
</div>
@@ -221,23 +260,27 @@ function PlaceholderType(props) {
case textInputWidget:
return props.isSignYourself || iswidgetEnable ? (
<textarea
placeholder={hint || t("widgets-name.text")}
rows={1}
value={widgetValue}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
placeholder={formatWidgetName()}
ref={textRef}
rows={50}
value={
widgetValue
}
className={`w-full resize-none overflow-hidden text-base-content item-center outline-none ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
name="text"
readOnly
disabled={props.isNeedSign && isReadOnly}
cols="50"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{hint || widgetTypeTranslation}</span>
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case cellsWidget: {
@@ -256,12 +299,14 @@ function PlaceholderType(props) {
isEnabled={iswidgetEnable}
count={count}
height={height}
value={cells.join("")}
value={
cells.join("")
}
editable={isEditable}
resizable={props?.isAllowModify}
fontSize={fontSize}
fontColor={fontColor}
hint={hint}
hint={formatWidgetName()}
onCellCountChange={handleCellResize}
/>
);
@@ -295,7 +340,7 @@ function PlaceholderType(props) {
}}
className="font-medium text-center"
>
{hint || widgetTypeTranslation}
{formatWidgetName()}
</div>
)}
</div>
@@ -304,9 +349,11 @@ function PlaceholderType(props) {
return iswidgetEnable ? (
<textarea
readOnly
placeholder={hint || widgetTypeTranslation}
placeholder={formatWidgetName()}
rows={1}
value={widgetValue}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
@@ -319,16 +366,18 @@ function PlaceholderType(props) {
/>
) : (
<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>
);
case "company":
return iswidgetEnable ? (
<textarea
readOnly
placeholder={hint || widgetTypeTranslation}
placeholder={formatWidgetName()}
rows={1}
value={widgetValue}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
@@ -341,16 +390,18 @@ function PlaceholderType(props) {
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{hint || widgetTypeTranslation}</span>
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case "job title":
return iswidgetEnable ? (
<textarea
readOnly
placeholder={hint || widgetTypeTranslation}
placeholder={formatWidgetName()}
rows={1}
value={widgetValue}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
@@ -363,7 +414,7 @@ function PlaceholderType(props) {
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{hint || widgetTypeTranslation}</span>
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case "date":
@@ -444,7 +495,7 @@ function PlaceholderType(props) {
}}
className="font-medium text-center"
>
{hint || widgetTypeTranslation}
{formatWidgetName()}
</div>
)}
</div>
@@ -453,9 +504,11 @@ function PlaceholderType(props) {
return iswidgetEnable ? (
<textarea
readOnly
placeholder={hint || widgetTypeTranslation}
placeholder={formatWidgetName()}
rows={1}
value={widgetValue}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
@@ -468,7 +521,7 @@ function PlaceholderType(props) {
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{hint || widgetTypeTranslation}</span>
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case radioButtonWidget:
@@ -510,18 +563,22 @@ function PlaceholderType(props) {
case textWidget:
return (
<textarea
name="text"
ref={textRef}
readOnly
placeholder={t("widgets-name.text")}
rows={1}
value={widgetValue}
className={textWidgetCls}
rows={50}
cols={50}
defaultValue={
widgetValue
}
style={{
fontFamily: "Arial, sans-serif",
fontSize: fontSize,
color: fontColor,
background: "white"
}}
cols="50"
className="w-full resize-none overflow-hidden text-base-content item-center outline-none"
/>
);
default:
@@ -546,7 +603,7 @@ function PlaceholderType(props) {
}}
className="font-medium"
>
{hint || widgetTypeTranslation}
{formatWidgetName()}
</div>
)}
</div>
@@ -72,7 +72,7 @@ const ImageComponent = (props) => {
<div className="cursor-pointer op-card border-[1px] border-gray-400 flex flex-col w-full h-full justify-center items-center ">
<img
alt="print img"
ref={(el) => (imageRefs.current[props?.position.key] = el)} // Assign ref dynamicallys
ref={(el) => (imageRefs.current[props?.id] = el)} // Assign ref dynamicallys
src={imgUrl}
draggable="false"
className="object-contain h-full w-full aspect-[5/2]"
@@ -90,19 +90,14 @@ const ImageComponent = (props) => {
) : (
<div
className="cursor-pointer op-card border-[1px] op-border-hover flex flex-col overflow-hidden w-full h-full aspect-[5/2] justify-center items-center"
onClick={() => imageRefs.current[props?.position.key]?.click()}
onClick={() => imageRefs.current[props?.id]?.click()}
>
{props?.imageLoaders[props?.position?.key] && (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-white/30 z-50">
<Loader />
</div>
)}
<input
type="file"
onChange={(e) => props?.onImageChange?.(e, props?.position)}
className="filetype"
accept="image/png,image/jpeg"
ref={(el) => (imageRefs.current[props?.position.key] = el)} // Assign ref dynamically
ref={(el) => (imageRefs.current[props?.id] = el)} // Assign ref dynamically
hidden
/>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
@@ -120,16 +115,12 @@ function PrefillWidgetModal(props) {
const loadedSet = useRef(new Set());
const initializedRef = useRef(false); // prevent rerun on state updates
const [image, setImage] = useState(null);
const [imageLoaders, setImageLoaders] = useState({});
const [currentWidget, setCurrentWidget] = useState("");
const [userList, setUserList] = useState([]);
const [totalImages, setTotalImages] = useState(0);
const [loadedImages, setLoadedImages] = useState(0);
const [loading, setLoading] = useState(false);
const years = range(1950, getYear(new Date()) + 16, 1);
const isAnyLoaderActive = Object.values(imageLoaders).some(
(val) => val === true
);
// useMemo to memoize the calculation of unique widgets
const uniqueWidget = useMemo(() => {
@@ -179,7 +170,7 @@ function PrefillWidgetModal(props) {
if (Array.isArray(prefillImg)) {
prefillImg.forEach((img) => dispatch(setPrefillImg(img)));
}
setImageLoaders({});
setLoading(false);
};
savePrefillImg();
}, [props.xyPosition]);
@@ -271,7 +262,7 @@ function PrefillWidgetModal(props) {
};
const handleSavePrefillImg = async (widgetDetails) => {
setImageLoaders((prev) => ({ ...prev, [widgetDetails?.key]: true }));
setLoading(true);
try {
const imageName = generatePdfName(16);
const imageUrl = await convertBase64ToFile(
@@ -419,9 +410,9 @@ function PrefillWidgetModal(props) {
// (tracks how many images have finished loading)
setLoadedImages((prev) => prev + 1);
}
setImageLoaders({});
setLoading(false);
};
const handleWidgetType = (position) => {
const handleWidgetType = (position, id) => {
switch (position?.type) {
case "checkbox":
return (
@@ -533,10 +524,10 @@ function PrefillWidgetModal(props) {
return (
<ImageComponent
position={position}
imageLoaders={imageLoaders}
onImageChange={onImageChange}
handleImageLoaded={handleImageLoaded}
handleClearImage={handleClearImage}
id={id}
/>
);
case radioButtonWidget:
@@ -658,7 +649,7 @@ function PrefillWidgetModal(props) {
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-4 w-full">
{uniqueWidget.map((x, id) => (
<div key={id} className="flex flex-col gap-2 w-full">
{handleWidgetType(x.widget)}
{handleWidgetType(x.widget, id)}
</div>
))}
</div>
@@ -727,7 +718,7 @@ function PrefillWidgetModal(props) {
</div>
<div className="flex gap-2 mx-4 mb-3">
<button
disabled={isAnyLoaderActive || props?.isSubmit}
disabled={props?.isSubmit}
className="op-btn op-btn-primary op-btn-sm w-[80px]"
onClick={() => handleEmbedPrefill(props?.item)}
>
@@ -1,4 +1,3 @@
import React from "react";
import ModalUi from "../../primitives/ModalUi";
import { fontColorArr, fontsizeArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
@@ -8,12 +8,16 @@ import {
cellsWidget,
textWidget
} from "../../constant/Utils";
import {
widgetNamesArr
} from "../../utils";
import { fontColorArr, fontsizeArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
const WidgetNameModal = (props) => {
const { t } = useTranslation();
const signTypes = props?.signatureType || signatureTypes;
const [lastSubmittedName, setLastSubmittedName] = useState("");
const [formdata, setFormdata] = useState({
name: "",
defaultValue: "",
@@ -46,7 +50,7 @@ const WidgetNameModal = (props) => {
[type]
);
const handleHint = () => {
const handleHintPlaceholder = () => {
const type = props.defaultdata?.type;
if (type === "signature") {
@@ -67,7 +71,7 @@ const WidgetNameModal = (props) => {
name: props.defaultdata?.options?.name || "",
defaultValue: props.defaultdata?.options?.defaultValue || "",
status: props.defaultdata?.options?.status || "required",
hint: props.defaultdata?.options?.hint || handleHint(),
hint: props.defaultdata?.options?.hint || "",
textvalidate:
props.defaultdata?.options?.validation?.type === "regex"
? props.defaultdata?.options?.validation?.pattern
@@ -75,12 +79,14 @@ const WidgetNameModal = (props) => {
isReadOnly: props.defaultdata?.options?.isReadOnly || false,
cellCount: props.defaultdata?.options?.cellCount || 5,
});
setLastSubmittedName(props.defaultdata?.options?.name || "");
} else {
setFormdata({
...formdata,
name: props.defaultdata?.options?.name || "",
cellCount: props.defaultdata?.options?.cellCount || 5,
});
setLastSubmittedName(props.defaultdata?.options?.name || "");
}
if (signTypes.length > 0) {
@@ -93,6 +99,19 @@ const WidgetNameModal = (props) => {
const handleSubmit = (e) => {
e.preventDefault();
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) {
const enabledSignTypes = signatureType?.filter((x) => x.enabled);
const isDefaultSignTypeOnly =
@@ -203,6 +222,7 @@ const WidgetNameModal = (props) => {
)
);
};
return (
<ModalUi
isOpen={props.isOpen}
@@ -292,7 +312,7 @@ const WidgetNameModal = (props) => {
{!props?.isSelfSign &&
!isSignOrInitials &&
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]">
{statusArr.map((data, ind) => {
return (
@@ -324,30 +344,29 @@ const WidgetNameModal = (props) => {
{[
textInputWidget,
cellsWidget,
].includes(props.defaultdata?.type) &&
props?.roleName !== "prefill" && (
<div className="flex items-center">
<input
id="isReadOnly"
name="isReadOnly"
type="checkbox"
checked={formdata.isReadOnly}
className="op-checkbox op-checkbox-xs"
onChange={() =>
setFormdata((prev) => ({
...formdata,
isReadOnly: !prev.isReadOnly
}))
}
/>
<label
className="ml-1.5 mb-0 capitalize text-[13px]"
htmlFor="isreadonly"
>
{t("read-only")}
</label>
</div>
)}
].includes(props.defaultdata?.type) && (
<div className="flex items-center">
<input
id="isReadOnly"
name="isReadOnly"
type="checkbox"
checked={formdata.isReadOnly}
className="op-checkbox op-checkbox-xs"
onChange={() =>
setFormdata((prev) => ({
...formdata,
isReadOnly: !prev.isReadOnly
}))
}
/>
<label
className="ml-1.5 mb-0 capitalize text-[13px]"
htmlFor="isreadonly"
>
{t("read-only")}
</label>
</div>
)}
</div>
)}
{isSignOrInitials && (
@@ -355,7 +374,7 @@ const WidgetNameModal = (props) => {
<label htmlFor="signaturetype" className="text-[14px] mb-[0.7rem]">
{t("allowed-signature-types")}
</label>
<div className=" ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
{signatureType.map((type, i) => {
return (
<div key={i} className="flex flex-row gap-[5px] items-center">
@@ -387,6 +406,7 @@ const WidgetNameModal = (props) => {
maxLength={40}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
name="hint"
placeholder={handleHintPlaceholder()}
value={formdata.hint}
onChange={(e) => handleChange(e)}
/>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
function DefaultSignature(props) {
return (
<div className="flex justify-center">
<div
className={`${props?.currWidgetsDetails?.type === "initials" ? "intialSignatureCanvas" : "signatureCanvas"} border-[1.3px] border-gray-300 rounded-[4px]`}
>
<img
src={props?.defaultSignImg}
draggable="false"
className="w-full h-full object-contain"
/>
</div>
</div>
);
}
export default DefaultSignature;
@@ -0,0 +1,21 @@
import SignatureCanvas from "react-signature-canvas";
function Draw(props) {
return (
<div className="flex justify-center">
<SignatureCanvas
ref={props?.canvasRef}
penColor={props?.penColor}
canvasProps={{
className: `${props?.currWidgetsDetails?.type === "initials" ? "intialSignatureCanvas" : "signatureCanvas"} border-[1.3px] border-gray-300 rounded-[4px]`
}}
onEnd={() =>
props?.handleSignatureChange(props?.canvasRef.current?.toDataURL())
}
dotSize={1}
/>
</div>
);
}
export default Draw;
@@ -0,0 +1,57 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
const ALLOWED_COLORS = ["blue", "red", "black"];
const COLOR_CLASS = {
blue: "text-blue-600",
red: "text-red-600",
black: "text-black",
white: "text-white"
};
function PenColorComponent({
providedColors,
penColor,
convertToImg,
fontSelect,
typedSignature,
setPenColor
}) {
const { t } = useTranslation();
const pensList = useMemo(() => {
const allowed = new Set(ALLOWED_COLORS);
const filtered = Array.isArray(providedColors)
? providedColors
.map((c) => String(c).toLowerCase())
.filter((c) => allowed.has(c))
: [];
// remove duplicates while preserving order
return filtered.length ? [...new Set(filtered)] : ALLOWED_COLORS;
}, [providedColors]);
return (
<div className="flex flex-row items-center m-[5px] gap-3">
<span className="text-base-content">{t("options")}</span>
{pensList.map((color) => {
const selected = penColor === color;
return (
<i
key={color}
role="button"
tabIndex={0}
aria-label={`Select ${color} pen`}
onClick={() => {
setPenColor?.(color);
convertToImg?.(fontSelect, typedSignature, color);
}}
className={`${COLOR_CLASS[color] || "text-base-content"} ${selected ? "border-current" : "border-white"} border-b-[2px] pb-0.5 cursor-pointer text-[16px] fa-light fa-pen-nib`}
></i>
);
})}
</div>
);
}
export default PenColorComponent;
@@ -0,0 +1,65 @@
import { useTranslation } from "react-i18next";
function TypeSignature(props) {
const { t } = useTranslation();
return (
<div>
<div className="flex justify-between items-center tabWidth rounded-[4px]">
<span className="ml-[5px] text-[12px] text-base-content">
{props?.currWidgetsDetails?.type === "initials"
? t("initial-teb")
: t("signature-tab")}
:
</span>
<input
maxLength={props?.currWidgetsDetails?.type === "initials" ? 3 : 30}
style={{ fontFamily: props?.fontSelect, color: props?.penColor }}
type="text"
className="bg-transparent ml-1 op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-[20px]"
placeholder={
props?.currWidgetsDetails?.type === "initials"
? t("initial-type")
: t("signature-type")
}
value={props?.typedSignature}
onChange={(e) => {
props?.setTypedSignature(e.target.value);
if (e.target.value?.trim()?.length > 0) {
props?.convertToImg(props?.fontSelect, e.target.value);
}
}}
/>
</div>
<div className="border-[1px] border-[#d6d3d3] mt-[10px] rounded-[4px] tabWidth">
{props?.fontOptions.map((font, ind) => {
return (
<div
key={ind}
style={{
cursor: "pointer",
fontFamily: font.value,
backgroundColor:
props?.fontSelect === font.value && "rgb(206 225 247)"
}}
onClick={() => {
props?.setFontSelect(font.value);
props?.convertToImg(font.value, props?.typedSignature);
}}
>
<div
className="py-[5px] px-[10px] text-[20px]"
style={{ color: props?.penColor }}
>
{props?.typedSignature
? props?.typedSignature
: t("Your-Signature")}
</div>
</div>
);
})}
</div>
</div>
);
}
export default TypeSignature;
@@ -0,0 +1,58 @@
import React, { useRef } from "react";
import { useTranslation } from "react-i18next";
function UploadImage(props) {
const { t } = useTranslation();
const imageRefs = useRef({});
const getImageRef = (key) => {
if (!imageRefs.current[key]) {
imageRefs.current[key] = React.createRef();
}
return imageRefs.current[key];
};
return (
<div>
{(props?.isImageSelect || props?.isStampOrImage) && !props?.image ? (
<div className="flex justify-center">
<div
className={`${props?.currWidgetsDetails?.type === "initials" ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-gray-300 flex flex-col justify-center items-center mb-[6px] cursor-pointer`}
onClick={() =>
getImageRef(props?.currWidgetsDetails?.key).current.click()
}
>
<input
type="file"
onChange={props?.onImageChange}
className="filetype"
accept="image/png,image/jpeg"
ref={getImageRef(props?.currWidgetsDetails?.key)}
hidden
/>
<i className="fa-light fa-cloud-upload-alt uploadImgLogo text-base-content"></i>
<div className="text-[10px] text-base-content">{t("upload")}</div>
</div>
</div>
) : (
<>
<div className="flex justify-center">
<div
className={`${props?.currWidgetsDetails?.type === "initials" ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-gray-300 mb-[6px] overflow-hidden`}
>
<img
alt="print img"
ref={getImageRef(props?.currWidgetsDetails?.key)}
src={props?.image?.src}
draggable="false"
className="object-contain h-full w-full"
/>
</div>
</div>
</>
)}
</div>
);
}
export default UploadImage;
@@ -0,0 +1,82 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Tooltip } from "react-tooltip";
//This file is for both Text and textInput widgets
export default function TextInput(props) {
const [isMultiline, setIsMultiline] = useState();
const { t } = useTranslation();
const handleKeyDown = (e) => {
if (!isMultiline && e.key === "Enter") {
e.preventDefault(); // disable new line in single-line mode
}
};
useEffect(() => {
if (props?.widgetValue) {
const isNextLine = props?.widgetValue.includes("\n");
if (isNextLine) {
setIsMultiline(true);
}
}
}, [props?.widgetValue]);
return (
<div style={{ position: "relative", width: "100%" }}>
{isMultiline ? (
<textarea
onBlur={props?.handleInputBlur}
ref={props?.widgetRef}
value={props?.widgetValue ?? ""} //use an empty string "" as a default
onChange={(e) => props?.handleOnchangeTextBox(e)}
onKeyDown={handleKeyDown}
rows={3}
placeholder={t("widgets-name.text")}
// className={`${textInputcls} resize-none outline-none max-h-[100px] `}
style={{
width: "100%",
padding: "12px 40px 12px 12px",
borderRadius: "20px",
border: "1px solid #dcdcdc",
fontSize: "16px",
resize: "none",
outline: "none"
}}
/>
) : (
<input
name="text"
ref={props?.widgetRef}
type="text"
placeholder={t("widgets-name.text")}
value={props?.widgetValue ?? ""} //use an empty string "" as a default
onChange={(e) => props?.handleOnchangeTextBox(e)}
className={`${props?.textInputcls} pr-4`}
onBlur={props?.handleInputBlur}
/>
)}
{!isMultiline && (
<>
<i
data-tooltip-id="toggle-multiline"
className="fa-light fa-file-lines"
size={20}
style={{
position: "absolute",
right: "12px",
top: "50%",
transform: "translateY(-50%)",
color: "#444",
cursor: "pointer"
}}
onClick={() => setIsMultiline((prev) => !prev)}
></i>
<Tooltip id={"toggle-multiline"}>
<span>Toggle Multiline Text</span>
</Tooltip>
</>
)}
</div>
);
}
@@ -1,5 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { NavLink } from "react-router";
const Menu = ({ item, isOpen, closeSidebar }) => {
@@ -7,6 +7,8 @@ const Menu = ({ item, isOpen, closeSidebar }) => {
"OpenSign™";
const drivename = appName === "OpenSign™" ? "OpenSign™" : "";
const { t } = useTranslation();
const { selectedMenu } = useSelector((state) => state.sidebar);
return (
<li key={item.title} role="none" className="my-0.5">
<NavLink
@@ -16,11 +18,9 @@ const Menu = ({ item, isOpen, closeSidebar }) => {
: `/${item.objectId}`
}
className={({ isActive }) =>
`${
isActive ? " bg-base-300 text-base-content" : ""
} flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
`${isActive && selectedMenu ? "bg-base-300 text-base-content" : ""} flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
}
onClick={closeSidebar}
onClick={() => closeSidebar(item.title)}
tabIndex={isOpen ? 0 : -1}
role="menuitem"
>
@@ -7,7 +7,10 @@ import sidebarList, { subSetting } from "../../json/menuJson";
import { useNavigate } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { useWindowSize } from "../../hook/useWindowSize";
import { toggleSidebar } from "../../redux/reducers/sidebarReducer";
import {
setSelectedMenu,
toggleSidebar
} from "../../redux/reducers/sidebarReducer";
const Sidebar = () => {
const { width } = useWindowSize();
@@ -29,6 +32,7 @@ const Sidebar = () => {
}, []);
const closeSidebar = () => {
dispatch(setSelectedMenu(true));
if (width <= 1023) {
dispatch(toggleSidebar(false));
}
@@ -58,10 +62,12 @@ const Sidebar = () => {
};
const toggleSubmenu = (title) => {
dispatch(setSelectedMenu(false));
setSubmenuOpen({ [title]: !submenuOpen[title] });
};
const handleMenuItem = () => {
dispatch(setSelectedMenu(true));
closeSidebar();
setSubmenuOpen({});
};
@@ -1,5 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { NavLink } from "react-router";
const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
@@ -8,6 +8,8 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
const drivename = appName === "OpenSign™" ? "OpenSign™" : "";
const { t } = useTranslation();
const { title, icon, children } = item;
const { selectedMenu } = useSelector((state) => state.sidebar);
return (
<li role="none" className="my-0.5">
<button
@@ -45,11 +47,9 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
: `/${childItem.objectId}`
}
className={({ isActive }) =>
`${
isActive ? "bg-base-300 text-base-content " : ""
} pl-4 flex items-center gap-x-5 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
`${isActive && selectedMenu ? "bg-base-300 text-base-content" : ""} pl-4 flex items-center gap-x-5 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
}
onClick={closeSidebar}
onClick={() => closeSidebar(childItem.title)}
role="menuitem"
tabIndex={submenuOpen ? 0 : -1}
>
+281 -194
View File
@@ -28,6 +28,11 @@ const appName = "OpenSign™";
export const defaultMailBody = `<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>`;
export const defaultMailSubject = `{{sender_name}} has requested you to sign {{document_title}}`;
export const nonPresentMaskCss = (base) => ({
...base,
width: "0px",
height: "0px"
});
export const randomId = (digit = 8) => {
// 1. Grab a cryptographically-secure 32-bit random value
@@ -116,6 +121,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.
*
@@ -447,14 +465,15 @@ export const addWidgetOptions = (
widgetValue
) => {
let defaultOpt;
const id = generateId(6);
if (placeholder) {
const countSameWidget = placeholder?.reduce((count, page) => {
return count + page.pos.filter((item) => item.type === type).length;
}, 0);
const count = countSameWidget + 1;
defaultOpt = { name: `${type}-${count}`, status: "required" };
defaultOpt = { name: `${type}-${id}-${count}`, status: "required" };
} else {
defaultOpt = { name: `${type}-1`, status: "required" };
defaultOpt = { name: `${type}-${id}-1`, status: "required" };
}
switch (type) {
case "signature":
@@ -464,7 +483,10 @@ export const addWidgetOptions = (
case "checkbox":
return { ...defaultOpt, isReadOnly: false, isHideLabel: false };
case textInputWidget:
return { ...defaultOpt, isReadOnly: false };
return {
...defaultOpt,
isReadOnly: false
};
case cellsWidget:
return {
...defaultOpt,
@@ -476,11 +498,20 @@ export const addWidgetOptions = (
case "initials":
return defaultOpt;
case "name":
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "company":
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "job title":
return { ...defaultOpt, defaultValue: widgetValue ? widgetValue : "" };
return {
...defaultOpt,
defaultValue: widgetValue ? widgetValue : ""
};
case "date": {
const dateFormat = signer?.DateFormat
? selectFormat(signer?.DateFormat)
@@ -505,8 +536,7 @@ export const addWidgetOptions = (
return {
...defaultOpt,
values: [],
isReadOnly: false,
isHideLabel: false
isReadOnly: false
};
case textWidget:
return defaultOpt;
@@ -523,14 +553,15 @@ export const addWidgetSelfsignOptions = (
) => {
let defaultOpt;
//condition to handle widgets name field
const id = generateId(6);
if (placeholder) {
const countSameWidget = placeholder?.reduce((count, page) => {
return count + page.pos.filter((item) => item.type === type).length;
}, 0);
const count = countSameWidget + 1;
defaultOpt = { name: `${type}-${count}`, status: "required" };
defaultOpt = { name: `${type}-${id}-${count}`, status: "required" };
} else {
defaultOpt = { name: `${type}-1`, status: "required" };
defaultOpt = { name: `${type}-${id}-1`, status: "required" };
}
switch (type) {
case "signature":
@@ -823,6 +854,7 @@ export const signPdfFun = async (
}
}
};
export const createDocument = async (
template,
placeholders,
@@ -874,6 +906,8 @@ export const createDocument = async (
const RedirectUrl = Doc?.RedirectUrl
? { RedirectUrl: Doc?.RedirectUrl }
: {};
const PenColors =
Doc?.PenColors?.length > 0 ? { PenColors: Doc?.PenColors } : {};
const TemplateId = Doc?.objectId
? {
TemplateId: {
@@ -896,11 +930,7 @@ export const createDocument = async (
className: "contracts_Users",
objectId: extUserId
},
CreatedBy: {
__type: "Pointer",
className: "_User",
objectId: creatorId
},
CreatedBy: { __type: "Pointer", className: "_User", objectId: creatorId },
Signers: signers,
SendinOrder: Doc?.SendinOrder || false,
AutomaticReminders: Doc?.AutomaticReminders || false,
@@ -914,7 +944,8 @@ export const createDocument = async (
...NotifyOnSignatures,
...Bcc,
...RedirectUrl,
...TemplateId
...TemplateId,
...PenColors
};
const remindOnceInEvery = Doc?.RemindOnceInEvery;
const TimeToCompleteDays = parseInt(Doc?.TimeToCompleteDays);
@@ -923,23 +954,27 @@ export const createDocument = async (
if (AutomaticReminders && reminderCount > 15) {
return { status: "error", id: "only-15-reminder-allowed" };
}
const url = `${localStorage.getItem("baseUrl")}classes/contracts_Document`;
const token = {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
try {
const res = await axios.post(
`${localStorage.getItem("baseUrl")}classes/contracts_Document`,
data,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
const res = await axios.post(url, data, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
...token
}
);
});
if (res) {
return { status: "success", id: res.data.objectId, data: res.data };
const result = res.data;
return { status: "success", id: result.objectId, data: result };
}
} catch (err) {
console.log("axois err ", err);
const message =
err?.response?.data?.error || err?.message || "something went wrong.";
console.error("error in create document:", message);
return { status: "error", id: "something-went-wrong-mssg" };
}
}
@@ -1011,7 +1046,8 @@ export const onChangeInput = (
dateFormat,
fontSize,
fontColor,
dateDetails
dateDetails,
textWidgetHeight
) => {
const isSigners = xyPosition.some(
(data) => data.signerPtr || data.Role === "prefill"
@@ -1061,6 +1097,16 @@ export const onChangeInput = (
}
}
};
} else if (
(currentPosition?.type === "text" ||
currentPosition?.type === textInputWidget) &&
textWidgetHeight &&
!value
) {
return {
...position,
Height: textWidgetHeight
};
} else {
return {
...position,
@@ -1111,6 +1157,15 @@ export const onChangeInput = (
}
}
};
} else if (
currentPosition?.type === "text" &&
textWidgetHeight &&
!value
) {
return {
...positionData,
Height: textWidgetHeight
};
} else {
return {
...positionData,
@@ -1579,7 +1634,9 @@ export function onSaveImage(
isAutoSign,
widgetsType,
isApplyAll,
imgUrl
imgUrl,
defaultStampImg,
defaultStampType
) {
// let getIMGWH;
const isSignOrInitials =
@@ -1590,12 +1647,12 @@ export function onSaveImage(
if (position.key === signKey) {
return {
...position,
SignUrl: imgUrl || image.src,
ImageType: image.imgType,
SignUrl: imgUrl || image?.src || defaultStampImg,
ImageType: image?.imgType || defaultStampType,
...(isSignOrInitials && { signatureType: signatureType || "" }),
options: {
...position.options,
response: imgUrl || image.src
response: imgUrl || image?.src || defaultStampImg
}
};
}
@@ -1617,16 +1674,17 @@ export function onSaveImage(
item.type === widgetsType && item.type !== "image"
? {
...item,
SignUrl: image.src,
ImageType: image.imgType,
SignUrl: image?.src || defaultStampImg,
ImageType: image?.imgType || defaultStampType,
options: {
...item.options,
response: image.src
response: image?.src || defaultStampImg
}
}
: item // Otherwise, keep it unchanged
)
}));
return updatedArray;
} else {
return updateXYposition;
@@ -1766,6 +1824,26 @@ export const isBase64 = (str) => {
/^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+)?;base64,[A-Za-z0-9+/=]+$/;
return base64Pattern.test(str);
};
// Break a long word into smaller parts character-by-character
const forceBreakLongWord = (word, width, font, fontSize) => {
const parts = [];
let current = "";
for (const char of word) {
const lineWidth = font.widthOfTextAtSize(current + char, fontSize);
if (lineWidth <= width) {
current += char;
} else {
parts.push(current);
current = char;
}
}
if (current) parts.push(current);
return parts;
};
//function for embed all type widgets in document using pdf-lib
export const embedWidgetsToDoc = async (
widgets,
@@ -2054,41 +2132,59 @@ export const embedWidgetsToDoc = async (
const lines = [];
let currentLine = "";
for (const word of textContent?.split(" ")) {
//get text line width
const lineWidth = font.widthOfTextAtSize(
`${currentLine} ${word}`,
fontSize
);
//check text content line width is less or equal to container width
if (lineWidth <= width) {
currentLine += ` ${word}`;
const words = textContent?.split(" ");
for (let word of words) {
const testLine = currentLine ? currentLine + " " + word : word;
const testWidth = font.widthOfTextAtSize(testLine, fontSize);
if (testWidth <= width) {
currentLine = testLine;
} else {
lines.push(currentLine.trim());
currentLine = `${word}`;
// If single long word
if (font.widthOfTextAtSize(word, fontSize) > width) {
// Break a long word into smaller parts character-by-character
const brokenParts = forceBreakLongWord(
word,
width,
font,
fontSize
);
// push currentLine before breaking word
if (currentLine.trim()) lines.push(currentLine.trim());
// push ALL broken parts in order
brokenParts.forEach((p) => lines.push(p));
currentLine = "";
} else {
// push current line and start new
if (currentLine.trim()) lines.push(currentLine.trim());
currentLine = word;
}
}
}
lines.push(currentLine.trim());
if (currentLine.trim()) lines.push(currentLine.trim());
return lines;
};
// Function to break text into lines based on when user go next line on press enter button
const breakTextIntoLines = (textContent, width) => {
const lines = [];
for (const word of textContent.split("\n")) {
const lineWidth = font.widthOfTextAtSize(`${word}`, fontSize);
const finalLines = [];
const paragraphs = textContent.split("\n"); // preserve user order
for (const para of paragraphs) {
const lineWidth = font.widthOfTextAtSize(para, fontSize);
//checking string length to container width
//if string length is less then container width it means user press enter button
if (lineWidth <= width) {
lines.push(word);
}
//else adjust text content according to width and send it in new line
else {
const newLine = NewbreakTextIntoLines(word, width);
lines.push(...newLine);
// user forced newline
finalLines.push(para);
} else {
// auto wrap paragraph
finalLines.push(...NewbreakTextIntoLines(para, width));
}
}
return lines;
return finalLines;
};
//check if text content have `\n` string it means user press enter to go next line and handle condition
//else auto adjust text content according to container width
@@ -2996,13 +3092,17 @@ export const getDefaultSignature = async (objectId) => {
const defaultInitial = res?.Initials
? await getBase64FromUrl(res?.Initials, true)
: "";
const defaultStamp = res?.Stamp
? await getBase64FromUrl(res?.Stamp, true)
: "";
return {
status: "success",
res: {
id: result?.id,
defaultSignature: defaultSignature,
defaultInitial: defaultInitial
defaultInitial: defaultInitial,
defaultStamp: defaultStamp
}
};
}
@@ -3440,7 +3540,10 @@ export const updateDateWidgetsRes = (
return placeHolders?.map((item) => {
if (item?.signerObjId === signerId || item?.Id === signerId) {
// Sort page number placeholders
const sortedPlaceHolder = [...item.placeHolder]
const placeHolder = Array.isArray(item.placeHolder)
? [...item.placeHolder]
: [];
const sortedPlaceHolder = placeHolder
.sort((a, b) => a.pageNumber - b.pageNumber)
.map((ph) => ({
...ph,
@@ -3872,25 +3975,17 @@ export const sendEmailToSigners = async (
} else {
data = { SendMail: true };
}
try {
await axios.put(
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${
pdfDetails[0]?.objectId
}`,
data,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": sessiontoken
}
}
);
} catch (err) {
console.log("axois err ", err);
}
} catch (e) {
console.log("error", e);
const docUrl = `${localStorage.getItem("baseUrl")}classes/contracts_Document`;
await axios.put(`${docUrl}/${pdfDetails[0]?.objectId}`, data, {
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": sessiontoken
}
});
} catch (error) {
const err = error?.response?.data?.error || error?.message;
console.error("Error while updating doc: ", err);
}
}
return { status: "success" };
@@ -3906,51 +4001,55 @@ export const sendEmailToSigners = async (
* @returns {Promise<File>} - A promise that resolves with a new PNG File.
*/
export function convertJpegToPng(base64Image, filename) {
const arr = base64Image.split(",");
const mimeMatch = arr[0].match(/:(.*?);/);
if (!mimeMatch) throw new Error("Invalid dataURL");
const mime = mimeMatch[1];
const type = mime.split("/")[1];
if (type === "png") {
return base64Image;
} else {
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) u8arr[n] = bstr.charCodeAt(n);
const inputFile = new File([u8arr], `${filename}.${type}`, { type: mime });
return new Promise((resolve, reject) => {
// ensure its JPEG/JPG (you can remove this check if not needed)
if (!/image\/jpe?g/.test(inputFile.type)) {
return reject(new Error("Input must be a JPEG or JPG image"));
}
if (base64Image) {
const arr = base64Image.split(",");
const mimeMatch = arr[0].match(/:(.*?);/);
if (!mimeMatch) throw new Error("Invalid dataURL");
const mime = mimeMatch[1];
const type = mime.split("/")[1];
if (type === "png") {
return base64Image;
} else {
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) u8arr[n] = bstr.charCodeAt(n);
const inputFile = new File([u8arr], `${filename}.${type}`, {
type: mime
});
return new Promise((resolve, reject) => {
// ensure its JPEG/JPG (you can remove this check if not needed)
if (!/image\/jpe?g/.test(inputFile.type)) {
return reject(new Error("Input must be a JPEG or JPG image"));
}
// Read the file as a data URL
const reader = new FileReader();
reader.onerror = () => reject(new Error("Failed to read the file"));
reader.onload = () => {
const img = new Image();
img.onerror = () => reject(new Error("Failed to load image"));
img.onload = () => {
// draw image onto a canvas
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// Read the file as a data URL
const reader = new FileReader();
reader.onerror = () => reject(new Error("Failed to read the file"));
reader.onload = () => {
const img = new Image();
img.onerror = () => reject(new Error("Failed to load image"));
img.onload = () => {
// draw image onto a canvas
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// directly get PNG as base64 data URL
try {
const pngDataUrl = canvas.toDataURL("image/png");
resolve(pngDataUrl);
} catch (err) {
reject(new Error("Failed to convert canvas to PNG"));
}
// directly get PNG as base64 data URL
try {
const pngDataUrl = canvas.toDataURL("image/png");
resolve(pngDataUrl);
} catch (err) {
reject(new Error("Failed to convert canvas to PNG"));
}
};
img.src = reader.result;
};
img.src = reader.result;
};
reader.readAsDataURL(inputFile);
});
reader.readAsDataURL(inputFile);
});
}
}
}
//function is used to get assigned signer's email
@@ -3963,74 +4062,68 @@ export const getSignerEmail = (data, signers) => {
//function is used to delete widgets
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) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
// Find the page inside placeHolder
const currentPage = placeHolder.find((p) => p.pageNumber === pageNumber);
if (!currentPage) return signerPos;
const getPageNumer = getPlaceHolder.filter(
(data) => data.pageNumber === pageNumber
// Remove widget from the selected page
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) {
const getXYdata = getPageNumer[0].pos.filter((data) => data.key !== key);
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;
}
}
}
return signerPos.map((s) =>
s.Id === Id ? { ...s, placeHolder: updatedPlaceHolder } : s
);
}
// -------------------------------------------------------
// 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
export const getDefaultDate = (dateStr, format) => {
@@ -4049,17 +4142,6 @@ export const getDefaultDate = (dateStr, format) => {
//function to get default format
export const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
export function generateId(length) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
//function to handle widget background color
export const handleBackground = (data, isNeedSign, uniqueId) => {
if (data) {
@@ -4076,3 +4158,8 @@ export const handleBackground = (data, isNeedSign, uniqueId) => {
return "rgba(203, 233, 237, 0.69)";
}
};
export function getBase64MimeType(base64) {
const match = base64.match(/^data:(image\/[a-zA-Z0-9+.-]+);base64,/);
return match ? match[1] : null;
}
+20
View File
@@ -533,3 +533,23 @@ export default function reportJson(id) {
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
}
];
+4 -4
View File
@@ -5,19 +5,19 @@ export const templateReportTour = [
selector: "[data-tut=reactourFirst]",
content: <Trans i18nKey="tour-mssg.report-1" />,
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "11px" }
},
{
selector: "[data-tut=reactourSecond]",
content: <Trans i18nKey="tour-mssg.redirect" />,
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: "[data-tut=tourbulksend]",
content: <Trans i18nKey="tour-mssg.bulksend" />,
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: "[data-tut=reactourThird]",
@@ -42,6 +42,6 @@ export const templateReportTour = [
</Trans>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
+30 -31
View File
@@ -1,4 +1,7 @@
import { useState, useEffect } from "react";
import {
nonPresentMaskCss
} from "../constant/Utils";
import Header from "../components/Header";
import Footer from "../components/Footer";
import Sidebar from "../components/sidebar/Sidebar";
@@ -6,7 +9,9 @@ import Tour from "../primitives/Tour";
import axios from "axios";
import { useDispatch, useSelector } from "react-redux";
import Parse from "parse";
import { useNavigate, Outlet } from "react-router";
import {
Outlet
} from "react-router";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
import { sessionStatus } from "../redux/reducers/userReducer";
@@ -16,9 +21,8 @@ const HomeLayout = () => {
const appName =
"OpenSign™";
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const dispatch = useDispatch();
const arr = useSelector((state) => state.TourSteps);
const tourArr = useSelector((state) => state.TourSteps);
const isValidSession = useSelector((state) => state.user.isValidSession);
const [isLoader, setIsLoader] = useState(true);
const [isCloseBtn, setIsCloseBtn] = useState(true);
@@ -55,7 +59,7 @@ const HomeLayout = () => {
dispatch(sessionStatus(true));
}
} catch (error) {
console.log("catch");
console.error("error in authentication:", error?.message);
// Session token is invalid or there was an error
dispatch(sessionStatus(false));
}
@@ -68,40 +72,36 @@ const HomeLayout = () => {
useEffect(() => {
if (arr && arr.length > 0) {
if (tourArr && tourArr.length > 0) {
handleDynamicSteps();
} else {
setIsTour(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [arr]);
}, [tourArr]);
const handleDynamicSteps = () => {
const github = "https://github.com/OpenSignLabs/OpenSign";
if (arr && arr.length > 0) {
// const resArr = arr;
const resArr = arr.map((obj, index) => {
if (arr.length - 1 === index) {
return { ...obj };
if (tourArr && tourArr.length > 0) {
const resArr = tourArr.map((obj, index) => {
if (tourArr.length - 1 === index) {
return obj;
} else {
return {
...obj,
actions: () => {
setIsCloseBtn(false);
}
};
return { ...obj, actions: () => setIsCloseBtn(false) };
}
});
setTourConfigs([
{
selector: '[data-tut="nonpresentmask"]',
content: t("tour-mssg.home-layout-1"),
position: "center"
position: "center",
styles: { fontSize: "13px", maskArea: nonPresentMaskCss }
},
{
selector: '[data-tut="tourbutton"]',
content: t("tour-mssg.home-layout-2"),
position: "top"
position: "top",
styles: { fontSize: "13px" }
},
...resArr,
{
@@ -122,20 +122,19 @@ const HomeLayout = () => {
</p>
</div>
),
position: "center"
position: "center",
styles: { fontSize: "13px", maskArea: nonPresentMaskCss }
}
]);
checkTourStatus();
}
};
const closeTour = async () => {
// console.log("closeTour");
setIsTour(false);
const serverUrl = localStorage.getItem("baseUrl");
const appId = localStorage.getItem("parseAppId");
const json = JSON.parse(localStorage.getItem("Extand_Class"));
const extUserId = json && json.length > 0 && json[0].objectId;
// console.log("extUserId ", extUserId)
let updatedTourStatus = [];
if (tourStatusArr.length > 0) {
@@ -209,15 +208,15 @@ const HomeLayout = () => {
</div>
</main>
</div>
<Tour
onRequestClose={closeTour}
steps={tourConfigs}
isOpen={isTour}
disableKeyboardNavigation={["esc"]}
// disableInteraction={true}
scrollOffset={-100}
showCloseButton={isCloseBtn}
/>
{isTour && (
<Tour
onRequestClose={closeTour}
steps={tourConfigs}
isOpen={isTour}
// scrollOffset={-100}
showCloseButton={isCloseBtn}
/>
)}
</>
)}
</div>
+2 -1
View File
@@ -42,7 +42,8 @@ const Dashboard = () => {
return {
selector: `[data-tut=${col.widget.data.tourSection}]`,
content: t(`tour-mssg.${col.widget.label}`),
position: "top"
position: "top",
styles: { fontSize: "13px" }
// style: { backgroundColor: "#abd4d2" },
};
});
+156 -78
View File
@@ -46,7 +46,8 @@ function Form() {
}
const Forms = (props) => {
const appName = "OpenSign™";
const appName =
"OpenSign™";
const { t } = useTranslation();
const abortController = new AbortController();
const inputFileRef = useRef(null);
@@ -69,7 +70,7 @@ const Forms = (props) => {
NotifyOnSignatures: "",
Bcc: [],
RedirectUrl: "",
AllowModifications: false
AllowModifications: false,
});
const [fileupload, setFileUpload] = useState("");
const [selectedFiles, setSelectedFiles] = useState([]);
@@ -83,6 +84,8 @@ const Forms = (props) => {
const [isCorrectPass, setIsCorrectPass] = useState(true);
const [isAdvanceOpt, setIsAdvanceOpt] = useState(false);
const [bcc, setBcc] = useState([]);
const pensList = ["blue", "red", "black"];
const [selectedColors, setSelectedColors] = useState(pensList);
const handleStrInput = (e) => {
setIsCorrectPass(true);
@@ -100,7 +103,8 @@ const Forms = (props) => {
extUserData?.IsTourEnabled === false
? "false"
: "true";
const fileSize = maxFileSize;
const fileSize =
maxFileSize;
useEffect(() => {
handleReset();
return () => abortController.abort();
@@ -112,12 +116,12 @@ const Forms = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const initializeValues = async () => {
setFormData((obj) => ({
...obj,
NotifyOnSignatures: true,
SendinOrder: sendinorder,
IsTourEnabled: istourenabled
}));
setFormData((obj) => ({
...obj,
NotifyOnSignatures: true,
SendinOrder: sendinorder,
IsTourEnabled: istourenabled
}));
};
// `removeFile` is used to reset progress, percentage and remove file if exists
@@ -276,7 +280,8 @@ const Forms = (props) => {
setfileload(false);
removeFile(e);
console.log("err in docx to pdf ", err);
const error = t("docx-error");
const error =
t("docx-error");
if (err?.code === 209) {
dispatch(sessionStatus(false));
} else {
@@ -305,25 +310,25 @@ const Forms = (props) => {
const name = generatePdfName(16);
const pdfName = `${name}.pdf`;
let uploadedUrl = "";
const parseFile = new Parse.File(
pdfName,
[...pdfBytes],
"application/pdf"
);
const response = await parseFile.save({
progress: (progressValue, loaded, total) => {
if (progressValue !== null) {
const percentCompleted = Math.round((loaded * 100) / total);
setpercentage(percentCompleted);
const parseFile = new Parse.File(
pdfName,
[...pdfBytes],
"application/pdf"
);
const response = await parseFile.save({
progress: (progressValue, loaded, total) => {
if (progressValue !== null) {
const percentCompleted = Math.round((loaded * 100) / total);
setpercentage(percentCompleted);
}
}
});
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
uploadedUrl = fileRes.url;
}
}
});
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
uploadedUrl = fileRes.url;
}
}
if (uploadedUrl) {
const tenantId = localStorage.getItem("TenantId");
const userId = extUserData?.UserId?.objectId;
@@ -339,6 +344,7 @@ const Forms = (props) => {
setSelectedFiles([]);
}
} catch (error) {
console.error("error ", error);
if (error?.code === 209) {
dispatch(sessionStatus(false));
} else {
@@ -385,13 +391,16 @@ const Forms = (props) => {
object.set("Description", formData?.Description);
object.set("Note", formData?.Note);
if (props.title === "Request Signatures") {
if (
extUserData?.TenantId?.RequestBody &&
extUserData?.TenantId?.RequestSubject
) {
object.set("RequestBody", extUserData?.TenantId?.RequestBody);
object.set("RequestSubject", extUserData?.TenantId?.RequestSubject);
}
if (
extUserData?.TenantId?.RequestBody &&
extUserData?.TenantId?.RequestSubject
) {
object.set("RequestBody", extUserData?.TenantId?.RequestBody);
object.set(
"RequestSubject",
extUserData?.TenantId?.RequestSubject
);
}
}
if (props.title !== "Sign Yourself") {
const isChecked = formData.SendinOrder === "false" ? false : true;
@@ -410,11 +419,13 @@ const Forms = (props) => {
object.set("RemindOnceInEvery", remindOnceInEvery);
object.set("IsTourEnabled", isTourEnabled);
object.set("TimeToCompleteDays", TimeToCompleteDays);
object.set("AllowModifications", false);
object.set("IsEnableOTP", false);
if (formData.NotifyOnSignatures !== undefined) {
object.set("NotifyOnSignatures", formData.NotifyOnSignatures);
}
object.set("PenColors", selectedColors);
object.set("AllowModifications", false);
object.set("IsEnableOTP", false);
if (formData.NotifyOnSignatures !== undefined) {
object.set("NotifyOnSignatures", formData.NotifyOnSignatures);
}
if (formData?.RedirectUrl) {
object.set("RedirectUrl", formData.RedirectUrl);
}
@@ -449,11 +460,12 @@ const Forms = (props) => {
if (res) {
setSigners([]);
setBcc([]);
setSelectedColors(pensList);
setFolder({ ObjectId: "", Name: "" });
const notifySign =
extUserData?.NotifyOnSignatures !== undefined
? extUserData?.NotifyOnSignatures
: true;
extUserData?.NotifyOnSignatures !== undefined
? extUserData?.NotifyOnSignatures
: true;
setFormData({
Name: "",
Description: "",
@@ -471,7 +483,7 @@ const Forms = (props) => {
IsEnableOTP: "false",
IsTourEnabled: istourenabled,
RedirectUrl: "",
AllowModifications: false
AllowModifications: false,
});
setFileUpload("");
setSelectedFiles([]);
@@ -531,11 +543,12 @@ const Forms = (props) => {
setIsReset(true);
setSigners([]);
setBcc([]);
setSelectedColors(pensList);
setFolder({ ObjectId: "", Name: "" });
const notifySign =
extUserData?.NotifyOnSignatures !== undefined
? extUserData?.NotifyOnSignatures
: true;
extUserData?.NotifyOnSignatures !== undefined
? extUserData?.NotifyOnSignatures
: true;
let obj = {
Name: "",
Description: "",
@@ -553,7 +566,7 @@ const Forms = (props) => {
IsTourEnabled: istourenabled,
NotifyOnSignatures: notifySign,
RedirectUrl: "",
AllowModifications: false
AllowModifications: false,
};
setFormData(obj);
removeFile();
@@ -575,27 +588,35 @@ const Forms = (props) => {
const name = generatePdfName(16);
const pdfFile = await decryptPdf(formData?.file, formData?.password);
setIsDecrypting(false);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
const parseFile = new Parse.File(name, [...flatPdf], "application/pdf");
await parseFile.save({
progress: (progressValue, loaded, total) => {
if (progressValue !== null) {
const percentCompleted = Math.round((loaded * 100) / total);
setpercentage(percentCompleted);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
const parseFile = new Parse.File(name, [...flatPdf], "application/pdf");
await parseFile.save({
progress: (progressValue, loaded, total) => {
if (progressValue !== null) {
const percentCompleted = Math.round((loaded * 100) / total);
setpercentage(percentCompleted);
}
}
});
// Retrieve the URL of the uploaded file
if (parseFile.url()) {
const fileRes = await getSecureUrl(parseFile.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
removeFile();
const title = generateTitleFromFilename(formData?.file?.name);
setFormData((obj) => ({ ...obj, password: "", Name: title }));
SaveFileSize(size, fileRes?.url, tenantId, userId);
return fileRes.url;
} else {
removeFile();
setFormData((prev) => ({ ...prev, password: "" }));
setIsDecrypting(false);
if (inputFileRef.current) {
inputFileRef.current.value = ""; // Set file input value to empty string
}
}
}
});
// Retrieve the URL of the uploaded file
if (parseFile.url()) {
const fileRes = await getSecureUrl(parseFile.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
removeFile();
const title = generateTitleFromFilename(formData?.file?.name);
setFormData((obj) => ({ ...obj, password: "", Name: title }));
SaveFileSize(size, fileRes?.url, tenantId, userId);
return fileRes.url;
} else {
removeFile();
setFormData((prev) => ({ ...prev, password: "" }));
@@ -604,14 +625,6 @@ const Forms = (props) => {
inputFileRef.current.value = ""; // Set file input value to empty string
}
}
} else {
removeFile();
setFormData((prev) => ({ ...prev, password: "" }));
setIsDecrypting(false);
if (inputFileRef.current) {
inputFileRef.current.value = ""; // Set file input value to empty string
}
}
} catch (err) {
removeFile();
if (err?.code === 209) {
@@ -644,6 +657,20 @@ const Forms = (props) => {
setFormData((obj) => ({ ...obj, NotifyOnSignatures: value }));
};
const handleColorsChange = (color) => {
setSelectedColors((prev) => {
// If user tries to uncheck the last remaining color block it
if (prev.length === 1 && prev.includes(color)) {
return prev;
}
// Normal toggle behavior
return prev.includes(color)
? prev.filter((c) => c !== color) // remove
: [...prev, color]; // add
});
};
return (
<div
className={`${isSubmit ? "" : "shadow-md rounded-box my-[2px] p-3 bg-base-100 text-base-content"}`}
@@ -1054,7 +1081,10 @@ const Forms = (props) => {
{isAdvanceOpt && (
<div
style={{
height: props.title === "New Template" ? "100px" : "280px"
height:
props.title === "New Template"
? "100px"
: "280px"
}}
className="w-[1px] bg-gray-300 m-auto hidden md:inline-block"
></div>
@@ -1163,7 +1193,11 @@ const Forms = (props) => {
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className={`flex items-center gap-2 ml-2 mb-1`}>
<div
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
@@ -1172,7 +1206,11 @@ const Forms = (props) => {
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className={`flex items-center gap-2 ml-2 mb-1`}>
<div
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
@@ -1196,6 +1234,46 @@ const Forms = (props) => {
onInput={(e) => e.target.setCustomValidity("")}
/>
</div>
{props.title !== "Sign Yourself" && (
<div className="text-xs mt-3">
<label htmlFor="penColors">
{t("pen-colors")}
<a
data-tooltip-id="pen-colors-tooltip"
className="ml-1"
>
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<Tooltip id="pen-colors-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("pen-colors")}</p>
<div>{t("pen-colors-help")}</div>
</div>
</Tooltip>
</label>
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
{pensList.map((color) => (
<div
key={color}
className="flex flex-row gap-[5px] items-center"
>
<input
className="mr-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
name="penColors"
checked={selectedColors.includes(color)}
onChange={() => handleColorsChange(color)}
/>
<div className="hover:underline underline-offset-2 cursor-default capitalize">
{color}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
+1 -1
View File
@@ -463,7 +463,7 @@ function GuestLogin() {
e.preventDefault();
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
? t("hide-optional-details")
+116 -20
View File
@@ -13,6 +13,14 @@ import Alert from "../primitives/Alert";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
import { sanitizeFileName } from "../utils";
const COLOR_CLASS = {
blue: "text-blue-600",
red: "text-red-600",
black: "text-black",
white: "text-white"
};
const ManageSign = () => {
const { t } = useTranslation();
const [penColor, setPenColor] = useState("blue");
@@ -29,10 +37,12 @@ const ManageSign = () => {
const [isInitials, setIsInitials] = useState(false);
const [id, setId] = useState("");
const [imgInitials, setImgInitials] = useState("");
const [stamp, setStamp] = useState();
const canvasRef = useRef(null);
const imageRef = useRef(null);
const initailsRef = useRef(null);
const imgInitialsRef = useRef(null);
const stampRef = useRef(null);
useEffect(() => {
fetchUserSign();
// eslint-disable-next-line
@@ -63,6 +73,9 @@ const ManageSign = () => {
setIsInitials(true);
setImgInitials(res?.Initials);
}
if (res && res?.Stamp) {
setStamp(res?.Stamp);
}
} else {
if (User?.get("name")) {
const sanitizename = generateTitleFromFilename(User?.get("name"));
@@ -171,11 +184,24 @@ const ManageSign = () => {
} else {
initialsUrl = imgInitials;
}
const isStampUrl = stamp?.includes("https") || stamp?.includes("http");
let stampFile;
if (stamp && !isStampUrl) {
stampFile = base64StringtoFile(stamp, `${replaceSpace}_stamp`);
}
let stampUrl;
if (stampFile && !isStampUrl) {
stampUrl = await uploadFile(stampFile);
} else {
stampUrl = stamp;
}
if (imgUrl) {
await saveEntry({
name: signName,
url: imgUrl,
initialsUrl: initialsUrl
initialsUrl: initialsUrl,
stampUrl: stampUrl
});
}
}
@@ -232,7 +258,8 @@ const ManageSign = () => {
userId: User,
initials: obj.initialsUrl,
id: id,
title: obj.name
title: obj.name,
stamp: obj?.stampUrl
});
setIsAlert({ type: "success", message: t("signature-saved-alert") });
return res;
@@ -274,6 +301,28 @@ const ManageSign = () => {
setIsValue(false);
}
};
const handleClearStamp = () => {
if (stampRef.current) {
stampRef.current.clear();
}
if (stampRef.current) {
stampRef.current.value = "";
}
setStamp("");
if (image) {
setIsValue(true);
}
};
const onStampChange = async (event) => {
if (event.target.files && event.target.files[0]) {
const file = event.target.files[0];
const base64Img = await toDataUrl(file);
setStamp(base64Img);
setIsValue(true);
} else {
setStamp("");
}
};
return (
<div className="relative h-full bg-base-100 text-base-content flex shadow-md rounded-box overflow-auto">
{isLoader && (
@@ -287,9 +336,9 @@ const ManageSign = () => {
<div className="text-[20px] font-semibold m-[10px] md:m-0 mb-2">
{t("my-signature")}
</div>
<div className="flex flex-col md:flex-row gap-0 md:gap-[12px]">
<div className="flex flex-col md:flex-row gap-2 md:gap-5">
<div className="relative">
<span className="font-medium select-none flex mb-[10px] pl-[10px]">
<span className="font-medium select-none flex mb-[10px]">
{t("signature")}
</span>
<input
@@ -300,7 +349,7 @@ const ManageSign = () => {
ref={imageRef}
hidden
/>
<div className="relative">
<div>
{image ? (
<div className="mysignatureCanvas relative border-[2px] border-[#888] rounded-box overflow-hidden">
<img
@@ -323,23 +372,24 @@ const ManageSign = () => {
dotSize={1}
/>
)}
<div className="penContainerDefault flex flex-row justify-between">
<div className="flex flex-row justify-between w-full">
<div>
{!image && (
<div className="flex flex-row gap-1.5 m-[5px]">
{allColor.map((data, key) => {
<div className="flex flex-row gap-1 m-[5px]">
{allColor.map((color, key) => {
const selected = penColor === color;
return (
<i
key={key}
onClick={() => setPenColor(allColor[key])}
className={`border-b-[2px] ${key === 0 && penColor === "blue" ? "border-blue-600" : key === 1 && penColor === "red" ? "border-red-500" : key === 2 && penColor === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
onClick={() => setPenColor(color)}
className={`${COLOR_CLASS[color] ?? ""} ${selected ? "border-current" : "border-white"} cursor-pointer border-b-[2px] pb-[2px] text-[16px] fa-light fa-pen-nib`}
></i>
);
})}
</div>
)}
</div>
<div className="flex flex-row gap-2 text-sm md:text-base mr-1">
<div className="flex flex-row gap-2 text-sm pr-2">
<div
type="button"
className="op-link"
@@ -364,7 +414,8 @@ const ManageSign = () => {
)}
</div>
</div>
<div className="relative">
<div>
<span className="font-medium select-none flex mb-[10px] pl-[10px]">
{t("initials")}
</span>
@@ -396,26 +447,29 @@ const ManageSign = () => {
dotSize={1}
/>
)}
<div className="flex flex-row justify-between w-[183px]">
<div
className={`${isInitials ? "justify-between" : ""} flex flex-row gap-1`}
>
<div>
{!isInitials && (
<div className="flex flex-row gap-1.5 m-[5px]">
{allColor.map((data, key) => {
<div className="flex flex-row gap-1 m-[5px]">
{allColor.map((color, key) => {
const selected = initialPen === color;
return (
<i
key={key}
onClick={() => setInitialPen(allColor[key])}
className={`border-b-[2px] ${key === 0 && initialPen === "blue" ? "border-blue-600" : key === 1 && initialPen === "red" ? "border-red-500" : key === 2 && initialPen === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
onClick={() => setInitialPen(color)}
className={`${COLOR_CLASS[color] ?? ""} ${selected ? "border-current" : "border-white"} cursor-pointer border-b-[2px] pb-[2px] text-[16px] fa-light fa-pen-nib`}
></i>
);
})}
</div>
)}
</div>
<div className="flex flex-row gap-1">
<div className="flex flex-row gap-1.5 text-sm">
<div
type="button"
className="op-link text-sm md:text-base mr-1"
className="op-link"
onClick={() => handleUploadInitials()}
>
{t("upload")}
@@ -423,7 +477,7 @@ const ManageSign = () => {
<div>
<div
type="button"
className="op-link text-sm md:text-base mr-1"
className="op-link"
onClick={() => handleClearInitials()}
>
{t("clear")}
@@ -433,6 +487,48 @@ const ManageSign = () => {
</div>
</div>
</div>
<div>
<span className="font-medium select-none flex mb-[10px] pl-[10px]">
Stamp
</span>
<div
onClick={() => stampRef.current.click()}
className="mystampCanvas opensigndark:bg-[#1f2937] cursor-pointer border-[2px] opensigncss:border-[#888] opensigndark:border-[#4b5563] rounded-box flex flex-col overflow-hidden w-full h-full aspect-[5/2] justify-center items-center mr-2"
>
{stamp ? (
<img
alt="signature"
src={stamp}
className="w-full h-full object-contain"
/>
) : (
<>
<input
onChange={onStampChange}
type="file"
className="filetype"
accept="image/png,image/jpeg"
hidden
ref={stampRef}
/>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[28px]"></i>
<div className="text-[15px] text-base-content">
{t("upload")}
</div>
</>
)}
</div>
<div className="flex justify-end pr-3">
<div
type="button"
className="op-link text-sm"
onClick={() => handleClearStamp()}
>
{t("clear")}
</div>
</div>
</div>
</div>
<div className="pt-3 ml-2 md:ml-0">
<button
+14 -16
View File
@@ -20,7 +20,7 @@ const DriveBody = lazyWithRetry(
() => import("../components/opensigndrive/DriveBody")
);
const dropdowncss =
"absolute right-0 py-2 text-[1rem] text-left bg-white border-[1px] border-gray-300 min-w-1 hover:bg-gray-300 rounded-[0.25rem] z-[800]";
"absolute right-0 py-2 text-[1rem] text-left bg-base-100 border-[1px] border-gray-300 min-w-1 rounded-[0.25rem] z-[800]";
const AppLoader = () => {
return (
<div className="h-[100vh] flex justify-center items-center">
@@ -98,7 +98,7 @@ function Opensigndrive() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourSecond"]',
@@ -109,7 +109,7 @@ function Opensigndrive() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourThird"]',
@@ -120,7 +120,7 @@ function Opensigndrive() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourForth"]',
@@ -131,7 +131,7 @@ function Opensigndrive() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const getDetails = async () => {
@@ -161,7 +161,7 @@ function Opensigndrive() {
/>
),
position: "bottom",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourSixth"]',
@@ -172,7 +172,7 @@ function Opensigndrive() {
/>
),
position: "bottom",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourSeventh"]',
@@ -183,7 +183,7 @@ function Opensigndrive() {
/>
),
position: "bottom",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
let newTour = [...tourConfigs, ...addMoreTour];
@@ -688,7 +688,7 @@ function Opensigndrive() {
</div>
{/* Mobile search toggle */}
<button
className="md:hidden p-2 flex justify-center items-center focus:outline-none rounded-md hover:bg-gray-200 text-[18px]"
className="md:hidden p-2 flex justify-center items-center focus:outline-none rounded-md hover:bg-base-300 text-[18px]"
aria-label="Search"
onClick={() => setMobileSearchOpen((open) => !open)}
>
@@ -699,7 +699,7 @@ function Opensigndrive() {
</button>
<div
id="folder-menu"
className={`${isOptions ? "dropdown show dropDownStyle" : "dropdown"} hidden md:block cursor-pointer hover:bg-gray-200 p-2 rounded-md`}
className={`${isOptions ? "dropdown show dropDownStyle" : "dropdown"} hidden md:block cursor-pointer hover:bg-base-300 p-2 rounded-md`}
onClick={handleFolderOptions}
>
<div data-tut="reactourSecond">
@@ -749,7 +749,7 @@ function Opensigndrive() {
>
<div
data-tut="reactourThird"
className="cursor-pointer flex flex-row items-center justify-center p-2 hover:bg-gray-200 rounded-md"
className="cursor-pointer flex flex-row items-center justify-center p-2 hover:bg-base-300 rounded-md"
data-toggle="dropdown"
>
<i
@@ -767,9 +767,7 @@ function Opensigndrive() {
</span>
</div>
<div
className={
isShowSort ? "dropdown-menu show" : "dropdown-menu"
}
className={`${isShowSort ? "dropdown-menu show" : "dropdown-menu"} bg-base-100 border-[1px] border-gray-300`}
aria-labelledby="dropdownMenuButton"
aria-expanded={isShowSort ? "true" : "false"}
>
@@ -817,7 +815,7 @@ function Opensigndrive() {
</div>
</div>
<div
className="cursor-pointer p-2 hover:bg-gray-200 rounded-md flex justify-center items-center"
className="cursor-pointer p-2 hover:bg-base-300 rounded-md flex justify-center items-center"
data-tut="reactourForth"
onClick={handleViewOption}
>
@@ -833,7 +831,7 @@ function Opensigndrive() {
onClick={() => setIsOptions(!isOptions)}
>
<div
className="p-3 flex items-center justify-center cursor-pointer rounded-md hover:bg-gray-200"
className="p-3 flex items-center justify-center cursor-pointer rounded-md hover:bg-base-300"
data-tut="reactourSecond"
>
<i
+44 -14
View File
@@ -14,7 +14,8 @@ import {
setDefaultSignImg,
resetWidgetState,
setPrefillImg,
setTypedSignFont
setTypedSignFont,
setMyStamp
} from "../redux/reducers/widgetSlice";
import {
contractDocument,
@@ -155,12 +156,10 @@ function PdfRequestFiles(
const [assignedWidgetId, setAssignedWidgetId] = useState([]);
const [showSignPagenumber, setShowSignPagenumber] = useState([]);
const [owner, setOwner] = useState({});
const [tenant, setTenant] = useState({});
const [tenantMailTemplate, setTenantMailTemplate] = useState({
body: "",
subject: ""
});
const [isOptionalDetails, setIsOptionalDetails] = useState(false);
const isMobile = window.innerWidth < 767;
let isGuestSignFlow = false;
let sendmail;
@@ -245,7 +244,6 @@ function PdfRequestFiles(
const filterSignTypes = signatureType?.filter(
(x) => x.enabled === true
);
setTenant(tenantDetails || {});
if (tenantDetails?.RequestBody) {
setTenantMailTemplate({
body: tenantDetails?.RequestBody,
@@ -409,6 +407,35 @@ function PdfRequestFiles(
} else {
const obj = documentData?.[0];
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 = [];
@@ -503,6 +530,7 @@ function PdfRequestFiles(
const initials = defaultSignRes?.res?.defaultInitial || "";
dispatch(setDefaultSignImg(sign));
dispatch(setMyInitial(initials));
dispatch(setMyStamp(defaultSignRes?.res?.defaultStamp));
} else {
dispatch(setSaveSignCheckbox({ isVisible: true }));
}
@@ -729,7 +757,7 @@ function PdfRequestFiles(
docId,
contactId,
objectId,
widgets
widgets,
);
if (resSign && resSign.status === "success") {
dispatch(setTypedSignFont("Fasthand"));
@@ -1074,7 +1102,7 @@ function PdfRequestFiles(
? t("signature-validate-alert", { minRequiredCount })
: t("signature-validate-alert-2"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
//function for get pdf page details
@@ -1231,7 +1259,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourFirst"]',
@@ -1243,7 +1271,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="pdfArea"]',
@@ -1255,7 +1283,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="pdftools"]',
@@ -1267,7 +1295,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourFifth"]',
@@ -1279,7 +1307,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const signedByStep = {
@@ -1292,7 +1320,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
};
//checking if signed by user component exist then add signed by tour step
const signedBy =
@@ -1311,7 +1339,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
};
//checking if AllowModifications is true then add allow widgets panel tour step
const allModifyWidgets = {
@@ -1324,7 +1352,7 @@ function PdfRequestFiles(
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
};
//handle signed by panel index if signed by exist then 2 else 1 to add tour step
@@ -1794,6 +1822,7 @@ function PdfRequestFiles(
!alreadySign &&
requestSignTourFunction()}
<Tour
key={`tour-min-required-${minRequiredCount || 0}`}
showNumber={false}
showNavigation={false}
showNavigationNumber={false}
@@ -2195,6 +2224,7 @@ function PdfRequestFiles(
setUniqueId={setUniqueId}
signatureTypes={signatureType}
allowCellResize={false}
penColors={pdfDetails?.[0]?.PenColors}
/>
)}
<DownloadPdfZip
+29 -20
View File
@@ -75,7 +75,6 @@ function PlaceHolderSign() {
const prefillImg = useSelector((state) => state.widget.prefillImg);
const isShowModal = useSelector((state) => state.widget.isShowModal);
const editorRef = useRef();
const { state } = useLocation();
const navigate = useNavigate();
const [defaultBody, setDefaultBody] = useState("");
const [defaultSubject, setDefaultSubject] = useState("");
@@ -1152,14 +1151,14 @@ function PlaceHolderSign() {
className="flex flex-row items-center op-link op-link-primary"
>
<i className="fa-light fa-copy" />
<span className=" hidden md:block ml-1 ">{t("copy-link")}</span>
<span className="hidden md:block ml-1">{t("copy-link")}</span>
</button>
<ShareButton
title={t("sign-url")}
text={t("sign-url")}
url={data.url}
>
<i className="fa-light fa-share-from-square op-link data-[theme=opensigndark]:op-link-primary data-[theme=opensigncss]:op-link-secondary no-underline"></i>
<i className="fa-light fa-share-from-square op-link opensigncss:op-link-secondary opensigndark:op-link-primary no-underline"></i>
</ShareButton>
</div>
</div>
@@ -1372,7 +1371,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="recipientArea"]',
@@ -1384,7 +1383,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="addRecipient"]',
@@ -1396,7 +1395,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="addWidgets"]',
@@ -1408,7 +1407,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="pdfArea"]',
@@ -1420,7 +1419,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="pdftools"]',
@@ -1432,7 +1431,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="headerArea"]',
@@ -1444,7 +1443,7 @@ function PlaceHolderSign() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
@@ -1617,7 +1616,7 @@ function PlaceHolderSign() {
{},
fontSize: textSize || 12,
fontColor: textColor || "black",
isReadOnly: defaultdata?.isReadOnly || false
isReadOnly: defaultdata?.isReadOnly || false,
}
};
} else if (position.type === cellsWidget) {
@@ -1637,17 +1636,20 @@ function PlaceHolderSign() {
{},
fontSize: textSize || 12,
fontColor: textColor || "black",
isReadOnly: defaultdata?.isReadOnly || false
isReadOnly: defaultdata?.isReadOnly || false,
}
};
}
else if (["signature"].includes(position.type)) {
else if (["signature", "initials"].includes(position.type)) {
return {
...position,
options: {
...position.options,
name: defaultdata.name,
hint: defaultdata?.hint || ""
hint: defaultdata?.hint || "",
...(defaultdata?.penColors?.length > 0 && {
penColors: defaultdata?.penColors
})
}
};
} else {
@@ -1660,7 +1662,7 @@ function PlaceHolderSign() {
defaultValue: defaultdata.defaultValue,
hint: defaultdata?.hint || "",
fontSize: textSize || 12,
fontColor: textColor || "black"
fontColor: textColor || "black",
}
};
}
@@ -1743,11 +1745,14 @@ function PlaceHolderSign() {
}
};
const handleLinkUser = (id) => {
setRoleName("");
setIsAddUser({ [id]: true });
};
//`handleAddUser` function to use add new user
const handleAddUser = (data, signerObjId) => {
const id = signerObjId ? signerObjId : uniqueId;
setRoleName("");
setCurrWidgetsDetails({});
if (isAddSigner) {
handleAddNewRecipients(data);
} else {
@@ -1841,7 +1846,7 @@ function PlaceHolderSign() {
selector: '[data-tut="assignSigner"]',
content: t("attach-signer-tour"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const prefillWidgetTour = [
@@ -1849,7 +1854,7 @@ function PlaceHolderSign() {
selector: '[data-tut="IsSigned"]',
content: t("text-field-tour"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const signatureWidgetTour = [
@@ -1857,7 +1862,7 @@ function PlaceHolderSign() {
selector: '[data-tut="isSignatureWidget"]',
content: t("signature-field-widget", { signersName }),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
@@ -1960,6 +1965,10 @@ function PlaceHolderSign() {
const RedirectUrl = updateDocument?.[0]?.RedirectUrl
? { RedirectUrl: updateDocument?.[0]?.RedirectUrl }
: {};
const penColors =
updateDocument?.[0]?.PenColors?.length > 0
? { PenColors: updateDocument?.[0]?.PenColors }
: {};
const data = {
...(updateDocument?.[0]?.URL ? { URL: updateDocument?.[0]?.URL } : {}),
Name: updateDocument?.[0]?.Name || "",
@@ -1976,6 +1985,7 @@ function PlaceHolderSign() {
: false,
TimeToCompleteDays:
parseInt(updateDocument?.[0]?.TimeToCompleteDays) || 15,
...penColors,
...Bcc,
...RedirectUrl
};
@@ -2014,8 +2024,6 @@ function PlaceHolderSign() {
onRequestClose={closeTour}
steps={tourConfig}
isOpen={placeholderTour}
rounded={5}
closeWithMask={false}
/>
<Tour
onRequestClose={() => setSignerExistModal(false)}
@@ -2486,6 +2494,7 @@ function PlaceHolderSign() {
isSave={true}
setUniqueId={setUniqueId}
signatureTypes={signatureType}
penColors={pdfDetails?.[0]?.PenColors}
/>
)}
<ModalUi
-4
View File
@@ -22,8 +22,6 @@ const Preferences = () => {
const appName =
"OpenSign™";
const { t } = useTranslation();
const Extand_Class = localStorage.getItem("Extand_Class");
const extClass = Extand_Class && JSON.parse(Extand_Class);
const [isalert, setIsAlert] = useState({ type: "success", msg: "" });
const [isTopLoader, setIsTopLoader] = useState(false);
const [isLoader, setIsLoader] = useState(false);
@@ -44,7 +42,6 @@ const Preferences = () => {
const [is12HourTime, setIs12HourTime] = useState(false);
const [isLTVEnabled, setIsLTVEnabled] = useState(false);
const [fileNameFormat, setFileNameFormat] = useState("DOCNAME");
const [userInfo, setUserInfo] = useState({});
const [tenantInfo, setTenantInfo] = useState({});
useEffect(() => {
@@ -87,7 +84,6 @@ const Preferences = () => {
const getUser = extUser.data?.result;
if (getUser) {
const _getUser = JSON.parse(JSON.stringify(getUser));
setUserInfo(_getUser);
setIsNotifyOnSignatures(
_getUser?.NotifyOnSignatures !== undefined
? _getUser?.NotifyOnSignatures
+7 -7
View File
@@ -63,7 +63,8 @@ import {
setDefaultSignImg,
setIsShowModal,
resetWidgetState,
setTypedSignFont
setTypedSignFont,
setMyStamp
} from "../redux/reducers/widgetSlice";
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
import WidgetNameModal from "../components/pdf/WidgetNameModal";
@@ -144,7 +145,6 @@ function SignYourSelf() {
const [isUploadPdf, setIsUploadPdf] = useState(false);
const [unSignedWidgetId, setUnSignedWidgetId] = useState("");
const [widgetsTour, setWidgetsTour] = useState(false);
const [owner, setOwner] = useState({});
const index = xyPosition?.findIndex((object) => {
return object.pageNumber === pageNumber;
@@ -193,7 +193,6 @@ function SignYourSelf() {
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [divRef.current, isSidebar]);
//function for get document details for perticular signer with signer'object id
const getDocumentDetails = async (showComplete) => {
try {
@@ -268,6 +267,7 @@ function SignYourSelf() {
);
dispatch(setDefaultSignImg(defaultSignRes?.res?.defaultSignature));
dispatch(setMyInitial(defaultSignRes?.res?.defaultInitial));
dispatch(setMyStamp(defaultSignRes?.res?.defaultStamp));
} else {
dispatch(setSaveSignCheckbox({ isVisible: true }));
}
@@ -893,7 +893,7 @@ function SignYourSelf() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourSecond"]',
@@ -905,7 +905,7 @@ function SignYourSelf() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="pdftools"]',
@@ -917,7 +917,7 @@ function SignYourSelf() {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
@@ -1216,7 +1216,7 @@ function SignYourSelf() {
selector: '[data-tut="IsSigned"]',
content: t("signature-validate-alert-2"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
return (
+50 -33
View File
@@ -38,7 +38,8 @@ import {
getOriginalWH,
defaultMailBody,
defaultMailSubject,
handleDeleteWidget
handleDeleteWidget,
nonPresentMaskCss
} from "../constant/Utils";
import RenderPdf from "../components/pdf/RenderPdf";
import "../styles/AddUser.css";
@@ -71,7 +72,6 @@ import ShareButton from "../primitives/ShareButton";
const TemplatePlaceholder = () => {
const { t } = useTranslation();
const journey = "Use Template";
const { templateId } = useParams();
const dispatch = useDispatch();
const prefillImg = useSelector((state) => state.widget.prefillImg);
@@ -160,6 +160,7 @@ const TemplatePlaceholder = () => {
const [documentDetails, setDocumentDetails] = useState();
const [copied, setCopied] = useState(false);
const [isUseTemplate, setIsUseTemplate] = useState(false);
const [prevRole, setPrevRole] = useState("");
const currentUser = localStorage.getItem(
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
);
@@ -628,7 +629,7 @@ const TemplatePlaceholder = () => {
selector: '[data-tut="reactourAddbtn"]',
content: t("add-user-template"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
@@ -957,24 +958,16 @@ const TemplatePlaceholder = () => {
);
if (res?.status === "unfilled") {
setIsUiLoading(false);
const emptyWidget = res?.emptyResponseObjects.map((item, index) => (
<span className="font-medium" key={index}>
{item.options.name}
</span>
));
const emptyWidget = res?.emptyResponseObjects
?.map((item) => item.options.name)
?.join(", ");
const timeInMiliSec = 6000;
showAlert(
"danger",
<>
The following required field(s) cannot be left empty:{" "}
{emptyWidget.map((item, index) => (
<span key={index}>
{index > 0 && ", "}
{item}
</span>
))}
. Please fill them out to proceed.
</>,
6000
t("prefill-unfilled-widget", {
emptyWidget: emptyWidget ? `[${emptyWidget}]` : ""
}),
timeInMiliSec
);
} else if (res?.status === "unattach signer") {
setIsUiLoading(false);
@@ -995,7 +988,7 @@ const TemplatePlaceholder = () => {
pdfDetails,
signerPos,
signersdata,
updatedPdfUrl
updatedPdfUrl,
);
if (res.status === "success") {
navigate(`/placeHolderSign/${res.id}`, {
@@ -1024,7 +1017,7 @@ const TemplatePlaceholder = () => {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourAddbtn"]',
@@ -1037,19 +1030,31 @@ const TemplatePlaceholder = () => {
),
position: "top",
observe: '[data-tut="reactourAddbtn--observe"]',
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
// {
// selector: '[data-tut="addWidgets"]',
// content: () => (
// <TourContentWithBtn
// message={t("tour-mssg.template-placeholder-2")}
// isDontShowCheckbox={!checkTourStatus}
// isChecked={handleDontShow}
// />
// ),
// position: "top",
// styles: { fontSize: "13px" }
// },
{
selector: '[data-tut="addWidgets"]',
selector: '[data-tut="nonpresentmask"]',
content: () => (
<TourContentWithBtn
message={t("tour-mssg.template-placeholder-2")}
isDontShowCheckbox={!checkTourStatus}
isChecked={handleDontShow}
/>
),
position: "top",
style: { fontSize: "13px" }
position: "center",
styles: { fontSize: "13px", maskArea: nonPresentMaskCss },
action: () => handleCloseRoleModal()
},
{
selector: '[data-tut="pdftools"]',
@@ -1061,7 +1066,7 @@ const TemplatePlaceholder = () => {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
},
{
selector: '[data-tut="headerArea"]',
@@ -1073,7 +1078,7 @@ const TemplatePlaceholder = () => {
/>
),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
@@ -1117,6 +1122,7 @@ const TemplatePlaceholder = () => {
// `handleAddSigner` is used to open Add Role Modal
const handleAddSigner = () => {
setIsModalRole(true);
setPrevRole(roleName || "");
setRoleName("");
};
@@ -1144,6 +1150,7 @@ const TemplatePlaceholder = () => {
setSignerPos((prev) => [...prev, signerPosObj]);
setIsModalRole(false);
setRoleName("");
setCurrWidgetsDetails({});
setUniqueId(Id);
setBlockColor(color[index]);
setIsSelectId(index);
@@ -1274,6 +1281,10 @@ const TemplatePlaceholder = () => {
const RedirectUrl = updateTemplate?.[0]?.RedirectUrl
? { RedirectUrl: updateTemplate?.[0]?.RedirectUrl }
: {};
const penColors =
updateTemplate?.[0]?.PenColors?.length > 0
? { PenColors: updateTemplate?.[0]?.PenColors }
: {};
const data = {
...(updateTemplate?.[0]?.URL ? { URL: updateTemplate?.[0]?.URL } : {}),
Name: updateTemplate?.[0]?.Name || "",
@@ -1290,6 +1301,7 @@ const TemplatePlaceholder = () => {
: false,
TimeToCompleteDays:
parseInt(updateTemplate?.[0]?.TimeToCompleteDays) || 15,
...penColors,
...Bcc,
...RedirectUrl
};
@@ -1305,6 +1317,7 @@ const TemplatePlaceholder = () => {
};
const handleCloseRoleModal = () => {
setRoleName(prevRole || "");
setIsModalRole(false);
};
const handleSaveWidgetsOptions = (
@@ -1503,13 +1516,16 @@ const TemplatePlaceholder = () => {
}
};
}
else if (["signature"].includes(position.type)) {
else if (["signature", "initials"].includes(position.type)) {
return {
...position,
options: {
...position.options,
name: defaultdata.name,
hint: defaultdata?.hint || ""
hint: defaultdata?.hint || "",
...(defaultdata?.penColors?.length > 0 && {
penColors: defaultdata?.penColors
})
}
};
} else {
@@ -1522,7 +1538,7 @@ const TemplatePlaceholder = () => {
defaultValue: defaultdata.defaultValue,
hint: defaultdata?.hint || "",
fontSize: textSize || 12,
fontColor: textColor || "black"
fontColor: textColor || "black",
}
};
}
@@ -1617,7 +1633,7 @@ const TemplatePlaceholder = () => {
selector: '[data-tut="isSignatureWidget"]',
content: t("signature-field-widget", { signersName }),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const showAlert = (type, message, duration = 1500) => {
@@ -1629,7 +1645,7 @@ const TemplatePlaceholder = () => {
selector: '[data-tut="IsSigned"]',
content: t("text-field-tour"),
position: "top",
style: { fontSize: "13px" }
styles: { fontSize: "13px" }
}
];
const copytoclipboard = (text) => {
@@ -2146,6 +2162,7 @@ const TemplatePlaceholder = () => {
isSave={true}
setUniqueId={setUniqueId}
signatureTypes={signatureType}
penColors={pdfDetails?.[0]?.PenColors}
/>
)}
<RotateAlert
+28 -50
View File
@@ -13,26 +13,8 @@ import {
import DeleteUserModal from "../primitives/DeleteUserModal";
import axios from "axios";
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 UserList = () => {
const { t } = useTranslation();
@@ -342,7 +324,7 @@ const UserList = () => {
{t(`report-heading.${item}`)}
</th>
))}
{actions?.length > 0 && (
{usersActions?.length > 0 && (
<th className="p-2 text-transparent pointer-events-none">
{t("action")}
</th>
@@ -375,10 +357,14 @@ const UserList = () => {
</td>
{handleActiveToggleVisibility(item) ? (
<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
id={`isdisabled-${item.objectId}`}
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}
onChange={() => handleToggleBtn(item)}
/>
@@ -425,8 +411,8 @@ const UserList = () => {
{isAdmin && (
<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">
{actions?.length > 0 &&
actions.map((act, index) => (
{usersActions?.length > 0 &&
usersActions?.map((act, index) => (
<React.Fragment key={index}>
{handleBtnVisibility(act, item) && (
<div
@@ -454,37 +440,29 @@ const UserList = () => {
)}
</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>
))}
</div>
</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>
))}
</tbody>
+13 -5
View File
@@ -1,4 +1,7 @@
import React, { useState, useEffect } from "react";
import React, {
useState,
useEffect,
} from "react";
import { Navigate, useNavigate } from "react-router";
import Parse from "parse";
import { SaveFileSize } from "../constant/saveFileSize";
@@ -6,7 +9,10 @@ import dp from "../assets/images/dp.png";
import { sanitizeFileName } from "../utils";
import axios from "axios";
import Tooltip from "../primitives/Tooltip";
import { getSecureUrl, handleSendOTP } from "../constant/Utils";
import {
getSecureUrl,
handleSendOTP
} from "../constant/Utils";
import ModalUi from "../primitives/ModalUi";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
@@ -127,7 +133,7 @@ function UserProfile() {
Name: obj.Name,
JobTitle: jobTitle,
Company: company,
Language: obj?.language || ""
Language: obj?.language || "",
};
await axios.put(
@@ -443,7 +449,7 @@ function UserProfile() {
<button
type="button"
onClick={(e) => {
editmode ? handleSubmit(e) : setEditMode(true);
editmode ? handleSubmit(e) : setEditMode(true);
}}
className="op-btn op-btn-primary md:w-[100px]"
>
@@ -454,7 +460,9 @@ function UserProfile() {
onClick={() =>
editmode ? handleCancel() : navigate("/changepassword")
}
className={`op-btn ${editmode ? "op-btn-ghost w-[100px]" : "op-btn-secondary"}`}
className={
`op-btn ${editmode ? "op-btn-ghost w-[100px]" : "op-btn-secondary"}`
}
>
{editmode ? t("cancel") : t("change-password")}
</button>
+1 -1
View File
@@ -1,4 +1,4 @@
// below code is used to handle undefined Promise.withResolvers for safari
// below code is used to handle undefined Promise.withResolvers for safari for react-pdf
if (typeof Promise.withResolvers === "undefined") {
Promise.withResolvers = function () {
let resolve, reject;
+10 -3
View File
@@ -1,11 +1,15 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import Loader from "./Loader";
import { useTranslation } from "react-i18next";
import axios from "axios";
import { getTenantDetails } from "../constant/Utils";
import { emailRegex } from "../constant/const";
import { useDispatch } from "react-redux";
import { sessionStatus } from "../redux/reducers/userReducer";
const AddContact = (props) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [email, setEmail] = useState("");
@@ -108,6 +112,7 @@ const AddContact = (props) => {
}
} else {
setIsLoader(false);
dispatch(sessionStatus(false));
alert(t("something-went-wrong-mssg"));
}
}
@@ -139,7 +144,9 @@ const AddContact = (props) => {
)}
<div className="w-full mx-auto p-[8px]">
{!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 && (
<div className="mb-[0.75rem] flex items-center mt-1">
@@ -248,7 +255,7 @@ const AddContact = (props) => {
e.preventDefault();
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
? t("hide-optional-details")
+1 -1
View File
@@ -158,7 +158,7 @@ const SignerCell = ({ reportName, item, handleRemovePrefill }) => {
</tr>
</thead>
<tbody>
{item.Placeholders.map(
{item?.Placeholders?.map(
(x, i) =>
x.Role !== "prefill" && (
<tr key={i} className="text-sm font-medium">
+68 -17
View File
@@ -1,33 +1,84 @@
import ReactTour from "reactour";
import { TourProvider, useTour } from "@reactour/tour";
import { useEffect, useRef } from "react";
import {
clearAllBodyScrollLocks,
disableBodyScroll,
enableBodyScroll
} from "body-scroll-lock";
function TourStateSync({ isOpen, onRequestClose }) {
const { isOpen: contextIsOpen, setIsOpen } = useTour();
const previousIsOpen = useRef(contextIsOpen);
useEffect(() => {
setIsOpen(isOpen);
}, [isOpen, setIsOpen]);
useEffect(() => {
if (previousIsOpen.current && !contextIsOpen && onRequestClose) {
onRequestClose();
}
previousIsOpen.current = contextIsOpen;
}, [contextIsOpen, onRequestClose]);
return null;
}
export default function Tour({
steps,
isOpen,
rounded,
rounded = 5,
className,
showNumber,
closeWithMask = false,
onRequestClose,
showNavigation,
showCloseButton,
showNavigationNumber,
disableKeyboardNavigation
showNavigationNumber
}) {
const radius = rounded || 5;
function disableBody() {
if (typeof window !== "undefined") {
disableBodyScroll(document.body);
}
}
function enableBody() {
if (typeof window !== "undefined") {
enableBodyScroll(document.body);
clearAllBodyScrollLocks();
}
}
return (
<ReactTour
<TourProvider
className={className}
steps={steps}
isOpen={isOpen}
rounded={radius}
scrollOffset={-100}
closeWithMask={closeWithMask}
disableKeyboardNavigation={disableKeyboardNavigation}
showCloseButton={showCloseButton}
onRequestClose={onRequestClose}
showNumber={showNumber}
defaultOpen={isOpen}
// scrollOffset={-100} // available only for v1
inViewThreshold={{ y: 150 }}
onClickMask={() => {}}
disableKeyboardNavigation={["esc"]}
showBadge={showNumber}
showNavigation={showNavigation}
showNavigationNumber={showNavigationNumber}
/>
showPrevNextButtons={showNavigation}
showDots={showNavigationNumber ?? showNavigation}
showCloseButton={showCloseButton}
padding={rounded}
onClickClose={(clickProps) => {
clickProps.setIsOpen(false);
}}
styles={{
popover: (base) => ({
...base,
borderRadius: rounded,
padding: "18px",
fontSize: "13px"
}),
maskArea: (base) => ({ ...base, rx: rounded }),
close: (base) => ({ ...base, right: 10, top: 10, outline: "none" })
}}
afterOpen={disableBody}
beforeClose={enableBody}
scrollSmooth
>
<TourStateSync isOpen={isOpen} onRequestClose={onRequestClose} />
</TourProvider>
);
}
@@ -2,7 +2,7 @@ import { createSlice } from "@reduxjs/toolkit";
const sidebarSlice = createSlice({
name: "sidebar",
initialState: { isOpen: true },
initialState: { isOpen: true, selectedMenu: true },
reducers: {
toggleSidebar: (state, action) => {
if (typeof action.payload === "undefined") {
@@ -12,9 +12,12 @@ const sidebarSlice = createSlice({
// payload provided → use it directly
state.isOpen = action.payload;
}
},
setSelectedMenu: (state, action) => {
state.selectedMenu = action.payload;
}
}
});
export const { toggleSidebar } = sidebarSlice.actions;
export const { toggleSidebar, setSelectedMenu } = sidebarSlice.actions;
export default sidebarSlice.reducer;
@@ -10,6 +10,7 @@ const initialState = {
signatureTypes: null,
defaultSignImg: null,
myInitial: null,
myStamp: null,
lastIndex: "",
scrollTriggerId: "",
prefillImg: [],
@@ -63,6 +64,9 @@ const widgetSlice = createSlice({
setPrefillImgLoad: (state, action) => {
state.prefillImgLoad = action.payload;
},
setMyStamp: (state, action) => {
state.myStamp = action.payload;
},
resetWidgetState: () => initialState
}
});
@@ -78,7 +82,8 @@ export const {
setScrollTriggerId,
setPrefillImg,
setPrefillImgLoad,
setTypedSignFont
setTypedSignFont,
setMyStamp
} = widgetSlice.actions;
export default widgetSlice.reducer;
@@ -2,8 +2,12 @@ import { useEffect, useState } from "react";
import Loader from "../../primitives/Loader";
import { useTranslation } from "react-i18next";
import Parse from "parse";
import { useDispatch } from "react-redux";
import { sessionStatus } from "../../redux/reducers/userReducer";
const EditContactForm = (props) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const [isLoader, setIsLoader] = useState(false);
const [formData, setFormData] = useState({
Name: "",
@@ -30,39 +34,49 @@ const EditContactForm = (props) => {
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
if (props.handleEditContact) {
try {
setIsLoader(true);
const params = {
contactId: props.contact.objectId,
name: formData.Name,
email: formData.Email,
phone: formData?.Phone,
company: formData?.Company,
jobTitle: formData?.JobTitle,
tenantId: localStorage.getItem("TenantId")
};
const res = await Parse.Cloud.run("editcontact", params);
const updateContact = {
...res,
Name: formData.Name,
Email: formData.Email,
Phone: formData?.Phone,
Company: formData?.Company,
JobTitle: formData?.JobTitle
};
props.handleEditContact(updateContact);
} catch (err) {
console.log("err in edit contact ", err);
if (err.code === 137) {
alert(t("contact-already-exists"));
} else {
alert(t("something-went-wrong-mssg"));
try {
const sessionToken = Parse.User?.current()?.getSessionToken();
if (localStorage.getItem("TenantId") && sessionToken) {
if (props.handleEditContact) {
try {
setIsLoader(true);
const params = {
contactId: props.contact.objectId,
name: formData.Name,
email: formData.Email,
phone: formData?.Phone,
company: formData?.Company,
jobTitle: formData?.JobTitle,
tenantId: localStorage.getItem("TenantId")
};
const res = await Parse.Cloud.run("editcontact", params);
const updateContact = {
...res,
Name: formData.Name,
Email: formData.Email,
Phone: formData?.Phone,
Company: formData?.Company,
JobTitle: formData?.JobTitle
};
props.handleEditContact(updateContact);
} catch (err) {
console.log("err in edit contact ", err);
if (err.code === 137) {
alert(t("contact-already-exists"));
} else {
alert(t("something-went-wrong-mssg"));
}
} finally {
setIsLoader(false);
props.handleClose && props.handleClose();
}
}
} finally {
setIsLoader(false);
props.handleClose && props.handleClose();
} else {
dispatch(sessionStatus(false));
}
} catch (err) {
console.error("invalid session or missing tenantId ", err);
dispatch(sessionStatus(false));
}
};
return (
@@ -1,11 +1,14 @@
import { useState, useEffect, useRef } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import * as XLSX from "xlsx";
import Parse from "parse";
import { emailRegex } from "../../constant/const";
import { useDispatch } from "react-redux";
import { sessionStatus } from "../../redux/reducers/userReducer";
const ImportContact = ({ setLoader, onImport, showAlert }) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const [currentImportPage, setCurrentImportPage] = useState(1);
const [importedData, setImportedData] = useState([]);
const [invalidRecords, setInvalidRecords] = useState(0);
@@ -183,40 +186,50 @@ const ImportContact = ({ setLoader, onImport, showAlert }) => {
e.stopPropagation();
setLoader(true);
try {
const filterdata = importedData.map((x) => ({
Name: x.Name,
Email: x.Email,
Phone: x.Phone,
Company: x.Company,
JobTitle: x.JobTitle,
TenantId: localStorage.getItem("TenantId")
}));
const contacts = JSON.stringify(filterdata);
const res = await Parse.Cloud.run("createbatchcontact", { contacts });
if (res) {
showAlert(
"info",
t("contact-imported", {
imported: res?.success || 0,
failed: res?.failed || 0
})
);
if (res?.success > 0) {
setTimeout(() => window.location.reload(), 1500);
const sessionToken = Parse.User?.current()?.getSessionToken();
if (localStorage.getItem("TenantId") && sessionToken) {
try {
const filterdata = importedData.map((x) => ({
Name: x.Name,
Email: x.Email,
Phone: x.Phone,
Company: x.Company,
JobTitle: x.JobTitle,
TenantId: localStorage.getItem("TenantId")
}));
const contacts = JSON.stringify(filterdata);
const res = await Parse.Cloud.run("createbatchcontact", { contacts });
if (res) {
showAlert(
"info",
t("contact-imported", {
imported: res?.success || 0,
failed: res?.failed || 0
})
);
if (res?.success > 0) {
setTimeout(() => window.location.reload(), 1500);
}
}
} catch (err) {
console.log("err while creating batch contact", err);
showAlert("danger", t("something-went-wrong-mssg"));
} finally {
onImport && onImport();
setImportedData([]);
setInvalidRecords(0);
}
} else {
dispatch(sessionStatus(false));
}
} catch (err) {
console.log("err while creating batch contact", err);
showAlert("danger", t("something-went-wrong-mssg"));
} finally {
onImport && onImport();
setImportedData([]);
setInvalidRecords(0);
console.error("invalid session or missing tenantId ", err);
dispatch(sessionStatus(false));
}
};
return (
<form onSubmit={handleImportData} className="p-[20px] h-full ">
<form onSubmit={handleImportData} className="p-[20px] h-full">
<div className="text-xs">
<label className="block ml-2">
{t("contacts-file")}
@@ -11,7 +11,6 @@ import Parse from "parse";
import {
copytoData,
fetchUrl,
formatDate,
getSignedUrl,
getTenantDetails,
handleSignatureType,
@@ -46,7 +45,6 @@ const DocumentsReport = (props) => {
const copyUrlRef = useRef(null);
const titleRef = useRef(null);
const titleElement = useElSize(titleRef);
const journey = "Use Template";
const appName =
"OpenSign™";
const { t } = useTranslation();
@@ -267,7 +265,7 @@ const DocumentsReport = (props) => {
[templateRes || templateDeatils],
placeholder || xyPosition,
signer || signerList,
templateRes?.URL || templateDeatils?.URL
templateRes?.URL || templateDeatils?.URL,
);
if (res.status === "success") {
navigate(`/placeHolderSign/${res.id}`, {
@@ -944,12 +942,14 @@ const DocumentsReport = (props) => {
);
if (res?.status === "unfilled") {
const emptyWidget = res?.emptyResponseObjects
.map((item) => `[ ${item.options.name}]`)
.join(", ");
?.map((item) => item.options.name)
?.join(", ");
const timeInMiliSec = 6000;
showAlert(
"danger",
t("prefill-unfilled-widget", { emptyWidget: emptyWidget }),
t("prefill-unfilled-widget", {
emptyWidget: emptyWidget ? `[${emptyWidget}]` : ""
}),
timeInMiliSec
);
} else if (res?.status === "unattach signer") {
@@ -16,7 +16,6 @@ import {
handleSignatureType,
replaceMailVaribles,
signatureTypes,
openInNewTab,
createDocument,
defaultMailBody,
defaultMailSubject
@@ -30,7 +29,9 @@ import "../../styles/quill.css";
import BulkSendUi from "../../components/BulkSendUi";
import Loader from "../../primitives/Loader";
import { serverUrl_fn } from "../../constant/appinfo";
import { Trans, useTranslation } from "react-i18next";
import {
useTranslation
} from "react-i18next";
import { useElSize } from "../../hook/useElSize";
import LottieWithLoader from "../../primitives/DotLottieReact";
import PrefillWidgetModal from "../../components/pdf/PrefillWidgetsModal";
@@ -42,7 +43,6 @@ import CustomizeMail from "../../components/pdf/CustomizeMail";
const TemplatesReport = (props) => {
const copyUrlRef = useRef(null);
const titleRef = useRef(null);
const journey = "Use Template";
const titleElement = useElSize(titleRef);
const prefillImg = useSelector((state) => state.widget.prefillImg);
const appName =
@@ -238,8 +238,8 @@ const TemplatesReport = (props) => {
navigate(`/${act.redirectUrl}/${item.objectId}`);
} else {
// handle Use template
const placeholder = item?.Placeholders;
const isRoleExist = placeholder.filter((x) => x.Role !== "prefill");
const placeholder = item?.Placeholders || [];
const isRoleExist = placeholder?.filter((x) => x.Role !== "prefill");
//condition to check atleast one role is present for use template
if (isRoleExist && isRoleExist?.length > 0) {
const checkIsSignatureExist = isRoleExist?.every((placeholderObj) =>
@@ -309,7 +309,7 @@ const TemplatesReport = (props) => {
[templateRes || templateDetails],
placeholder || xyPosition,
signer || signerList,
templateRes?.URL || templateDetails?.URL
templateRes?.URL || templateDetails?.URL,
);
if (res.status === "success") {
navigate(`/placeHolderSign/${res.id}`, {
@@ -346,9 +346,13 @@ const TemplatesReport = (props) => {
setIsShareWith({ [item.objectId]: true });
}
else if (act.action === "duplicate") {
const isPrefill = item?.Placeholders?.some((p) => p?.Role === "prefill");
setError(isPrefill ? t("duplicate-template-error") : "");
setIsModal({ [`duplicate_${item.objectId}`]: true });
const hasDuplicate = utils.hasDuplicateWidgetNames(item?.Placeholders);
if (hasDuplicate) {
setError(t("duplicate-template-widget-error"));
setIsModal({ [`duplicate_${item.objectId}`]: true });
} else {
setIsModal({ [`duplicate_${item.objectId}`]: true });
}
} else if (act.action === "rename") {
setIsModal({ [`rename_${item.objectId}`]: true });
} else if (act.action === "edit") {
@@ -727,7 +731,7 @@ const TemplatesReport = (props) => {
// `handleBulkSend` is used to open modal as well as fetch template
// and show Ui on the basis template response handleBulkSendTemplate
const handleBulkSend = async (template) => {
const isPrefillExist = template?.Placeholders.some(
const isPrefillExist = template?.Placeholders?.some(
(x) => x.Role === "prefill"
);
if (isPrefillExist) {
@@ -756,6 +760,7 @@ const TemplatesReport = (props) => {
}
};
// `handleShareWith` is used to save teams in sharedWith field
const handleShareWith = async (e, template) => {
e.preventDefault();
@@ -895,12 +900,14 @@ const TemplatesReport = (props) => {
);
if (res?.status === "unfilled") {
const emptyWidget = res?.emptyResponseObjects
.map((item) => `[ ${item.options.name}]`)
.join(", ");
?.map((item) => item.options.name)
?.join(", ");
const timeInMiliSec = 6000;
showAlert(
"danger",
t("prefill-unfilled-widget", { emptyWidget: emptyWidget }),
t("prefill-unfilled-widget", {
emptyWidget: emptyWidget ? `[${emptyWidget}]` : ""
}),
timeInMiliSec
);
} else if (res?.status === "unattach signer") {
+32 -25
View File
@@ -44,8 +44,26 @@
}
.mysignatureCanvas {
width: 456px;
height: 180px;
/* width: 456px;
height: 180px; */
width: 370px;
height: 146px;
}
.mystampCanvas {
/* width: 446px;
height: 180px; */
width: 370px;
height: 146px;
}
.intialSignature {
border: 2px solid #888;
background-color: rgb(255, 255, 255);
/* width: 180px;
height: 180px; */
width: 150px;
height: 150px;
}
[data-theme="opensigndark"] .tabWidth {
@@ -70,13 +88,6 @@
/* blue-500 */
}
.intialSignature {
border: 2px solid #888;
background-color: rgb(255, 255, 255);
width: 180px;
height: 180px;
}
/* Dark mode support for initials box in /managesign */
[data-theme="opensigndark"] .intialSignature {
background-color: #1f2937 !important;
@@ -117,10 +128,6 @@
border-color: #4b5563 !important;
}
.penContainerDefault {
width: 460px;
}
.icon {
position: absolute;
font-size: 14px;
@@ -450,7 +457,7 @@ option {
to prevent sudden quick movement (as the
navigation bar gets a new position at the top of the
page (position:fixed and top:0) */
.stickyHead+.content {
.stickyHead + .content {
padding-top: 60px;
}
}
@@ -466,11 +473,12 @@ option {
height: 118px;
}
.tabWidth {
.mystampCanvas {
width: 300px;
height: 118px;
}
.penContainerDefault {
.tabWidth {
width: 300px;
}
@@ -498,18 +506,17 @@ option {
width: 280px;
height: 111px;
}
.mystampCanvas {
width: 280px;
height: 111px;
}
.tabWidth {
width: 280px;
}
.penContainerDefault {
width: 280px;
}
.signTab {
font-weight: 500 !important;
font-size: 10px !important;
}
@@ -528,12 +535,12 @@ option {
width: 230px;
height: 91px;
}
.tabWidth {
.mystampCanvas {
width: 230px;
height: 91px;
}
.penContainerDefault {
.tabWidth {
width: 230px;
}
@@ -545,4 +552,4 @@ option {
.uploadImgLogo {
font-size: 20px;
}
}
}
+2
View File
@@ -161,6 +161,8 @@ export const handleCheckPrefillCreateDoc = async (
);
if (res.status === "success") {
return res;
} else if (res.status === "error") {
return res;
}
} else {
const res = { status: "unattach signer" };
+59 -1
View File
@@ -13,7 +13,9 @@ export const saveToMySign = async (widget) => {
const fileName =
widget?.type === "signature"
? `${replaceSpace}__sign`
: `${replaceSpace}__initials`;
: widget?.type === "initials"
? `${replaceSpace}__initials`
: `${replaceSpace}_stamp`;
const file = base64StringtoFile(base64, fileName);
const fileUrl = await uploadFile(file, User?.id);
// below code is used to save or update default signature, initials, stamp
@@ -25,6 +27,8 @@ export const saveToMySign = async (widget) => {
params.initials = fileUrl; // save initials image url
} else if (widget?.type === "signature") {
params.signature = fileUrl; // save signature image url
} else if (widget?.type === "stamp") {
params.stamp = fileUrl; // save stamp image url
}
const signRes = await Parse.Cloud.run("savesignature", params);
return { base64File: base64, id: signRes?.id };
@@ -41,6 +45,42 @@ export const hasSignatureWidget = (s) =>
(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
@@ -77,3 +117,21 @@ export const applyNumberFormulasToPages = (pages = []) => {
// Return the cloned, updated pages
return clonedPages;
};
export const getInitials = (name) => {
if (!name) return "";
// Trim and split by any amount of whitespace
const parts = name.trim().split(/\s+/);
// Only one word → return first letter of that word
if (parts.length === 1) {
return parts[0][0].toUpperCase();
}
// More than one word → take first and second words
const first = parts[0];
const second = parts[1];
return `${first[0].toUpperCase()} ${second[0].toUpperCase()}`;
};
+5 -1
View File
@@ -6,7 +6,11 @@ module.exports = {
},
plugins: [
require("daisyui"),
function ({ addUtilities, theme }) {
function ({ addUtilities, addVariant }) {
// ✅ Variants that match html[data-theme="..."] (or any ancestor with data-theme)
addVariant("opensigncss", '[data-theme="opensigncss"] &');
addVariant("opensigndark", '[data-theme="opensigndark"] &');
addUtilities({
// Prevent iOS long-press popup
".touch-callout-none": {
+11
View File
@@ -160,6 +160,17 @@ export const smtpsecure = process.env.SMTP_PORT && process.env.SMTP_PORT !== '46
export const smtpenable =
process.env.SMTP_ENABLE && process.env.SMTP_ENABLE.toLowerCase() === 'true' ? true : false;
export const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export function signPayload(payload, secret) {
if (payload && secret) {
const signature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return { 'x-webhook-signature': signature };
} else {
return {};
}
}
// `generateId` is used to unique Id for fileAdapter
export function generateId(length) {
@@ -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;
const queue = [];
function runWithLimit(task) {
return new Promise((resolve, reject) => {
const run = () => {
@@ -34,7 +37,7 @@ function runWithLimit(task) {
});
}
// -------------------- Timeout helper (async version = no TS hint) --------------------
// -------------------- Timeout helper --------------------
/**
* @template T
* @param {Promise<T>} promise
@@ -58,6 +61,7 @@ export async function withTimeout(promise, ms, label = 'operation') {
const storage = multer.memoryStorage();
export const upload = multer({
storage,
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB hard limit at multer level
fileFilter: (req, file, cb) => {
const okExt = /\.docx$/i.test(file.originalname || '');
const okMime =
@@ -109,14 +113,30 @@ export default async function docxtopdf(req, res) {
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 pdfBuffer = await runWithLimit(() =>
withTimeout(convertLibre(req.file.buffer, '.pdf', undefined), 60_000, 'DOCX->PDF')
);
// FIX: Increased timeout to 90s for large files, added nice priority
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;
let fileUrl;
if (activeFileAdapter) {
@@ -140,12 +160,23 @@ export default async function docxtopdf(req, res) {
return res.status(200).json({ message: 'success.', url: fileUrl });
} catch (err) {
const msg =
// More specific error messages
let msg =
err?.response?.data?.error || err?.response?.data || err?.message || 'Something went wrong.';
console.log(`Error in docxtopdf: ${msg}`);
// Friendly message to the client
const message =
'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 });
}
}
+2
View File
@@ -61,6 +61,7 @@ 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.
Parse.Cloud.afterSave('contracts_Document', DocumentAftersave);
@@ -132,3 +133,4 @@ 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);
@@ -8,12 +8,16 @@ async function SignatureAfterFind(request) {
const obj = request.objects[0];
const ImageURL = obj?.get('ImageURL') && obj?.get('ImageURL');
const Initials = obj?.get('Initials') && obj?.get('Initials');
const Stamp = obj?.get('Stamp') && obj?.get('Stamp');
if (ImageURL) {
obj.set('ImageURL', getPresignedUrl(ImageURL));
}
if (Initials) {
obj.set('Initials', getPresignedUrl(Initials));
}
if (Stamp) {
obj.set('Stamp', getPresignedUrl(Stamp));
}
return [obj];
}
}
@@ -23,12 +27,16 @@ async function SignatureAfterFind(request) {
const obj = request.objects[0];
const ImageURL = obj?.get('ImageURL') && obj?.get('ImageURL');
const Initials = obj?.get('Initials') && obj?.get('Initials');
const Stamp = obj?.get('Stamp') && obj?.get('Stamp');
if (ImageURL) {
obj.set('ImageURL', presignedlocalUrl(ImageURL));
}
if (Initials) {
obj.set('Initials', presignedlocalUrl(Initials));
}
if (Stamp) {
obj.set('Stamp', presignedlocalUrl(Stamp));
}
return [obj];
}
}
@@ -101,11 +101,7 @@ async function sendMail(document, publicUrl) {
}
async function batchQuery(userId, Documents, Ip, parseConfig, type, publicUrl) {
const extCls = new Parse.Query('contracts_Users');
extCls.equalTo('UserId', {
__type: 'Pointer',
className: '_User',
objectId: userId,
});
extCls.equalTo('UserId', { __type: 'Pointer', className: '_User', objectId: userId });
const resExt = await extCls.first({ useMasterKey: true });
if (resExt) {
const _resExt = JSON.parse(JSON.stringify(resExt));
@@ -189,10 +185,10 @@ async function batchQuery(userId, Documents, Ip, parseConfig, type, publicUrl) {
},
}
: {}),
...(x?.PenColors?.length > 0 ? { PenColors: x?.PenColors } : {}),
},
};
});
// console.log('requests ', requests);
if (requests?.length > 0) {
const newrequests = [requests?.[0]];
const response = await axios.post('batch', { requests: newrequests }, parseConfig);
@@ -1,5 +1,5 @@
export default async function manageSign(request) {
const { signature, userId, initials, id, title } = request.params;
const { signature, userId, initials, id, title, stamp } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
@@ -16,6 +16,7 @@ export default async function manageSign(request) {
signatureCls.set('Initials', initials ? initials : '');
signatureCls.set('ImageURL', signature ? signature : '');
signatureCls.set('SignatureName', title ? title : '');
signatureCls.set('Stamp', stamp ? stamp : '');
if (userPtr) {
signatureCls.set('UserId', userPtr);
}
@@ -61,6 +61,7 @@ export default async function saveAsTemplate(request) {
status: 'required',
response: '',
...(p?.options?.defaultValue ? { defaultValue: '' } : {}),
...(p?.options?.isReadOnly ? { isReadOnly: false } : {}),
},
})),
}));
@@ -89,7 +90,14 @@ export default async function saveAsTemplate(request) {
pos: (page.pos || []).map(widget => {
// if there is a defaultValue in options, zero it out
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
return widget;
@@ -106,6 +114,9 @@ export default async function saveAsTemplate(request) {
if (_docRes?.Bcc?.length > 0) {
templateCls.set('Bcc', _docRes?.Bcc);
}
if (_docRes?.PenColors?.length > 0) {
templateCls.set('PenColors', _docRes?.PenColors);
}
const res = await templateCls.save(null, { useMasterKey: true });
return res;
} else {
@@ -1,5 +1,5 @@
export default async function saveSignature(request) {
const { signature, userId, initials, id, title } = request.params;
const { signature, userId, initials, id, title, stamp } = request.params;
if (!userId) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Missing userId parameter.');
@@ -19,6 +19,9 @@ export default async function saveSignature(request) {
if (signature) {
signatureCls.set('ImageURL', signature);
}
if (stamp) {
signatureCls.set('Stamp', stamp);
}
if (title) {
signatureCls.set('SignatureName', title);
}
@@ -0,0 +1,72 @@
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;
const sessiontoken = request.headers?.sessiontoken;
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) {
let userId;
if (sessiontoken) {
const userRes = await axios.get(serverUrl + '/users/me', {
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': 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(
`triggerEvent error: `,
err?.response?.data?.error || err?.message || 'Something went wrong!'
);
return { message: 'Something went wrong!' };
}
}
@@ -0,0 +1,19 @@
/**
*
* @param {Parse} Parse
*/
exports.up = async Parse => {
const schema = new Parse.Schema('contracts_Signature');
schema.addString('Stamp');
return schema.update();
};
/**
*
* @param {Parse} Parse
*/
exports.down = async Parse => {
const schema = new Parse.Schema('contracts_Signature');
schema.deleteField('Stamp');
return schema.update();
};
@@ -0,0 +1,27 @@
/**
*
* @param {Parse} Parse
*/
exports.up = async Parse => {
const docSchema = new Parse.Schema('contracts_Document');
docSchema.addArray('PenColors');
await docSchema.update();
const templateSchema = new Parse.Schema('contracts_Template');
templateSchema.addArray('PenColors');
await templateSchema.update();
};
/**
*
* @param {Parse} Parse
*/
exports.down = async Parse => {
const docSchema = new Parse.Schema('contracts_Document');
docSchema.deleteField('PenColors');
await docSchema.update();
const templateSchema = new Parse.Schema('contracts_Template');
templateSchema.deleteField('PenColors');
await templateSchema.update();
};
+3 -2
View File
@@ -17,7 +17,7 @@ import { exec } from 'child_process';
import { createTransport } from 'nodemailer';
import { appName, cloudServerUrl, serverAppId, smtpenable, smtpsecure, useLocal } from './Utils.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 maintenance_mode_message from 'aws-sdk/lib/maintenance_mode_message.js';
let fsAdapter;
@@ -119,6 +119,7 @@ export const config = {
allowClientClassCreation: false,
allowExpiredAuthDataToken: false,
enableInsecureAuthAdapters: false,
databaseOptions: { allowPublicExplain: false },
encodeParseObjectInCloudFunction: true,
...(isMailAdapter === true
? {
@@ -240,7 +241,7 @@ if (!process.env.TESTING) {
console.log('opensign-server running on port ' + port + '.');
const isWindows = process.platform === 'win32';
// console.log('isWindows', isWindows);
createContactIndex();
runDbMigrations();
const migrate = isWindows
? `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`;
@@ -18,8 +18,7 @@ export default async function createContactIndex() {
const migrationExists = await migrationCollection.findOne({ name: migrationName });
if (migrationExists) {
console.log(' INFO No migrations were executed, database schema was already up to date.');
console.log(' SUCCESS Successfully ran indexed migrations directly on db.');
console.log(' INFO The unqiue index for contracts_Contactbook is already present.');
return;
}
@@ -61,10 +60,9 @@ export default async function createContactIndex() {
// Insert the document
await migrationdb.insertOne(schemaDocument);
console.log(' Unique index created successfully.');
console.log(' SUCCESS Successfully ran indexed migrations directly on db.');
console.log(' SUCCESS The unqiue index for contracts_Contactbook is already created.');
} catch (error) {
console.log(' ERROR running indexed migration:', error);
console.log(' ERROR Running unqiue index for contracts_Contactbook migration:', error);
} finally {
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();
}
+3416 -8908
View File
File diff suppressed because it is too large Load Diff
+22 -19
View File
@@ -1,6 +1,6 @@
{
"name": "open_sign_server",
"version": "2.30.0",
"version": "2.34.0",
"description": "opensign server using the parse-server module",
"repository": {
"type": "git",
@@ -18,40 +18,42 @@
"watch": "nodemon index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.936.0",
"@aws-sdk/s3-request-presigner": "^3.936.0",
"@aws-sdk/client-s3": "^3.952.0",
"@aws-sdk/s3-request-presigner": "^3.952.0",
"@parse/fs-files-adapter": "^3.0.0",
"@parse/push-adapter": "^8.1.0",
"@parse/s3-files-adapter": "^4.2.0",
"@pdf-lib/fontkit": "^1.1.1",
"@signpdf/placeholder-pdf-lib": "^3.2.6",
"@signpdf/signer-p12": "^3.2.4",
"@signpdf/signpdf": "^3.2.5",
"aws-sdk": "^2.1692.0",
"aws-sdk": "^2.1693.0",
"axios": "^1.13.2",
"coherentpdf": "^2.5.5",
"cors": "^2.8.5",
"date-fns-tz": "^3.2.0",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express": "^5.2.1",
"form-data": "^4.0.5",
"generate-api-key": "^1.0.2",
"googleapis": "^166.0.0",
"googleapis": "^169.0.0",
"libreoffice-convert": "^1.7.0",
"mailgun.js": "^12.1.1",
"mongodb": "^5.9.2",
"mailgun.js": "^12.4.0",
"moment": "^2.30.1",
"mongodb": "^7.0.0",
"multer": "^2.0.2",
"multer-s3": "^3.0.1",
"node-forge": "^1.3.1",
"nodemailer": "^7.0.10",
"node-forge": "^1.3.3",
"nodemailer": "^7.0.11",
"p-limit": "^7.2.0",
"parse": "^7.0.2",
"parse": "^8.0.0",
"parse-dbtool": "^1.2.0",
"parse-server": "^8.4.0",
"parse-server-api-mail-adapter": "^4.1.0",
"parse-server": "^8.6.2",
"parse-server-api-mail-adapter": "^5.0.2",
"pdf-lib": "^1.17.1",
"posthog-node": "^5.13.2",
"posthog-node": "^5.17.2",
"qrcode": "^1.5.4",
"rate-limiter-flexible": "^8.2.1",
"rate-limiter-flexible": "^9.0.1",
"sharp": "^0.34.5",
"speakeasy": "^2.0.0",
"ws": "^8.18.3"
@@ -59,17 +61,18 @@
"type": "module",
"devDependencies": {
"@babel/eslint-parser": "^7.28.5",
"eslint": "^9.39.1",
"eslint": "^9.39.2",
"jasmine": "^5.12.0",
"mongodb-runner": "^5.11.1",
"mongodb-runner": "^6.4.0",
"nodemon": "^3.1.11",
"nyc": "^17.1.0",
"prettier": "^3.6.2"
"prettier": "^3.7.4"
},
"overrides": {
"ws": "$ws",
"parse": "$parse",
"form-data": "$form-data"
"form-data": "$form-data",
"@parse/push-adapter": "$@parse/push-adapter"
},
"engines": {
"node": "18 || 20 || 22"