mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-09-03 16:28:56 +02:00
Compare commits
27
Commits
build_deps
...
v2.38.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a56b87a702 | ||
|
|
acb0f3083c | ||
|
|
2e2e535b70 | ||
|
|
336cfb3482 | ||
|
|
f1278d32a8 | ||
|
|
79d184cf88 | ||
|
|
cd2904b9f0 | ||
|
|
fce20a165b | ||
|
|
3a737ea1b7 | ||
|
|
e035592e94 | ||
|
|
0891cf55fc | ||
|
|
5d328b51c3 | ||
|
|
336114f6ec | ||
|
|
eb8e7b089b | ||
|
|
bce0e4971e | ||
|
|
4fb8aa9a16 | ||
|
|
8a2329dd60 | ||
|
|
75a40fbf43 | ||
|
|
334edb3c0c | ||
|
|
6465aa7c2c | ||
|
|
4593ce7fec | ||
|
|
b4ffdf0dc9 | ||
|
|
3c8b8835c7 | ||
|
|
002b0d5277 | ||
|
|
47348b990e | ||
|
|
7894a6edb8 | ||
|
|
a878ca1a96 |
@@ -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
|
||||
|
||||
Generated
+3757
-2981
File diff suppressed because it is too large
Load Diff
+65
-40
@@ -1,59 +1,79 @@
|
||||
{
|
||||
"name": "open_sign",
|
||||
"version": "2.30.0",
|
||||
"version": "2.37.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@imgly/background-removal": "^1.7.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.16.2",
|
||||
"@lottiefiles/dotlottie-react": "^0.18.3",
|
||||
"@mui/icons-material": "^5.15.10",
|
||||
"@mui/material": "^5.15.10",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reduxjs/toolkit": "^2.10.1",
|
||||
"axios": "^1.13.2",
|
||||
"@radix-ui/themes": "^3.3.0",
|
||||
"@reactour/tour": "^3.8.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@usewaypoint/block-avatar": "^0.0.3",
|
||||
"@usewaypoint/block-button": "^0.0.3",
|
||||
"@usewaypoint/block-columns-container": "^0.0.3",
|
||||
"@usewaypoint/block-container": "^0.0.2",
|
||||
"@usewaypoint/block-divider": "^0.0.4",
|
||||
"@usewaypoint/block-heading": "^0.0.3",
|
||||
"@usewaypoint/block-html": "^0.0.3",
|
||||
"@usewaypoint/block-image": "^0.0.5",
|
||||
"@usewaypoint/block-spacer": "^0.0.3",
|
||||
"@usewaypoint/block-text": "^0.0.7",
|
||||
"@usewaypoint/document-core": "^0.0.6",
|
||||
"@usewaypoint/email-builder": "^0.0.9",
|
||||
"axios": "^1.13.5",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.6.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"i18next": "^25.8.13",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"juice": "^11.1.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"parse": "^7.0.2",
|
||||
"parse": "^8.1.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pkijs": "^3.3.3",
|
||||
"prettier": "^3.8.1",
|
||||
"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.4",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-confetti": "^6.4.0",
|
||||
"react-datepicker": "^8.9.0",
|
||||
"react-datepicker": "^9.1.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.4",
|
||||
"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-quill-new": "^3.6.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-konva": "^19.2.2",
|
||||
"react-pdf": "^10.4.1",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
"react-router": "^7.9.6",
|
||||
"react-router": "^7.13.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.3.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"
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.5.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||
@@ -93,38 +113,43 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.0",
|
||||
"@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",
|
||||
"@babel/runtime-corejs2": "^7.28.6",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@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.14",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"@vitest/ui": "^4.0.18",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"babel-loader": "^10.0.0",
|
||||
"commitizen": "^4.3.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-loader": "^7.1.4",
|
||||
"daisyui": "^4.12.24",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jsdom": "^27.2.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"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.1",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4"
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"overrides": {
|
||||
"tmp": "$tmp",
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || 22"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@
|
||||
"password-length": "Das Passwort sollte 8 Zeichen lang sein",
|
||||
"password-case": "Das Passwort sollte einen Großbuchstaben, einen Kleinbuchstaben und eine Zahl enthalten",
|
||||
"password-special-char": "Das Passwort sollte ein Sonderzeichen enthalten",
|
||||
"agreee": "Ich stimme den",
|
||||
"agree": "Ich stimme den",
|
||||
"term": "Nutzungsbedingungen zu",
|
||||
"subscribe-to-opensign": "Abonnieren Sie den OpenSign-Newsletter",
|
||||
"register": "Registrieren",
|
||||
@@ -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.",
|
||||
@@ -220,7 +224,7 @@
|
||||
"New Template": "Neue Vorlage",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "Datei ausgewählt",
|
||||
"files-selected": "Datei(en) ausgewählt",
|
||||
"template-title": "Vorlagentitel",
|
||||
"document-title": "Dokumenttitel",
|
||||
"title": "Titel",
|
||||
@@ -237,7 +241,6 @@
|
||||
"auto-reminder": "Automatische Erinnerung",
|
||||
"remind-once": "Einmal erinnern alle (Tage)",
|
||||
"next": "Weiter",
|
||||
"select-folder": "Ordner auswählen",
|
||||
"OpenSign-drive": "{{appName}} Drive",
|
||||
"drive-document-status": {
|
||||
"Completed": "Abgeschlossen",
|
||||
@@ -266,7 +269,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",
|
||||
@@ -292,7 +295,7 @@
|
||||
"edit-team": "Team bearbeiten",
|
||||
"name-of-team": "Name des Teams",
|
||||
"prev": "Zurück",
|
||||
"no-data-avaliable": "Keine Daten verfügbar",
|
||||
"no-data-available": "Keine Daten verfügbar",
|
||||
"add-team": "Team hinzufügen",
|
||||
"page-not-found": "Seite nicht gefunden",
|
||||
"users-from-teams": "Benutzer aus Teams",
|
||||
@@ -303,7 +306,7 @@
|
||||
"delete-user": "Benutzer löschen",
|
||||
"delete": "Löschen",
|
||||
"add-user": "Benutzer hinzufügen",
|
||||
"password-generateed": "Das Passwort wird nur einmal generiert; stellen Sie sicher, dass Sie es kopieren.",
|
||||
"password-generated": "Das Passwort wird nur einmal generiert; stellen Sie sicher, dass Sie es kopieren.",
|
||||
"Team status": "Teamstatus",
|
||||
"user-deactivated": "Benutzer deaktiviert.",
|
||||
"user-activated": "Benutzer aktiviert.",
|
||||
@@ -333,7 +336,7 @@
|
||||
"quick-send-alert-1": "Alle Rollen in diesem Dokument sind derzeit mit Kontakten verknüpft. Um Kopien dieser Vorlage schnell an mehrere Unterzeichner zu senden, stellen Sie sicher, dass mindestens eine Rolle keinem Kontakt zugeordnet ist.",
|
||||
"quick-send-alert-2": "Bitte stellen Sie sicher, dass für alle Empfänger mindestens ein Signatur-Widget hinzugefügt wurde.",
|
||||
"quick-send-alert-3": "Bitte fügen Sie diesem template mindestens eine Rolle hinzu.",
|
||||
"quick-send-alert-4": "Das Limit für Schnellsendungen wurde erreicht.",
|
||||
"quick-send-alert-4": "{{flow}} unterstützt bis zu {{count}} Unterzeichner pro Batch. Teilen Sie Ihre Liste auf und versuchen Sie es erneut.",
|
||||
"copy-link": "Link kopieren",
|
||||
"copy": "Kopieren",
|
||||
"revoke-document": "Dokument widerrufen",
|
||||
@@ -389,7 +392,8 @@
|
||||
"radio button": "Radiobutton",
|
||||
"image": "Bild",
|
||||
"email": "E-Mail",
|
||||
"number": "Zahl"
|
||||
"number": "Zahl",
|
||||
"draw": "Zeichnen"
|
||||
},
|
||||
"fields": "Felder",
|
||||
"recipients": "Empfänger",
|
||||
@@ -433,16 +437,18 @@
|
||||
"reset-password-alert-1": "Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet.",
|
||||
"reset-password-alert-2": "Bitte richten Sie den E-Mail-Adapter ein.",
|
||||
"reset-password-alert-3": "Setzen Sie Ihr Passwort zurück",
|
||||
"faild-animation": "Animation konnte nicht geladen werden",
|
||||
"failed-animation": "Animation konnte nicht geladen werden",
|
||||
"apply": "Anwenden",
|
||||
"select-columns": "Spalten auswählen",
|
||||
"copy-type": {
|
||||
"All pages": "Alle Seiten",
|
||||
"All pages but last": "Alle Seiten außer der letzten",
|
||||
"All pages but first": "Alle Seiten außer der ersten",
|
||||
"Next to current widget": "Neben dem aktuellen Widget"
|
||||
"Next to current widget": "Neben dem aktuellen Widget",
|
||||
"Page range": "Seitenbereich"
|
||||
},
|
||||
"options": "Optionen",
|
||||
"minimun-check": "Minimale Anzahl",
|
||||
"minimum-check": "Minimale Anzahl",
|
||||
"maximum-check": "Maximale Anzahl",
|
||||
"cell-count": "Zellzahl",
|
||||
"default-value": "Standardwert",
|
||||
@@ -512,14 +518,14 @@
|
||||
"placeholder-alert-4": "Sie haben erfolgreich E-Mails an alle Empfänger gesendet!",
|
||||
"placeholder-mail-alert": "Sie haben erfolgreich eine E-Mail an {{name}} gesendet. Die nachfolgenden Unterzeichner erhalten eine E-Mail, sobald {{name}} das Dokument unterzeichnet.",
|
||||
"placeholder-mail-alert-you": "Nachfolgende Unterzeichner erhalten E-Mails, sobald Sie das Dokument unterschreiben.",
|
||||
"placeholder-alert-5": "Möchten Sie die Dokumente jetzt unterschreiben?",
|
||||
"placeholder-alert-5": "Sie können jetzt unterschreiben oder es an Ihre E-Mail senden, um es später abzuschließen.",
|
||||
"placeholder-alert-6": "Bitte richten Sie den E-Mail-Adapter ein, um E-Mails zu senden!",
|
||||
"placeholder-alert-7": "Bitte wählen Sie einen Unterzeichner aus, um Platzhalter hinzuzufügen!",
|
||||
"email-subject": "E-Mail-Betreff",
|
||||
"email-body": "E-Mail-Inhalt",
|
||||
"email-placeholder": "E-Mail-Text eingeben",
|
||||
"reset-to-default": "Auf Standard zurücksetzen",
|
||||
"cutomize-email": "E-Mail anpassen",
|
||||
"customize-email": "E-Mail anpassen",
|
||||
"upgrade-to-customize-email": "Upgrade, um E-Mails anzupassen",
|
||||
"sign-url": "Signatur-URL",
|
||||
"mails-sent": "E-Mails gesendet",
|
||||
@@ -584,7 +590,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!",
|
||||
@@ -621,7 +627,7 @@
|
||||
"date": "Datum",
|
||||
"report-not-found": "Bericht nicht gefunden",
|
||||
"please-wait": "Bitte warten",
|
||||
"select-foler": "Ordner auswählen",
|
||||
"select-folder": "Ordner auswählen",
|
||||
"generated-on": "Erstellt am",
|
||||
"text-field": "Textfeld",
|
||||
"font-size": "Schriftgröße",
|
||||
@@ -680,13 +686,13 @@
|
||||
"public-template": "Dieses Video zeigt Ihnen, wie Sie Ihr personalisiertes öffentliches Profil einrichten, z. B. 'https://opensign.me/Ihr-Benutzername'. Sie lernen auch, wie Sie Ihren Slogan anpassen und Ihre Vorlagen für öffentliche Signaturen verfügbar machen.",
|
||||
"allowModify-widgets": "Sie können jedes dieser Felder per Drag & Drop in das Dokument ziehen, zusätzlich zu den Feldern, die der Dokumentersteller bereits für Sie festgelegt hat."
|
||||
},
|
||||
"enter-email-plaholder": "E-Mail-Adresse hinzufügen und Enter drücken",
|
||||
"enter-email-placeholder": "E-Mail-Adresse hinzufügen und Enter drücken",
|
||||
"success-email-alert": "E-Mail erfolgreich gesendet!",
|
||||
"expired-doc-title": "Abgelaufenes Dokument",
|
||||
"expired-on-mssg": "Dieses Dokument ist am {{expiredDate}} abgelaufen und nicht mehr verfügbar zum Unterzeichnen.",
|
||||
"signature-validate-alert": "Bitte bestätigen Sie, dass Sie mindestens {{minRequiredCount}} Kontrollkästchen ausgewählt haben.",
|
||||
"signature-validate-alert-2": "Stellen Sie sicher, dass dieses Feld korrekt ausgefüllt ist und alle Anforderungen erfüllt.",
|
||||
"remaing-users": "Verbleibende Benutzer",
|
||||
"remaining-users": "Verbleibende Benutzer",
|
||||
"Role": "Rolle",
|
||||
"additional-users": "Bitte kaufen Sie zusätzliche Benutzer hinzu.",
|
||||
"Quantity-of-users": "Anzahl der Benutzer",
|
||||
@@ -700,7 +706,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",
|
||||
@@ -708,7 +714,7 @@
|
||||
"rotate-left": "Nach links drehen",
|
||||
"rotate-alert-mssg": "Alle Widgets auf dieser Seite gehen verloren. Sind Sie sicher, dass Sie fortfahren möchten?",
|
||||
"templateid": "Vorlagen-ID",
|
||||
"bulk-send-subcription-alert": "Bitte upgraden Sie auf den Professional- oder Team-Plan, um den Massenversand zu nutzen.",
|
||||
"bulk-send-subscription-alert": "Bitte upgraden Sie auf den Professional- oder Team-Plan, um den Massenversand zu nutzen.",
|
||||
"generate-test-token": "Test-Token generieren",
|
||||
"regenerate-test-token": "Test-Token neu generieren",
|
||||
"help-test-token": "Dieses Token kann verwendet werden, um die APIs unter https://sandbox.opensignlabs.com/api/v1 zu testen und unbegrenzt Dokumente zu signieren. Beachten Sie, dass die Sandbox-API Ihre Dokumente mit selbstsignierten Zertifikaten signiert, die möglicherweise von Adobe nicht als gültig erkannt werden. Nach Abschluss Ihrer Tests können Sie auf einen unserer kostenpflichtigen Pläne upgraden, um ein Produktionstoken zu generieren.",
|
||||
@@ -728,8 +734,9 @@
|
||||
"copied-code": "KOPIERT",
|
||||
"Installation": "Installation",
|
||||
"Usage": "Nutzung",
|
||||
"insufficient-credits": "Nicht genügend Signatur-Credits",
|
||||
"insufficient-credits-mssg": "Der Besitzer dieses Dokuments verfügt derzeit nicht über die notwendigen OpenSign-Credits, damit Sie unterzeichnen können. Bitte wenden Sie sich an den Besitzer, wenn Sie weitere Informationen benötigen.",
|
||||
"insufficient-credits": "Unzureichende Signaturguthaben. Bitte kontaktieren Sie {{ownerEmail}}",
|
||||
"insufficient-credits-title": "Nicht genügend Signatur-Credits",
|
||||
"insufficient-credits-mssg": "Der Besitzer dieses Dokuments verfügt derzeit nicht über die notwendigen {{appName}}-Credits, damit Sie unterzeichnen können. Bitte wenden Sie sich an den Besitzer, wenn Sie weitere Informationen benötigen.",
|
||||
"angular-npm-mssg-1": "Um OpenSign in Ihr Angular-Projekt zu integrieren, führen Sie einfach den folgenden Befehl aus:",
|
||||
"quota-mail-info-head": "Monatliches E-Mail-Limit",
|
||||
"quota-mail-info": "Um die Servicequalität aufrechtzuerhalten und Spam zu verhindern, erlaubt OpenSign bis zu 15 E-Mails pro Monat im kostenlosen Plan. Upgraden Sie jetzt für unbegrenzten E-Mail-Versand.",
|
||||
@@ -861,7 +868,6 @@
|
||||
"js-snippet-msg-1": "Die TemplateId erhalten Sie von der",
|
||||
"js-snippet-msg-2": "Vorlagen verwalten",
|
||||
"js-snippet-msg-3": "Seite.",
|
||||
"agrrement-alert": "Um fortzufahren, müssen Sie der Offenlegung über elektronische Aufzeichnungen und Unterschriften zustimmen.",
|
||||
"webhook-already-exists": "Die URL existiert bereits! Bitte versuchen Sie eine andere.",
|
||||
"webhook-must-be-secure": "Die Webhook-URL muss sicher sein und https:// verwenden.",
|
||||
"provide-test-token": "Bitte generieren Sie einen API-Test-Token.",
|
||||
@@ -1025,7 +1031,7 @@
|
||||
},
|
||||
"signer-already-present": "Unterzeichner bereits vorhanden",
|
||||
"kiosk-sign": "Kiosk-Unterschrift",
|
||||
"dont-have-access-to-template": "Das template wurde gelöscht oder Sie haben keinen Zugriff. Bitte kontaktieren Sie den Absender.",
|
||||
"do-not-have-access-to-template": "Das template wurde gelöscht oder Sie haben keinen Zugriff. Bitte kontaktieren Sie den Absender.",
|
||||
"kiosk-info": "Kiosk Modus ermöglicht es Ihnen, persönliche Unterschriften schnell und effizient zu erfassen. Ideal für Messen, Veranstaltungen oder Laufkundschaft, bei denen alle Unterzeichner physisch anwesend sind. ",
|
||||
"learn-more": "Mehr erfahren",
|
||||
"finish-mssg": "Sind Sie sicher, dass Sie das Dokument abschließen möchten?",
|
||||
@@ -1081,8 +1087,6 @@
|
||||
"search-contacts": "Kontakte durchsuchen…",
|
||||
"edit-draft": "Entwurf bearbeiten",
|
||||
"add-role-alert": "Bitte fügen Sie mindestens eine Rolle hinzu",
|
||||
"invalid-email-found": "Ungültige E-Mail gefunden: {{email}}",
|
||||
"duplicate-email-found": "Doppelte E-Mail gefunden: {{email}}",
|
||||
"vertical": "Vertikal",
|
||||
"horizontal": "Horizontal",
|
||||
"billing": "Abrechnung",
|
||||
@@ -1096,7 +1100,7 @@
|
||||
"access-denied": "Zugriff verweigert",
|
||||
"upgrade": "Upgrade",
|
||||
"do-not-access-app": "Sie haben keinen Zugriff auf diese Anwendung.",
|
||||
"dont-have-access": "Sie haben keinen Zugriff.",
|
||||
"do-not-have-access": "Sie haben keinen Zugriff.",
|
||||
"valid-email-alert": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
||||
"otp-not-validate": "OTP ist ungültig.",
|
||||
"domain-not-allowed": "Diese Domain ist nicht erlaubt",
|
||||
@@ -1113,10 +1117,12 @@
|
||||
"branding-help": "Branding ermöglicht White-Labeling für Ihre App",
|
||||
"custom-sub-domain": "Benutzerdefinierte Subdomain",
|
||||
"app-name": "App Name",
|
||||
"provide-domain-name": "Geben Sie Ihren Domainnamen an",
|
||||
"provide-domain-name": "Es sind nur Subdomains erlaubt (z. B. app.deinedomain.com). Primär-/Root-Domains wie deinedomain.com werden nicht unterstützt.",
|
||||
"provide-app-name-help": "Geben Sie einen eindeutigen App-Namen an. Er wird als Ihr Markenname in der Anwendung sowie in allen E-Mails angezeigt, überall dort, wo das Branding angewendet wird.",
|
||||
"upload-app-logo": "Laden Sie ein Logo im .png-, .jpg- oder .svg-Format hoch. Empfohlene Größe: {{size}} Pixel (Seitenverhältnis 5:2) für beste Anzeigequalität.",
|
||||
"upload-app-favicon": "Laden Sie ein quadratisches Favicon-Bild (.ico, .png oder .svg) hoch. Empfohlene Größe: {{size}} Pixel für beste Browserkompatibilität.",
|
||||
"provide-app-name": "Geben Sie Ihren App-Namen an",
|
||||
"logo": "Logo",
|
||||
"upload-app-logo": "Laden Sie Ihr App-Logo hoch",
|
||||
"prefill-unfilled-widget": "Die folgenden Pflichtfelder dürfen nicht leer sein: {{emptyWidget}}. Bitte füllen Sie diese aus, um fortzufahren.",
|
||||
"Dashboard": "Armaturenbrett",
|
||||
"Analytics": "Analytik",
|
||||
@@ -1239,6 +1245,7 @@
|
||||
},
|
||||
"read-only-date-error": "Das schreibgeschützte Datums-Widget muss ein Standarddatum haben.",
|
||||
"set-date": "Datum festlegen",
|
||||
"default-date": "Standarddatum",
|
||||
"set-today": "Unterzeichnungsdatum",
|
||||
"enter-name": "Name eingeben",
|
||||
"enter-email": "E-Mail eingeben",
|
||||
@@ -1274,5 +1281,120 @@
|
||||
"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",
|
||||
"signing-date": "Signierdatum",
|
||||
"date-widget": "Datums-Widget",
|
||||
"mail-failed": "Beim Senden der E-Mail ist etwas schiefgelaufen. Bitte versuchen Sie es erneut.",
|
||||
"mail-status-head": "Nächster Schritt: Ihr Dokument unterschreiben?",
|
||||
"send-to-email": "Per E-Mail senden",
|
||||
"date-pref-help-sub-title": "Mit den Datums-Widget-Einstellungen können Sie die Standardkonfiguration für das Datums-Widget festlegen. Diese Standards werden automatisch angewendet, wenn Sie ein neues Datums-Widget zu einem Dokument oder einer Vorlage hinzufügen. Sie können jedes Datums-Widget jederzeit individuell anpassen, indem Sie auf das Zahnradsymbol klicken.",
|
||||
"date-pref-help-format": "Wählen Sie das standardmäßige Datumsformat für Datums-Widgets.",
|
||||
"date-pref-help-default-date": "Legen Sie einen Standard-Datumswert für Datums-Widgets fest.",
|
||||
"date-pref-help-signing-date": "Wenn aktiviert, verwendet das Datums-Widget automatisch das Datum, an dem der Unterzeichner das Dokument signiert.",
|
||||
"date-pref-help-read-only": "Wenn aktiviert, ist das Datums-Widget für den Unterzeichner schreibgeschützt.",
|
||||
"upload-csv": "CSV hochladen",
|
||||
"file-selected": "Datei ausgewählt",
|
||||
"bulk-send-csv-helper": "Verwenden Sie Header für Unterzeichnerrollen (Rollen-E-Mail/Name/Telefon) und Vorabfüllwerte (Vorabfüllung: Feldname). Erforderliche Vorabfüllfelder müssen ausgefüllt sein.",
|
||||
"bulk-send-csv-helper-note": "Hinweis: Für Vorabfüllfelder vom Typ Datum in der CSV muss der Wert in folgendem Format angegeben werden: {{value}} (Beispiel: {{example}}).",
|
||||
"bulk-send-csv-summary": "{{total}} Zeilen aus der CSV importiert.",
|
||||
"bulk-send-csv-missing-headers": "Erforderliche CSV-Header fehlen. Rollen: {{roles}}.",
|
||||
"bulk-send-no-records": "Bitte fügen Sie vor dem Senden mindestens einen Empfänger hinzu.",
|
||||
"csv-only-support": "Es werden nur CSV-Dateien unterstützt.",
|
||||
"csv-no-records": "Keine Datensätze in der CSV gefunden.",
|
||||
"select-at-least-option": "Bitte wählen Sie mindestens eine Option aus.",
|
||||
"draw-required": "Bitte zeichnen Sie in das Feld.",
|
||||
"enter-smtp-username": "SMTP-Benutzernamen eingeben",
|
||||
"enter-webhook-url": "Webhook-URL eingeben",
|
||||
"enter-user-name": "Benutzernamen eingeben",
|
||||
"enter-tagline": "Slogan eingeben",
|
||||
"enter-password-or-click-autogenerate": "Passwort eingeben oder auf „Automatisch generieren“ klicken",
|
||||
"enter-daily-email-quota": "Tägliches E-Mail-Kontingent eingeben",
|
||||
"enter-custom-regular-expression": "Benutzerdefinierten regulären Ausdruck eingeben",
|
||||
"enter-value": "{{value}} eingeben",
|
||||
"are-you-sure-want-to-send-document": "Sind Sie sicher, dass Sie das Dokument senden möchten?",
|
||||
"favicon": "Favicon",
|
||||
"delete-two-factor-authentication": "Sind Sie sicher, dass Sie die Zwei-Faktor-Authentifizierung löschen möchten?",
|
||||
"paste-html-here": "HTML hier einfügen",
|
||||
"open-email-builder": "Sie können eine E-Mail-Vorlage mit der <1>E-Mail-Builder</1>-Plattform erstellen und den HTML-Code kopieren.",
|
||||
"contact-saved": "Kontakt erfolgreich gespeichert.",
|
||||
"remove-limit-of-user": "Sind Sie sicher, dass Sie das Limit dieses Benutzers aufheben möchten? Der Benutzer kann dann alle in Ihrer Organisation verfügbaren Premium-Credits nutzen, die unter allen Benutzern geteilt werden.",
|
||||
"current-expiry-date": "Aktuelles Ablaufdatum",
|
||||
"reminder-error": "„Einmal alle (Tage) erinnern“ darf die „Zeit zur Fertigstellung (Tage)“ nicht überschreiten.",
|
||||
"showing-to-of-records": "Zeige {{record}} bis {{recordPerPage}} von {{totalRows}} Einträgen",
|
||||
"records-per-page": "Einträge pro Seite",
|
||||
"email-not-found-in-row": "E-Mail in Zeile {{row}} nicht gefunden.",
|
||||
"invalid-email-found-in-row": "Ungültige E-Mail in Zeile {{row}} gefunden: {{email}}",
|
||||
"duplicate-email-found-in-row": "Doppelte E-Mail in Zeile {{row}} gefunden: {{email}}",
|
||||
"prefill": "Vorabfüllung",
|
||||
"recipients-&-data": "Empfänger & Daten",
|
||||
"response": "Antwort",
|
||||
"prefill-tab-help": "Dies sind die gemeinsamen Vorlagenfelder, die auf alle Dokumente in diesem Massenversand angewendet werden.",
|
||||
"recipient-tab-help-p1": "Im nächsten Schritt werden Sie:",
|
||||
"recipient-tab-help-p2": "Eine CSV mit Empfängerdetails hochladen oder",
|
||||
"recipient-tab-help-p3": "Empfängerinformationen manuell eingeben",
|
||||
"recipient-tab-help-p4": "Jeder Empfänger erhält sein eigenes Dokument, das aus dieser Vorlage generiert wird.",
|
||||
"response-tab-help": "Überprüfen Sie Ihre Antworten.",
|
||||
"summary": "Zusammenfassung",
|
||||
"prefill-fields": "Vorabfüllfelder",
|
||||
"documents": "Dokumente",
|
||||
"message": "Nachricht",
|
||||
"formula-help-p1": "Erfassen Sie numerische Eingaben und führen Sie dynamische Berechnungen mit Formeln durch.",
|
||||
"example": "Beispiel",
|
||||
"formula-help-p2": "Um eine Summe in number-3 anzuzeigen, verwenden Sie {{number-1}} + {{number-2}}.",
|
||||
"formula-help-p3": "Um einen Wert in number-4 zu berechnen, verwenden Sie ({{number-1}} * {{number-2}}) / {{number-3}}.",
|
||||
"formula-help-p4": "Jedes Zahlen-Widget muss einen eindeutigen Namen haben (z. B. number-1), da Formeln auf diesen Bezeichnern basieren.",
|
||||
"note-consume-premium-credits": "Hinweis: Diese Funktion verbraucht Premium-Credits.",
|
||||
"note-consume-premium-credits-on-document-send": "Hinweis: Diese Funktion verbraucht Premium-Credits. Sie werden jedes Mal verbraucht, wenn ein Dokument versendet wird – unabhängig davon, ob es unterschrieben wird oder nicht.",
|
||||
"webhook-authentication-help": "{{appName}} sichert jeden Webhook, indem eine HMAC-SHA256-Signatur mit Ihrem Webhook-Sicherheitsschlüssel und dem unveränderten Request-Payload generiert wird. Diese Signatur wird im Header x-webhook-signature gesendet. Ihr Server muss dieselbe Signatur mit dem empfangenen Payload und Ihrem geheimen Schlüssel generieren. Stimmen beide Signaturen überein, ist die Webhook-Anfrage authentisch und wurde nicht verändert.",
|
||||
"invalid-widget-found-in-row": "Ungültiger Wert in Zeile {{row}} für Widget {{widget}} erkannt."
|
||||
}
|
||||
@@ -68,14 +68,14 @@
|
||||
"loading": "Loading...",
|
||||
"of": "of",
|
||||
"sign-SSO": "Sign in with SSO",
|
||||
"login-page": "Login P+age",
|
||||
"login-page": "Login Page",
|
||||
"or": "OR",
|
||||
"signup-page": "Signup page",
|
||||
"password-match-length": "New password and confirmation password should match",
|
||||
"password-length": "Password should be 8 characters long",
|
||||
"password-case": "Password should contain uppercase letter, lowercase letter, digit",
|
||||
"password-special-char": " Password should contain special character",
|
||||
"agreee": "I agree to the",
|
||||
"agree": "I agree to the",
|
||||
"term": "Terms of Service",
|
||||
"subscribe-to-opensign": "Subscribe to OpenSign newsletter",
|
||||
"register": "Register",
|
||||
@@ -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.",
|
||||
@@ -220,7 +224,7 @@
|
||||
"New Template": "New template",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "file(s) selected",
|
||||
"files-selected": "file(s) selected",
|
||||
"template-title": "Template title",
|
||||
"document-title": "Document title",
|
||||
"title": "Title",
|
||||
@@ -237,7 +241,6 @@
|
||||
"auto-reminder": "Auto reminder",
|
||||
"remind-once": " Remind once in every (Days)",
|
||||
"next": "Next",
|
||||
"select-folder": "Select folder",
|
||||
"OpenSign-drive": "{{appName}} Drive",
|
||||
"drive-document-status": {
|
||||
"Completed": "Completed",
|
||||
@@ -266,7 +269,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",
|
||||
@@ -292,7 +295,7 @@
|
||||
"edit-team": "Edit team",
|
||||
"name-of-team": "Name of team",
|
||||
"prev": "Prev",
|
||||
"no-data-avaliable": "No data available",
|
||||
"no-data-available": "No data available",
|
||||
"add-team": "Add team",
|
||||
"page-not-found": "Page not found",
|
||||
"users-from-teams": "users from Teams",
|
||||
@@ -303,7 +306,7 @@
|
||||
"delete-user": "Delete user",
|
||||
"delete": "Delete",
|
||||
"add-user": "Add user",
|
||||
"password-generateed": "Password will only be generated once; make sure to copy it.",
|
||||
"password-generated": "Password will only be generated once; make sure to copy it.",
|
||||
"Team status": "Team status",
|
||||
"user-deactivated": "User deactivated.",
|
||||
"user-activated": "User activated.",
|
||||
@@ -333,7 +336,7 @@
|
||||
"quick-send-alert-1": "All roles in this document are currently linked to contacts. To quick send copies of this template to multiple signers, please ensure that at least one role is not linked to any contact.",
|
||||
"quick-send-alert-2": "Please ensure there's at least one signature widget added for all recipients.",
|
||||
"quick-send-alert-3": "Please add at least one role to this template.",
|
||||
"quick-send-alert-4": "Quick send reached limit.",
|
||||
"quick-send-alert-4": "{{flow}} supports up to {{count}} signers per batch. Split your list and try again.",
|
||||
"copy-link": "Copy link",
|
||||
"copy": "Copy",
|
||||
"revoke-document": "Revoke document",
|
||||
@@ -344,7 +347,7 @@
|
||||
"body": "Body",
|
||||
"add-contact": "Add contact",
|
||||
"edit-contact": "Edit contact",
|
||||
"add-signer-alert": "Contact already exist! Please select it from 'Signers' dropdown",
|
||||
"add-signer-alert": "Contact already exists! Please select it from 'Signers' dropdown",
|
||||
"record-delete-alert": "Record deleted successfully!",
|
||||
"record-revoke-alert": "Record revoked successfully!",
|
||||
"mail-sent-alert": "Mail sent successfully.",
|
||||
@@ -389,7 +392,8 @@
|
||||
"radio button": "radio button",
|
||||
"image": "image",
|
||||
"email": "email",
|
||||
"number": "number"
|
||||
"number": "number",
|
||||
"draw": "draw"
|
||||
},
|
||||
"fields": "Fields",
|
||||
"recipients": "Recipients",
|
||||
@@ -433,17 +437,18 @@
|
||||
"reset-password-alert-1": "Reset password link has been sent to your email id",
|
||||
"reset-password-alert-2": "Please setup email adapter ",
|
||||
"reset-password-alert-3": "Reset Your Password",
|
||||
"faild-animation": "Failed to load animation",
|
||||
"failed-animation": "Failed to load animation",
|
||||
"apply": "Apply",
|
||||
"select-columns": "Select columns",
|
||||
"copy-type": {
|
||||
"All pages": "All pages",
|
||||
"All pages but last": "All pages but last",
|
||||
"All pages but first": "All pages but first",
|
||||
"Next to current widget": "Next to current widget"
|
||||
"Next to current widget": "Next to current widget",
|
||||
"Page range": "Page range"
|
||||
},
|
||||
"options": "Options",
|
||||
"minimun-check": "Minimun check",
|
||||
"minimum-check": "Minimum check",
|
||||
"maximum-check": "Maximum check",
|
||||
"cell-count": "Cell count",
|
||||
"default-value": "Default value",
|
||||
@@ -513,14 +518,14 @@
|
||||
"placeholder-alert-4": "You have successfully sent mails to all recipients!",
|
||||
"placeholder-mail-alert": "You have successfully sent email to {{name}}. Subsequent signers will get email(s) once {{name}} signs the document",
|
||||
"placeholder-mail-alert-you": "Subsequent signers will get email(s) once you sign the document.",
|
||||
"placeholder-alert-5": "Do you want to sign the document right now?",
|
||||
"placeholder-alert-5": "You can sign now or send it to your email to complete later.",
|
||||
"placeholder-alert-6": "Please setup mail adapter to send mail!",
|
||||
"placeholder-alert-7": "Please select signer for add placeholder!",
|
||||
"email-subject": "email subject",
|
||||
"email-body": "email body",
|
||||
"email-placeholder": "add body of email",
|
||||
"reset-to-default": "Reset to default",
|
||||
"cutomize-email": "Cutomize email",
|
||||
"customize-email": "Customize email",
|
||||
"upgrade-to-customize-email": "Upgrade to customize Email",
|
||||
"sign-url": "Sign url",
|
||||
"mails-sent": "Mails Sent",
|
||||
@@ -537,12 +542,12 @@
|
||||
"sign here": "Sign here",
|
||||
"guest-email-alert": "verification code is sent to your email",
|
||||
"get-otp-alert": "you will get a verification code via Email",
|
||||
"user-already-exist": "User already exist.",
|
||||
"user-already-exist": "User already exists.",
|
||||
"server-error": "Internal server error !",
|
||||
"fill-required-details!": "Please fill required details!",
|
||||
"sign-up-form": "Sign up form",
|
||||
"sign-up": "Sign up",
|
||||
"folder-already-exist!": "folder already exist!",
|
||||
"folder-already-exist!": "folder already exists!",
|
||||
"please-select-position!": "please select position!",
|
||||
"default-sign-alert": "Are you sure you want to auto-sign all requested {{widgetsType}} widgets?",
|
||||
"Your-Signature": "Your signature",
|
||||
@@ -551,7 +556,7 @@
|
||||
"pdf-download": "PDF download",
|
||||
"generate-certificate-alert": " Your completion certificate is being generated. Please wait momentarily. If the download doesn't start shortly, click the button again.",
|
||||
"generate-certificate-err": "We are currently experiencing an issue with certificate generation. Please try again later.",
|
||||
"folder-already-exist": "Folder already exist!",
|
||||
"folder-already-exist": "Folder already exists!",
|
||||
"folder-created-successfully": "Folder created successfully!",
|
||||
"fill-folder-name": "Please fill folder name",
|
||||
"already-exists-this-username": "User already exists with this username!",
|
||||
@@ -562,7 +567,7 @@
|
||||
"add-admin": "Add admin",
|
||||
"opensign-setup": "{{appName}} setup",
|
||||
"join-discord": "Join our discord server",
|
||||
"admin-already-exist": "Admin already exist.",
|
||||
"admin-already-exist": "Admin already exists.",
|
||||
"password-update-alert-1": "Password updated successfully.",
|
||||
"password-update-alert-2": "Your current password is missing or incorrect.",
|
||||
"password-update-alert-3": "Your current password is missing or incorrect.",
|
||||
@@ -585,7 +590,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!",
|
||||
@@ -622,7 +627,7 @@
|
||||
"date": "Date",
|
||||
"report-not-found": "Report not found",
|
||||
"please-wait": "please wait",
|
||||
"select-foler": "Select folder",
|
||||
"select-folder": "Select folder",
|
||||
"generated-on": "Generated on",
|
||||
"text-field": "Text field",
|
||||
"font-size": "Font size",
|
||||
@@ -681,13 +686,13 @@
|
||||
"public-template": "This video demonstrates how to set up your personalized public profile, such as 'https://opensign.me/your-username'. You'll also learn how to customize your tagline and make your templates available for public signing.",
|
||||
"allowModify-widgets": "You can drag and drop any of these fields onto the document, in addition to the fields already designated for you by the document creator."
|
||||
},
|
||||
"enter-email-plaholder": "Add an email address and hit enter",
|
||||
"enter-email-placeholder": "Add an email address and hit enter",
|
||||
"success-email-alert": "Email sent successfully!",
|
||||
"expired-doc-title": "Expired document",
|
||||
"expired-on-mssg": "This document expired on {{expiredDate}} and is no longer available to sign.",
|
||||
"signature-validate-alert": "Please confirm that you have selected at least {{minRequiredCount}} checkboxes.",
|
||||
"signature-validate-alert-2": "Ensure this field is accurately filled and meets all requirements.",
|
||||
"remaing-users": "Remaining users",
|
||||
"remaining-users": "Remaining users",
|
||||
"Role": "Role",
|
||||
"additional-users": "Please purchase add-on users.",
|
||||
"Quantity-of-users": "Quantity of users",
|
||||
@@ -696,12 +701,12 @@
|
||||
"Proceed": "Proceed",
|
||||
"Select": "Select",
|
||||
"Add-seats": "Add seats",
|
||||
"user-name-exist": "user name already exist",
|
||||
"user-name-exist": "user name already exists",
|
||||
"format": "format",
|
||||
"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",
|
||||
@@ -709,7 +714,7 @@
|
||||
"rotate-left": "Rotate left",
|
||||
"rotate-alert-mssg": "All widgets on this page will be lost. Are you sure you want to proceed?",
|
||||
"templateid": "Template-Id",
|
||||
"bulk-send-subcription-alert": "Please upgrade to Professional or Team plan to use bulk send.",
|
||||
"bulk-send-subscription-alert": "Please upgrade to Professional or Team plan to use bulk send.",
|
||||
"generate-test-token": "Generate test token",
|
||||
"regenerate-test-token": "Regenerate test token",
|
||||
"help-test-token": "This token can be used to test the APIs at the https://sandbox.opensignlabs.com/api/v1 endpoint, allowing you to conduct unlimited document signatures. Please note that the sandbox API will sign your documents with self-signed certificates, which may not be recognized as valid by Adobe. Once you've completed your testing, you can upgrade to one of our paid plans to generate a production token.",
|
||||
@@ -729,8 +734,9 @@
|
||||
"copied-code": "COPIED",
|
||||
"Installation": "Installation",
|
||||
"Usage": "Usage",
|
||||
"insufficient-credits": "Insufficient signing credits",
|
||||
"insufficient-credits-mssg": "The owner of this document currently lacks the necessary OpenSign credits for you to sign. Please reach out to the owner if you require further details.",
|
||||
"insufficient-credits": "Insufficient signing credits. Please contact {{ownerEmail}}",
|
||||
"insufficient-credits-title": "Insufficient signing credits",
|
||||
"insufficient-credits-mssg": "The owner of this document currently lacks the necessary {{appName}} credits for you to sign. Please reach out to the owner if you require further details.",
|
||||
"angular-npm-mssg-1": "To integrate OpenSign into your Angular project, simply run the following command:",
|
||||
"quota-mail-info-head": "Monthly Email Limit",
|
||||
"quota-mail-info": "To maintain service quality and prevent spam, OpenSign allows up to 15 emails per month on the free plan. Upgrade now for unlimited email sending.",
|
||||
@@ -862,7 +868,6 @@
|
||||
"js-snippet-msg-1": "You can get the TemplateId from the",
|
||||
"js-snippet-msg-2": "Manage Templates",
|
||||
"js-snippet-msg-3": "page.",
|
||||
"agrrement-alert": "To proceed, you must consent to the electronic record and signature disclosure.",
|
||||
"webhook-already-exists": "The URL already exists! Please try a different one.",
|
||||
"webhook-must-be-secure": "The webhook URL must be secure and use https://",
|
||||
"provide-test-token": "Please generate api test token",
|
||||
@@ -1026,7 +1031,7 @@
|
||||
},
|
||||
"signer-already-present": "Signer already present",
|
||||
"kiosk-sign": "Kiosk Sign",
|
||||
"dont-have-access-to-template": "The template has been deleted or you don't have access. Please contact the sender.",
|
||||
"do-not-have-access-to-template": "The template has been deleted or you don't have access. Please contact the sender.",
|
||||
"kiosk-info": "Kiosk Mode lets you collect in-person signatures quickly and efficiently. Ideal for trade shows, events, or walk-in scenarios where all signers are physically present. ",
|
||||
"learn-more": "Learn more",
|
||||
"finish-mssg": " Are you sure you want to finish the document ?",
|
||||
@@ -1082,8 +1087,6 @@
|
||||
"search-contacts": "Search contacts…",
|
||||
"add-role-alert": "Please add at least one role",
|
||||
"edit-draft": "Edit draft",
|
||||
"invalid-email-found": "Invalid email found: {{email}}",
|
||||
"duplicate-email-found": "Duplicate email found: {{email}}",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal",
|
||||
"billing": "Billing",
|
||||
@@ -1097,7 +1100,7 @@
|
||||
"access-denied": "Access denied",
|
||||
"upgrade": "Upgrade",
|
||||
"do-not-access-app": "You don't have access to this application.",
|
||||
"dont-have-access": "You don't have access.",
|
||||
"do-not-have-access": "You don't have access.",
|
||||
"valid-email-alert": "Please enter a valid email address.",
|
||||
"otp-not-validate": "OTP is not valid.",
|
||||
"domain-not-allowed": "This domain is not allowed",
|
||||
@@ -1114,10 +1117,12 @@
|
||||
"branding-help": "Branding provides white labelling to your app",
|
||||
"custom-sub-domain": "Custom sub-domain",
|
||||
"app-name": "App Name",
|
||||
"provide-domain-name": "provide your domain name",
|
||||
"provide-domain-name": "Only subdomains are allowed (e.g., app.yourdomain.com). Primary/root domains like yourdomain.com are not supported.",
|
||||
"provide-app-name-help": "Provide a unique app name. It will be displayed as your brand name within the application and in all emails wherever the branding is applied.",
|
||||
"upload-app-logo": "Upload a logo in .png, .jpg, or .svg format. Recommended size: {{size}} pixels (5:2 aspect ratio) for best display quality.",
|
||||
"upload-app-favicon": "Upload a square favicon image (.ico, .png, or .svg). Recommended size is {{size}} pixels for best browser compatibility.",
|
||||
"provide-app-name": "provide your app name",
|
||||
"logo": "Logo",
|
||||
"upload-app-logo": "upload your app logo",
|
||||
"prefill-unfilled-widget": "The following required field(s) cannot be left empty: {{emptyWidget}} Please fill them out to proceed.",
|
||||
"Dashboard": "Dashboard",
|
||||
"Analytics": "Analytics",
|
||||
@@ -1240,6 +1245,7 @@
|
||||
},
|
||||
"read-only-date-error": "Read-only date widget must have a default date.",
|
||||
"set-date": "Set Date",
|
||||
"default-date": "Default Date",
|
||||
"set-today": "Signing Date",
|
||||
"enter-name": "Enter name",
|
||||
"enter-email": "Enter email",
|
||||
@@ -1275,5 +1281,120 @@
|
||||
"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",
|
||||
"signing-date": "Signing Date",
|
||||
"date-widget": "Date Widget",
|
||||
"mail-failed": "Something went wrong while sending the email. Please try again.",
|
||||
"mail-status-head": "Next step: Sign your document ?",
|
||||
"send-to-email": "Send to Email",
|
||||
"date-pref-help-sub-title": "Date Widget Preferences let you define the default settings for the Date widget. These defaults are automatically applied whenever you add a new Date widget to a document or template. You can still customize any individual Date widget at any time by clicking the gear icon.",
|
||||
"date-pref-help-format": "Choose the default date format used for Date widgets.",
|
||||
"date-pref-help-default-date": "Set a default date value for Date widgets.",
|
||||
"date-pref-help-signing-date": "When enabled, the Date widget automatically uses the date the signer signs the document.",
|
||||
"date-pref-help-read-only": "When enabled, the Date widget is read-only for the signer.",
|
||||
"upload-csv": "Upload CSV",
|
||||
"file-selected": "file selected",
|
||||
"bulk-send-csv-helper": "Use headers for signer roles (Role Email/Name/Phone) and prefill values (Prefill: Field Name). Required prefill fields must be completed.",
|
||||
"bulk-send-csv-helper-note": "Note: For Date-type prefill fields in the CSV, the value must be provided in this format: {{value}} (example: {{example}}).",
|
||||
"bulk-send-csv-summary": "Imported {{total}} rows from CSV.",
|
||||
"bulk-send-csv-missing-headers": "Missing required CSV headers. Roles: {{roles}}.",
|
||||
"bulk-send-no-records": "Please add at least one recipient before sending.",
|
||||
"csv-only-support": "Only CSV files are supported.",
|
||||
"csv-no-records": "No records found in the CSV.",
|
||||
"select-at-least-option": "Please select at least one option.",
|
||||
"draw-required": "Please draw in the box.",
|
||||
"enter-smtp-username": "Enter your SMTP username",
|
||||
"enter-webhook-url": "Enter webhook url",
|
||||
"enter-user-name": "Enter user name",
|
||||
"enter-tagline": "Enter tagline",
|
||||
"enter-password-or-click-autogenerate": "Enter password or click Autogenerate",
|
||||
"enter-daily-email-quota": "Enter daily email quota",
|
||||
"enter-custom-regular-expression": "Enter custom regular expression",
|
||||
"enter-value": "Enter {{value}}",
|
||||
"are-you-sure-want-to-send-document": "Are you sure want to send document?",
|
||||
"favicon": "Favicon",
|
||||
"delete-two-factor-authentication": "Are you sure you want to delete two-factor authentication?",
|
||||
"paste-html-here": "Paste HTML Here",
|
||||
"open-email-builder": "You can create email template using <1>email builder</1> platform and copy html code.",
|
||||
"contact-saved": "Contact saved successfully.",
|
||||
"remove-limit-of-user": "Are you sure you want to remove this user's limit? They will be able to consume all your organization's available premium credits that are shared across all users.",
|
||||
"current-expiry-date": "Current expiry date",
|
||||
"reminder-error": "Remind once in every (Days) must not exceed Time to complete (Days)",
|
||||
"showing-to-of-records": "Showing {{record}} to {{recordPerPage}} of {{totalRows}} records",
|
||||
"records-per-page": "Records per page",
|
||||
"email-not-found-in-row": "Email not found in row {{row}}",
|
||||
"invalid-email-found-in-row": "Invalid email found in row {{row}}: {{email}}",
|
||||
"duplicate-email-found-in-row": "Duplicate email found in row {{row}}: {{email}}",
|
||||
"prefill": "Prefill",
|
||||
"recipients-&-data": "Recipients & Data",
|
||||
"response": "Response",
|
||||
"prefill-tab-help": "These are the common template fields that will apply to all documents in this bulk send.",
|
||||
"recipient-tab-help-p1": "In this next step, you'll",
|
||||
"recipient-tab-help-p2": "Upload a CSV with recipient details or",
|
||||
"recipient-tab-help-p3": "Manually enter recipient information",
|
||||
"recipient-tab-help-p4": "Each recipient will receive their own document generated from this template.",
|
||||
"response-tab-help": "Review your responses.",
|
||||
"summary": "Summary",
|
||||
"prefill-fields": "Prefill fields",
|
||||
"documents": "Documents",
|
||||
"message": "Message",
|
||||
"formula-help-p1": "Collect numeric input and perform dynamic calculations using formulas.",
|
||||
"example": "Example",
|
||||
"formula-help-p2": "To display a total in number-3, use {{number-1}} + {{number-2}}.",
|
||||
"formula-help-p3": "To calculate a value in number-4, use ({{number-1}} * {{number-2}}) / {{number-3}}.",
|
||||
"formula-help-p4": "Each number widget must have a unique name (e.g., number-1), as formulas rely on these identifiers.",
|
||||
"note-consume-premium-credits": "Note: This feature consumes premium credits.",
|
||||
"note-consume-premium-credits-on-document-send": "Note: This feature consumes premium credits. It is consumed any time a document is sent out, regardless of whether it is signed or not.",
|
||||
"webhook-authentication-help": "{{appName}} secures every webhook by generating an HMAC-SHA256 signature using your webhook security key and the raw request payload. This signature is sent in the x-webhook-signature header. Your server must generate the same signature using the received payload and your secret key. If both signatures match, the webhook request is authentic and has not been modified.",
|
||||
"invalid-widget-found-in-row": "Invalid value detected in row {{row}} for widget {{widget}}."
|
||||
}
|
||||
@@ -75,7 +75,7 @@
|
||||
"password-length": "La contraseña debe tener 8 caracteres",
|
||||
"password-case": "La contraseña debe incluir mayúsculas, minúsculas y números",
|
||||
"password-special-char": " La contraseña debe incluir caracteres especiales",
|
||||
"agreee": "Estoy de acuerdo con los",
|
||||
"agree": "Estoy de acuerdo con los",
|
||||
"term": "Términos de Servicio",
|
||||
"subscribe-to-opensign": "Suscribirse al boletín de OpenSign",
|
||||
"register": "Registrarse",
|
||||
@@ -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.",
|
||||
@@ -220,7 +224,7 @@
|
||||
"New Template": "Nueva plantilla",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "archivo seleccionado",
|
||||
"files-selected": "archivo(s) seleccionado(s)",
|
||||
"template-title": "Título de la plantilla",
|
||||
"document-title": "Título del documento",
|
||||
"title": "Título",
|
||||
@@ -237,7 +241,6 @@
|
||||
"auto-reminder": "Recordatorio automático",
|
||||
"remind-once": " Recordar una vez cada (Días)",
|
||||
"next": "Siguiente",
|
||||
"select-folder": "Seleccionar carpeta",
|
||||
"OpenSign-drive": "{{appName}} Drive",
|
||||
"drive-document-status": {
|
||||
"Completed": "Completado",
|
||||
@@ -266,7 +269,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",
|
||||
@@ -292,7 +295,7 @@
|
||||
"edit-team": "Editar equipo",
|
||||
"name-of-team": "Nombre del equipo",
|
||||
"prev": "Anterior",
|
||||
"no-data-avaliable": "No hay datos disponibles",
|
||||
"no-data-available": "No hay datos disponibles",
|
||||
"add-team": "Agregar equipo",
|
||||
"page-not-found": "Página no encontrada",
|
||||
"users-from-teams": "usuarios de equipos",
|
||||
@@ -303,7 +306,7 @@
|
||||
"delete-user": "Eliminar usuario",
|
||||
"delete": "Eliminar",
|
||||
"add-user": "Agregar usuario",
|
||||
"password-generateed": "La contraseña solo será generada una vez; asegúrate de copiarla.",
|
||||
"password-generated": "La contraseña solo será generada una vez; asegúrate de copiarla.",
|
||||
"Team status": "Estado del equipo",
|
||||
"user-deactivated": "Usuario desactivado.",
|
||||
"user-activated": "Usuario activado.",
|
||||
@@ -333,7 +336,7 @@
|
||||
"quick-send-alert-1": "Todos los roles de este documento están actualmente vinculados a contactos. Para enviar rápidamente copias de esta plantilla a varios firmantes, por favor, asegúrate de que al menos un rol no esté vinculado a ningún contacto.",
|
||||
"quick-send-alert-2": "Por favor, asegúrate de que hay al menos un widget de firma añadido para cada destinatario.",
|
||||
"quick-send-alert-3": "Veuillez ajouter au moins un rôle à ce template.",
|
||||
"quick-send-alert-4": "El envío rápido ha alcanzado el límite.",
|
||||
"quick-send-alert-4": "{{flow}} admite hasta {{count}} firmantes por lote. Divida su lista e inténtelo nuevamente.",
|
||||
"copy-link": "Copiar enlace",
|
||||
"copy": "Copiar",
|
||||
"revoke-document": "Revocar documento",
|
||||
@@ -389,7 +392,8 @@
|
||||
"radio button": "botón de radio",
|
||||
"image": "imagen",
|
||||
"email": "correo",
|
||||
"number": "número"
|
||||
"number": "número",
|
||||
"draw": "Dibujar"
|
||||
},
|
||||
"fields": "Campos",
|
||||
"recipients": "Destinatarios",
|
||||
@@ -433,17 +437,18 @@
|
||||
"reset-password-alert-1": "El enlace para restablecer la contraseña ha sido enviado a tu correo electrónico",
|
||||
"reset-password-alert-2": "Por favor, configura un adaptador de correo ",
|
||||
"reset-password-alert-3": "Restablece tu contraseña",
|
||||
"faild-animation": "Error al cargar la animación",
|
||||
"failed-animation": "Error al cargar la animación",
|
||||
"apply": "Aplicar",
|
||||
"select-columns": "Seleccionar columnas",
|
||||
"copy-type": {
|
||||
"All pages": "Todas las páginas",
|
||||
"All pages but last": "Todas las páginas menos la última",
|
||||
"All pages but first": "Todas las páginas menos la primera",
|
||||
"Next to current widget": "Junto al widget actual"
|
||||
"Next to current widget": "Junto al widget actual",
|
||||
"Page range": "Rango de páginas"
|
||||
},
|
||||
"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",
|
||||
@@ -513,14 +518,14 @@
|
||||
"placeholder-alert-4": "¡Has enviado exitosamente correos a todos los destinatarios!",
|
||||
"placeholder-mail-alert": "Has enviado un correo electrónico con éxito a {{name}}. Los siguientes firmantes recibirán un correo electrónico una vez que {{name}} firme el documento.",
|
||||
"placeholder-mail-alert-you": "Los firmantes siguientes recibirán un correo electrónico una vez que usted firme el documento.",
|
||||
"placeholder-alert-5": "¿Quieres firmar documentos ahora mismo?",
|
||||
"placeholder-alert-5": "Puede firmar ahora o enviarlo a su correo electrónico para completarlo más tarde.",
|
||||
"placeholder-alert-6": "¡Por favor, configura el adaptador de correo para enviar correos!",
|
||||
"placeholder-alert-7": "¡Por favor, selecciona un firmante para agregar un marcador de posición!",
|
||||
"email-subject": "asunto del correo",
|
||||
"email-body": "cuerpo del correo",
|
||||
"email-placeholder": "agregar cuerpo del correo",
|
||||
"reset-to-default": "Restablecer a por defecto",
|
||||
"cutomize-email": "Personalizar correo",
|
||||
"customize-email": "Personalizar correo",
|
||||
"upgrade-to-customize-email": "Mejora para personalizar el correo",
|
||||
"sign-url": "URL de firma",
|
||||
"mails-sent": "Correos enviados",
|
||||
@@ -585,7 +590,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!",
|
||||
@@ -622,7 +627,7 @@
|
||||
"date": "Fecha",
|
||||
"report-not-found": "Reporte no encontrado",
|
||||
"please-wait": "por favor, espera",
|
||||
"select-foler": "Seleccionar carpeta",
|
||||
"select-folder": "Seleccionar carpeta",
|
||||
"generated-on": "Generado el",
|
||||
"text-field": "Campo de texto",
|
||||
"font-size": "Tamaño de la fuente",
|
||||
@@ -681,13 +686,13 @@
|
||||
"public-template": "Este video muestra cómo puedes configurar tu perfil público personalizado, como «https://opensign.me/tu-nombre-de-usuario». También aprenderás cómo personalizar tu eslogan y cómo hacer tus plantillas disponibles para su firmado público.",
|
||||
"allowModify-widgets": "Puede arrastrar y soltar cualquiera de estos campos en el documento, además de los campos ya designados para usted por el creador del documento"
|
||||
},
|
||||
"enter-email-plaholder": "Agrega una dirección de correo y presiona «Enter»",
|
||||
"enter-email-placeholder": "Agrega una dirección de correo y presiona «Enter»",
|
||||
"success-email-alert": "¡Correo enviado exitosamente!",
|
||||
"expired-doc-title": "Documento expirado",
|
||||
"expired-on-mssg": "Este documento expiro el {{expiredDate}} y ya no está disponible para ser firmado.",
|
||||
"signature-validate-alert": "Por favor, confirma que has seleccionado al menos {{minRequiredCount}} casillas.",
|
||||
"signature-validate-alert-2": "Asegúrate de que este campo esté correctamente rellenado y que cumpla con todos los requisitos.",
|
||||
"remaing-users": "Usuarios restantes",
|
||||
"remaining-users": "Usuarios restantes",
|
||||
"Role": "Rol",
|
||||
"additional-users": "Por favor, compra el complemento Usuarios.",
|
||||
"Quantity-of-users": "Cantidad de usuarios",
|
||||
@@ -701,7 +706,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",
|
||||
@@ -709,7 +714,7 @@
|
||||
"rotate-left": "Rotar a la izquierda",
|
||||
"rotate-alert-mssg": "Todos los widgets de esta página se perderán. ¿En definitiva quieres proceder?",
|
||||
"templateid": "ID de plantilla",
|
||||
"bulk-send-subcription-alert": "Por favor, mejora al plan a Professional o Team para usar envío en masa.",
|
||||
"bulk-send-subscription-alert": "Por favor, mejora al plan a Professional o Team para usar envío en masa.",
|
||||
"generate-test-token": "Generar token de prueba",
|
||||
"regenerate-test-token": "Regenerar token de prueba",
|
||||
"help-test-token": "Este token puede ser usado para probar las API en el punto de acceso «https://sandbox.opensignlabs.com/api/v1», permitiéndote realizar firmas de documentos ilimitadas. Por favor, ten en cuenta que el API de pruebas firmará tus documentos con certificados autofirmados, que podrían no ser reconocidos por Adobe como válidos. Una vez hayas completado tus pruebas, puedes mejorar a uno de nuestros planes de pago para generar un token de producción.",
|
||||
@@ -729,8 +734,9 @@
|
||||
"copied-code": "COPIADO",
|
||||
"Installation": "Instalación",
|
||||
"Usage": "Uso",
|
||||
"insufficient-credits": "Créditos de firma insuficientes",
|
||||
"insufficient-credits-mssg": "El propietario de este documento actualmente carece de los créditos OpenSign necesarios para que puedas firmarlo. Por favor, contacta al propietario si necesitas más detalles.",
|
||||
"insufficient-credits": "Créditos de firma insuficientes. Por favor contacte con {{ownerEmail}}",
|
||||
"insufficient-credits-title": "Créditos de firma insuficientes",
|
||||
"insufficient-credits-mssg": "El propietario de este documento actualmente carece de los créditos {{appName}} necesarios para que puedas firmarlo. Por favor, contacta al propietario si necesitas más detalles.",
|
||||
"angular-npm-mssg-1": "Para integrar OpenSign a tu proyecto Angular, simplemente ejecuta los siguientes comandos:",
|
||||
"quota-mail-info-head": "Límite mensual de correos electrónicos",
|
||||
"quota-mail-info": "Para mantener la calidad del servicio y prevenir el spam, OpenSign permite hasta 15 correos electrónicos por mes en el plan gratuito. Mejora tu plan ahora para envíos ilimitados de correos electrónicos.",
|
||||
@@ -862,7 +868,6 @@
|
||||
"js-snippet-msg-1": "Puede obtener el TemplateId en la página Administrar plantillas",
|
||||
"js-snippet-msg-2": "Administrar plantillas",
|
||||
"js-snippet-msg-3": "página.",
|
||||
"agrrement-alert": "Para continuar, debe consentir la divulgación de registros y firmas electrónicas.",
|
||||
"webhook-already-exists": "¡La URL ya existe! Pruebe con uno diferente.",
|
||||
"webhook-must-be-secure": "La URL del webhook debe ser segura y utilizar https://",
|
||||
"provide-test-token": "Por favor genere un token de prueba de API",
|
||||
@@ -1026,7 +1031,7 @@
|
||||
},
|
||||
"signer-already-present": "Firmante ya presente",
|
||||
"kiosk-sign": "Firma en quiosco",
|
||||
"dont-have-access-to-template": "El template ha sido eliminado o no tiene acceso. Por favor, contacte al remitente.",
|
||||
"do-not-have-access-to-template": "El template ha sido eliminado o no tiene acceso. Por favor, contacte al remitente.",
|
||||
"kiosk-info": "El Modo Kiosco le permite recopilar firmas en persona de forma rápida y eficiente. Ideal para ferias, eventos o situaciones con personas que firman en el lugar. ",
|
||||
"learn-more": "Más información",
|
||||
"finish-mssg": "¿Está seguro de que desea finalizar el documento?",
|
||||
@@ -1080,8 +1085,6 @@
|
||||
"search-templates": "Buscar plantillas…",
|
||||
"search-documents": "Buscar documentos…",
|
||||
"search-contacts": "Buscar contactos…",
|
||||
"invalid-email-found": "Correo electrónico no válido encontrado: {{email}}",
|
||||
"duplicate-email-found": "Correo electrónico duplicado encontrado: {{email}}",
|
||||
"vertical": "Vertical",
|
||||
"add-role-alert": "Por favor, agregue al menos un rol",
|
||||
"edit-draft": "Editar borrador",
|
||||
@@ -1097,7 +1100,7 @@
|
||||
"access-denied": "Acceso denegado",
|
||||
"upgrade": "Actualizar",
|
||||
"do-not-access-app": "No tienes acceso a esta aplicación.",
|
||||
"dont-have-access": "No tienes acceso.",
|
||||
"do-not-have-access": "No tienes acceso.",
|
||||
"valid-email-alert": "Por favor ingresa una dirección de correo electrónico válida.",
|
||||
"otp-not-validate": "OTP no es válido.",
|
||||
"domain-not-allowed": "Este dominio no está permitido",
|
||||
@@ -1114,10 +1117,12 @@
|
||||
"branding-help": "El branding permite el white labelling de su aplicación",
|
||||
"custom-sub-domain": "Subdominio personalizado",
|
||||
"app-name": "Nombre de la aplicación",
|
||||
"provide-domain-name": "Proporcione su nombre de dominio",
|
||||
"provide-domain-name": "Solo se permiten subdominios (p. ej., app.tudominio.com). No se admiten dominios principales/raíz como tudominio.com.",
|
||||
"provide-app-name-help": "Indique un nombre único para la aplicación. Se mostrará como el nombre de su marca dentro de la aplicación y en todos los correos electrónicos donde se aplique la marca.",
|
||||
"upload-app-logo": "Sube un logotipo en formato .png, .jpg o .svg. Tamaño recomendado: {{size}} píxeles (relación de aspecto 5:2) para la mejor calidad de visualización.",
|
||||
"upload-app-favicon": "Sube una imagen cuadrada de favicon (.ico, .png o .svg). El tamaño recomendado es {{size}} píxeles para la mejor compatibilidad con el navegador.",
|
||||
"provide-app-name": "Proporcione el nombre de su aplicación",
|
||||
"logo": "Logo",
|
||||
"upload-app-logo": "Suba el logotipo de su aplicación",
|
||||
"prefill-unfilled-widget": "Los siguientes campos obligatorios no pueden quedar vacíos: {{emptyWidget}}. Por favor, complételos para continuar.",
|
||||
"Dashboard": "Panel",
|
||||
"Analytics": "Analítica",
|
||||
@@ -1240,6 +1245,7 @@
|
||||
},
|
||||
"read-only-date-error": "El widget de fecha de solo lectura debe tener una fecha predeterminada.",
|
||||
"set-date": "Establecer fecha",
|
||||
"default-date": "Fecha predeterminada",
|
||||
"set-today": "Fecha de firma",
|
||||
"enter-name": "Ingrese nombre",
|
||||
"enter-email": "Ingrese correo electrónico",
|
||||
@@ -1275,5 +1281,120 @@
|
||||
"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",
|
||||
"signing-date": "Fecha de firma",
|
||||
"date-widget": "Widget de fecha",
|
||||
"mail-failed": "Algo salió mal al enviar el correo electrónico. Por favor, inténtelo de nuevo.",
|
||||
"mail-status-head": "Siguiente paso: ¿Firmar su documento?",
|
||||
"send-to-email": "Enviar por correo electrónico",
|
||||
"date-pref-help-sub-title": "Las preferencias del widget de fecha le permiten definir la configuración predeterminada del widget de fecha. Estos valores se aplican automáticamente cada vez que añade un nuevo widget de fecha a un documento o plantilla. Aún puede personalizar cualquier widget de fecha individual en cualquier momento haciendo clic en el icono de engranaje.",
|
||||
"date-pref-help-format": "Elija el formato de fecha predeterminado utilizado para los widgets de fecha.",
|
||||
"date-pref-help-default-date": "Establezca un valor de fecha predeterminado para los widgets de fecha.",
|
||||
"date-pref-help-signing-date": "Cuando está habilitado, el widget de fecha utiliza automáticamente la fecha en que el firmante firma el documento.",
|
||||
"date-pref-help-read-only": "Cuando está habilitado, el widget de fecha es de solo lectura para el firmante.",
|
||||
"upload-csv": "Subir CSV",
|
||||
"file-selected": "archivo seleccionado",
|
||||
"bulk-send-csv-helper": "Use encabezados para los roles de firmantes (Correo/Nombre/Teléfono del rol) y valores de prellenado (Prellenado: Nombre del campo). Los campos de prellenado obligatorios deben completarse.",
|
||||
"bulk-send-csv-helper-note": "Nota: Para los campos de prellenado de tipo fecha en el CSV, el valor debe proporcionarse en el siguiente formato: {{value}} (ejemplo: {{example}}).",
|
||||
"bulk-send-csv-summary": "Se importaron {{total}} filas del CSV.",
|
||||
"bulk-send-csv-missing-headers": "Faltan encabezados obligatorios del CSV. Roles: {{roles}}.",
|
||||
"bulk-send-no-records": "Agregue al menos un destinatario antes de enviar.",
|
||||
"csv-only-support": "Solo se admiten archivos CSV.",
|
||||
"csv-no-records": "No se encontraron registros en el CSV.",
|
||||
"select-at-least-option": "Por favor, seleccione al menos una opción.",
|
||||
"draw-required": "Por favor, dibuje en el campo.",
|
||||
"enter-smtp-username": "Ingrese su nombre de usuario SMTP",
|
||||
"enter-webhook-url": "Ingrese la URL del webhook",
|
||||
"enter-user-name": "Ingrese el nombre de usuario",
|
||||
"enter-tagline": "Ingrese el eslogan",
|
||||
"enter-password-or-click-autogenerate": "Ingrese la contraseña o haga clic en Autogenerar",
|
||||
"enter-daily-email-quota": "Ingrese la cuota diaria de correo electrónico",
|
||||
"enter-custom-regular-expression": "Ingrese una expresión regular personalizada",
|
||||
"enter-value": "Ingrese {{value}}",
|
||||
"are-you-sure-want-to-send-document": "¿Está seguro de que desea enviar el documento?",
|
||||
"favicon": "Favicon",
|
||||
"delete-two-factor-authentication": "¿Está seguro de que desea eliminar la autenticación de dos factores?",
|
||||
"paste-html-here": "Pegar HTML aquí",
|
||||
"open-email-builder": "Puede crear una plantilla de correo electrónico utilizando la plataforma <1>email builder</1> y copiar el código HTML.",
|
||||
"contact-saved": "Contacto guardado correctamente.",
|
||||
"remove-limit-of-user": "¿Está seguro de que desea eliminar el límite de este usuario? Podrá consumir todos los créditos premium disponibles de su organización que se comparten entre todos los usuarios.",
|
||||
"current-expiry-date": "Fecha de vencimiento actual",
|
||||
"reminder-error": "“Recordar una vez cada (días)” no debe exceder el “Tiempo para completar (días)”.",
|
||||
"showing-to-of-records": "Mostrando {{record}} a {{recordPerPage}} de {{totalRows}} registros",
|
||||
"records-per-page": "Registros por página",
|
||||
"email-not-found-in-row": "No se encontró correo electrónico en la fila {{row}}.",
|
||||
"invalid-email-found-in-row": "Correo electrónico no válido encontrado en la fila {{row}}: {{email}}",
|
||||
"duplicate-email-found-in-row": "Correo electrónico duplicado encontrado en la fila {{row}}: {{email}}",
|
||||
"prefill": "Prellenado",
|
||||
"recipients-&-data": "Destinatarios y datos",
|
||||
"response": "Respuesta",
|
||||
"prefill-tab-help": "Estos son los campos comunes de la plantilla que se aplicarán a todos los documentos en este envío masivo.",
|
||||
"recipient-tab-help-p1": "En este siguiente paso, usted:",
|
||||
"recipient-tab-help-p2": "Subirá un CSV con los detalles de los destinatarios o",
|
||||
"recipient-tab-help-p3": "Ingresará manualmente la información de los destinatarios",
|
||||
"recipient-tab-help-p4": "Cada destinatario recibirá su propio documento generado a partir de esta plantilla.",
|
||||
"response-tab-help": "Revise sus respuestas.",
|
||||
"summary": "Resumen",
|
||||
"prefill-fields": "Campos de prellenado",
|
||||
"documents": "Documentos",
|
||||
"message": "Mensaje",
|
||||
"formula-help-p1": "Recopile entradas numéricas y realice cálculos dinámicos usando fórmulas.",
|
||||
"example": "Ejemplo",
|
||||
"formula-help-p2": "Para mostrar un total en number-3, use {{number-1}} + {{number-2}}.",
|
||||
"formula-help-p3": "Para calcular un valor en number-4, use ({{number-1}} * {{number-2}}) / {{number-3}}.",
|
||||
"formula-help-p4": "Cada widget numérico debe tener un nombre único (por ejemplo, number-1), ya que las fórmulas dependen de estos identificadores.",
|
||||
"note-consume-premium-credits": "Nota: Esta función consume créditos premium.",
|
||||
"note-consume-premium-credits-on-document-send": "Nota: Esta función consume créditos premium. Se consumen cada vez que se envía un documento, independientemente de si se firma o no.",
|
||||
"webhook-authentication-help": "{{appName}} protege cada webhook generando una firma HMAC-SHA256 utilizando su clave de seguridad del webhook y el payload original de la solicitud. Esta firma se envía en el encabezado x-webhook-signature. Su servidor debe generar la misma firma utilizando el payload recibido y su clave secreta. Si ambas firmas coinciden, la solicitud del webhook es auténtica y no ha sido modificada.",
|
||||
"invalid-widget-found-in-row": "Se detectó un valor no válido en la fila {{row}} para el widget {{widget}}."
|
||||
}
|
||||
@@ -75,7 +75,7 @@
|
||||
"password-length": "Le mot de passe doit être de plus de 8 caractères",
|
||||
"password-case": "Le mot de passe doit contenir une lettre majuscule, minuscule et un chiffre ",
|
||||
"password-special-char": "Le mot de passe doit contenir un caractère spécial",
|
||||
"agreee": "Je suis d'accord avec le",
|
||||
"agree": "Je suis d'accord avec le",
|
||||
"term": "Conditions d'utilisation",
|
||||
"subscribe-to-opensign": "Abonnez-vous à la newsletter OpenSign",
|
||||
"register": "S'enregistrer ",
|
||||
@@ -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.",
|
||||
@@ -220,7 +224,7 @@
|
||||
"New Template": "Nouveau modèle",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "fichier sélectionné",
|
||||
"files-selected": "fichier(s) sélectionné(s)",
|
||||
"template-title": "Titre du modèle",
|
||||
"document-title": "Titre du document",
|
||||
"title": "Titre",
|
||||
@@ -237,7 +241,6 @@
|
||||
"auto-reminder": "Rappel automatique",
|
||||
"remind-once": "Rappeler une fois tous les (jours)",
|
||||
"next": "Suivant",
|
||||
"select-folder": "Sélectionner le dossier",
|
||||
"OpenSign-drive": "Lecteur {{appName}}",
|
||||
"drive-document-status": {
|
||||
"Completed": "Complété",
|
||||
@@ -266,7 +269,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",
|
||||
@@ -292,7 +295,7 @@
|
||||
"edit-team": "Modifier l'équipe",
|
||||
"name-of-team": "Nom de l'équipe",
|
||||
"prev": "précédent",
|
||||
"no-data-avaliable": "pas de données disponibles",
|
||||
"no-data-available": "pas de données disponibles",
|
||||
"add-team": "Ajouter une équipe",
|
||||
"page-not-found": "Page non trouvée",
|
||||
"users-from-teams": "utilisateurs de votre Équipe",
|
||||
@@ -303,7 +306,7 @@
|
||||
"delete-user": "Supprimer l'utilisateur",
|
||||
"delete": "Supprimer",
|
||||
"add-user": "Ajouter un utilisateur",
|
||||
"password-generateed": "Le mot de passe ne sera généré qu'une seule fois ; assurez-vous de le copier.",
|
||||
"password-generated": "Le mot de passe ne sera généré qu'une seule fois ; assurez-vous de le copier.",
|
||||
"Team status": "Statut de l'équipe",
|
||||
"user-deactivated": "Utilisateur désactivé.",
|
||||
"user-activated": "Activé par l'utilisateur.",
|
||||
@@ -333,7 +336,7 @@
|
||||
"quick-send-alert-1": "Tous les rôles dans ce document sont actuellement liés à des contacts. Pour envoyer rapidement des copies de ce modèle à plusieurs signataires, veuillez vous assurer qu'au moins un rôle n'est lié à aucun contact.",
|
||||
"quick-send-alert-2": "Veuillez vous assurer qu'au moins un widget de signature est ajouté pour tous les destinataires.",
|
||||
"quick-send-alert-3": "Veuillez ajouter au moins un rôle à ce template.",
|
||||
"quick-send-alert-4": "L'envoi rapide a atteint la limite.",
|
||||
"quick-send-alert-4": "{{flow}} prend en charge jusqu'à {{count}} signataires par lot. Divisez votre liste et réessayez.",
|
||||
"copy-link": "Copier le lien",
|
||||
"copy": "Copier",
|
||||
"revoke-document": "Révoquer le document",
|
||||
@@ -389,7 +392,8 @@
|
||||
"radio button": "bouton radio",
|
||||
"image": "image",
|
||||
"email": "e-mail",
|
||||
"number": "nombre"
|
||||
"number": "nombre",
|
||||
"draw": "Dessiner"
|
||||
},
|
||||
"fields": "Des champs",
|
||||
"recipients": "Destinataires",
|
||||
@@ -433,16 +437,18 @@
|
||||
"reset-password-alert-1": "Le lien de réinitialisation du mot de passe a été envoyé à votre identifiant de messagerie",
|
||||
"reset-password-alert-2": "Veuillez configurer l'adaptateur de messagerie",
|
||||
"reset-password-alert-3": "Réinitialisez votre mot de passe",
|
||||
"faild-animation": "Échec du chargement de l'animation",
|
||||
"failed-animation": "Échec du chargement de l'animation",
|
||||
"apply": "Appliquer",
|
||||
"select-columns": "Sélectionner les colonnes",
|
||||
"copy-type": {
|
||||
"All pages": "Toutes les pages",
|
||||
"All pages but last": "Toutes les pages sauf la dernière",
|
||||
"All pages but first": "Toutes les pages sauf la première",
|
||||
"Next to current widget": "À côté du widget actuel"
|
||||
"Next to current widget": "À côté du widget actuel",
|
||||
"Page range": "Plage de pages"
|
||||
},
|
||||
"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",
|
||||
@@ -512,14 +518,14 @@
|
||||
"placeholder-alert-4": "Vous avez envoyé avec succès des mails à tous les",
|
||||
"placeholder-mail-alert": "Vous avez envoyé un e-mail avec succès à {{name}}. Les signataires suivants recevront un e-mail une fois que {{name}} aura signé le document.",
|
||||
"placeholder-mail-alert-you": "Les signataires suivants recevront un e-mail une fois que vous aurez signé le document.",
|
||||
"placeholder-alert-5": "Voulez-vous signer des documents maintenant ?",
|
||||
"placeholder-alert-5": "Vous pouvez signer maintenant ou l’envoyer à votre e-mail pour le terminer plus tard.",
|
||||
"placeholder-alert-6": "Veuillez configurer l'adaptateur de messagerie pour envoyer du courrier !",
|
||||
"placeholder-alert-7": "Veuillez sélectionner le signataire pour ajouter un espace réservé !",
|
||||
"email-subject": "Sujet du courriel",
|
||||
"email-body": "corps de l'e-mail",
|
||||
"email-placeholder": "Ajouter le corps de l'e-mail",
|
||||
"reset-to-default": "Réinitialiser aux valeurs par défaut",
|
||||
"cutomize-email": "Personnaliser l'e-mail",
|
||||
"customize-email": "Personnaliser l'e-mail",
|
||||
"upgrade-to-customize-email": "Mise à niveau pour personnaliser l'e-mail",
|
||||
"sign-url": "Signer l'URL",
|
||||
"mails-sent": "Mails envoyés avec succès !",
|
||||
@@ -584,7 +590,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 ! ",
|
||||
@@ -621,7 +627,7 @@
|
||||
"date": "Date",
|
||||
"report-not-found": "Rapport introuvable",
|
||||
"please-wait": "S'il vous plaît, attendez",
|
||||
"select-foler": "Sélectionner le dossier",
|
||||
"select-folder": "Sélectionner le dossier",
|
||||
"generated-on": "Généré le",
|
||||
"text-field": "Champ de texte",
|
||||
"font-size": "Taille de police",
|
||||
@@ -680,14 +686,14 @@
|
||||
"public-template": "Cette vidéo montre comment configurer votre profil public personnalisé, tel que 'https://opensign.me/your-username'. Vous apprendrez également comment personnaliser votre slogan et rendre vos modèles disponibles pour la signature publique.",
|
||||
"allowModify-widgets": "Vous pouvez faire glisser et déposer n'importe lequel de ces champs sur le document, en plus des champs déjà désignés pour vous par le créateur du document."
|
||||
},
|
||||
"enter-email-plaholder": "Ajoutez une adresse email et appuyez sur Entrée",
|
||||
"enter-email-placeholder": "Ajoutez une adresse email et appuyez sur Entrée",
|
||||
"success-email-alert": "E-mail envoyé avec succès!",
|
||||
"expired-doc-title": "Document expiré",
|
||||
"expired-on-mssg": "Ce document a expiré le {{expiredDate}} et n'est plus disponible pour signature.",
|
||||
"signature-validate-alert": "Veuillez confirmer que vous avez coché au moins {{minRequiredCount}} cases.",
|
||||
"signature-validate-alert-2": "Assurez-vous que ce champ est rempli avec précision et répond à toutes les exigences.",
|
||||
"user-name-exist": "le nom d'utilisateur existe déjà",
|
||||
"remaing-users": "Utilisateurs restants",
|
||||
"remaining-users": "Utilisateurs restants",
|
||||
"Role": "Rôle",
|
||||
"additional-users": "Veuillez acheter des utilisateurs supplémentaires.",
|
||||
"Quantity-of-users": "Quantité d'utilisateurs",
|
||||
@@ -701,14 +707,14 @@
|
||||
"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",
|
||||
"rotate-left": "Faire pivoter à gauche",
|
||||
"rotate-alert-mssg": "Tous les widgets de cette page seront perdus. Êtes-vous sûr de vouloir continuer ?",
|
||||
"templateid": "ID de modèle",
|
||||
"bulk-send-subcription-alert": "Veuillez passer au forfait Professionnel ou Équipe pour utiliser Quicksend.",
|
||||
"bulk-send-subscription-alert": "Veuillez passer au forfait Professionnel ou Équipe pour utiliser Quicksend.",
|
||||
"generate-test-token": "Générer jeton de test",
|
||||
"regenerate-test-token": "Régénérer le jeton de test",
|
||||
"help-test-token": "Ce jeton peut être utilisé pour tester les API au niveau du point de terminaison https://sandbox.opensignlabs.com/api/v1, vous permettant ainsi d'effectuer un nombre illimité de signatures de documents. Veuillez noter que l'API sandbox signera vos documents avec des certificats auto-signés, qui peuvent ne pas être reconnus comme valides par Adobe. Une fois vos tests terminés, vous pouvez passer à l'un de nos forfaits payants pour générer un jeton de production.",
|
||||
@@ -728,8 +734,9 @@
|
||||
"copied-code": "COPIÉ",
|
||||
"Installation": "Installation",
|
||||
"Usage": "Usage",
|
||||
"insufficient-credits": "Crédits de signature insuffisants",
|
||||
"insufficient-credits-mssg": "Le propriétaire de ce document ne dispose actuellement pas des crédits OpenSign nécessaires pour que vous puissiez le signer. Veuillez contacter le propriétaire si vous avez besoin de plus de détails.",
|
||||
"insufficient-credits": "Crédits de signature insuffisants. Veuillez contacter {{ownerEmail}}",
|
||||
"insufficient-credits-title": "Crédits de signature insuffisants",
|
||||
"insufficient-credits-mssg": "Le propriétaire de ce document ne dispose actuellement pas des crédits {{appName}} nécessaires pour que vous puissiez le signer. Veuillez contacter le propriétaire si vous avez besoin de plus de détails.",
|
||||
"angular-npm-mssg-1": "Pour intégrer OpenSign dans votre projet Angular, exécutez simplement la commande suivante :",
|
||||
"quota-mail-info-head": "Limite mensuelle d'e-mails",
|
||||
"quota-mail-info": "Pour maintenir la qualité du service et prévenir le spam, OpenSign permet jusqu'à 15 e-mails par mois avec le plan gratuit. Passez à l'offre supérieure pour un envoi illimité d'e-mails.",
|
||||
@@ -861,7 +868,6 @@
|
||||
"term-cond-p30": " ou par email",
|
||||
"js-snippet-msg": "Pour intégrer des modèles {{appName}} dans vos sites Web ou pages de destination HTML, vous pouvez utiliser le code ci-dessous:",
|
||||
"js-snippet-msg-1": "Vous pouvez obtenir le TemplateId à partir de la page Gérer les modèles.",
|
||||
"agrrement-alert": "Pour procéder, vous devez consentir à la divulgation des enregistrements et signatures électroniques.",
|
||||
"webhook-already-exists": "L'URL existe déjà ! Veuillez en essayer un autre.",
|
||||
"webhook-must-be-secure": "L'URL du webhook doit être sécurisée et utiliser https://",
|
||||
"provide-test-token": "Veuillez générer un jeton de test API",
|
||||
@@ -1025,7 +1031,7 @@
|
||||
},
|
||||
"signer-already-present": "Signataire déjà présent",
|
||||
"kiosk-sign": "Signature sur kiosque",
|
||||
"dont-have-access-to-template": "Le template a été supprimé ou vous n'y avez pas accès. Veuillez contacter l'expéditeur.",
|
||||
"do-not-have-access-to-template": "Le template a été supprimé ou vous n'y avez pas accès. Veuillez contacter l'expéditeur.",
|
||||
"kiosk-info": "Le Mode Kiosque vous permet de recueillir des signatures en personne rapidement et efficacement. Idéal pour les salons, événements ou situations où tous les signataires sont physiquement présents. ",
|
||||
"learn-more": "En savoir plus",
|
||||
"finish-mssg": "Êtes-vous sûr de vouloir terminer le document ?",
|
||||
@@ -1081,8 +1087,6 @@
|
||||
"search-contacts": "Rechercher des contacts…",
|
||||
"add-role-alert": "Veuillez ajouter au moins un rôle",
|
||||
"edit-draft": "Modifier le brouillon",
|
||||
"invalid-email-found": "Adresse e-mail invalide trouvée : {{email}}",
|
||||
"duplicate-email-found": "Adresse e-mail en double trouvée : {{email}}",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal",
|
||||
"billing": "Facturation",
|
||||
@@ -1096,7 +1100,7 @@
|
||||
"access-denied": "Accès refusé",
|
||||
"upgrade": "Mettre à jour",
|
||||
"do-not-access-app": "Vous n'avez pas accès à cette application.",
|
||||
"dont-have-access": "Vous n'avez pas accès.",
|
||||
"do-not-have-access": "Vous n'avez pas accès.",
|
||||
"valid-email-alert": "Veuillez saisir une adresse e-mail valide.",
|
||||
"otp-not-validate": "OTP non valide.",
|
||||
"domain-not-allowed": "Ce domaine n'est pas autorisé",
|
||||
@@ -1113,10 +1117,12 @@
|
||||
"branding-help": "Le branding permet le white labelling de votre application",
|
||||
"custom-sub-domain": "Sous-domaine personnalisé",
|
||||
"app-name": "Nom de l'application",
|
||||
"provide-domain-name": "Fournissez votre nom de domaine",
|
||||
"provide-domain-name": "Seuls les sous-domaines sont autorisés (p. ex., app.votredomaine.com). Les domaines principaux/racine comme votredomaine.com ne sont pas pris en charge.",
|
||||
"provide-app-name-help": "Indiquez un nom d'application unique. Il sera affiché comme le nom de votre marque dans l'application et dans tous les e-mails partout où l'image de marque est appliquée.",
|
||||
"upload-app-logo": "Téléversez un logo au format .png, .jpg ou .svg. Taille recommandée : {{size}} pixels (ratio 5:2) pour une meilleure qualité d'affichage.",
|
||||
"upload-app-favicon": "Téléversez une image de favicon carrée (.ico, .png ou .svg). La taille recommandée est de {{size}} pixels pour une meilleure compatibilité avec les navigateurs.",
|
||||
"provide-app-name": "Fournissez le nom de votre application",
|
||||
"logo": "Logo",
|
||||
"upload-app-logo": "Téléchargez le logo de votre application",
|
||||
"prefill-unfilled-widget": "Les champs obligatoires suivants ne peuvent pas être vides : {{emptyWidget}}. Veuillez les remplir pour continuer.",
|
||||
"Dashboard": "Tableau de bord",
|
||||
"Analytics": "Analytique",
|
||||
@@ -1239,6 +1245,7 @@
|
||||
},
|
||||
"read-only-date-error": "Le widget de date en lecture seule doit avoir une date par défaut.",
|
||||
"set-date": "Définir la date",
|
||||
"default-date": "Date par défaut",
|
||||
"set-today": "Date de signature",
|
||||
"enter-name": "Saisir le nom",
|
||||
"enter-email": "Saisir l'e-mail",
|
||||
@@ -1274,5 +1281,120 @@
|
||||
"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",
|
||||
"signing-date": "Date de signature",
|
||||
"date-widget": "Widget de date",
|
||||
"mail-failed": "Une erreur s'est produite lors de l'envoi de l'e-mail. Veuillez réessayer.",
|
||||
"mail-status-head": "Étape suivante : signer votre document ?",
|
||||
"send-to-email": "Envoyer par e-mail",
|
||||
"date-pref-help-sub-title": "Les préférences du widget de date vous permettent de définir les paramètres par défaut du widget de date. Ces paramètres sont automatiquement appliqués lorsque vous ajoutez un nouveau widget de date à un document ou à un modèle. Vous pouvez toujours personnaliser chaque widget de date individuellement en cliquant sur l'icône d'engrenage.",
|
||||
"date-pref-help-format": "Choisissez le format de date par défaut utilisé pour les widgets de date.",
|
||||
"date-pref-help-default-date": "Définissez une valeur de date par défaut pour les widgets de date.",
|
||||
"date-pref-help-signing-date": "Lorsqu'elle est activée, le widget de date utilise automatiquement la date à laquelle le signataire signe le document.",
|
||||
"date-pref-help-read-only": "Lorsqu'elle est activée, le widget de date est en lecture seule pour le signataire.",
|
||||
"upload-csv": "Téléverser un CSV",
|
||||
"file-selected": "fichier sélectionné",
|
||||
"bulk-send-csv-helper": "Utilisez des en-têtes pour les rôles des signataires (E-mail/Nom/Téléphone du rôle) et les valeurs de préremplissage (Préremplissage : Nom du champ). Les champs de préremplissage requis doivent être complétés.",
|
||||
"bulk-send-csv-helper-note": "Remarque : Pour les champs de préremplissage de type date dans le CSV, la valeur doit être fournie dans le format suivant : {{value}} (exemple : {{example}}).",
|
||||
"bulk-send-csv-summary": "{{total}} lignes importées depuis le CSV.",
|
||||
"bulk-send-csv-missing-headers": "En-têtes CSV requis manquants. Rôles : {{roles}}.",
|
||||
"bulk-send-no-records": "Veuillez ajouter au moins un destinataire avant l'envoi.",
|
||||
"csv-only-support": "Seuls les fichiers CSV sont pris en charge.",
|
||||
"csv-no-records": "Aucun enregistrement trouvé dans le CSV.",
|
||||
"select-at-least-option": "Veuillez sélectionner au moins une option.",
|
||||
"draw-required": "Veuillez dessiner dans le champ.",
|
||||
"enter-smtp-username": "Saisissez votre nom d'utilisateur SMTP",
|
||||
"enter-webhook-url": "Saisissez l'URL du webhook",
|
||||
"enter-user-name": "Saisissez le nom d'utilisateur",
|
||||
"enter-tagline": "Saisissez le slogan",
|
||||
"enter-password-or-click-autogenerate": "Saisissez le mot de passe ou cliquez sur Génération automatique",
|
||||
"enter-daily-email-quota": "Saisissez le quota quotidien d'e-mails",
|
||||
"enter-custom-regular-expression": "Saisissez une expression régulière personnalisée",
|
||||
"enter-value": "Saisissez {{value}}",
|
||||
"are-you-sure-want-to-send-document": "Êtes-vous sûr de vouloir envoyer le document ?",
|
||||
"favicon": "Favicon",
|
||||
"delete-two-factor-authentication": "Êtes-vous sûr de vouloir supprimer l'authentification à deux facteurs ?",
|
||||
"paste-html-here": "Collez le HTML ici",
|
||||
"open-email-builder": "Vous pouvez créer un modèle d'e-mail à l'aide de la plateforme <1>email builder</1> et copier le code HTML.",
|
||||
"contact-saved": "Contact enregistré avec succès.",
|
||||
"remove-limit-of-user": "Êtes-vous sûr de vouloir supprimer la limite de cet utilisateur ? Il pourra alors utiliser tous les crédits premium disponibles de votre organisation, partagés entre tous les utilisateurs.",
|
||||
"current-expiry-date": "Date d'expiration actuelle",
|
||||
"reminder-error": "« Rappeler une fois tous les (jours) » ne doit pas dépasser le « Temps pour terminer (jours) ».",
|
||||
"showing-to-of-records": "Affichage de {{record}} à {{recordPerPage}} sur {{totalRows}} enregistrements",
|
||||
"records-per-page": "Enregistrements par page",
|
||||
"email-not-found-in-row": "Aucun e-mail trouvé à la ligne {{row}}.",
|
||||
"invalid-email-found-in-row": "E-mail invalide trouvé à la ligne {{row}} : {{email}}",
|
||||
"duplicate-email-found-in-row": "E-mail en double trouvé à la ligne {{row}} : {{email}}",
|
||||
"prefill": "Préremplissage",
|
||||
"recipients-&-data": "Destinataires et données",
|
||||
"response": "Réponse",
|
||||
"prefill-tab-help": "Ce sont les champs communs du modèle qui s'appliqueront à tous les documents de cet envoi groupé.",
|
||||
"recipient-tab-help-p1": "Dans cette étape suivante, vous allez :",
|
||||
"recipient-tab-help-p2": "Téléverser un CSV avec les détails des destinataires ou",
|
||||
"recipient-tab-help-p3": "Saisir manuellement les informations des destinataires",
|
||||
"recipient-tab-help-p4": "Chaque destinataire recevra son propre document généré à partir de ce modèle.",
|
||||
"response-tab-help": "Vérifiez vos réponses.",
|
||||
"summary": "Résumé",
|
||||
"prefill-fields": "Champs de préremplissage",
|
||||
"documents": "Documents",
|
||||
"message": "Message",
|
||||
"formula-help-p1": "Collectez des entrées numériques et effectuez des calculs dynamiques à l'aide de formules.",
|
||||
"example": "Exemple",
|
||||
"formula-help-p2": "Pour afficher un total dans number-3, utilisez {{number-1}} + {{number-2}}.",
|
||||
"formula-help-p3": "Pour calculer une valeur dans number-4, utilisez ({{number-1}} * {{number-2}}) / {{number-3}}.",
|
||||
"formula-help-p4": "Chaque widget numérique doit avoir un nom unique (par ex. number-1), car les formules dépendent de ces identifiants.",
|
||||
"note-consume-premium-credits": "Remarque : Cette fonctionnalité consomme des crédits premium.",
|
||||
"note-consume-premium-credits-on-document-send": "Remarque : Cette fonctionnalité consomme des crédits premium. Les crédits sont consommés chaque fois qu'un document est envoyé, qu'il soit signé ou non.",
|
||||
"webhook-authentication-help": "{{appName}} sécurise chaque webhook en générant une signature HMAC-SHA256 à l'aide de votre clé de sécurité webhook et du payload brut de la requête. Cette signature est envoyée dans l'en-tête x-webhook-signature. Votre serveur doit générer la même signature en utilisant le payload reçu et votre clé secrète. Si les deux signatures correspondent, la requête webhook est authentique et n'a pas été modifiée.",
|
||||
"invalid-widget-found-in-row": "Valeur invalide détectée à la ligne {{row}} pour le widget {{widget}}."
|
||||
}
|
||||
@@ -75,7 +75,7 @@
|
||||
"password-length": "पासवर्ड 8 वर्ण लंबा होना चाहिए",
|
||||
"password-case": "पासवर्ड में अपरकेस अक्षर, लोअरकेस अक्षर, अंक होना चाहिए",
|
||||
"password-special-char": " पासवर्ड में विशेष वर्ण होना चाहिए",
|
||||
"agreee": "मैं सहमत हूँ",
|
||||
"agree": "मैं सहमत हूँ",
|
||||
"term": "सेवा की शर्तें",
|
||||
"subscribe-to-opensign": "OpenSign न्यूज़लेटर की सदस्यता लें",
|
||||
"register": "पंजीकृत करें",
|
||||
@@ -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": "ये वे दस्तावेज़ हैं जिन्हें आपने शुरू तो किया है लेकिन भेजने के लिए अंतिम रूप नहीं दिया है।",
|
||||
@@ -220,7 +224,7 @@
|
||||
"New Template": "नया टेम्पलेट",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "फ़ाइल चयनित",
|
||||
"files-selected": "फ़ाइलें चयनित",
|
||||
"template-title": "टेम्पलेट शीर्षक",
|
||||
"document-title": "दस्तावेज़ शीर्षक",
|
||||
"title": "शीर्षक",
|
||||
@@ -237,7 +241,6 @@
|
||||
"auto-reminder": "स्वतः अनुस्मारक",
|
||||
"remind-once": " प्रत्येक (दिन) में एक बार याद दिलाएं",
|
||||
"next": "अगला",
|
||||
"select-folder": "फ़ोल्डर चुनें",
|
||||
"OpenSign-drive": "{{appName}} ड्राइव",
|
||||
"drive-document-status": {
|
||||
"Completed": "पूरा हुआ",
|
||||
@@ -266,7 +269,7 @@
|
||||
"signature": "हस्ताक्षर",
|
||||
"upload-image": " छवि अपलोड करें",
|
||||
"clear": "स्पष्ट",
|
||||
"upload-signature/Image": "कृपया हस्ताक्षर/छवि अपलोड करें",
|
||||
"upload-signature/Image": "कृपया हस्ताक्षर/छवि अपलोड करें/मोहर",
|
||||
"initials": "हस्ताक्षर",
|
||||
"API": "एपीआई",
|
||||
"api-token": "एपीआई टोकन",
|
||||
@@ -292,7 +295,7 @@
|
||||
"edit-team": "टीम संपादित करें",
|
||||
"name-of-team": "टीम का नाम",
|
||||
"prev": "पिछला",
|
||||
"no-data-avaliable": "कोई डेटा उपलब्ध नहीं है",
|
||||
"no-data-available": "कोई डेटा उपलब्ध नहीं है",
|
||||
"add-team": "टीम जोड़ें",
|
||||
"page-not-found": "पृष्ठ नहीं मिला",
|
||||
"users-from-teams": "टीमों से उपयोगकर्ता",
|
||||
@@ -303,7 +306,7 @@
|
||||
"delete-user": "उपयोगकर्ता हटाएं",
|
||||
"delete": "हटाएं",
|
||||
"add-user": "उपयोगकर्ता जोड़ें",
|
||||
"password-generateed": "पासवर्ड केवल एक बार उत्पन्न होगा; इसे कॉपी करना सुनिश्चित करें।",
|
||||
"password-generated": "पासवर्ड केवल एक बार उत्पन्न होगा; इसे कॉपी करना सुनिश्चित करें।",
|
||||
"Team status": "टीम स्थिति",
|
||||
"user-deactivated": "उपयोगकर्ता निष्क्रिय।",
|
||||
"user-activated": "उपयोगकर्ता सक्रिय।",
|
||||
@@ -333,7 +336,7 @@
|
||||
"quick-send-alert-1": "इस दस्तावेज़ में सभी भूमिकाएँ वर्तमान में संपर्कों से जुड़ी हुई हैं। इस टेम्पलेट की प्रतियां कई हस्ताक्षरकर्ताओं को त्वरित रूप से भेजने के लिए, कृपया सुनिश्चित करें कि कम से कम एक भूमिका किसी भी संपर्क से जुड़ी नहीं है।",
|
||||
"quick-send-alert-2": "कृपया सुनिश्चित करें कि सभी प्राप्तकर्ताओं के लिए कम से कम एक हस्ताक्षर विजेट जोड़ा गया है।",
|
||||
"quick-send-alert-3": "कृपया इस टेम्पलेट में कम से कम एक भूमिका जोड़ें।",
|
||||
"quick-send-alert-4": "त्वरित भेजें सीमा तक पहुँच गया।",
|
||||
"quick-send-alert-4": "{{flow}} प्रति बैच अधिकतम {{count}} साइनरों का समर्थन करता है। अपनी सूची विभाजित करें और पुनः प्रयास करें।",
|
||||
"copy-link": "लिंक कॉपी करें",
|
||||
"copy": "कॉपी करें",
|
||||
"revoke-document": "दस्तावेज़ रद्द करें",
|
||||
@@ -389,7 +392,8 @@
|
||||
"radio button": "रेडियो बटन",
|
||||
"image": "छवि",
|
||||
"email": "ईमेल",
|
||||
"number": "संख्या"
|
||||
"number": "संख्या",
|
||||
"draw": "आरेखित करें"
|
||||
},
|
||||
"fields": "क्षेत्र",
|
||||
"recipients": "प्राप्तकर्ता",
|
||||
@@ -433,16 +437,18 @@
|
||||
"reset-password-alert-1": "पासवर्ड रीसेट लिंक आपके ईमेल आईडी पर भेज दिया गया है",
|
||||
"reset-password-alert-2": "कृपया ईमेल एडॉप्टर सेटअप करें ",
|
||||
"reset-password-alert-3": "अपना पासवर्ड रीसेट करें",
|
||||
"faild-animation": "एनिमेशन लोड करने में विफल",
|
||||
"failed-animation": "एनिमेशन लोड करने में विफल",
|
||||
"apply": "लागू करें",
|
||||
"select-columns": "स्तंभ चुनें",
|
||||
"copy-type": {
|
||||
"All pages": "सभी पृष्ठ",
|
||||
"All pages but last": "अंतिम को छोड़कर सभी पृष्ठ",
|
||||
"All pages but first": "पहले को छोड़कर सभी पृष्ठ",
|
||||
"Next to current widget": "वर्तमान विजेट के आगे"
|
||||
"Next to current widget": "वर्तमान विजेट के आगे",
|
||||
"Page range": "पृष्ठ सीमा"
|
||||
},
|
||||
"options": "विकल्प",
|
||||
"minimun-check": "न्यूनतम जाँच",
|
||||
"minimum-check": "न्यूनतम जाँच",
|
||||
"maximum-check": "अधिकतम जाँच",
|
||||
"cell-count": "सेल्स संख्या",
|
||||
"default-value": "डिफ़ॉल्ट मान",
|
||||
@@ -512,14 +518,14 @@
|
||||
"placeholder-alert-4": "आपने सभी प्राप्तकर्ताओं को सफलतापूर्वक मेल भेज दिए हैं!",
|
||||
"placeholder-mail-alert": "आपने {{name}} को सफलतापूर्वक ईमेल भेज दिया है। {{name}} द्वारा दस्तावेज़ पर हस्ताक्षर करने के बाद बाद के हस्ताक्षरकर्ताओं को ईमेल प्राप्त होंगे",
|
||||
"placeholder-mail-alert-you": "आपके दस्तावेज़ पर हस्ताक्षर करने के बाद अगले हस्ताक्षरकर्ता को ईमेल प्राप्त होगा।",
|
||||
"placeholder-alert-5": "क्या आप अभी दस्तावेज़ पर हस्ताक्षर करना चाहते हैं?",
|
||||
"placeholder-alert-5": "आप अभी हस्ताक्षर कर सकते हैं या इसे बाद में पूरा करने के लिए अपने ईमेल पर भेज सकते हैं।",
|
||||
"placeholder-alert-6": "मेल भेजने के लिए कृपया मेल एडॉप्टर सेटअप करें!",
|
||||
"placeholder-alert-7": "प्लेसहोल्डर जोड़ने के लिए कृपया हस्ताक्षरकर्ता चुनें!",
|
||||
"email-subject": "ईमेल विषय",
|
||||
"email-body": "ईमेल मुख्य भाग",
|
||||
"email-placeholder": "ईमेल का मुख्य भाग जोड़ें",
|
||||
"reset-to-default": "डिफ़ॉल्ट पर रीसेट करें",
|
||||
"cutomize-email": "ईमेल अनुकूलित करें",
|
||||
"customize-email": "ईमेल अनुकूलित करें",
|
||||
"upgrade-to-customize-email": "ईमेल अनुकूलित करने के लिए अपग्रेड करें",
|
||||
"sign-url": "यूआरएल पर हस्ताक्षर करें",
|
||||
"mails-sent": "मेल भेजे गए",
|
||||
@@ -584,7 +590,7 @@
|
||||
"provide-your-details": "अपना विवरण प्रदान करें",
|
||||
"provide-email": "कृपया ईमेल प्रदान करें।",
|
||||
"additional-info": "अतिरिक्त जानकारी",
|
||||
"signature-saved-alert": "हस्ताक्षर सफलतापूर्वक सहेजा गया।",
|
||||
"signature-saved-alert": "सफलतापूर्वक सहेजा गया।",
|
||||
"saved-successfully": "सफलतापूर्वक सहेजा गया।",
|
||||
"fill-field": "कृपया इस फ़ील्ड को भरें",
|
||||
"error-template": "त्रुटि: टेम्पलेट नहीं मिला!",
|
||||
@@ -621,7 +627,7 @@
|
||||
"date": "दिनांक",
|
||||
"report-not-found": "रिपोर्ट नहीं मिली",
|
||||
"please-wait": "कृपया प्रतीक्षा करें",
|
||||
"select-foler": "फ़ोल्डर चुनें",
|
||||
"select-folder": "फ़ोल्डर चुनें",
|
||||
"generated-on": "पर उत्पन्न",
|
||||
"text-field": "पाठ क्षेत्र",
|
||||
"font-size": "फ़ॉन्ट आकार",
|
||||
@@ -680,13 +686,13 @@
|
||||
"public-template": "यह वीडियो दर्शाता है कि आप अपना व्यक्तिगत सार्वजनिक प्रोफ़ाइल कैसे सेट अप कर सकते हैं, जैसे 'https://opensign.me/your-username'। आप यह भी सीखेंगे कि अपनी टैगलाइन को कैसे अनुकूलित करें और अपने टेम्पलेट्स को सार्वजनिक हस्ताक्षर के लिए उपलब्ध कराएं।",
|
||||
"allowModify-widgets": "आप इन फ़ील्ड में से किसी को भी दस्तावेज़ पर खींच और छोड़ सकते हैं, इसके अतिरिक्त दस्तावेज़ निर्माता द्वारा आपके लिए पहले से निर्दिष्ट फ़ील्ड के। "
|
||||
},
|
||||
"enter-email-plaholder": "एक ईमेल पता जोड़ें और एंटर दबाएं",
|
||||
"enter-email-placeholder": "एक ईमेल पता जोड़ें और एंटर दबाएं",
|
||||
"success-email-alert": "ईमेल सफलतापूर्वक भेजा गया!",
|
||||
"expired-doc-title": "समाप्त दस्तावेज़",
|
||||
"expired-on-mssg": "यह दस्तावेज़ {{expiredDate}} को समाप्त हो गया है और अब हस्ताक्षर के लिए उपलब्ध नहीं है।",
|
||||
"signature-validate-alert": "कृपया पुष्टि करें कि आपने कम से कम {{minRequiredCount}} चेकबॉक्स चुने हैं।",
|
||||
"signature-validate-alert-2": "सुनिश्चित करें कि यह फ़ील्ड सटीक रूप से भरा गया है और सभी आवश्यकताओं को पूरा करता है।",
|
||||
"remaing-users": "शेष उपयोगकर्ता",
|
||||
"remaining-users": "शेष उपयोगकर्ता",
|
||||
"Role": "भूमिका",
|
||||
"additional-users": "कृपया ऐड-ऑन उपयोगकर्ता खरीदें।",
|
||||
"Quantity-of-users": "उपयोगकर्ताओं की मात्रा",
|
||||
@@ -700,7 +706,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": "प्रीमियम क्रेडिट खरीदें",
|
||||
@@ -708,7 +714,7 @@
|
||||
"rotate-left": "बाईं ओर घुमाएँ",
|
||||
"rotate-alert-mssg": "इस पृष्ठ पर सभी विजेट खो जाएंगे। क्या आप वाकई आगे बढ़ना चाहते हैं?",
|
||||
"templateid": "टेम्पलेट-आईडी",
|
||||
"bulk-send-subcription-alert": "थोक भेजने का उपयोग करने के लिए कृपया व्यावसायिक या टीम योजना में अपग्रेड करें।",
|
||||
"bulk-send-subscription-alert": "थोक भेजने का उपयोग करने के लिए कृपया व्यावसायिक या टीम योजना में अपग्रेड करें।",
|
||||
"generate-test-token": "परीक्षण टोकन उत्पन्न करें",
|
||||
"regenerate-test-token": "परीक्षण टोकन पुनः उत्पन्न करें",
|
||||
"help-test-token": "इस टोकन का उपयोग https://sandbox.opensignlabs.com/api/v1 समापन बिंदु पर एपीआई का परीक्षण करने के लिए किया जा सकता है, जिससे आप असीमित दस्तावेज़ हस्ताक्षर कर सकते हैं। कृपया ध्यान दें कि सैंडबॉक्स एपीआई आपके दस्तावेज़ों पर स्व-हस्ताक्षरित प्रमाणपत्रों के साथ हस्ताक्षर करेगा, जिन्हें एडोब द्वारा मान्य के रूप में मान्यता नहीं दी जा सकती है। एक बार जब आप अपना परीक्षण पूरा कर लेते हैं, तो आप उत्पादन टोकन उत्पन्न करने के लिए हमारी किसी भुगतान योजना में अपग्रेड कर सकते हैं।",
|
||||
@@ -728,8 +734,9 @@
|
||||
"copied-code": "कॉपी किया गया",
|
||||
"Installation": "इंस्टॉलेशन",
|
||||
"Usage": "उपयोग",
|
||||
"insufficient-credits": "अपर्याप्त हस्ताक्षर क्रेडिट",
|
||||
"insufficient-credits-mssg": "इस दस्तावेज़ के स्वामी के पास वर्तमान में आपके हस्ताक्षर करने के लिए आवश्यक OpenSign क्रेडिट की कमी है। यदि आपको अधिक विवरण चाहिए तो कृपया स्वामी से संपर्क करें।",
|
||||
"insufficient-credits": "हस्ताक्षर क्रेडिट अपर्याप्त हैं। कृपया {{ownerEmail}} से संपर्क करें",
|
||||
"insufficient-credits-title": "अपर्याप्त हस्ताक्षर क्रेडिट",
|
||||
"insufficient-credits-mssg": "इस दस्तावेज़ के स्वामी के पास वर्तमान में आपके हस्ताक्षर करने के लिए आवश्यक {{appName}} क्रेडिट की कमी है। यदि आपको अधिक विवरण चाहिए तो कृपया स्वामी से संपर्क करें।",
|
||||
"angular-npm-mssg-1": "OpenSign को अपने Angular प्रोजेक्ट में एकीकृत करने के लिए, बस निम्नलिखित कमांड चलाएँ:",
|
||||
"quota-mail-info-head": "मासिक ईमेल सीमा",
|
||||
"quota-mail-info": "सेवा की गुणवत्ता बनाए रखने और स्पैम को रोकने के लिए, OpenSign निःशुल्क योजना पर प्रति माह 15 ईमेल तक की अनुमति देता है। असीमित ईमेल भेजने के लिए अभी अपग्रेड करें।",
|
||||
@@ -861,7 +868,6 @@
|
||||
"js-snippet-msg-1": "आप टेम्पलेट आईडी इससे प्राप्त कर सकते हैं",
|
||||
"js-snippet-msg-2": "टेम्पलेट प्रबंधित करें",
|
||||
"js-snippet-msg-3": "पृष्ठ।",
|
||||
"agrrement-alert": "आगे बढ़ने के लिए, आपको इलेक्ट्रॉनिक रिकॉर्ड और हस्ताक्षर प्रकटीकरण के लिए सहमति देनी होगी।",
|
||||
"webhook-already-exists": "URL पहले से मौजूद है! कृपया कोई दूसरा प्रयास करें।",
|
||||
"webhook-must-be-secure": "वेबहूक URL सुरक्षित होना चाहिए और https:// का उपयोग करना चाहिए",
|
||||
"provide-test-token": "कृपया एपीआई परीक्षण टोकन उत्पन्न करें",
|
||||
@@ -1025,7 +1031,7 @@
|
||||
},
|
||||
"signer-already-present": "हस्ताक्षरकर्ता पहले से मौजूद है",
|
||||
"kiosk-sign": "कियोस्क साइन",
|
||||
"dont-have-access-to-template": "टेम्पलेट हटा दिया गया है या आपके पास पहुंच नहीं है। कृपया प्रेषक से संपर्क करें।",
|
||||
"do-not-have-access-to-template": "टेम्पलेट हटा दिया गया है या आपके पास पहुंच नहीं है। कृपया प्रेषक से संपर्क करें।",
|
||||
"kiosk-info": "कियोस्क मोड आपको व्यक्तिगत रूप से हस्ताक्षर शीघ्रता और कुशलता से एकत्र करने देता है। ट्रेड शो, कार्यक्रमों या वॉक-इन परिदृश्यों के लिए आदर्श जहां सभी हस्ताक्षरकर्ता शारीरिक रूप से मौजूद हों। ",
|
||||
"learn-more": "और जानें",
|
||||
"finish-mssg": " क्या आप वाकई दस्तावेज़ समाप्त करना चाहते हैं?",
|
||||
@@ -1081,8 +1087,6 @@
|
||||
"search-contacts": "संपर्क खोजें…",
|
||||
"add-role-alert": "कृपया कम से कम एक भूमिका जोड़ें",
|
||||
"edit-draft": "ड्राफ्ट संपादित करें",
|
||||
"invalid-email-found": "अमान्य ईमेल पाया गया: {{email}}",
|
||||
"duplicate-email-found": "डुप्लिकेट ईमेल पाया गया: {{email}}",
|
||||
"vertical": "वर्टिकल",
|
||||
"horizontal": "हॉरिज़ॉन्टल",
|
||||
"billing": "बिलिंग",
|
||||
@@ -1096,7 +1100,7 @@
|
||||
"access-denied": "पहुंच अस्वीकृत",
|
||||
"upgrade": "अपग्रेड",
|
||||
"do-not-access-app": "आपको इस एप्लिकेशन तक पहुंच नहीं है।",
|
||||
"dont-have-access": "आपको पहुंच नहीं है।",
|
||||
"do-not-have-access": "आपको पहुंच नहीं है।",
|
||||
"valid-email-alert": "कृपया एक मान्य ईमेल पता दर्ज करें।",
|
||||
"otp-not-validate": "ओटीपी मान्य नहीं है।",
|
||||
"domain-not-allowed": "यह डोमेन अनुमति नहीं है",
|
||||
@@ -1113,10 +1117,12 @@
|
||||
"branding-help": "ब्रांडिंग आपकी ऐप में वाइट लेबलिंग की सुविधा देती है",
|
||||
"custom-sub-domain": "कस्टम सब-डोमेन",
|
||||
"app-name": "ऐप का नाम",
|
||||
"provide-domain-name": "अपना डोमेन नाम दर्ज करें",
|
||||
"provide-domain-name": "केवल सबडोमेन की अनुमति है (उदा., app.yourdomain.com)। yourdomain.com जैसे प्राथमिक/रूट डोमेन समर्थित नहीं हैं।",
|
||||
"provide-app-name-help": "एक अनोखा ऐप नाम प्रदान करें। यह एप्लिकेशन के भीतर आपके ब्रांड नाम के रूप में और सभी ईमेल में, जहाँ भी ब्रांडिंग लागू हो, प्रदर्शित होगा।",
|
||||
"upload-app-logo": ".png, .jpg या .svg फ़ॉर्मेट में लोगो अपलोड करें। सर्वोत्तम डिस्प्ले गुणवत्ता के लिए अनुशंसित आकार: {{size}} पिक्सेल (5:2 आस्पेक्ट रेशियो)।",
|
||||
"upload-app-favicon": "एक चौकोर फ़ेविकॉन इमेज (.ico, .png या .svg) अपलोड करें। सर्वोत्तम ब्राउज़र अनुकूलता के लिए अनुशंसित आकार: {{size}} पिक्सेल।",
|
||||
"provide-app-name": "अपना ऐप नाम दर्ज करें",
|
||||
"logo": "लोगो",
|
||||
"upload-app-logo": "अपने ऐप का लोगो अपलोड करें",
|
||||
"prefill-unfilled-widget": "निम्न अनिवार्य फ़ील्ड खाली नहीं छोड़े जा सकते: {{emptyWidget}}। कृपया आगे बढ़ने के लिए इन्हें भरें।",
|
||||
"Dashboard": "डैशबोर्ड",
|
||||
"Analytics": "विश्लेषिकी",
|
||||
@@ -1239,6 +1245,7 @@
|
||||
},
|
||||
"read-only-date-error": "रीड-ओनली दिनांक विजेट में एक डिफ़ॉल्ट दिनांक होना आवश्यक है।",
|
||||
"set-date": "दिनांक सेट करें",
|
||||
"default-date": "डिफ़ॉल्ट तिथि",
|
||||
"set-today": "हस्ताक्षर की तिथि",
|
||||
"enter-name": "नाम दर्ज करें",
|
||||
"enter-email": "ईमेल दर्ज करें",
|
||||
@@ -1274,5 +1281,120 @@
|
||||
"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": "पुनः जनरेट करें",
|
||||
"signing-date": "हस्ताक्षर तिथि",
|
||||
"date-widget": "तिथि विजेट",
|
||||
"mail-failed": "ईमेल भेजते समय कुछ गलत हो गया। कृपया पुनः प्रयास करें।",
|
||||
"mail-status-head": "अगला कदम: अपने दस्तावेज़ पर हस्ताक्षर करें?",
|
||||
"send-to-email": "ईमेल पर भेजें",
|
||||
"date-pref-help-sub-title": "डेट विजेट प्राथमिकताएँ आपको डेट विजेट के लिए डिफ़ॉल्ट सेटिंग्स निर्धारित करने की अनुमति देती हैं। जब भी आप किसी दस्तावेज़ या टेम्पलेट में नया डेट विजेट जोड़ते हैं, ये डिफ़ॉल्ट सेटिंग्स स्वतः लागू हो जाती हैं। आप गियर आइकन पर क्लिक करके किसी भी डेट विजेट को कभी भी अनुकूलित कर सकते हैं।",
|
||||
"date-pref-help-format": "डेट विजेट के लिए उपयोग किए जाने वाले डिफ़ॉल्ट तिथि प्रारूप का चयन करें।",
|
||||
"date-pref-help-default-date": "डेट विजेट के लिए एक डिफ़ॉल्ट तिथि मान सेट करें।",
|
||||
"date-pref-help-signing-date": "सक्षम होने पर, डेट विजेट स्वतः उस तिथि का उपयोग करता है जब साइनर दस्तावेज़ पर हस्ताक्षर करता है।",
|
||||
"date-pref-help-read-only": "सक्षम होने पर, डेट विजेट साइनर के लिए केवल-पढ़ने योग्य होता है।",
|
||||
"upload-csv": "CSV अपलोड करें",
|
||||
"file-selected": "फ़ाइल चयनित",
|
||||
"bulk-send-csv-helper": "साइनर भूमिकाओं (भूमिका ईमेल/नाम/फ़ोन) और प्रीफ़िल मानों (प्रीफ़िल: फ़ील्ड नाम) के लिए हेडर का उपयोग करें। आवश्यक प्रीफ़िल फ़ील्ड भरना अनिवार्य है।",
|
||||
"bulk-send-csv-helper-note": "नोट: CSV में तिथि-प्रकार के प्रीफ़िल फ़ील्ड्स के लिए मान इस प्रारूप में प्रदान किया जाना चाहिए: {{value}} (उदाहरण: {{example}})।",
|
||||
"bulk-send-csv-summary": "CSV से {{total}} पंक्तियाँ आयात की गईं।",
|
||||
"bulk-send-csv-missing-headers": "आवश्यक CSV हेडर अनुपलब्ध हैं। भूमिकाएँ: {{roles}}।",
|
||||
"bulk-send-no-records": "भेजने से पहले कृपया कम से कम एक प्राप्तकर्ता जोड़ें।",
|
||||
"csv-only-support": "केवल CSV फ़ाइलें समर्थित हैं।",
|
||||
"csv-no-records": "CSV में कोई रिकॉर्ड नहीं मिला।",
|
||||
"select-at-least-option": "कृपया कम से कम एक विकल्प चुनें।",
|
||||
"draw-required": "कृपया बॉक्स में ड्रॉ करें।",
|
||||
"enter-smtp-username": "अपना SMTP उपयोगकर्ता नाम दर्ज करें",
|
||||
"enter-webhook-url": "Webhook URL दर्ज करें",
|
||||
"enter-user-name": "उपयोगकर्ता नाम दर्ज करें",
|
||||
"enter-tagline": "टैगलाइन दर्ज करें",
|
||||
"enter-password-or-click-autogenerate": "पासवर्ड दर्ज करें या ऑटो-जनरेट पर क्लिक करें",
|
||||
"enter-daily-email-quota": "दैनिक ईमेल कोटा दर्ज करें",
|
||||
"enter-custom-regular-expression": "कस्टम रेगुलर एक्सप्रेशन दर्ज करें",
|
||||
"enter-value": "{{value}} दर्ज करें",
|
||||
"are-you-sure-want-to-send-document": "क्या आप वाकई दस्तावेज़ भेजना चाहते हैं?",
|
||||
"favicon": "फ़ेविकॉन",
|
||||
"delete-two-factor-authentication": "क्या आप वाकई दो-कारक प्रमाणीकरण हटाना चाहते हैं?",
|
||||
"paste-html-here": "यहाँ HTML पेस्ट करें",
|
||||
"open-email-builder": "आप <1>email builder</1> प्लेटफ़ॉर्म का उपयोग करके ईमेल टेम्पलेट बना सकते हैं और HTML कोड कॉपी कर सकते हैं।",
|
||||
"contact-saved": "संपर्क सफलतापूर्वक सहेजा गया।",
|
||||
"remove-limit-of-user": "क्या आप वाकई इस उपयोगकर्ता की सीमा हटाना चाहते हैं? इसके बाद यह उपयोगकर्ता आपकी संस्था के सभी उपलब्ध प्रीमियम क्रेडिट का उपयोग कर सकेगा, जो सभी उपयोगकर्ताओं के बीच साझा किए जाते हैं।",
|
||||
"current-expiry-date": "वर्तमान समाप्ति तिथि",
|
||||
"reminder-error": "“हर (दिन) में एक बार याद दिलाएं” का मान “पूरा करने का समय (दिन)” से अधिक नहीं होना चाहिए।",
|
||||
"showing-to-of-records": "{{totalRows}} रिकॉर्ड में से {{record}} से {{recordPerPage}} तक दिखाया जा रहा है",
|
||||
"records-per-page": "प्रति पृष्ठ रिकॉर्ड",
|
||||
"email-not-found-in-row": "पंक्ति {{row}} में ईमेल नहीं मिला।",
|
||||
"invalid-email-found-in-row": "पंक्ति {{row}} में अमान्य ईमेल मिला: {{email}}",
|
||||
"duplicate-email-found-in-row": "पंक्ति {{row}} में डुप्लिकेट ईमेल मिला: {{email}}",
|
||||
"prefill": "प्रीफ़िल",
|
||||
"recipients-&-data": "प्राप्तकर्ता और डेटा",
|
||||
"response": "प्रतिक्रिया",
|
||||
"prefill-tab-help": "ये सामान्य टेम्पलेट फ़ील्ड हैं जो इस बल्क सेंड के सभी दस्तावेज़ों पर लागू होंगे।",
|
||||
"recipient-tab-help-p1": "इस अगले चरण में, आप:",
|
||||
"recipient-tab-help-p2": "प्राप्तकर्ता विवरण के साथ CSV अपलोड करेंगे या",
|
||||
"recipient-tab-help-p3": "प्राप्तकर्ता जानकारी मैन्युअल रूप से दर्ज करेंगे",
|
||||
"recipient-tab-help-p4": "प्रत्येक प्राप्तकर्ता को इस टेम्पलेट से जनरेट किया गया अपना दस्तावेज़ प्राप्त होगा।",
|
||||
"response-tab-help": "अपनी प्रतिक्रियाओं की समीक्षा करें।",
|
||||
"summary": "सारांश",
|
||||
"prefill-fields": "प्रीफ़िल फ़ील्ड",
|
||||
"documents": "दस्तावेज़",
|
||||
"message": "संदेश",
|
||||
"formula-help-p1": "संख्यात्मक इनपुट एकत्र करें और सूत्रों का उपयोग करके डायनामिक गणनाएँ करें।",
|
||||
"example": "उदाहरण",
|
||||
"formula-help-p2": "number-3 में कुल दिखाने के लिए {{number-1}} + {{number-2}} का उपयोग करें।",
|
||||
"formula-help-p3": "number-4 में मान निकालने के लिए ({{number-1}} * {{number-2}}) / {{number-3}} का उपयोग करें।",
|
||||
"formula-help-p4": "प्रत्येक नंबर विजेट का एक अद्वितीय नाम होना चाहिए (जैसे number-1), क्योंकि सूत्र इन पहचानकर्ताओं पर निर्भर करते हैं।",
|
||||
"note-consume-premium-credits": "नोट: यह सुविधा प्रीमियम क्रेडिट का उपयोग करती है।",
|
||||
"note-consume-premium-credits-on-document-send": "नोट: यह सुविधा प्रीमियम क्रेडिट का उपयोग करती है। दस्तावेज़ भेजे जाने पर हर बार क्रेडिट उपयोग किए जाते हैं, चाहे दस्तावेज़ पर हस्ताक्षर किए जाएँ या नहीं।",
|
||||
"webhook-authentication-help": "{{appName}} प्रत्येक वेबहुक को सुरक्षित करता है, आपके वेबहुक सुरक्षा कुंजी और मूल अनुरोध पेलोड का उपयोग करके HMAC-SHA256 हस्ताक्षर उत्पन्न करके। यह हस्ताक्षर x-webhook-signature हेडर में भेजा जाता है। आपके सर्वर को प्राप्त पेलोड और आपकी सीक्रेट कुंजी का उपयोग करके वही हस्ताक्षर उत्पन्न करना होगा। यदि दोनों हस्ताक्षर मेल खाते हैं, तो वेबहुक अनुरोध प्रमाणिक है और उसमें कोई परिवर्तन नहीं किया गया है।",
|
||||
"invalid-widget-found-in-row": "पंक्ति {{row}} में विजेट {{widget}} के लिए अमान्य मान पाया गया।"
|
||||
}
|
||||
@@ -75,7 +75,7 @@
|
||||
"password-length": "La password deve contenere almeno 8 caratteri",
|
||||
"password-case": "La password deve contenere una lettera maiuscola, una minuscola e un numero",
|
||||
"password-special-char": "La password deve contenere un carattere speciale",
|
||||
"agreee": "Accetto i",
|
||||
"agree": "Accetto i",
|
||||
"term": "Termini di servizio",
|
||||
"subscribe-to-opensign": "Iscriviti alla newsletter di OpenSign",
|
||||
"register": "Registrati",
|
||||
@@ -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.",
|
||||
@@ -220,7 +224,7 @@
|
||||
"New Template": "Nuovo modello",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "file selezionato",
|
||||
"files-selected": "file selezionati",
|
||||
"template-title": "Titolo del modello",
|
||||
"document-title": "Titolo del documento",
|
||||
"title": "Titolo",
|
||||
@@ -237,7 +241,6 @@
|
||||
"auto-reminder": "Promemoria automatico",
|
||||
"remind-once": "Ricorda una volta ogni (giorni)",
|
||||
"next": "Avanti",
|
||||
"select-folder": "Seleziona cartella",
|
||||
"OpenSign-drive": "{{appName}} Drive",
|
||||
"drive-document-status": {
|
||||
"Completed": "Completato",
|
||||
@@ -266,7 +269,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",
|
||||
@@ -292,7 +295,7 @@
|
||||
"edit-team": "Modifica Team",
|
||||
"name-of-team": "Nome del Team",
|
||||
"prev": "Precedente",
|
||||
"no-data-avaliable": "Nessun dato disponibile",
|
||||
"no-data-available": "Nessun dato disponibile",
|
||||
"add-team": "Aggiungi Team",
|
||||
"page-not-found": "Pagina non trovata",
|
||||
"users-from-teams": "utenti dai Team",
|
||||
@@ -303,7 +306,7 @@
|
||||
"delete-user": "Elimina utente",
|
||||
"delete": "Elimina",
|
||||
"add-user": "Aggiungi utente",
|
||||
"password-generateed": "La password verrà generata solo una volta; assicurati di copiarla.",
|
||||
"password-generated": "La password verrà generata solo una volta; assicurati di copiarla.",
|
||||
"Team status": "Stato del Team",
|
||||
"user-deactivated": "Utente disattivato.",
|
||||
"user-activated": "Utente attivato.",
|
||||
@@ -333,7 +336,7 @@
|
||||
"quick-send-alert-1": "Tutti i ruoli in questo documento sono attualmente collegati a contatti. Per inviare rapidamente copie di questo modello a più firmatari, assicurati che almeno un ruolo non sia collegato a nessun contatto.",
|
||||
"quick-send-alert-2": "Assicurati che ci sia almeno un widget firma aggiunto per tutti i destinatari.",
|
||||
"quick-send-alert-3": "Si prega di aggiungere almeno un ruolo a questo template.",
|
||||
"quick-send-alert-4": "Limite di invio rapido raggiunto.",
|
||||
"quick-send-alert-4": "{{flow}} supporta fino a {{count}} firmatari per batch. Dividi il tuo elenco e riprova.",
|
||||
"copy-link": "Copia link",
|
||||
"copy": "Copia",
|
||||
"revoke-document": "Revoca documento",
|
||||
@@ -389,7 +392,8 @@
|
||||
"radio button": "pulsante di opzione",
|
||||
"image": "immagine",
|
||||
"email": "email",
|
||||
"number": "numero"
|
||||
"number": "numero",
|
||||
"draw": "Disegno"
|
||||
},
|
||||
"fields": "Campi",
|
||||
"recipients": "Destinatari",
|
||||
@@ -433,16 +437,18 @@
|
||||
"reset-password-alert-1": "Il link per reimpostare la password è stato inviato al tuo indirizzo email",
|
||||
"reset-password-alert-2": "Si prega di configurare l'adattatore email",
|
||||
"reset-password-alert-3": "Reimposta la tua password",
|
||||
"faild-animation": "Caricamento animazione non riuscito",
|
||||
"failed-animation": "Caricamento animazione non riuscito",
|
||||
"apply": "Applica",
|
||||
"select-columns": "Seleziona colonne",
|
||||
"copy-type": {
|
||||
"All pages": "Tutte le pagine",
|
||||
"All pages but last": "Tutte le pagine tranne l'ultima",
|
||||
"All pages but first": "Tutte le pagine tranne la prima",
|
||||
"Next to current widget": "Accanto al widget corrente"
|
||||
"Next to current widget": "Accanto al widget corrente",
|
||||
"Page range": "Intervallo di pagine"
|
||||
},
|
||||
"options": "Opzioni",
|
||||
"minimun-check": "Controllo minimo",
|
||||
"minimum-check": "Controllo minimo",
|
||||
"maximum-check": "Controllo massimo",
|
||||
"cell-count": "conteggio delle cellule",
|
||||
"default-value": "Valore predefinita",
|
||||
@@ -512,14 +518,14 @@
|
||||
"placeholder-alert-4": "Hai inviato con successo le mail a tutti i destinatari!",
|
||||
"placeholder-mail-alert": "Hai inviato con successo un'e-mail a {{name}}. I firmatari successivi riceveranno un'e-mail una volta che {{name}} avrà firmato il documento.",
|
||||
"placeholder-mail-alert-you": "I firmatari successivi riceveranno un'email dopo che avrai firmato il documento.",
|
||||
"placeholder-alert-5": "Vuoi firmare i documenti ora?",
|
||||
"placeholder-alert-5": "Puoi firmare ora oppure inviarlo alla tua email per completarlo in seguito.",
|
||||
"placeholder-alert-6": "Configura l'adattatore email per inviare mail!",
|
||||
"placeholder-alert-7": "Seleziona il firmatario per aggiungere un segnaposto!",
|
||||
"email-subject": "Oggetto email",
|
||||
"email-body": "Corpo email",
|
||||
"email-placeholder": "aggiungi corpo dell'email",
|
||||
"reset-to-default": "Reimposta predefinita",
|
||||
"cutomize-email": "Personalizza Email",
|
||||
"customize-email": "Personalizza Email",
|
||||
"upgrade-to-customize-email": "Aggiorna per personalizzare l'Email",
|
||||
"sign-url": "URL Firma",
|
||||
"mails-sent": "Mail inviate",
|
||||
@@ -584,7 +590,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!",
|
||||
@@ -621,7 +627,7 @@
|
||||
"date": "Data",
|
||||
"report-not-found": "Report non trovato",
|
||||
"please-wait": "attendere prego",
|
||||
"select-foler": "Seleziona cartella",
|
||||
"select-folder": "Seleziona cartella",
|
||||
"generated-on": "Generato il",
|
||||
"text-field": "Campo di testo",
|
||||
"font-size": "Dimensione carattere",
|
||||
@@ -680,13 +686,13 @@
|
||||
"public-template": "Questo video dimostra come configurare il tuo profilo pubblico personalizzato, come 'https://opensign.me/tuo-username'. Imparerai anche come personalizzare il tuo slogan e rendere i tuoi modelli disponibili per la firma pubblica.",
|
||||
"allowModify-widgets": "È possibile trascinare e rilasciare uno qualsiasi di questi campi nel documento, oltre ai campi già designati dal creatore del documento."
|
||||
},
|
||||
"enter-email-plaholder": "Aggiungi un indirizzo email e premi invio",
|
||||
"enter-email-placeholder": "Aggiungi un indirizzo email e premi invio",
|
||||
"success-email-alert": "Email inviata con successo!",
|
||||
"expired-doc-title": "Documento Scaduto",
|
||||
"expired-on-mssg": "Questo documento è scaduto il {{expiredDate}} e non è più disponibile per la firma.",
|
||||
"signature-validate-alert": "Conferma di aver selezionato almeno {{minRequiredCount}} caselle di controllo.",
|
||||
"signature-validate-alert-2": "Assicurati che questo campo sia compilato correttamente e soddisfi tutti i requisiti.",
|
||||
"remaing-users": "Utenti rimanenti",
|
||||
"remaining-users": "Utenti rimanenti",
|
||||
"Role": "Ruolo",
|
||||
"additional-users": "Acquista utenti aggiuntivi.",
|
||||
"Quantity-of-users": "Quantità di utenti",
|
||||
@@ -700,7 +706,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",
|
||||
@@ -708,7 +714,7 @@
|
||||
"rotate-left": "Ruota a sinistra",
|
||||
"rotate-alert-mssg": "Tutti i widget su questa pagina andranno persi. Sei sicuro di voler procedere?",
|
||||
"templateid": "ID Modello",
|
||||
"bulk-send-subcription-alert": "Esegui l'upgrade al piano Professionale o Team per utilizzare l'invio in massa.",
|
||||
"bulk-send-subscription-alert": "Esegui l'upgrade al piano Professionale o Team per utilizzare l'invio in massa.",
|
||||
"generate-test-token": "Genera Token di Test",
|
||||
"regenerate-test-token": "Rigenera Token di Test",
|
||||
"help-test-token": "Questo token può essere utilizzato per testare le API all'endpoint https://sandbox.opensignlabs.com/api/v1, consentendoti di firmare documenti illimitati. Nota che le API sandbox firmeranno i tuoi documenti con certificati autofirmati, che potrebbero non essere riconosciuti come validi da Adobe. Una volta completati i test, puoi eseguire l'upgrade a uno dei nostri piani a pagamento per generare un token di produzione.",
|
||||
@@ -728,8 +734,9 @@
|
||||
"copied-code": "COPIATO",
|
||||
"Installation": "Installazione",
|
||||
"Usage": "Utilizzo",
|
||||
"insufficient-credits": "Crediti di firma insufficienti",
|
||||
"insufficient-credits-mssg": "Il proprietario di questo documento non dispone attualmente dei crediti OpenSign necessari affinché tu possa firmare. Contatta il proprietario per ulteriori dettagli.",
|
||||
"insufficient-credits": "Crediti di firma insufficienti. Si prega di contattare {{ownerEmail}}",
|
||||
"insufficient-credits-title": "Crediti di firma insufficienti",
|
||||
"insufficient-credits-mssg": "Il proprietario di questo documento non dispone attualmente dei crediti {{appName}} necessari affinché tu possa firmare. Contatta il proprietario per ulteriori dettagli.",
|
||||
"angular-npm-mssg-1": "Per integrare OpenSign nel tuo progetto Angular, esegui semplicemente il seguente comando:",
|
||||
"quota-mail-info-head": "Limite mensile di email",
|
||||
"quota-mail-info": "Per mantenere la qualità del servizio e prevenire lo spam, OpenSign consente fino a 15 email al mese con il piano gratuito. Esegui l'upgrade ora per l'invio illimitato di email.",
|
||||
@@ -861,7 +868,6 @@
|
||||
"js-snippet-msg-1": "Puoi ottenere il TemplateId dalla",
|
||||
"js-snippet-msg-2": "pagina di gestione modelli",
|
||||
"js-snippet-msg-3": "pagina.",
|
||||
"agrrement-alert": "Per procedere, devi acconsentire alla divulgazione delle registrazioni elettroniche e delle firme.",
|
||||
"webhook-already-exists": "L'URL esiste già! Prova con uno diverso.",
|
||||
"webhook-must-be-secure": "L'URL del webhook deve essere sicuro e utilizzare https://",
|
||||
"provide-test-token": "Si prega di generare il token di test API",
|
||||
@@ -1025,7 +1031,7 @@
|
||||
},
|
||||
"signer-already-present": "Firmatario già presente",
|
||||
"kiosk-sign": "Firma su chiosco",
|
||||
"dont-have-access-to-template": "Il template è stato eliminato o non hai accesso. Si prega di contattare il mittente.",
|
||||
"do-not-have-access-to-template": "Il template è stato eliminato o non hai accesso. Si prega di contattare il mittente.",
|
||||
"kiosk-info": "La Modalità Kiosk consente di raccogliere firme in presenza in modo rapido ed efficiente. Ideale per fiere, eventi o situazioni con firmatari fisicamente presenti. ",
|
||||
"learn-more": "Scopri di più",
|
||||
"finish-mssg": "Sei sicuro di voler completare il documento?",
|
||||
@@ -1081,8 +1087,6 @@
|
||||
"search-contacts": "Cerca contatti…",
|
||||
"add-role-alert": "Si prega di aggiungere almeno un ruolo",
|
||||
"edit-draft": "Modifica bozza",
|
||||
"invalid-email-found": "Email non valida trovata: {{email}}",
|
||||
"duplicate-email-found": "Email duplicata trovata: {{email}}",
|
||||
"vertical": "Verticale",
|
||||
"horizontal": "Orizzontale",
|
||||
"billing": "Fatturazione",
|
||||
@@ -1096,7 +1100,7 @@
|
||||
"access-denied": "Accesso negato",
|
||||
"upgrade": "Aggiorna",
|
||||
"do-not-access-app": "Non hai accesso a questa applicazione.",
|
||||
"dont-have-access": "Non hai accesso.",
|
||||
"do-not-have-access": "Non hai accesso.",
|
||||
"valid-email-alert": "Inserisci un indirizzo email valido.",
|
||||
"otp-not-validate": "OTP non valido.",
|
||||
"domain-not-allowed": "Questo dominio non è consentito",
|
||||
@@ -1113,10 +1117,12 @@
|
||||
"branding-help": "Il branding consente il white labelling della tua app",
|
||||
"custom-sub-domain": "Sottodominio personalizzato",
|
||||
"app-name": "Nome dell'app",
|
||||
"provide-domain-name": "Fornisci il tuo nome di dominio",
|
||||
"provide-domain-name": "Sono consentiti solo i sottodomini (ad es., app.tuodominio.com). I domini principali/root come tuodominio.com non sono supportati.",
|
||||
"provide-app-name-help": "Fornisci un nome univoco per l'app. Verrà mostrato come nome del tuo brand nell'applicazione e in tutte le e-mail ovunque venga applicato il branding.",
|
||||
"upload-app-logo": "Carica un logo in formato .png, .jpg o .svg. Dimensione consigliata: {{size}} pixel (rapporto 5:2) per la migliore qualità di visualizzazione.",
|
||||
"upload-app-favicon": "Carica un'immagine favicon quadrata (.ico, .png o .svg). La dimensione consigliata è {{size}} pixel per la migliore compatibilità del browser.",
|
||||
"provide-app-name": "Fornisci il nome della tua app",
|
||||
"logo": "Logo",
|
||||
"upload-app-logo": "Carica il logo della tua app",
|
||||
"prefill-unfilled-widget": "I seguenti campi obbligatori non possono essere lasciati vuoti: {{emptyWidget}}. Per favore compilali per procedere.",
|
||||
"Dashboard": "Dashboard",
|
||||
"Analytics": "Analitica",
|
||||
@@ -1239,6 +1245,7 @@
|
||||
},
|
||||
"read-only-date-error": "Il widget data di sola lettura deve avere una data predefinita.",
|
||||
"set-date": "Imposta data",
|
||||
"default-date": "Data predefinita",
|
||||
"set-today": "Data di firma",
|
||||
"enter-name": "Inserisci nome",
|
||||
"enter-email": "Inserisci e-mail",
|
||||
@@ -1274,5 +1281,120 @@
|
||||
"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",
|
||||
"signing-date": "Data di firma",
|
||||
"date-widget": "Widget data",
|
||||
"mail-failed": "Si è verificato un errore durante l'invio dell'e-mail. Per favore riprova.",
|
||||
"mail-status-head": "Passaggio successivo: firmare il tuo documento?",
|
||||
"send-to-email": "Invia tramite email",
|
||||
"date-pref-help-sub-title": "Le preferenze del widget Data consentono di definire le impostazioni predefinite per il widget Data. Queste impostazioni vengono applicate automaticamente ogni volta che si aggiunge un nuovo widget Data a un documento o a un modello. È comunque possibile personalizzare qualsiasi widget Data in qualsiasi momento facendo clic sull'icona a forma di ingranaggio.",
|
||||
"date-pref-help-format": "Scegli il formato di data predefinito utilizzato per i widget Data.",
|
||||
"date-pref-help-default-date": "Imposta un valore di data predefinito per i widget Data.",
|
||||
"date-pref-help-signing-date": "Quando abilitato, il widget Data utilizza automaticamente la data in cui il firmatario firma il documento.",
|
||||
"date-pref-help-read-only": "Quando abilitato, il widget Data è in sola lettura per il firmatario.",
|
||||
"upload-csv": "Carica CSV",
|
||||
"file-selected": "file selezionato",
|
||||
"bulk-send-csv-helper": "Utilizza le intestazioni per i ruoli dei firmatari (Email/Nome/Telefono del ruolo) e i valori di precompilazione (Precompilazione: Nome campo). I campi di precompilazione obbligatori devono essere compilati.",
|
||||
"bulk-send-csv-helper-note": "Nota: Per i campi di precompilazione di tipo data nel CSV, il valore deve essere fornito in questo formato: {{value}} (esempio: {{example}}).",
|
||||
"bulk-send-csv-summary": "{{total}} righe importate dal CSV.",
|
||||
"bulk-send-csv-missing-headers": "Intestazioni CSV obbligatorie mancanti. Ruoli: {{roles}}.",
|
||||
"bulk-send-no-records": "Aggiungi almeno un destinatario prima dell'invio.",
|
||||
"csv-only-support": "Sono supportati solo file CSV.",
|
||||
"csv-no-records": "Nessun record trovato nel CSV.",
|
||||
"select-at-least-option": "Seleziona almeno un'opzione.",
|
||||
"draw-required": "Per favore, disegna nel riquadro.",
|
||||
"enter-smtp-username": "Inserisci il nome utente SMTP",
|
||||
"enter-webhook-url": "Inserisci l'URL del webhook",
|
||||
"enter-user-name": "Inserisci il nome utente",
|
||||
"enter-tagline": "Inserisci lo slogan",
|
||||
"enter-password-or-click-autogenerate": "Inserisci la password o fai clic su Genera automaticamente",
|
||||
"enter-daily-email-quota": "Inserisci la quota giornaliera di e-mail",
|
||||
"enter-custom-regular-expression": "Inserisci un'espressione regolare personalizzata",
|
||||
"enter-value": "Inserisci {{value}}",
|
||||
"are-you-sure-want-to-send-document": "Sei sicuro di voler inviare il documento?",
|
||||
"favicon": "Favicon",
|
||||
"delete-two-factor-authentication": "Sei sicuro di voler eliminare l'autenticazione a due fattori?",
|
||||
"paste-html-here": "Incolla HTML qui",
|
||||
"open-email-builder": "Puoi creare un modello di e-mail utilizzando la piattaforma <1>email builder</1> e copiare il codice HTML.",
|
||||
"contact-saved": "Contatto salvato con successo.",
|
||||
"remove-limit-of-user": "Sei sicuro di voler rimuovere il limite di questo utente? Potrà consumare tutti i crediti premium disponibili della tua organizzazione, condivisi tra tutti gli utenti.",
|
||||
"current-expiry-date": "Data di scadenza attuale",
|
||||
"reminder-error": "“Ricorda una volta ogni (giorni)” non deve superare il “Tempo per completare (giorni)”.",
|
||||
"showing-to-of-records": "Visualizzazione da {{record}} a {{recordPerPage}} di {{totalRows}} record",
|
||||
"records-per-page": "Record per pagina",
|
||||
"email-not-found-in-row": "Email non trovata nella riga {{row}}.",
|
||||
"invalid-email-found-in-row": "Email non valida trovata nella riga {{row}}: {{email}}",
|
||||
"duplicate-email-found-in-row": "Email duplicata trovata nella riga {{row}}: {{email}}",
|
||||
"prefill": "Precompilazione",
|
||||
"recipients-&-data": "Destinatari e dati",
|
||||
"response": "Risposta",
|
||||
"prefill-tab-help": "Questi sono i campi comuni del modello che verranno applicati a tutti i documenti in questo invio massivo.",
|
||||
"recipient-tab-help-p1": "Nel passaggio successivo, dovrai:",
|
||||
"recipient-tab-help-p2": "Caricare un CSV con i dettagli dei destinatari oppure",
|
||||
"recipient-tab-help-p3": "Inserire manualmente le informazioni dei destinatari",
|
||||
"recipient-tab-help-p4": "Ogni destinatario riceverà il proprio documento generato da questo modello.",
|
||||
"response-tab-help": "Rivedi le tue risposte.",
|
||||
"summary": "Riepilogo",
|
||||
"prefill-fields": "Campi di precompilazione",
|
||||
"documents": "Documenti",
|
||||
"message": "Messaggio",
|
||||
"formula-help-p1": "Raccogli input numerici ed esegui calcoli dinamici utilizzando formule.",
|
||||
"example": "Esempio",
|
||||
"formula-help-p2": "Per visualizzare un totale in number-3, usa {{number-1}} + {{number-2}}.",
|
||||
"formula-help-p3": "Per calcolare un valore in number-4, usa ({{number-1}} * {{number-2}}) / {{number-3}}.",
|
||||
"formula-help-p4": "Ogni widget numerico deve avere un nome univoco (es. number-1), poiché le formule si basano su questi identificatori.",
|
||||
"note-consume-premium-credits": "Nota: Questa funzione consuma crediti premium.",
|
||||
"note-consume-premium-credits-on-document-send": "Nota: Questa funzione consuma crediti premium. I crediti vengono consumati ogni volta che un documento viene inviato, indipendentemente dal fatto che venga firmato o meno.",
|
||||
"webhook-authentication-help": "{{appName}} protegge ogni webhook generando una firma HMAC-SHA256 utilizzando la tua chiave di sicurezza webhook e il payload originale della richiesta. Questa firma viene inviata nell'intestazione x-webhook-signature. Il tuo server deve generare la stessa firma utilizzando il payload ricevuto e la tua chiave segreta. Se entrambe le firme coincidono, la richiesta webhook è autentica e non è stata modificata.",
|
||||
"invalid-widget-found-in-row": "Valore non valido rilevato nella riga {{row}} per il widget {{widget}}."
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -35,6 +35,8 @@ const UpdateExistUserAdmin = lazyWithRetry(
|
||||
const Preferences = lazyWithRetry(() => import("./pages/Preferences"));
|
||||
const Login = lazyWithRetry(() => import("./pages/Login"));
|
||||
const VerifyDocument = lazyWithRetry(() => import("./pages/VerifyDocument"));
|
||||
const EmailBuilder = lazyWithRetry(() => import("./pages/EmailBuilder"));
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
|
||||
const AppLoader = () => {
|
||||
return (
|
||||
@@ -137,6 +139,7 @@ function App() {
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/success" element={<DocSuccessPage />} />
|
||||
<Route path="/emailbuilder" element={<EmailBuilder />} />
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
import {
|
||||
useTranslation
|
||||
} from "react-i18next";
|
||||
import { withSessionValidation } from "../utils";
|
||||
|
||||
function generatePassword(length) {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
@@ -67,7 +69,7 @@ const AddUser = (props) => {
|
||||
}
|
||||
};
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
const handleSubmit = withSessionValidation(async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!emailRegex.test(formdata.email)) {
|
||||
@@ -130,7 +132,7 @@ const AddUser = (props) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Define a function to handle the "add yourself" checkbox
|
||||
const handleReset = () => {
|
||||
@@ -216,7 +218,7 @@ const AddUser = (props) => {
|
||||
></i>
|
||||
</div>
|
||||
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
|
||||
{t("password-generateed")}
|
||||
{t("password-generated")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
@@ -225,7 +227,6 @@ const AddUser = (props) => {
|
||||
className="block text-xs font-semibold"
|
||||
>
|
||||
{t("phone")}
|
||||
{/* <span className="text-[red] text-[13px]"> *</span> */}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
import SuggestionInput from "./shared/fields/SuggestionInput";
|
||||
import Loader from "../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
emailRegex,
|
||||
} from "../constant/const";
|
||||
const BulkSendUi = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [forms, setForms] = useState([]);
|
||||
const formRef = useRef(null);
|
||||
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
|
||||
const [isSubmit, setIsSubmit] = useState(false);
|
||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
||||
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [signers, setSigners] = useState([]);
|
||||
const [emails, setEmails] = useState([]);
|
||||
const [isPrefillExist, setIsPrefillExist] = useState(false);
|
||||
useEffect(() => {
|
||||
signatureExist();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
//function to check at least one signature field exist
|
||||
const signatureExist = async () => {
|
||||
const isPrefill = props?.Placeholders.some((x) => x?.Role === "prefill");
|
||||
if (isPrefill) {
|
||||
setIsPrefillExist(isPrefill);
|
||||
}
|
||||
setIsDisableBulkSend(false);
|
||||
const getPlaceholder = props?.Placeholders;
|
||||
const removePrefill = getPlaceholder.filter((x) => x?.Role !== "prefill");
|
||||
const checkIsSignatureExistt = removePrefill?.every((placeholderObj) =>
|
||||
placeholderObj?.placeHolder?.some((holder) =>
|
||||
holder?.pos?.some((posItem) => posItem?.type === "signature")
|
||||
)
|
||||
);
|
||||
setIsSignatureExist(checkIsSignatureExistt);
|
||||
setIsLoader(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (scrollOnNextUpdate && formRef.current) {
|
||||
formRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
});
|
||||
setScrollOnNextUpdate(false);
|
||||
}
|
||||
}, [forms, scrollOnNextUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
(() => {
|
||||
if (props?.Placeholders?.length > 0) {
|
||||
let users = [];
|
||||
let emails = [];
|
||||
props?.Placeholders?.forEach((element) => {
|
||||
const signerEmail = element?.email || element?.signerPtr?.Email;
|
||||
|
||||
// only add when there's a non-empty signerEmail
|
||||
if (signerEmail) {
|
||||
emails = [...emails, signerEmail];
|
||||
}
|
||||
if (!element.signerObjId) {
|
||||
users = [
|
||||
...users,
|
||||
{
|
||||
fieldId: element.Id,
|
||||
email: "",
|
||||
label: element.Role,
|
||||
signer: {}
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
setEmails(emails);
|
||||
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
|
||||
const signer = props.item?.Signers?.filter((x) => x?.objectId);
|
||||
setSigners(signer);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
const handleInputChange = (index, signer, fieldIndex) => {
|
||||
const newForms = [...forms];
|
||||
newForms[index].fields[fieldIndex].email = signer?.Email
|
||||
? signer?.Email
|
||||
: signer || "";
|
||||
newForms[index].fields[fieldIndex].signer = signer?.objectId ? signer : "";
|
||||
setForms(newForms);
|
||||
};
|
||||
|
||||
|
||||
const handleRemoveForm = (index) => {
|
||||
const updatedForms = forms.filter((_, i) => i !== index);
|
||||
setForms(updatedForms);
|
||||
};
|
||||
|
||||
function validateEmails(data) {
|
||||
for (const item of data) {
|
||||
let email = "";
|
||||
for (const field of item.fields) {
|
||||
if (!emailRegex.test(field.email)) {
|
||||
alert(t("invalid-email-found", { email: field.email }));
|
||||
return false;
|
||||
} else if (email === field.email || emails?.includes(field.email)) {
|
||||
alert(t("duplicate-email-found", { email: field.email }));
|
||||
return false;
|
||||
} else {
|
||||
email = field.email;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsSubmit(true);
|
||||
if (validateEmails(forms)) {
|
||||
// Create a copy of Placeholders array from props.item
|
||||
let Placeholders = [...props.Placeholders];
|
||||
// Initialize an empty array to store updated documents
|
||||
let Documents = [];
|
||||
// Loop through each form
|
||||
forms.forEach((form) => {
|
||||
//checking if user enter email which already exist as a signer then add user in a signers array
|
||||
let existSigner = [];
|
||||
form.fields.map((data) => {
|
||||
if (data.signer) {
|
||||
existSigner.push(data.signer);
|
||||
}
|
||||
});
|
||||
// Map through the copied Placeholders array to update email values
|
||||
const updatedPlaceholders = Placeholders.map((placeholder) => {
|
||||
// Find the field in the current form that matches the placeholder Id
|
||||
const field = form.fields.find(
|
||||
(element) => parseInt(element.fieldId) === placeholder.Id
|
||||
);
|
||||
// If a matching field is found, update the email value in the placeholder
|
||||
const signer = field?.signer?.objectId ? field.signer : "";
|
||||
if (field) {
|
||||
if (signer) {
|
||||
return {
|
||||
...placeholder,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...placeholder,
|
||||
email: field.email,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
}
|
||||
}
|
||||
// If no matching field is found, keep the placeholder as is
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Push a new document object with updated Placeholders into the Documents array
|
||||
if (existSigner?.length > 0) {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
|
||||
});
|
||||
} else {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
SignatureType: props.signatureType,
|
||||
Signers: signers
|
||||
});
|
||||
}
|
||||
});
|
||||
await batchQuery(Documents);
|
||||
} else {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const batchQuery = async (Documents) => {
|
||||
const token =
|
||||
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
|
||||
const functionsUrl = `${localStorage.getItem(
|
||||
"baseUrl"
|
||||
)}functions/batchdocuments`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
...token,
|
||||
};
|
||||
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);
|
||||
} finally {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isLoader ? (
|
||||
<div className="w-full h-[100px] flex justify-center items-center z-[999]">
|
||||
<Loader />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{isSubmit && (
|
||||
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
{isPrefillExist ? (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("prefill-bulk-error")}
|
||||
</div>
|
||||
) : !isDisableBulkSend ? (
|
||||
<>
|
||||
{props.Placeholders?.length > 0 ? (
|
||||
isSignatureExist ? (
|
||||
<>
|
||||
{props.Placeholders?.some((x) => !x.signerObjId) ? (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="min-h-max max-h-[250px] overflow-y-auto">
|
||||
{forms?.map((form, index) => (
|
||||
<div
|
||||
key={form.Id}
|
||||
className="p-3 op-card border-[1px] border-gray-400 mt-3 mx-4 mb-4 bg-base-200 text-base-content grid grid-cols-1 md:grid-cols-2 gap-2 relative"
|
||||
>
|
||||
{form?.fields?.map((field, fieldIndex) => (
|
||||
<div
|
||||
className="flex flex-col"
|
||||
key={field.fieldId}
|
||||
>
|
||||
<label className="block text-xs font-semibold">
|
||||
{field.label}
|
||||
</label>
|
||||
<SuggestionInput
|
||||
required
|
||||
type="email"
|
||||
value={field.value}
|
||||
index={fieldIndex}
|
||||
onChange={(signer) =>
|
||||
handleInputChange(
|
||||
index,
|
||||
signer,
|
||||
fieldIndex
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{forms?.length > 1 && (
|
||||
<button
|
||||
onClick={() => handleRemoveForm(index)}
|
||||
className="absolute right-3 top-1 text-[red] border-[1px] border-[red] rounded-lg w-[1.7rem] h-[1.7rem]"
|
||||
>
|
||||
<i className="fa-light fa-trash"></i>
|
||||
</button>
|
||||
)}
|
||||
<div ref={formRef}></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col mx-4 mb-4 gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-accent focus:outline-none"
|
||||
>
|
||||
<i className="fa-light fa-paper-plane"></i>{" "}
|
||||
<span>{t("send")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-1")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-2")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-3")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkSendUi;
|
||||
@@ -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
|
||||
}) => {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DateFormat = ({ selectDate, dateFormatList, handleChangeFormat }) => {
|
||||
const selectedFormatIndex = dateFormatList?.findIndex(
|
||||
(item) => item.format === selectDate?.format
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:items-center md:flex-row gap-y-3 my-2">
|
||||
<span className="capitalize">{t("format")}: </span>
|
||||
<select
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-2"
|
||||
value={selectedFormatIndex >= 0 ? selectedFormatIndex : ""}
|
||||
onChange={(e) => handleChangeFormat(e)}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{t("select-date-format")}
|
||||
</option>
|
||||
{dateFormatList.map((data, ind) => {
|
||||
return (
|
||||
<option className="text-[13px]" value={ind} key={ind}>
|
||||
{data?.date ? data?.date : "nodata"}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span className="text-xs text-gray-400 ml-1 uppercase">
|
||||
{selectDate.format}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateFormat;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { forwardRef } from "react";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import {
|
||||
getDefaultDate,
|
||||
getMonth,
|
||||
getYear,
|
||||
months,
|
||||
years
|
||||
} from "../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DatePicker = ({
|
||||
selectDate,
|
||||
format,
|
||||
minDate,
|
||||
maxDate,
|
||||
onChange,
|
||||
handleClear,
|
||||
label,
|
||||
showLabel = true,
|
||||
showClear = true,
|
||||
dateClassName = ""
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const CustomInput = forwardRef(({ value, onClick }, ref) => (
|
||||
<div
|
||||
className="w-full border-gray-400 rounded-[50px] border-[1px] px-3 text-xs py-2 focus:outline-none hover:border-base-content flex items-center justify-between"
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
<span className={`${dateClassName} truncate`}>{value}</span>
|
||||
<i className={`fa-light fa-calendar`}></i>
|
||||
</div>
|
||||
));
|
||||
CustomInput.displayName = "CustomInput";
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLabel && (
|
||||
<span className="flex-shrink-0">{label || t("default-date")}: </span>
|
||||
)}
|
||||
<ReactDatePicker
|
||||
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||
<div className="flex justify-start md:ml-2">
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={months[getMonth(date)]}
|
||||
onChange={({ target: { value } }) =>
|
||||
changeMonth(months.indexOf(value))
|
||||
}
|
||||
>
|
||||
{months.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={getYear(date)}
|
||||
onChange={({ target: { value } }) => changeYear(value)}
|
||||
>
|
||||
{years.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
wrapperClassName="w-full"
|
||||
closeOnScroll={true}
|
||||
selected={getDefaultDate(selectDate?.date, selectDate?.format)}
|
||||
popperPlacement="top-end"
|
||||
customInput={<CustomInput />}
|
||||
onChange={(date) => onChange(date)}
|
||||
dateFormat={
|
||||
selectDate ? selectDate?.format : format ? format : "MM/dd/yyyy"
|
||||
}
|
||||
portalId="root-portal"
|
||||
/>
|
||||
{showClear && handleClear && (
|
||||
<span
|
||||
onClick={() => handleClear()}
|
||||
className="underline text-blue-500 cursor-pointer ml-2"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
@@ -0,0 +1,130 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import DOMPurify from "dompurify";
|
||||
import juice from "juice";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
const EmailBodyEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
smallscreen = false,
|
||||
bodyName,
|
||||
isReset = false,
|
||||
isTemplateLoaded
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [inputHtml, setInputHtml] = useState("");
|
||||
const [cleanPreview, setCleanPreview] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
initProcessContent();
|
||||
}, [isTemplateLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset) {
|
||||
initProcessContent(value);
|
||||
}
|
||||
}, [isReset]);
|
||||
|
||||
const initProcessContent = () => {
|
||||
// 1. Sanitize immediately to strip scripts/malicious tags
|
||||
const sanitized = DOMPurify.sanitize(value, {
|
||||
USE_PROFILES: { html: true }, // Ensures basic HTML structure
|
||||
ADD_ATTR: ["target"] // Allow links to open in new tabs
|
||||
});
|
||||
|
||||
// 2. Inline CSS for email compatibility
|
||||
// Note: Juice is fast, but for huge files, you might debounce this
|
||||
try {
|
||||
const inlined = juice(sanitized);
|
||||
setInputHtml(inlined);
|
||||
onChange?.(inlined);
|
||||
setCleanPreview(inlined);
|
||||
} catch (err) {
|
||||
onChange?.(sanitized);
|
||||
setInputHtml(sanitized); // Fallback if juice fails
|
||||
setCleanPreview(sanitized);
|
||||
}
|
||||
};
|
||||
|
||||
const processContent = (value) => {
|
||||
// 1. Sanitize immediately to strip scripts/malicious tags
|
||||
const sanitized = DOMPurify.sanitize(value, {
|
||||
USE_PROFILES: { html: true }, // Ensures basic HTML structure
|
||||
ADD_ATTR: ["target"] // Allow links to open in new tabs
|
||||
});
|
||||
|
||||
// 2. Inline CSS for email compatibility
|
||||
// Note: Juice is fast, but for huge files, you might debounce this
|
||||
try {
|
||||
const inlined = juice(sanitized);
|
||||
setCleanPreview(inlined);
|
||||
onChange?.(inlined);
|
||||
} catch (err) {
|
||||
setCleanPreview(sanitized); // Fallback if juice fails
|
||||
onChange?.(sanitized);
|
||||
}
|
||||
};
|
||||
const handleChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setInputHtml(value);
|
||||
processContent(value);
|
||||
};
|
||||
|
||||
const screen = smallscreen ? "flex-col" : "flex-col md:flex-row ";
|
||||
const template =
|
||||
bodyName === "request"
|
||||
? "#sample/requestemail"
|
||||
: bodyName === "completion"
|
||||
? "#sample/completionemail"
|
||||
: "#";
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm ">
|
||||
<Trans i18nKey={"open-email-builder"}>
|
||||
{"You can create email template using "}
|
||||
<a
|
||||
href={`/emailbuilder${template}`}
|
||||
target="_blank"
|
||||
referrerPolicy="no-referrer"
|
||||
className="op-link op-link-primary font-medium"
|
||||
>
|
||||
email builder
|
||||
</a>
|
||||
{" platform and copy html code."}
|
||||
</Trans>
|
||||
</p>
|
||||
<div className={`flex ${screen} gap-5`}>
|
||||
{/* Editor Pane */}
|
||||
<div className="flex flex-1 flex-col mt-2">
|
||||
<label>{t("paste-html-here")}:</label>
|
||||
<div className="flex-1 mt-1 p-3 text-xs op-textarea op-textarea-bordered min-h-[70vh]">
|
||||
<textarea
|
||||
className="w-full min-h-[70vh] focus:outline-none"
|
||||
value={inputHtml}
|
||||
onChange={(e) => handleChange(e)}
|
||||
placeholder="<html><body><h1>Hello!</h1></body></html>"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live Preview Pane */}
|
||||
<div className="flex flex-1 flex-col mt-2">
|
||||
<label>{t("preview")}</label>
|
||||
<div className="flex-1 mt-1 bg-white border-[1px] op-textarea border-[#ccc] min-h-[70vh]">
|
||||
{cleanPreview && (
|
||||
<iframe
|
||||
title="Safe Preview"
|
||||
srcDoc={cleanPreview}
|
||||
// SECURITY: sandbox prevents scripts from running even if they slip through
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox"
|
||||
className="w-full min-h-[70vh]"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailBodyEditor;
|
||||
@@ -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"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo
|
||||
} from "react";
|
||||
import axios from "axios";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
emailRegex,
|
||||
} from "../../constant/const";
|
||||
import {
|
||||
getSignedUrl,
|
||||
isBase64,
|
||||
getBase64FromUrl,
|
||||
generateId,
|
||||
} from "../../constant/Utils";
|
||||
import {
|
||||
handleEmbedPrefillToDoc,
|
||||
isWidgetResponseCompatible,
|
||||
formatCSVDate,
|
||||
normalizeKey,
|
||||
loadPdfOnce,
|
||||
} from "../../utils";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
setBulkLoader,
|
||||
} from "../../redux/reducers/widgetSlice";
|
||||
import Table from "./components/Table";
|
||||
import PrefillWidgets from "./components/PrefillWidgets";
|
||||
import WizardHeader from "./components/WizardHeader";
|
||||
import ResponseTab from "./components/ResponseTab";
|
||||
import { steps } from "../../json/BulkSendSteps";
|
||||
|
||||
const EXCLUDED_PREFILL_TYPES = new Set(["image", "draw"]);
|
||||
const EXCLUDED_WIDGET_TYPES = new Set([
|
||||
"image",
|
||||
"signature",
|
||||
"stamp",
|
||||
"initials"
|
||||
]);
|
||||
const ALL_EXCLUDED_TYPES = new Set([
|
||||
...EXCLUDED_PREFILL_TYPES,
|
||||
...EXCLUDED_WIDGET_TYPES
|
||||
]);
|
||||
|
||||
const BulkSendUi = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const dispatch = useDispatch();
|
||||
const { isBulkLoader } = useSelector((state) => state.widget);
|
||||
const [forms, setForms] = useState([]);
|
||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [signers, setSigners] = useState([]);
|
||||
const [emails, setEmails] = useState([]);
|
||||
const [isVacantRoles, setIsVacantRoles] = useState(false);
|
||||
const [fields, setFields] = useState([]);
|
||||
const [headers, setHeaders] = useState([]);
|
||||
const [prefillWidgets, setPrefillWidgets] = useState([]);
|
||||
const [step, setStep] = useState(0);
|
||||
const [stepsList, setStepsList] = useState(steps(t));
|
||||
const [stepKey, setStepKey] = useState(steps(t)?.[0]?.key || "");
|
||||
const [message, setMessage] = useState({ status: "", message: "" });
|
||||
|
||||
const tokenHeader = useMemo(() => {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId")
|
||||
};
|
||||
return {
|
||||
...headers,
|
||||
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
|
||||
};
|
||||
}, [props?.jwttoken]);
|
||||
|
||||
// Initialize form state (fields, widgets/responses, emails, headers, forms, signers, allowedForm) from template placeholders with existing values.
|
||||
const initializeWidgetsWithValues = async () => {
|
||||
if (props?.Placeholders?.length > 0) {
|
||||
const roles = props?.Placeholders?.filter((p) => !p.signerObjId) || [];
|
||||
const emails = [];
|
||||
|
||||
const users = await Promise.all(
|
||||
roles?.map(async (role) => {
|
||||
const isPrefill = role.Role?.toLowerCase() === "prefill";
|
||||
const uniqueNames = new Set();
|
||||
const pages = role?.placeHolder ?? [];
|
||||
const widgets = await Promise.all(
|
||||
pages.flatMap((page) =>
|
||||
page.pos
|
||||
.filter((widget) => {
|
||||
const widgetName = widget?.options?.name;
|
||||
if (!widgetName) return false; //|| widget?.options?.isReadOnly
|
||||
// exclude certain widget types for non-prefill
|
||||
if (EXCLUDED_WIDGET_TYPES.has(widget.type) && !isPrefill)
|
||||
return false;
|
||||
if (widget?.options?.formula) return false;
|
||||
// unique by widget name
|
||||
if (uniqueNames.has(widgetName)) return false;
|
||||
uniqueNames.add(widgetName);
|
||||
return true;
|
||||
})
|
||||
.map(async (widget) => {
|
||||
let response = "";
|
||||
if (ALL_EXCLUDED_TYPES.has(widget.type)) {
|
||||
const url =
|
||||
widget.options?.response ||
|
||||
widget.options?.defaultValue ||
|
||||
"";
|
||||
if (url && !isBase64(url)) {
|
||||
const signedUrl = await getSignedUrl(
|
||||
url,
|
||||
"", // docId
|
||||
props.item.objectId, // templateId
|
||||
);
|
||||
|
||||
response = await getBase64FromUrl(signedUrl, true);
|
||||
} else {
|
||||
response = "";
|
||||
}
|
||||
} else {
|
||||
if (widget.type === "date") {
|
||||
const value =
|
||||
widget.options?.response ||
|
||||
widget.options?.defaultValue ||
|
||||
"";
|
||||
response = value ? formatCSVDate(widget, value) : "";
|
||||
} else {
|
||||
response =
|
||||
widget.options?.response ||
|
||||
widget.options?.defaultValue ||
|
||||
"";
|
||||
}
|
||||
}
|
||||
const label = `${role.Role}::${widget.options?.name}`;
|
||||
|
||||
return {
|
||||
...widget,
|
||||
options: { ...widget.options, response: response },
|
||||
label: label,
|
||||
response,
|
||||
pageNumber: page.pageNumber
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
const signerEmail =
|
||||
normalizeKey(role?.email) || role?.signerPtr?.Email;
|
||||
if (signerEmail) emails.push(signerEmail);
|
||||
|
||||
return {
|
||||
fieldId: role.Id,
|
||||
email: "",
|
||||
label: role.Role,
|
||||
signer: {},
|
||||
widgets: widgets
|
||||
};
|
||||
})
|
||||
);
|
||||
const prefills = users.find((r) => r?.label?.toLowerCase() === "prefill");
|
||||
const signers = users.filter(
|
||||
(r) => r?.label?.toLowerCase() !== "prefill"
|
||||
);
|
||||
setFields(signers);
|
||||
if (prefills && prefills?.widgets?.length) {
|
||||
setPrefillWidgets(prefills?.widgets);
|
||||
} else {
|
||||
setStepsList((s) => s.filter((step) => step.key !== "prefill"));
|
||||
setStepKey("recipients");
|
||||
setStep(0);
|
||||
}
|
||||
setEmails(emails);
|
||||
|
||||
// Build headers with required ordering:
|
||||
// 1) all role emails first
|
||||
// 2) then all widgets
|
||||
const emailHeaders = [];
|
||||
const widgetHeaders = [];
|
||||
for (const role of signers) {
|
||||
const isPrefill = role?.label?.toLowerCase() === "prefill";
|
||||
|
||||
if (!isPrefill) {
|
||||
emailHeaders.push({
|
||||
label: `${role.label}::Email`,
|
||||
role: role.label,
|
||||
type: "role",
|
||||
isRequired: true
|
||||
});
|
||||
}
|
||||
|
||||
for (const widget of role?.widgets ?? []) {
|
||||
const isRequired =
|
||||
widget?.options?.status === "required" && isPrefill;
|
||||
|
||||
widgetHeaders.push({
|
||||
label: widget?.label,
|
||||
role: role?.label,
|
||||
type: widget?.type,
|
||||
isRequired
|
||||
});
|
||||
}
|
||||
}
|
||||
setHeaders([...emailHeaders, ...widgetHeaders]);
|
||||
setForms((prev) => [...prev, { Id: generateId(8), fields: signers }]);
|
||||
const signer = props.item?.Signers?.filter((x) => x?.objectId);
|
||||
setSigners(signer);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
signatureExist();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const checkSignatureAndRoles = (placeholders = []) => {
|
||||
const filtered = placeholders.filter((x) => x?.Role !== "prefill");
|
||||
|
||||
const isSignExist =
|
||||
filtered?.length > 0 &&
|
||||
filtered.every((p) =>
|
||||
p?.placeHolder?.some((h) =>
|
||||
h?.pos?.some((x) => x?.type === "signature")
|
||||
)
|
||||
);
|
||||
setIsSignatureExist(isSignExist);
|
||||
setIsVacantRoles(filtered?.some((x) => !x.signerObjId));
|
||||
};
|
||||
|
||||
//function to check at least one signature field exist
|
||||
const signatureExist = async () => {
|
||||
setIsLoader(true);
|
||||
await initializeWidgetsWithValues();
|
||||
checkSignatureAndRoles(props?.Placeholders);
|
||||
setIsLoader(false);
|
||||
};
|
||||
|
||||
const handleInputChange = (formIndex, signer, role) => {
|
||||
setForms((prev) => {
|
||||
const next = [...prev];
|
||||
const form = next[formIndex];
|
||||
if (!form) return prev;
|
||||
|
||||
const fields = [...(form.fields ?? [])];
|
||||
const fieldId = fields.findIndex((f) => f.label === role);
|
||||
if (fieldId < 0) return prev;
|
||||
|
||||
const email = signer?.Email ? signer.Email : signer || "";
|
||||
|
||||
fields[fieldId] = {
|
||||
...fields[fieldId],
|
||||
email: normalizeKey(email),
|
||||
signer: signer?.objectId ? signer : ""
|
||||
};
|
||||
|
||||
next[formIndex] = { ...form, fields };
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleWidgetDetails = (
|
||||
value = "",
|
||||
formIndex,
|
||||
roleIndex = 0,
|
||||
widgetIndex
|
||||
) => {
|
||||
setForms((prev) => {
|
||||
const next = [...prev];
|
||||
const form = next[formIndex];
|
||||
if (!form) return prev;
|
||||
|
||||
const fields = [...(form.fields ?? [])];
|
||||
const field = fields[roleIndex];
|
||||
if (!field) return prev;
|
||||
|
||||
const widgets = [...(field.widgets ?? [])];
|
||||
const w = widgets[widgetIndex];
|
||||
if (!w) return prev;
|
||||
|
||||
widgets[widgetIndex] = {
|
||||
...w,
|
||||
options: { ...w?.options, response: value },
|
||||
response: value
|
||||
};
|
||||
fields[roleIndex] = { ...field, widgets };
|
||||
next[formIndex] = { ...form, fields };
|
||||
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function validateEmails(data) {
|
||||
for (const [rowIndex, item] of data.entries()) {
|
||||
let email = "";
|
||||
const rowNumber = rowIndex + 1;
|
||||
const filterFields = item?.fields || [];
|
||||
for (const field of filterFields) {
|
||||
// skip prefill
|
||||
if (field?.label === "prefill") continue;
|
||||
const params = { email: field.email, row: rowNumber };
|
||||
if (!field.email) {
|
||||
alert(t("email-not-found-in-row", { ...params }));
|
||||
return false;
|
||||
} else if (!emailRegex.test(field.email)) {
|
||||
alert(t("invalid-email-found-in-row", { ...params }));
|
||||
return false;
|
||||
} else if (email === field.email || emails?.includes(field.email)) {
|
||||
alert(t("duplicate-email-found-in-row", { ...params }));
|
||||
return false;
|
||||
} else {
|
||||
email = field.email;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// `updateWidgetValues` will update responses for all widget types
|
||||
const updateWidgetValues = (widgetValues = [], placeholders = []) => {
|
||||
if (!widgetValues?.length) return placeholders;
|
||||
const responseByName = new Map(
|
||||
widgetValues
|
||||
.map((w) => [w?.options?.name, w?.response])
|
||||
.filter(([name]) => !!name)
|
||||
);
|
||||
|
||||
return placeholders.map((ph) => {
|
||||
const updatedPages = (ph?.placeHolder ?? []).map((page) => {
|
||||
const updatedPos = (page?.pos ?? []).map((widgetDetails) => {
|
||||
const name = widgetDetails?.options?.name;
|
||||
if (!name || !responseByName.has(name)) return widgetDetails;
|
||||
|
||||
const newResponse = responseByName?.get(name) || "";
|
||||
const isValidResponse = isWidgetResponseCompatible(
|
||||
widgetDetails,
|
||||
newResponse
|
||||
);
|
||||
|
||||
if (!isValidResponse) return widgetDetails;
|
||||
|
||||
return {
|
||||
...widgetDetails,
|
||||
options: {
|
||||
...widgetDetails.options,
|
||||
// update response; if you DON'T want undefined to overwrite, keep the ?? fallback
|
||||
response:
|
||||
newResponse ??
|
||||
widgetDetails.options?.response ??
|
||||
widgetDetails.options?.defaultValue
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return { ...page, pos: updatedPos };
|
||||
});
|
||||
|
||||
return { ...ph, placeHolder: updatedPages };
|
||||
});
|
||||
};
|
||||
|
||||
// `removeWidgetValues` will clear out responses for draw/image/signature/stamp/initials widgets
|
||||
const removeWidgetValues = (widgetValues = [], placeholders = []) => {
|
||||
if (!widgetValues?.length) return placeholders;
|
||||
return placeholders.map((ph) => {
|
||||
const updatedPages = (ph?.placeHolder ?? []).map((page) => {
|
||||
const updatedPos = (page?.pos ?? []).map((widgetDetails) => {
|
||||
const isImageType = ALL_EXCLUDED_TYPES.has(widgetDetails?.type);
|
||||
return {
|
||||
...widgetDetails,
|
||||
options: {
|
||||
...widgetDetails.options,
|
||||
// update response; if you DON'T want undefined to overwrite, keep the ?? fallback
|
||||
response: isImageType ? "" : widgetDetails.options?.response,
|
||||
defaultValue: isImageType
|
||||
? ""
|
||||
: widgetDetails.options?.defaultValue
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return { ...page, pos: updatedPos };
|
||||
});
|
||||
|
||||
return { ...ph, placeHolder: updatedPages };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dispatch(setBulkLoader(true));
|
||||
if (!forms.length) {
|
||||
alert(t("bulk-send-no-records"));
|
||||
dispatch(setBulkLoader(false));
|
||||
return;
|
||||
}
|
||||
if (validateEmails(forms)) {
|
||||
// Create a copy of Placeholders array from props.item
|
||||
let Placeholders = [...props.Placeholders];
|
||||
|
||||
let pdfUrl = props.item?.URL || props.item?.SignedUrl;
|
||||
const prefillExist = Placeholders?.some(
|
||||
(data) => data.Role === "prefill"
|
||||
);
|
||||
if (prefillExist) {
|
||||
const updatedPrefills = updateWidgetValues(
|
||||
prefillWidgets,
|
||||
Placeholders
|
||||
);
|
||||
const prefillDetails = updatedPrefills?.find(
|
||||
(data) => data.Role === "prefill"
|
||||
);
|
||||
const pdfSignedUrl = await getSignedUrl(
|
||||
pdfUrl,
|
||||
"", //docId
|
||||
props.item.objectId, // templateId
|
||||
);
|
||||
const pdfArrayBuffer = await loadPdfOnce(pdfSignedUrl);
|
||||
if (pdfArrayBuffer === "Error") {
|
||||
const error = t("something-went-wrong-mssg");
|
||||
alert(error);
|
||||
dispatch(setBulkLoader(false));
|
||||
return;
|
||||
}
|
||||
pdfUrl = await handleEmbedPrefillToDoc(
|
||||
prefillDetails,
|
||||
1, // scale
|
||||
pdfArrayBuffer,
|
||||
[], // prefillImg,
|
||||
props.item?.ExtUserPtr?.UserId?.objectId // userId
|
||||
);
|
||||
|
||||
if (pdfUrl?.error) {
|
||||
const error = pdfUrl?.error?.includes("not compatible")
|
||||
? t("pdf-uncompatible", { appName: appName })
|
||||
: pdfUrl?.error;
|
||||
alert(error);
|
||||
dispatch(setBulkLoader(false));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize an empty array to store updated documents
|
||||
let Documents = [];
|
||||
let error = "";
|
||||
// Loop through each form
|
||||
for (const form of forms) {
|
||||
//checking if user enter email which already exist as a signer then add user in a signers array
|
||||
let existSigner = [];
|
||||
form.fields.map((data) => {
|
||||
if (data.signer) {
|
||||
existSigner.push(data.signer);
|
||||
}
|
||||
});
|
||||
// Map through the copied Placeholders array to update email values
|
||||
const updatedPlaceholders = Placeholders.map((placeholder) => {
|
||||
// Find the field in the current form that matches the placeholder Id
|
||||
const field = form.fields.find(
|
||||
(element) => parseInt(element.fieldId) === placeholder.Id
|
||||
);
|
||||
// If a matching field is found, update the email value in the placeholder
|
||||
const signer = field?.signer?.objectId ? field.signer : "";
|
||||
if (field) {
|
||||
if (signer) {
|
||||
return {
|
||||
...placeholder,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...placeholder,
|
||||
email: normalizeKey(field.email),
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
}
|
||||
}
|
||||
// If no matching field is found, keep the placeholder as is
|
||||
return placeholder;
|
||||
});
|
||||
const widgetValues = form?.fields?.flatMap((f) => f.widgets);
|
||||
const updateWidgetPlaceholder = updateWidgetValues(
|
||||
widgetValues,
|
||||
updatedPlaceholders
|
||||
);
|
||||
let signedUrl = pdfUrl;
|
||||
const placeholders = removeWidgetValues(
|
||||
widgetValues,
|
||||
updateWidgetPlaceholder
|
||||
);
|
||||
if (!signedUrl) {
|
||||
error = t("something-went-wrong-mssg");
|
||||
dispatch(setBulkLoader(false));
|
||||
break;
|
||||
}
|
||||
// Push a new document object with updated Placeholders into the Documents array
|
||||
if (existSigner?.length > 0) {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
URL: signedUrl,
|
||||
Placeholders: placeholders,
|
||||
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
|
||||
});
|
||||
} else {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
URL: signedUrl,
|
||||
Placeholders: placeholders,
|
||||
SignatureType: props.signatureType,
|
||||
Signers: signers
|
||||
});
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
alert(error);
|
||||
} else {
|
||||
await batchQuery(Documents);
|
||||
}
|
||||
} else {
|
||||
dispatch(setBulkLoader(false));
|
||||
}
|
||||
};
|
||||
|
||||
const batchQuery = async (Documents) => {
|
||||
const functionsUrl = `${localStorage.getItem("baseUrl")}functions/batchdocuments`;
|
||||
const headers = {
|
||||
...tokenHeader,
|
||||
};
|
||||
const params = { Documents: JSON.stringify(Documents) };
|
||||
try {
|
||||
const res = await axios.post(functionsUrl, params, { headers: headers });
|
||||
if (res.data && res.data.result) {
|
||||
setStep((s) => Math.min(2, s + 1));
|
||||
setStepKey("response");
|
||||
setMessage({
|
||||
status: "success",
|
||||
message: "Documents sent successfully."
|
||||
});
|
||||
// props.handleClose("success", Documents?.length);
|
||||
}
|
||||
} catch (err) {
|
||||
const message =
|
||||
err?.response?.data?.error || err?.message || "something went wrong.";
|
||||
console.error("Error sending documents:", message);
|
||||
setMessage({ status: "failed", message });
|
||||
// props.handleClose("error", 0, message);
|
||||
} finally {
|
||||
dispatch(setBulkLoader(false));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (isLoader) {
|
||||
return (
|
||||
<div className="w-full h-[100px] flex justify-center items-center z-[999]">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{props.Placeholders?.length > 0 ? (
|
||||
isSignatureExist ? (
|
||||
isVacantRoles ? (
|
||||
<>
|
||||
<div className="border-t mt-3" />
|
||||
{/* Wizard header */}
|
||||
<div className="px-6 py-3">
|
||||
<WizardHeader
|
||||
steps={stepsList}
|
||||
step={step}
|
||||
// onStepClick={(i) => i <= step && setStep(i)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{stepKey === "prefill" && (
|
||||
<PrefillWidgets
|
||||
prefills={prefillWidgets}
|
||||
setPrefills={setPrefillWidgets}
|
||||
onNext={() => {
|
||||
setStep((s) => Math.min(2, s + 1));
|
||||
setStepKey("recipients");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{stepKey === "recipients" && (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="min-h-max max-h-[250px] overflow-auto">
|
||||
{
|
||||
forms?.length > 0 && (
|
||||
<div className="mx-4">
|
||||
<Table
|
||||
headers={headers}
|
||||
rowData={forms}
|
||||
handleInputChange={handleInputChange}
|
||||
handleWidgetDetails={handleWidgetDetails}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap pb-3 pt-2 px-3 gap-3 justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-accent w-[150px] focus:outline-none"
|
||||
>
|
||||
<i className="fa-light fa-paper-plane"></i>
|
||||
<span>{t("send")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
{stepKey === "response" && (
|
||||
<ResponseTab
|
||||
prefillCount={prefillWidgets?.length}
|
||||
documentCount={forms?.length}
|
||||
message={message}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-1")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-2")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-3")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkSendUi;
|
||||
@@ -0,0 +1,62 @@
|
||||
import RenderWidgets from "./RenderWidgets";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const PrefillWidgets = ({ prefills = [], setPrefills, onNext }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleWidgetDetails = (value, widgetIndex) => {
|
||||
setPrefills((prev) => {
|
||||
const widgets = [...(prev ?? [])];
|
||||
const w = widgets[widgetIndex];
|
||||
if (!w) return prev;
|
||||
widgets[widgetIndex] = {
|
||||
...w,
|
||||
options: { ...w.options, response: value },
|
||||
response: value
|
||||
};
|
||||
return widgets;
|
||||
});
|
||||
};
|
||||
|
||||
const handleNext = (e) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
onNext?.();
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{prefills?.length > 0 && (
|
||||
<form
|
||||
onSubmit={handleNext}
|
||||
className="m-3 md:m-6 text-base-content flex flex-col relative"
|
||||
>
|
||||
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-4 w-full">
|
||||
{prefills.map((widget, index) => (
|
||||
<RenderWidgets
|
||||
key={widget.key}
|
||||
showLabel
|
||||
widget={widget}
|
||||
handleWidgetDetails={(value) =>
|
||||
handleWidgetDetails(value, index)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row flex-wrap mt-3 gap-3 justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-primary w-[150px] focus:outline-none"
|
||||
>
|
||||
{t("next")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrefillWidgets;
|
||||
@@ -0,0 +1,646 @@
|
||||
import { forwardRef, memo, useEffect, useMemo, useRef, useState } from "react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import SignatureCanvas from "react-signature-canvas";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import moment from "moment";
|
||||
import {
|
||||
changeDateToMomentFormat,
|
||||
compressedFileSize,
|
||||
generateId,
|
||||
getMonth,
|
||||
getYear,
|
||||
months,
|
||||
radioButtonWidget,
|
||||
textInputWidget,
|
||||
textWidget,
|
||||
years
|
||||
} from "../../../constant/Utils";
|
||||
import PenColorComponent from "../../pdf/tab/PenColorComponent";
|
||||
import { getDatePickerDate, toHtmlPattern } from "../../../utils";
|
||||
import CellsInput from "../../shared/fields/CellsInput";
|
||||
import { emailRegex } from "../../../constant/const";
|
||||
|
||||
const inputOpt = new Set(["text", "email", "number"]);
|
||||
const PREFILL_PREFIX = "prefill::";
|
||||
|
||||
const inputTypes = {
|
||||
email: emailRegex,
|
||||
number: /^[0-9\s]*$/,
|
||||
ssn: /^(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}$/
|
||||
};
|
||||
const inputValidation = (pattern, type) => {
|
||||
if (pattern) return pattern;
|
||||
return inputTypes[type] || "";
|
||||
};
|
||||
const canavasTheme = `opensigncss:bg-white opensigndark:bg-[#121212] opensigndark:border-[#f6f3f4]/20 opensigncss:border-gray-300 opensigndark:hover:border-white opensigncss:hover:border-black border-[1px]`;
|
||||
|
||||
const widgetLabelCss = (isRequired = false) => {
|
||||
return `${isRequired ? "after:content-['_*'] after:text-red-500" : ""} block text-xs font-semibold`;
|
||||
};
|
||||
|
||||
const DateWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const format = widget?.options?.validation?.format || "MM/dd/yyyy";
|
||||
|
||||
const PrefillDateInput = forwardRef(({ value, onClick }, ref) => (
|
||||
<div
|
||||
style={{ fontFamily: "Arial, sans-serif" }}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full"
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
{value ? value : format}
|
||||
<i className="fa-light fa-calendar ml-[5px]"></i>
|
||||
</div>
|
||||
));
|
||||
PrefillDateInput.displayName = "PrefillDateInput";
|
||||
|
||||
const handleDate = (widget) => {
|
||||
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
|
||||
try {
|
||||
return getDatePickerDate(widget?.response, format);
|
||||
} catch (err) {
|
||||
console.error("handleDate error ", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
//function to set date with required date format onchange date
|
||||
const handleOnDateChange = (date) => {
|
||||
let updateDate = date;
|
||||
let newDate;
|
||||
const isSpecialDateFormat =
|
||||
format && ["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(format);
|
||||
if (isSpecialDateFormat) {
|
||||
newDate = moment(updateDate).format(changeDateToMomentFormat(format));
|
||||
} else {
|
||||
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
|
||||
newDate = new Date(updateDate);
|
||||
newDate = moment(newDate.getTime()).format(
|
||||
changeDateToMomentFormat(format)
|
||||
);
|
||||
}
|
||||
onChange(newDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-max">
|
||||
<DatePicker
|
||||
portalId="datepicker-portal-root"
|
||||
id={`widget-${widget?.options?.name}-${widget.key}`}
|
||||
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||
<div className="flex justify-start ml-2">
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={months[getMonth(date)]}
|
||||
onChange={({ target: { value } }) =>
|
||||
changeMonth(months.indexOf(value))
|
||||
}
|
||||
>
|
||||
{months.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={getYear(date)}
|
||||
onChange={({ target: { value } }) => changeYear(value)}
|
||||
>
|
||||
{years.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
wrapperClassName="w-full"
|
||||
closeOnScroll={true}
|
||||
selected={handleDate(widget)}
|
||||
onChange={(date) => handleOnDateChange(date, widget)}
|
||||
customInput={<PrefillDateInput />}
|
||||
dateFormat={widget?.options?.validation?.format || "MM/dd/yyyy"}
|
||||
required={isRequired}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const TextWidget = ({
|
||||
widget,
|
||||
isRequired,
|
||||
onChange,
|
||||
showLabel,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const inputType = widget?.options?.validation?.type || "";
|
||||
const serverRegex = widget?.options?.validation?.pattern;
|
||||
const isPredfineType = inputType ? inputOpt?.has(inputType) : "";
|
||||
const regExpression = inputValidation(serverRegex, inputType);
|
||||
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
|
||||
const [value, setValue] = useState(() => {
|
||||
const response =
|
||||
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
|
||||
return inputType === "number" && response ? Number(response) : response;
|
||||
});
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const text = e.target.value || "";
|
||||
setValue(text);
|
||||
onChange(text);
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-max">
|
||||
<input
|
||||
type={isPredfineType ? inputType : "text"}
|
||||
id={`widget-${widget?.options?.name}-${widget.key}`}
|
||||
value={value}
|
||||
placeholder={
|
||||
widget?.options?.hint ||
|
||||
t("enter-value", { value: widget?.options?.name })
|
||||
}
|
||||
className={[
|
||||
"op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs",
|
||||
].join(" ")}
|
||||
onChange={(e) => handleInputChange(e)}
|
||||
pattern={pattern || undefined} // if no pattern, browser won't do pattern validation
|
||||
onInvalid={(e) => {
|
||||
const el = e.currentTarget;
|
||||
// ✅ Only override message if pattern exists AND the error is patternMismatch
|
||||
if (pattern && el.validity.patternMismatch) {
|
||||
el.setCustomValidity(t("validation-alert-1"));
|
||||
} else {
|
||||
el.setCustomValidity("");
|
||||
}
|
||||
}}
|
||||
onInput={(e) => {
|
||||
e.currentTarget.setCustomValidity("");
|
||||
}}
|
||||
required={isRequired}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const CheckboxWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedCheckbox, setSelectedCheckbox] = useState(
|
||||
() => widget?.options?.response || widget?.options?.defaultValue || []
|
||||
);
|
||||
const groupName = `checkbox-group-${widget.key}`;
|
||||
const noneSelected = (selectedCheckbox?.length ?? 0) === 0;
|
||||
|
||||
const handleCheckboxValue = (isChecked, ind) => {
|
||||
// ✅ clear hidden input error immediately
|
||||
const hidden = document.querySelector(`input[name="${groupName}"]`);
|
||||
hidden?.setCustomValidity("");
|
||||
|
||||
const prevArr = Array.isArray(selectedCheckbox) ? selectedCheckbox : [];
|
||||
|
||||
const updateResponse = isChecked
|
||||
? prevArr.includes(ind)
|
||||
? prevArr
|
||||
: [...prevArr, ind] // add once
|
||||
: prevArr.filter((v) => v !== ind); // remove
|
||||
|
||||
setSelectedCheckbox(updateResponse);
|
||||
onChange(updateResponse);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-y-1 min-w-max relative">
|
||||
{widget.options?.values?.map((value, ind) => (
|
||||
<div
|
||||
key={ind}
|
||||
className="select-none-cls flex items-center text-center gap-0.5"
|
||||
>
|
||||
<input
|
||||
id={`checkbox-${widget.key + ind}`}
|
||||
className="mt-[2px] op-checkbox op-checkbox-xs"
|
||||
type="checkbox"
|
||||
checked={selectedCheckbox.includes(ind)}
|
||||
onChange={(e) => handleCheckboxValue(e.target.checked, ind)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`checkbox-${widget.key + ind}`}
|
||||
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
|
||||
>
|
||||
{value}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
{/* ✅ the validator */}
|
||||
{isRequired && (
|
||||
<input
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
className="absolute opacity-0 pointer-events-none w-0.5 h-0.5 left-[0.46rem] top-4"
|
||||
name={groupName}
|
||||
value={noneSelected ? "" : "selected"} // empty => invalid, non-empty => valid
|
||||
required
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("select-at-least-option"))
|
||||
}
|
||||
onChange={(e) => e.target.setCustomValidity("")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const DropdownWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const { t } = useTranslation();
|
||||
const [selected, setSelected] = useState(
|
||||
() =>
|
||||
widget?.options?.response?.trim() ||
|
||||
widget?.options?.defaultValue?.trim() ||
|
||||
""
|
||||
);
|
||||
|
||||
const handleDropdownChange = (value) => {
|
||||
const select = value?.trim();
|
||||
setSelected(select);
|
||||
onChange(select);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-max">
|
||||
<select
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-base-content w-full"
|
||||
id={`widget-${widget?.options?.name}-${widget?.key}`}
|
||||
value={selected}
|
||||
onChange={(e) => handleDropdownChange(e.target.value)}
|
||||
required={isRequired}
|
||||
>
|
||||
{/* Default/Title option */}
|
||||
<option value="" disabled hidden>
|
||||
{t("choose-one")}
|
||||
</option>
|
||||
{widget?.options?.values?.map((option, ind) => (
|
||||
<option key={ind} value={option?.trim()}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const RadioButtonWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const [selected, setSelected] = useState(
|
||||
() =>
|
||||
widget?.options?.response?.trim() ||
|
||||
widget?.options?.defaultValue?.trim() ||
|
||||
""
|
||||
);
|
||||
const id = generateId(4);
|
||||
|
||||
const handleRadioChange = (value) => {
|
||||
const select = value?.trim();
|
||||
setSelected(select);
|
||||
onChange(select);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-y-1 min-w-max">
|
||||
{widget.options?.values?.map((data, ind) => (
|
||||
<div
|
||||
key={ind}
|
||||
className="select-none-cls flex items-center text-center gap-0.5"
|
||||
>
|
||||
<input
|
||||
id={`radio-${widget.key + ind}`}
|
||||
className="mt-[2px] op-radio op-radio-xs"
|
||||
name={`radio-group-${widget.key}-${id}`} // ✅ same name => browser treats all radios as ONE group
|
||||
type="radio"
|
||||
required={isRequired && ind === 0} // ✅ set required ONCE (on first radio) to make the whole group mandatory
|
||||
checked={selected === data?.trim()}
|
||||
onChange={() => handleRadioChange(data)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`radio-${widget.key + ind}`}
|
||||
className="text-xs mb-0 ml-[2px] cursor-pointer"
|
||||
>
|
||||
{data}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ImageWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const { t } = useTranslation();
|
||||
const imageRef = useRef(null);
|
||||
const [img, setImg] = useState({ src: "", imgType: "" });
|
||||
const [defaultImg, setDefaultImg] = useState({ src: "", imgType: "" });
|
||||
|
||||
useEffect(() => {
|
||||
if (widget?.response) {
|
||||
setDefaultImg({ src: widget?.response, imgType: "" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onImageChange = (e) => {
|
||||
if (e.target.files && e.target.files?.[0]) {
|
||||
const file = e.target.files?.[0];
|
||||
compressedFileSize(file, ({ src, imgType }) => {
|
||||
onChange(src);
|
||||
setImg({ src, imgType });
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleClearImage = () => {
|
||||
setDefaultImg({ src: "", imgType: "" });
|
||||
setImg({ src: "", imgType: "" });
|
||||
onChange("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col prefillCanvas">
|
||||
{defaultImg?.src || img?.src ? (
|
||||
<div
|
||||
className={`cursor-pointer rounded-box ${canavasTheme} flex flex-col w-full h-full justify-center items-center`}
|
||||
>
|
||||
<img
|
||||
alt={`image_${widget?.options?.name}`}
|
||||
src={img.src || defaultImg?.src}
|
||||
draggable="false"
|
||||
className="object-contain h-full w-full aspect-[5/2]"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`cursor-pointer rounded-box ${canavasTheme} overflow-hidden w-full h-full aspect-[5/2] flex flex-col justify-center items-center`}
|
||||
onClick={() => imageRef.current?.click()}
|
||||
>
|
||||
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
|
||||
<div className="text-[10px] text-base-content">{t("upload")}</div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => onImageChange?.(e, widget)}
|
||||
className="filetype w-[1px] h-[1px] opacity-0"
|
||||
accept="image/png,image/jpeg"
|
||||
ref={imageRef} // Assign ref dynamically
|
||||
required={isRequired}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(defaultImg?.src || img?.src) && (
|
||||
<span
|
||||
onClick={handleClearImage}
|
||||
className="flex justify-start text-blue-500 underline cursor-pointer ml-1"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const DrawWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const { t } = useTranslation();
|
||||
const canvasRef = useRef(null);
|
||||
const sigRequiredRef = useRef(null);
|
||||
const [hasDraw, setHasDraw] = useState("");
|
||||
const [penColor, setPenColor] = useState("blue");
|
||||
const [image, setImage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (widget?.response) {
|
||||
setImage(widget?.response);
|
||||
setHasDraw("signed");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSignatureChange = () => {
|
||||
const draw = canvasRef.current?.isEmpty?.() ? "" : "signed";
|
||||
setHasDraw(draw);
|
||||
onChange(canvasRef.current.toDataURL());
|
||||
|
||||
// clear custom error once user signs
|
||||
sigRequiredRef.current?.setCustomValidity("");
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setImage("");
|
||||
if (canvasRef?.current) {
|
||||
canvasRef.current.clear();
|
||||
onChange("");
|
||||
setHasDraw("");
|
||||
// clear custom error once user signs
|
||||
sigRequiredRef.current?.setCustomValidity("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="relative w-fit">
|
||||
{image ? (
|
||||
<div className={`prefillCanvas ${canavasTheme} rounded-[10px]`}>
|
||||
<img
|
||||
alt={`draw_${widget?.options?.name}`}
|
||||
src={image}
|
||||
className="prefillCanvas object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<SignatureCanvas
|
||||
ref={canvasRef}
|
||||
penColor={penColor}
|
||||
canvasProps={{
|
||||
className: `prefillCanvas ${canavasTheme} rounded-[10px]`
|
||||
}}
|
||||
onEnd={() => handleSignatureChange()}
|
||||
dotSize={1}
|
||||
/>
|
||||
)}
|
||||
{/* ✅ Native form validation hook */}
|
||||
<input
|
||||
ref={sigRequiredRef}
|
||||
type="text"
|
||||
value={hasDraw}
|
||||
required={isRequired}
|
||||
tabIndex={-1}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("draw-required"))}
|
||||
onChange={() => {}}
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 w-[1px] h-[1px] pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between mt-[8px] w-[200px]">
|
||||
<PenColorComponent
|
||||
penColor={penColor}
|
||||
setPenColor={setPenColor}
|
||||
hideLabel
|
||||
penSize="sm"
|
||||
/>
|
||||
<span
|
||||
onClick={handleClear}
|
||||
className="flex justify-start text-blue-500 underline cursor-pointer"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CellsWidget = ({
|
||||
widget,
|
||||
isRequired,
|
||||
onChange,
|
||||
showLabel,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const count = widget?.options?.cellCount || 1;
|
||||
const [word, setWord] = useState("");
|
||||
const inputType = widget?.options?.validation?.type || "";
|
||||
const serverRegex = widget?.options?.validation?.pattern;
|
||||
const regExpression = inputValidation(serverRegex, inputType);
|
||||
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
|
||||
|
||||
useEffect(() => {
|
||||
const response =
|
||||
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
|
||||
const isNumber = inputType === "number" && response;
|
||||
setWord(isNumber ? Number(response) : response);
|
||||
}, []);
|
||||
|
||||
const handleChange = (words) => {
|
||||
const value = words;
|
||||
setWord(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="relative">
|
||||
<CellsInput
|
||||
hint={
|
||||
widget?.options?.hint ||
|
||||
t("enter-value", { value: widget?.options?.name })
|
||||
}
|
||||
count={count}
|
||||
value={word}
|
||||
onChange={handleChange}
|
||||
only="all"
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
id={`widget-${widget?.options?.name}-${widget.key}`}
|
||||
value={word}
|
||||
required={isRequired}
|
||||
tabIndex={-1}
|
||||
onChange={() => {}}
|
||||
pattern={pattern || undefined} // if no pattern, browser won't do pattern validation
|
||||
onInvalid={(e) => {
|
||||
const el = e.currentTarget;
|
||||
// ✅ Only override message if pattern exists AND the error is patternMismatch
|
||||
if (pattern && el.validity.patternMismatch) {
|
||||
el.setCustomValidity(t("validation-alert-1"));
|
||||
} else {
|
||||
el.setCustomValidity("");
|
||||
}
|
||||
}}
|
||||
onInput={(e) => {
|
||||
e.currentTarget.setCustomValidity("");
|
||||
}}
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 w-[1px] h-[1px] pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RenderWidgets = ({
|
||||
widget,
|
||||
handleWidgetDetails,
|
||||
showLabel = false,
|
||||
}) => {
|
||||
const label = widget?.label?.toLowerCase() ?? "";
|
||||
const isPrefill = label.includes(PREFILL_PREFIX);
|
||||
const commonProps = {
|
||||
widget,
|
||||
isRequired: widget.options?.status === "required" && isPrefill,
|
||||
showLabel: showLabel,
|
||||
onChange: handleWidgetDetails,
|
||||
};
|
||||
|
||||
switch (widget.type) {
|
||||
case "date":
|
||||
return <DateWidget {...commonProps} />;
|
||||
case textWidget:
|
||||
case textInputWidget:
|
||||
case "name":
|
||||
case "email":
|
||||
case "company":
|
||||
case "job title":
|
||||
case "number":
|
||||
return <TextWidget {...commonProps} />;
|
||||
case "checkbox":
|
||||
return <CheckboxWidget {...commonProps} />;
|
||||
case radioButtonWidget:
|
||||
return <RadioButtonWidget {...commonProps} />;
|
||||
case "dropdown":
|
||||
return <DropdownWidget {...commonProps} />;
|
||||
case "image":
|
||||
case "stamp":
|
||||
return <ImageWidget {...commonProps} />;
|
||||
case "initials":
|
||||
case "draw":
|
||||
return <DrawWidget {...commonProps} />;
|
||||
case "cells":
|
||||
return <CellsWidget {...commonProps} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(RenderWidgets);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function ResponseTab({
|
||||
prefillCount = 0,
|
||||
documentCount = 0,
|
||||
message = { status: "", message: "" }
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { status, message: msg } = message;
|
||||
return (
|
||||
<div className="rounded-box border p-4 m-3 md:m-6 ">
|
||||
<div className="font-medium">{t("summary")}</div>
|
||||
<ul className="mt-2 list-disc pl-5 text-base-content/80">
|
||||
<li>
|
||||
{t("prefill-fields")}: {prefillCount}
|
||||
</li>
|
||||
<li>
|
||||
{t("documents")}: {documentCount}
|
||||
</li>
|
||||
<li>
|
||||
{t("status")}: {status}
|
||||
</li>
|
||||
{msg && (
|
||||
<li>
|
||||
{t("message")}: {msg}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResponseTab;
|
||||
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
useState
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SuggestionInput from "../../shared/fields/SuggestionInput";
|
||||
import RenderWidgets from "./RenderWidgets";
|
||||
|
||||
const requiredAsteriskCls = (isRequired = false) => {
|
||||
return isRequired ? "after:content-['_*'] after:text-red-500" : "";
|
||||
};
|
||||
const Table = ({
|
||||
headers = [],
|
||||
rowData = [],
|
||||
handleInputChange,
|
||||
handleWidgetDetails,
|
||||
initialPerPage = 25,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(initialPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const currentData = rowData.slice(startIndex, endIndex);
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
|
||||
{/* Table */}
|
||||
<div
|
||||
className="overflow-x-auto border border-gray-300 rounded-lg"
|
||||
>
|
||||
<table className="op-table border-collapse w-full">
|
||||
<thead className="text-[13px] text-center">
|
||||
<tr className="border-y-[1px]">
|
||||
{headers?.map((header) => (
|
||||
<th
|
||||
key={header.label}
|
||||
className={`${requiredAsteriskCls(header?.isRequired)} p-2`}
|
||||
>
|
||||
{header.label}
|
||||
</th>
|
||||
))}
|
||||
{rowData.length > 1 && <th className="p-2">{t("action")}</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentData.length > 0 &&
|
||||
currentData.map((form, formIndex) => {
|
||||
const globalIndex = startIndex + formIndex;
|
||||
const fields = form?.fields ?? [];
|
||||
|
||||
const emailFields = fields.filter((f) => f.label !== "prefill");
|
||||
|
||||
const widgets = fields.flatMap((f, fieldIndex) =>
|
||||
(f.widgets ?? []).map((widget, widgetIndex) => ({
|
||||
widget,
|
||||
fieldIndex, // keep the fieldIndex that produced this widget
|
||||
widgetIndex
|
||||
}))
|
||||
);
|
||||
return (
|
||||
<tr key={form.Id} id={`table-row-${formIndex}`}>
|
||||
{emailFields.map((field, fieldIndex) => (
|
||||
<td key={field.fieldId} className="p-2">
|
||||
<SuggestionInput
|
||||
required
|
||||
type="email"
|
||||
value={field.email ?? ""}
|
||||
index={fieldIndex}
|
||||
onChange={(signer) =>
|
||||
handleInputChange(globalIndex, signer, field.label)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
|
||||
{widgets.map(({ widget, fieldIndex, widgetIndex }) => (
|
||||
<td key={`${form.Id}-${widget.key}`} className="p-2">
|
||||
<RenderWidgets
|
||||
widget={widget}
|
||||
handleWidgetDetails={(value) =>
|
||||
handleWidgetDetails(
|
||||
value,
|
||||
globalIndex, // formIndex,
|
||||
fieldIndex,
|
||||
widgetIndex
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -0,0 +1,74 @@
|
||||
/* ---------------- Wizard Header ---------------- */
|
||||
|
||||
import React from "react";
|
||||
|
||||
function WizardHeader({ steps, step, onStepClick }) {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row flex-wrap items-center justify-center gap-0 md:gap-2">
|
||||
{steps.map((s, i) => {
|
||||
const isActive = i === step;
|
||||
const isDone = i < step;
|
||||
|
||||
return (
|
||||
<React.Fragment key={s.key}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onStepClick?.(i)}
|
||||
className={[
|
||||
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-xs lg:text-sm font-medium border focus:outline-none",
|
||||
isActive
|
||||
? "op-bg-primary text-primary-content border-primary"
|
||||
: isDone
|
||||
? "bg-base-100 text-base-content border-base-300 hover:bg-base-200"
|
||||
: "bg-base-100 text-base-content/70 border-base-300"
|
||||
].join(" ")}
|
||||
title={s.label}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
"inline-flex h-5 w-5 items-center justify-center rounded-full text-xs",
|
||||
isActive
|
||||
? "op-bg-primary-content/15 text-primary-content"
|
||||
: isDone
|
||||
? "bg-base-200 text-base-content"
|
||||
: "bg-base-200 text-base-content/70"
|
||||
].join(" ")}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
|
||||
<span className="whitespace-nowrap">{s.label}</span>
|
||||
|
||||
{/* info icon like screenshot */}
|
||||
<a data-tooltip-id={`${s.key}-tooltip`} className="ml-1">
|
||||
<span
|
||||
className={[
|
||||
"inline-flex h-[1.10rem] w-[1.10rem] items-center justify-center border-[1px] rounded-full",
|
||||
!isActive
|
||||
? "border-[#33bbff] text-[#33bbff]"
|
||||
: "border border-base-300 text-base-300"
|
||||
].join(" ")}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<i className="fa-light fa-question rounded-full text-[13px]"></i>
|
||||
</span>
|
||||
</a>
|
||||
<span className="text-left">{s.help}</span>
|
||||
</button>
|
||||
|
||||
{i < steps.length - 1 && (
|
||||
<span
|
||||
className="mx-1 select-none text-base-content/40 text-center text-2xl md:mb-2 rotate-90 md:rotate-0"
|
||||
aria-hidden="true"
|
||||
>
|
||||
›
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WizardHeader;
|
||||
@@ -5,6 +5,7 @@ import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
|
||||
import { useNavigate } from "react-router";
|
||||
import Tooltip from "../../primitives/Tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
|
||||
const DashboardCard = (props) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -14,7 +15,7 @@ const DashboardCard = (props) => {
|
||||
const [response, setresponse] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const renderData = async () => {
|
||||
const renderData = withSessionValidation(async () => {
|
||||
if (props.Data.queryType === "function") {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -186,9 +187,9 @@ const DashboardCard = (props) => {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const filterRender = async () => {
|
||||
const filterRender = withSessionValidation(async () => {
|
||||
if (props.FilterData && props.FilterData.queryType === "function") {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -266,7 +267,7 @@ const DashboardCard = (props) => {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const setFormat = (val) => {
|
||||
switch (props.Format) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import reportJson from "../../json/ReportJson";
|
||||
import axios from "axios";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
function DashboardReport(props) {
|
||||
const { t } = useTranslation();
|
||||
const [List, setList] = useState([]);
|
||||
@@ -46,7 +47,7 @@ function DashboardReport(props) {
|
||||
// eslint-disable-next-line
|
||||
}, [isNextRecord]);
|
||||
|
||||
const handleSearchChange = async (e) => {
|
||||
const handleSearchChange = withSessionValidation(async (e) => {
|
||||
const term = e.target.value.toLowerCase();
|
||||
setSearchTerm(term);
|
||||
if (debounceTimer.current) {
|
||||
@@ -82,7 +83,7 @@ function DashboardReport(props) {
|
||||
}
|
||||
}, 300);
|
||||
setIsSearchResult(false);
|
||||
};
|
||||
});
|
||||
|
||||
const handleSearchPaste = (e) => {
|
||||
setTimeout(() => {
|
||||
@@ -98,96 +99,95 @@ function DashboardReport(props) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getReportData = async (
|
||||
id,
|
||||
skipUserRecord = 0,
|
||||
limit = 20,
|
||||
term = searchTerm
|
||||
) => {
|
||||
setIsLoader(true);
|
||||
const json = reportJson(id);
|
||||
if (json) {
|
||||
setActions(json.actions);
|
||||
setReportName(json.reportName);
|
||||
setHeading(json.heading);
|
||||
const currentUser = Parse.User.current().id;
|
||||
const getReportData = withSessionValidation(
|
||||
async (id, skipUserRecord = 0, limit = 20, term = searchTerm) => {
|
||||
setIsLoader(true);
|
||||
const json = reportJson(id);
|
||||
if (json) {
|
||||
setActions(json.actions);
|
||||
setReportName(json.reportName);
|
||||
setHeading(json.heading);
|
||||
const currentUser = Parse.User.current().id;
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
try {
|
||||
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
||||
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||
if (term) {
|
||||
params.searchTerm = term;
|
||||
}
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(url, params, {
|
||||
headers: headers,
|
||||
signal: abortController.signal // is used to cancel fetch query
|
||||
});
|
||||
if (id === "5Go51Q7T8r") {
|
||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
try {
|
||||
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
||||
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||
if (term) {
|
||||
params.searchTerm = term;
|
||||
}
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(url, params, {
|
||||
headers: headers,
|
||||
signal: abortController.signal // is used to cancel fetch query
|
||||
});
|
||||
if (id === "5Go51Q7T8r") {
|
||||
const listData = res.data?.result.filter(
|
||||
(x) => x.Signers.length > 0
|
||||
);
|
||||
if (isSigner) {
|
||||
let isRecord;
|
||||
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
||||
isRecord = obj?.AuditTrail.some(
|
||||
(item) =>
|
||||
item?.UserPtr?.UserId?.objectId === currentUser &&
|
||||
item.Activity === "Signed"
|
||||
);
|
||||
} else {
|
||||
isRecord = false;
|
||||
}
|
||||
if (isRecord === false) {
|
||||
arr.push(obj);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
);
|
||||
if (isSigner) {
|
||||
let isRecord;
|
||||
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
||||
isRecord = obj?.AuditTrail.some(
|
||||
(item) =>
|
||||
item?.UserPtr?.UserId?.objectId === currentUser &&
|
||||
item.Activity === "Signed"
|
||||
);
|
||||
} else {
|
||||
isRecord = false;
|
||||
}
|
||||
if (isRecord === false) {
|
||||
arr.push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (arr.length === docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||
);
|
||||
} else {
|
||||
if (res.data.result.length >= docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setIsNextRecord(false);
|
||||
if (!res.data.result.error) {
|
||||
if (arr.length === docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0
|
||||
? [...prevRecord, ...res.data.result]
|
||||
: res.data.result
|
||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||
);
|
||||
} else {
|
||||
if (res.data.result.length >= docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setIsNextRecord(false);
|
||||
if (!res.data.result.error) {
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0
|
||||
? [...prevRecord, ...res.data.result]
|
||||
: res.data.result
|
||||
);
|
||||
}
|
||||
}
|
||||
setIsLoader(false);
|
||||
} catch (err) {
|
||||
const isCancel = axios.isCancel(err);
|
||||
if (!isCancel) {
|
||||
console.log("err ", err);
|
||||
setIsLoader(false);
|
||||
}
|
||||
}
|
||||
setIsLoader(false);
|
||||
} catch (err) {
|
||||
const isCancel = axios.isCancel(err);
|
||||
if (!isCancel) {
|
||||
console.log("err ", err);
|
||||
setIsLoader(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
}
|
||||
};
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{isLoader ? (
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
import { TEditorBlock } from '../../../documents/editor/core';
|
||||
import { setDocument, useDocument, useSelectedBlockId } from '../../../documents/editor/EditorContext';
|
||||
|
||||
import AvatarSidebarPanel from './input-panels/AvatarSidebarPanel';
|
||||
import ButtonSidebarPanel from './input-panels/ButtonSidebarPanel';
|
||||
import ColumnsContainerSidebarPanel from './input-panels/ColumnsContainerSidebarPanel';
|
||||
import ContainerSidebarPanel from './input-panels/ContainerSidebarPanel';
|
||||
import DividerSidebarPanel from './input-panels/DividerSidebarPanel';
|
||||
import EmailLayoutSidebarPanel from './input-panels/EmailLayoutSidebarPanel';
|
||||
import HeadingSidebarPanel from './input-panels/HeadingSidebarPanel';
|
||||
import HtmlSidebarPanel from './input-panels/HtmlSidebarPanel';
|
||||
import ImageSidebarPanel from './input-panels/ImageSidebarPanel';
|
||||
import SpacerSidebarPanel from './input-panels/SpacerSidebarPanel';
|
||||
import TextSidebarPanel from './input-panels/TextSidebarPanel';
|
||||
|
||||
function renderMessage(val: string) {
|
||||
return (
|
||||
<Box sx={{ m: 3, p: 1, border: '1px dashed', borderColor: 'divider' }}>
|
||||
<Typography color="text.secondary">{val}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ConfigurationPanel() {
|
||||
const document = useDocument();
|
||||
const selectedBlockId = useSelectedBlockId();
|
||||
|
||||
if (!selectedBlockId) {
|
||||
return renderMessage('Click on a block to inspect.');
|
||||
}
|
||||
const block = document[selectedBlockId];
|
||||
if (!block) {
|
||||
return renderMessage(`Block with id ${selectedBlockId} was not found. Click on a block to reset.`);
|
||||
}
|
||||
|
||||
const setBlock = (conf: TEditorBlock) => setDocument({ [selectedBlockId]: conf });
|
||||
const { data, type } = block;
|
||||
switch (type) {
|
||||
case 'Avatar':
|
||||
return <AvatarSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Button':
|
||||
return <ButtonSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'ColumnsContainer':
|
||||
return (
|
||||
<ColumnsContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />
|
||||
);
|
||||
case 'Container':
|
||||
return <ContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Divider':
|
||||
return <DividerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Heading':
|
||||
return <HeadingSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Html':
|
||||
return <HtmlSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Image':
|
||||
return <ImageSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'EmailLayout':
|
||||
return <EmailLayoutSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Spacer':
|
||||
return <SpacerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Text':
|
||||
return <TextSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
default:
|
||||
return <pre>{JSON.stringify(block, null, ' ')}</pre>;
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { AspectRatioOutlined } from "@mui/icons-material";
|
||||
import { ToggleButton } from "@mui/material";
|
||||
import {
|
||||
AvatarProps,
|
||||
AvatarPropsDefaults,
|
||||
AvatarPropsSchema
|
||||
} from "@usewaypoint/block-avatar";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||
import SliderInput from "./helpers/inputs/SliderInput";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type AvatarSidebarPanelProps = {
|
||||
data: AvatarProps;
|
||||
setData: (v: AvatarProps) => void;
|
||||
};
|
||||
export default function AvatarSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: AvatarSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = AvatarPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
const size = data.props?.size ?? AvatarPropsDefaults.size;
|
||||
const imageUrl = data.props?.imageUrl ?? AvatarPropsDefaults.imageUrl;
|
||||
const alt = data.props?.alt ?? AvatarPropsDefaults.alt;
|
||||
const shape = data.props?.shape ?? AvatarPropsDefaults.shape;
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Avatar block">
|
||||
<SliderInput
|
||||
label="Size"
|
||||
iconLabel={<AspectRatioOutlined sx={{ color: "text.secondary" }} />}
|
||||
units="px"
|
||||
step={3}
|
||||
min={32}
|
||||
max={256}
|
||||
defaultValue={size}
|
||||
onChange={(size) => {
|
||||
updateData({ ...data, props: { ...data.props, size } });
|
||||
}}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Shape"
|
||||
defaultValue={shape}
|
||||
onChange={(shape) => {
|
||||
updateData({ ...data, props: { ...data.props, shape } });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="circle">Circle</ToggleButton>
|
||||
<ToggleButton value="square">Square</ToggleButton>
|
||||
<ToggleButton value="rounded">Rounded</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<TextInput
|
||||
label="Image URL"
|
||||
defaultValue={imageUrl}
|
||||
onChange={(imageUrl) => {
|
||||
updateData({ ...data, props: { ...data.props, imageUrl } });
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Alt text"
|
||||
defaultValue={alt}
|
||||
onChange={(alt) => {
|
||||
updateData({ ...data, props: { ...data.props, alt } });
|
||||
}}
|
||||
/>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={["textAlign", "padding"]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { ToggleButton } from "@mui/material";
|
||||
import {
|
||||
ButtonProps,
|
||||
ButtonPropsDefaults,
|
||||
ButtonPropsSchema
|
||||
} from "@usewaypoint/block-button";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import ColorInput from "./helpers/inputs/ColorInput";
|
||||
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type ButtonSidebarPanelProps = {
|
||||
data: ButtonProps;
|
||||
setData: (v: ButtonProps) => void;
|
||||
};
|
||||
export default function ButtonSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: ButtonSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ButtonPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
const text = data.props?.text ?? ButtonPropsDefaults.text;
|
||||
const url = data.props?.url ?? ButtonPropsDefaults.url;
|
||||
const fullWidth = data.props?.fullWidth ?? ButtonPropsDefaults.fullWidth;
|
||||
const size = data.props?.size ?? ButtonPropsDefaults.size;
|
||||
const buttonStyle =
|
||||
data.props?.buttonStyle ?? ButtonPropsDefaults.buttonStyle;
|
||||
const buttonTextColor =
|
||||
data.props?.buttonTextColor ?? ButtonPropsDefaults.buttonTextColor;
|
||||
const buttonBackgroundColor =
|
||||
data.props?.buttonBackgroundColor ??
|
||||
ButtonPropsDefaults.buttonBackgroundColor;
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Button block">
|
||||
<TextInput
|
||||
label="Text"
|
||||
defaultValue={text}
|
||||
onChange={(text) =>
|
||||
updateData({ ...data, props: { ...data.props, text } })
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="Url"
|
||||
defaultValue={url}
|
||||
onChange={(url) =>
|
||||
updateData({ ...data, props: { ...data.props, url } })
|
||||
}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Width"
|
||||
defaultValue={fullWidth ? "FULL_WIDTH" : "AUTO"}
|
||||
onChange={(v) =>
|
||||
updateData({
|
||||
...data,
|
||||
props: { ...data.props, fullWidth: v === "FULL_WIDTH" }
|
||||
})
|
||||
}
|
||||
>
|
||||
<ToggleButton value="FULL_WIDTH">Full</ToggleButton>
|
||||
<ToggleButton value="AUTO">Auto</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<RadioGroupInput
|
||||
label="Size"
|
||||
defaultValue={size}
|
||||
onChange={(size) =>
|
||||
updateData({ ...data, props: { ...data.props, size } })
|
||||
}
|
||||
>
|
||||
<ToggleButton value="x-small">Xs</ToggleButton>
|
||||
<ToggleButton value="small">Sm</ToggleButton>
|
||||
<ToggleButton value="medium">Md</ToggleButton>
|
||||
<ToggleButton value="large">Lg</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<RadioGroupInput
|
||||
label="Style"
|
||||
defaultValue={buttonStyle}
|
||||
onChange={(buttonStyle) =>
|
||||
updateData({ ...data, props: { ...data.props, buttonStyle } })
|
||||
}
|
||||
>
|
||||
<ToggleButton value="rectangle">Rectangle</ToggleButton>
|
||||
<ToggleButton value="rounded">Rounded</ToggleButton>
|
||||
<ToggleButton value="pill">Pill</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<ColorInput
|
||||
label="Text color"
|
||||
defaultValue={buttonTextColor}
|
||||
onChange={(buttonTextColor) =>
|
||||
updateData({ ...data, props: { ...data.props, buttonTextColor } })
|
||||
}
|
||||
/>
|
||||
<ColorInput
|
||||
label="Button color"
|
||||
defaultValue={buttonBackgroundColor}
|
||||
onChange={(buttonBackgroundColor) =>
|
||||
updateData({
|
||||
...data,
|
||||
props: { ...data.props, buttonBackgroundColor }
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MultiStylePropertyPanel
|
||||
names={[
|
||||
"backgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontWeight",
|
||||
"textAlign",
|
||||
"padding"
|
||||
]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import {
|
||||
SpaceBarOutlined,
|
||||
VerticalAlignBottomOutlined,
|
||||
VerticalAlignCenterOutlined,
|
||||
VerticalAlignTopOutlined
|
||||
} from "@mui/icons-material";
|
||||
import { ToggleButton } from "@mui/material";
|
||||
|
||||
import ColumnsContainerPropsSchema, {
|
||||
ColumnsContainerProps
|
||||
} from "../../../../documents/blocks/ColumnsContainer/ColumnsContainerPropsSchema";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import ColumnWidthsInput from "./helpers/inputs/ColumnWidthsInput";
|
||||
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||
import SliderInput from "./helpers/inputs/SliderInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type ColumnsContainerPanelProps = {
|
||||
data: ColumnsContainerProps;
|
||||
setData: (v: ColumnsContainerProps) => void;
|
||||
};
|
||||
export default function ColumnsContainerPanel({
|
||||
data,
|
||||
setData
|
||||
}: ColumnsContainerPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ColumnsContainerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Columns block">
|
||||
<RadioGroupInput
|
||||
label="Number of columns"
|
||||
defaultValue={data.props?.columnsCount === 2 ? "2" : "3"}
|
||||
onChange={(v) => {
|
||||
updateData({
|
||||
...data,
|
||||
props: { ...data.props, columnsCount: v === "2" ? 2 : 3 }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="2">2</ToggleButton>
|
||||
<ToggleButton value="3">3</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<ColumnWidthsInput
|
||||
defaultValue={data.props?.fixedWidths}
|
||||
onChange={(fixedWidths) => {
|
||||
updateData({ ...data, props: { ...data.props, fixedWidths } });
|
||||
}}
|
||||
/>
|
||||
<SliderInput
|
||||
label="Columns gap"
|
||||
iconLabel={<SpaceBarOutlined sx={{ color: "text.secondary" }} />}
|
||||
units="px"
|
||||
step={4}
|
||||
marks
|
||||
min={0}
|
||||
max={80}
|
||||
defaultValue={data.props?.columnsGap ?? 0}
|
||||
onChange={(columnsGap) =>
|
||||
updateData({ ...data, props: { ...data.props, columnsGap } })
|
||||
}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Alignment"
|
||||
defaultValue={data.props?.contentAlignment ?? "middle"}
|
||||
onChange={(contentAlignment) => {
|
||||
updateData({ ...data, props: { ...data.props, contentAlignment } });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="top">
|
||||
<VerticalAlignTopOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="middle">
|
||||
<VerticalAlignCenterOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="bottom">
|
||||
<VerticalAlignBottomOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={["backgroundColor", "padding"]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import React, { useState } from "react";
|
||||
import type { ZodError } from "zod";
|
||||
|
||||
import ContainerPropsSchema, {
|
||||
ContainerProps
|
||||
} from "../../../../documents/blocks/Container/ContainerPropsSchema";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type ContainerSidebarPanelProps = {
|
||||
data: ContainerProps;
|
||||
setData: (v: ContainerProps) => void;
|
||||
};
|
||||
|
||||
export default function ContainerSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: ContainerSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ContainerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<BaseSidebarPanel title="Container block">
|
||||
<MultiStylePropertyPanel
|
||||
names={["backgroundColor", "borderColor", "borderRadius", "padding"]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { HeightOutlined } from "@mui/icons-material";
|
||||
import {
|
||||
DividerProps,
|
||||
DividerPropsDefaults,
|
||||
DividerPropsSchema
|
||||
} from "@usewaypoint/block-divider";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import ColorInput from "./helpers/inputs/ColorInput";
|
||||
import SliderInput from "./helpers/inputs/SliderInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type DividerSidebarPanelProps = {
|
||||
data: DividerProps;
|
||||
setData: (v: DividerProps) => void;
|
||||
};
|
||||
export default function DividerSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: DividerSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = DividerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
const lineColor = data.props?.lineColor ?? DividerPropsDefaults.lineColor;
|
||||
const lineHeight = data.props?.lineHeight ?? DividerPropsDefaults.lineHeight;
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Divider block">
|
||||
<ColorInput
|
||||
label="Color"
|
||||
defaultValue={lineColor}
|
||||
onChange={(lineColor) =>
|
||||
updateData({ ...data, props: { ...data.props, lineColor } })
|
||||
}
|
||||
/>
|
||||
<SliderInput
|
||||
label="Height"
|
||||
iconLabel={<HeightOutlined sx={{ color: "text.secondary" }} />}
|
||||
units="px"
|
||||
step={1}
|
||||
min={1}
|
||||
max={24}
|
||||
defaultValue={lineHeight}
|
||||
onChange={(lineHeight) =>
|
||||
updateData({ ...data, props: { ...data.props, lineHeight } })
|
||||
}
|
||||
/>
|
||||
<MultiStylePropertyPanel
|
||||
names={["backgroundColor", "padding"]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { WidthWide, RoundedCornerOutlined } from "@mui/icons-material";
|
||||
import EmailLayoutPropsSchema, {
|
||||
EmailLayoutProps
|
||||
} from "../../../../documents/blocks/EmailLayout/EmailLayoutPropsSchema";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import ColorInput, { NullableColorInput } from "./helpers/inputs/ColorInput";
|
||||
import { NullableFontFamily } from "./helpers/inputs/FontFamily";
|
||||
import SliderInput from "./helpers/inputs/SliderInput";
|
||||
|
||||
type EmailLayoutSidebarFieldsProps = {
|
||||
data: EmailLayoutProps;
|
||||
setData: (v: EmailLayoutProps) => void;
|
||||
};
|
||||
export default function EmailLayoutSidebarFields({
|
||||
data,
|
||||
setData
|
||||
}: EmailLayoutSidebarFieldsProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = EmailLayoutPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Global">
|
||||
<ColorInput
|
||||
label="Backdrop color"
|
||||
defaultValue={data.backdropColor ?? "#F5F5F5"}
|
||||
onChange={(backdropColor) => updateData({ ...data, backdropColor })}
|
||||
/>
|
||||
<ColorInput
|
||||
label="Canvas color"
|
||||
defaultValue={data.canvasColor ?? "#FFFFFF"}
|
||||
onChange={(canvasColor) => updateData({ ...data, canvasColor })}
|
||||
/>
|
||||
<NullableColorInput
|
||||
label="Canvas border color"
|
||||
defaultValue={data.borderColor ?? null}
|
||||
onChange={(borderColor) => updateData({ ...data, borderColor })}
|
||||
/>
|
||||
<SliderInput
|
||||
iconLabel={<WidthWide />}
|
||||
units="px"
|
||||
step={10}
|
||||
min={320}
|
||||
max={1200}
|
||||
label="Canvas width"
|
||||
defaultValue={data.canvasWidth ?? 600}
|
||||
onChange={(canvasWidth) => updateData({ ...data, canvasWidth })}
|
||||
/>
|
||||
<SliderInput
|
||||
iconLabel={<RoundedCornerOutlined />}
|
||||
units="px"
|
||||
step={4}
|
||||
marks
|
||||
min={0}
|
||||
max={48}
|
||||
label="Canvas border radius"
|
||||
defaultValue={data.borderRadius ?? 0}
|
||||
onChange={(borderRadius) => updateData({ ...data, borderRadius })}
|
||||
/>
|
||||
<NullableFontFamily
|
||||
label="Font family"
|
||||
defaultValue="MODERN_SANS"
|
||||
onChange={(fontFamily) => updateData({ ...data, fontFamily })}
|
||||
/>
|
||||
<ColorInput
|
||||
label="Text color"
|
||||
defaultValue={data.textColor ?? "#262626"}
|
||||
onChange={(textColor) => updateData({ ...data, textColor })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { ToggleButton } from "@mui/material";
|
||||
import {
|
||||
HeadingProps,
|
||||
HeadingPropsDefaults,
|
||||
HeadingPropsSchema
|
||||
} from "@usewaypoint/block-heading";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type HeadingSidebarPanelProps = {
|
||||
data: HeadingProps;
|
||||
setData: (v: HeadingProps) => void;
|
||||
};
|
||||
export default function HeadingSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: HeadingSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = HeadingPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Heading block">
|
||||
<TextInput
|
||||
label="Content"
|
||||
rows={3}
|
||||
defaultValue={data.props?.text ?? HeadingPropsDefaults.text}
|
||||
onChange={(text) => {
|
||||
updateData({ ...data, props: { ...data.props, text } });
|
||||
}}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Level"
|
||||
defaultValue={data.props?.level ?? HeadingPropsDefaults.level}
|
||||
onChange={(level) => {
|
||||
updateData({ ...data, props: { ...data.props, level } });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="h1">H1</ToggleButton>
|
||||
<ToggleButton value="h2">H2</ToggleButton>
|
||||
<ToggleButton value="h3">H3</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<MultiStylePropertyPanel
|
||||
names={[
|
||||
"color",
|
||||
"backgroundColor",
|
||||
"fontFamily",
|
||||
"fontWeight",
|
||||
"textAlign",
|
||||
"padding"
|
||||
]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { HtmlProps, HtmlPropsSchema } from "@usewaypoint/block-html";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type HtmlSidebarPanelProps = {
|
||||
data: HtmlProps;
|
||||
setData: (v: HtmlProps) => void;
|
||||
};
|
||||
export default function HtmlSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: HtmlSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = HtmlPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Html block">
|
||||
<TextInput
|
||||
label="Content"
|
||||
rows={5}
|
||||
defaultValue={data.props?.contents ?? ""}
|
||||
onChange={(contents) =>
|
||||
updateData({ ...data, props: { ...data.props, contents } })
|
||||
}
|
||||
/>
|
||||
<MultiStylePropertyPanel
|
||||
names={[
|
||||
"color",
|
||||
"backgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"textAlign",
|
||||
"padding"
|
||||
]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import {
|
||||
VerticalAlignBottomOutlined,
|
||||
VerticalAlignCenterOutlined,
|
||||
VerticalAlignTopOutlined
|
||||
} from "@mui/icons-material";
|
||||
import { Stack, ToggleButton } from "@mui/material";
|
||||
import { ImageProps, ImagePropsSchema } from "@usewaypoint/block-image";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||
import TextDimensionInput from "./helpers/inputs/TextDimensionInput";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type ImageSidebarPanelProps = {
|
||||
data: ImageProps;
|
||||
setData: (v: ImageProps) => void;
|
||||
};
|
||||
export default function ImageSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: ImageSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ImagePropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Image block">
|
||||
<TextInput
|
||||
label="Source URL"
|
||||
defaultValue={data.props?.url ?? ""}
|
||||
onChange={(v) => {
|
||||
const url = v.trim().length === 0 ? null : v.trim();
|
||||
updateData({ ...data, props: { ...data.props, url } });
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Alt text"
|
||||
defaultValue={data.props?.alt ?? ""}
|
||||
onChange={(alt) =>
|
||||
updateData({ ...data, props: { ...data.props, alt } })
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="Click through URL"
|
||||
defaultValue={data.props?.linkHref ?? ""}
|
||||
onChange={(v) => {
|
||||
const linkHref = v.trim().length === 0 ? null : v.trim();
|
||||
updateData({ ...data, props: { ...data.props, linkHref } });
|
||||
}}
|
||||
/>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextDimensionInput
|
||||
label="Width"
|
||||
defaultValue={data.props?.width}
|
||||
onChange={(width) =>
|
||||
updateData({ ...data, props: { ...data.props, width } })
|
||||
}
|
||||
/>
|
||||
<TextDimensionInput
|
||||
label="Height"
|
||||
defaultValue={data.props?.height}
|
||||
onChange={(height) =>
|
||||
updateData({ ...data, props: { ...data.props, height } })
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<RadioGroupInput
|
||||
label="Alignment"
|
||||
defaultValue={data.props?.contentAlignment ?? "middle"}
|
||||
onChange={(contentAlignment) =>
|
||||
updateData({ ...data, props: { ...data.props, contentAlignment } })
|
||||
}
|
||||
>
|
||||
<ToggleButton value="top">
|
||||
<VerticalAlignTopOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="middle">
|
||||
<VerticalAlignCenterOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="bottom">
|
||||
<VerticalAlignBottomOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={["backgroundColor", "textAlign", "padding"]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { HeightOutlined } from "@mui/icons-material";
|
||||
import {
|
||||
SpacerProps,
|
||||
SpacerPropsDefaults,
|
||||
SpacerPropsSchema
|
||||
} from "@usewaypoint/block-spacer";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import SliderInput from "./helpers/inputs/SliderInput";
|
||||
|
||||
type SpacerSidebarPanelProps = {
|
||||
data: SpacerProps;
|
||||
setData: (v: SpacerProps) => void;
|
||||
};
|
||||
export default function SpacerSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: SpacerSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = SpacerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Spacer block">
|
||||
<SliderInput
|
||||
label="Height"
|
||||
iconLabel={<HeightOutlined sx={{ color: "text.secondary" }} />}
|
||||
units="px"
|
||||
step={4}
|
||||
min={4}
|
||||
max={128}
|
||||
defaultValue={data.props?.height ?? SpacerPropsDefaults.height}
|
||||
onChange={(height) =>
|
||||
updateData({ ...data, props: { ...data.props, height } })
|
||||
}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { TextProps, TextPropsSchema } from "@usewaypoint/block-text";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import BooleanInput from "./helpers/inputs/BooleanInput";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type TextSidebarPanelProps = {
|
||||
data: TextProps;
|
||||
setData: (v: TextProps) => void;
|
||||
};
|
||||
export default function TextSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: TextSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = TextPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Text block">
|
||||
<TextInput
|
||||
label="Content"
|
||||
rows={5}
|
||||
defaultValue={data.props?.text ?? ""}
|
||||
onChange={(text) =>
|
||||
updateData({ ...data, props: { ...data.props, text } })
|
||||
}
|
||||
/>
|
||||
<BooleanInput
|
||||
label="Markdown (GitHub flavored)"
|
||||
defaultValue={data.props?.markdown ?? false}
|
||||
onChange={(markdown) =>
|
||||
updateData({ ...data, props: { ...data.props, markdown } })
|
||||
}
|
||||
/>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={[
|
||||
"color",
|
||||
"backgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontWeight",
|
||||
"textAlign",
|
||||
"padding"
|
||||
]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
|
||||
type SidebarPanelProps = {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
export default function BaseSidebarPanel({
|
||||
title,
|
||||
children
|
||||
}: SidebarPanelProps) {
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
color="text.secondary"
|
||||
sx={{ display: "block", mb: 2 }}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Stack spacing={5} mb={3}>
|
||||
{children}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { FormControlLabel, Switch } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export default function BooleanInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<FormControlLabel
|
||||
label={label}
|
||||
control={
|
||||
<Switch
|
||||
checked={value}
|
||||
onChange={(_, checked: boolean) => {
|
||||
setValue(checked);
|
||||
onChange(checked);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { AddOutlined, CloseOutlined } from "@mui/icons-material";
|
||||
import { ButtonBase, InputLabel, Menu, Stack } from "@mui/material";
|
||||
|
||||
import Picker from "./Picker";
|
||||
|
||||
const BUTTON_SX = {
|
||||
border: "1px solid",
|
||||
borderColor: "cadet.400",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "4px",
|
||||
bgcolor: "#FFFFFF"
|
||||
};
|
||||
|
||||
type Props =
|
||||
| {
|
||||
nullable: true;
|
||||
label: string;
|
||||
onChange: (value: string | null) => void;
|
||||
defaultValue: string | null;
|
||||
}
|
||||
| {
|
||||
nullable: false;
|
||||
label: string;
|
||||
onChange: (value: string) => void;
|
||||
defaultValue: string;
|
||||
};
|
||||
export default function ColorInput({
|
||||
label,
|
||||
defaultValue,
|
||||
onChange,
|
||||
nullable
|
||||
}: Props) {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const handleClickOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const renderResetButton = () => {
|
||||
if (!nullable) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value !== "string" || value.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setValue(null);
|
||||
onChange(null);
|
||||
}}
|
||||
>
|
||||
<CloseOutlined fontSize="small" sx={{ color: "grey.600" }} />
|
||||
</ButtonBase>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOpenButton = () => {
|
||||
if (value) {
|
||||
return (
|
||||
<ButtonBase
|
||||
onClick={handleClickOpen}
|
||||
sx={{ ...BUTTON_SX, bgcolor: value }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ButtonBase onClick={handleClickOpen} sx={{ ...BUTTON_SX }}>
|
||||
<AddOutlined fontSize="small" />
|
||||
</ButtonBase>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack alignItems="flex-start">
|
||||
<InputLabel sx={{ mb: 0.5 }}>{label}</InputLabel>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{renderOpenButton()}
|
||||
{renderResetButton()}
|
||||
</Stack>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
MenuListProps={{
|
||||
sx: { height: "auto", padding: 0 }
|
||||
}}
|
||||
>
|
||||
<Picker
|
||||
value={value || ""}
|
||||
onChange={(v) => {
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import React, { useState } from "react";
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||
|
||||
import { Box, Stack, SxProps } from "@mui/material";
|
||||
|
||||
import Swatch from "./Swatch";
|
||||
|
||||
const DEFAULT_PRESET_COLORS = [
|
||||
"#E11D48",
|
||||
"#DB2777",
|
||||
"#C026D3",
|
||||
"#9333EA",
|
||||
"#7C3AED",
|
||||
"#4F46E5",
|
||||
"#2563EB",
|
||||
"#0284C7",
|
||||
"#0891B2",
|
||||
"#0D9488",
|
||||
"#059669",
|
||||
"#16A34A",
|
||||
"#65A30D",
|
||||
"#CA8A04",
|
||||
"#D97706",
|
||||
"#EA580C",
|
||||
"#DC2626",
|
||||
"#FFFFFF",
|
||||
"#FAFAFA",
|
||||
"#F5F5F5",
|
||||
"#E5E5E5",
|
||||
"#D4D4D4",
|
||||
"#A3A3A3",
|
||||
"#737373",
|
||||
"#525252",
|
||||
"#404040",
|
||||
"#262626",
|
||||
"#171717",
|
||||
"#0A0A0A",
|
||||
"#000000"
|
||||
];
|
||||
|
||||
const SX: SxProps = {
|
||||
p: 1,
|
||||
".react-colorful__pointer ": {
|
||||
width: 16,
|
||||
height: 16
|
||||
},
|
||||
".react-colorful__saturation": {
|
||||
mb: 1,
|
||||
borderRadius: "4px"
|
||||
},
|
||||
".react-colorful__last-control": {
|
||||
borderRadius: "4px"
|
||||
},
|
||||
".react-colorful__hue-pointer": {
|
||||
width: "4px",
|
||||
borderRadius: "4px",
|
||||
height: 24,
|
||||
cursor: "col-resize"
|
||||
},
|
||||
".react-colorful__saturation-pointer": {
|
||||
cursor: "all-scroll"
|
||||
},
|
||||
input: {
|
||||
padding: 1,
|
||||
border: "1px solid",
|
||||
borderColor: "grey.300",
|
||||
borderRadius: "4px",
|
||||
width: "100%"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
};
|
||||
export default function Picker({ value, onChange }: Props) {
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
const handleChange = (v: string) => {
|
||||
setInternalValue(v);
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(v)) {
|
||||
onChange(v);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={1} sx={SX}>
|
||||
<HexColorPicker color={value} onChange={handleChange} />
|
||||
<Swatch
|
||||
paletteColors={DEFAULT_PRESET_COLORS}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Box pt={1}>
|
||||
<HexColorInput prefixed color={internalValue} onChange={handleChange} />
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Button, SxProps } from "@mui/material";
|
||||
|
||||
type Props = {
|
||||
paletteColors: string[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
const TILE_BUTTON: SxProps = {
|
||||
width: 24,
|
||||
height: 24
|
||||
};
|
||||
export default function Swatch({ paletteColors, value, onChange }: Props) {
|
||||
const renderButton = (colorValue: string) => {
|
||||
return (
|
||||
<Button
|
||||
key={colorValue}
|
||||
onClick={() => onChange(colorValue)}
|
||||
sx={{
|
||||
...TILE_BUTTON,
|
||||
backgroundColor: colorValue,
|
||||
border: "1px solid",
|
||||
borderColor: value === colorValue ? "black" : "grey.200",
|
||||
minWidth: 24,
|
||||
display: "inline-flex",
|
||||
"&:hover": {
|
||||
backgroundColor: colorValue,
|
||||
borderColor: "grey.500"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
width="100%"
|
||||
sx={{
|
||||
display: "grid",
|
||||
gap: 1,
|
||||
gridTemplateColumns: "1fr 1fr 1fr 1fr 1fr 1fr"
|
||||
}}
|
||||
>
|
||||
{paletteColors.map((c) => renderButton(c))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
|
||||
import BaseColorInput from "./BaseColorInput";
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
onChange: (value: string) => void;
|
||||
defaultValue: string;
|
||||
};
|
||||
export default function ColorInput(props: Props) {
|
||||
return <BaseColorInput {...props} nullable={false} />;
|
||||
}
|
||||
|
||||
type NullableProps = {
|
||||
label: string;
|
||||
onChange: (value: null | string) => void;
|
||||
defaultValue: null | string;
|
||||
};
|
||||
export function NullableColorInput(props: NullableProps) {
|
||||
return <BaseColorInput {...props} nullable />;
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Stack } from '@mui/material';
|
||||
|
||||
import TextDimensionInput from './TextDimensionInput';
|
||||
|
||||
export const DEFAULT_2_COLUMNS = [6] as [number];
|
||||
export const DEFAULT_3_COLUMNS = [4, 8] as [number, number];
|
||||
|
||||
type TWidthValue = number | null | undefined;
|
||||
type FixedWidths = [
|
||||
//
|
||||
number | null | undefined,
|
||||
number | null | undefined,
|
||||
number | null | undefined,
|
||||
];
|
||||
type ColumnsLayoutInputProps = {
|
||||
defaultValue: FixedWidths | null | undefined;
|
||||
onChange: (v: FixedWidths | null | undefined) => void;
|
||||
};
|
||||
export default function ColumnWidthsInput({ defaultValue, onChange }: ColumnsLayoutInputProps) {
|
||||
const [currentValue, setCurrentValue] = useState<[TWidthValue, TWidthValue, TWidthValue]>(() => {
|
||||
if (defaultValue) {
|
||||
return defaultValue;
|
||||
}
|
||||
return [null, null, null];
|
||||
});
|
||||
|
||||
const setIndexValue = (index: 0 | 1 | 2, value: number | null | undefined) => {
|
||||
const nValue: FixedWidths = [...currentValue];
|
||||
nValue[index] = value;
|
||||
setCurrentValue(nValue);
|
||||
onChange(nValue);
|
||||
};
|
||||
|
||||
const columnsCountValue = 3;
|
||||
let column3 = null;
|
||||
if (columnsCountValue === 3) {
|
||||
column3 = (
|
||||
<TextDimensionInput
|
||||
label="Column 3"
|
||||
defaultValue={currentValue?.[2]}
|
||||
onChange={(v) => {
|
||||
setIndexValue(2, v);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<TextDimensionInput
|
||||
label="Column 1"
|
||||
defaultValue={currentValue?.[0]}
|
||||
onChange={(v) => {
|
||||
setIndexValue(0, v);
|
||||
}}
|
||||
/>
|
||||
<TextDimensionInput
|
||||
label="Column 2"
|
||||
defaultValue={currentValue?.[1]}
|
||||
onChange={(v) => {
|
||||
setIndexValue(1, v);
|
||||
}}
|
||||
/>
|
||||
{column3}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { MenuItem, TextField } from '@mui/material';
|
||||
|
||||
import { FONT_FAMILIES } from '../../../../../../documents/blocks/helpers/fontFamily';
|
||||
|
||||
const OPTIONS = FONT_FAMILIES.map((option) => (
|
||||
<MenuItem key={option.key} value={option.key} sx={{ fontFamily: option.value }}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
type NullableProps = {
|
||||
label: string;
|
||||
onChange: (value: null | string) => void;
|
||||
defaultValue: null | string;
|
||||
};
|
||||
export function NullableFontFamily({ label, onChange, defaultValue }: NullableProps) {
|
||||
const [value, setValue] = useState(defaultValue ?? 'inherit');
|
||||
return (
|
||||
<TextField
|
||||
select
|
||||
variant="standard"
|
||||
label={label}
|
||||
value={value}
|
||||
onChange={(ev) => {
|
||||
const v = ev.target.value;
|
||||
setValue(v);
|
||||
onChange(v === null ? null : v);
|
||||
}}
|
||||
>
|
||||
<MenuItem value="inherit">Match email settings</MenuItem>
|
||||
{OPTIONS}
|
||||
</TextField>
|
||||
);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { TextFieldsOutlined } from '@mui/icons-material';
|
||||
import { InputLabel, Stack } from '@mui/material';
|
||||
|
||||
import RawSliderInput from './raw/RawSliderInput';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: number;
|
||||
onChange: (v: number) => void;
|
||||
};
|
||||
export default function FontSizeInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const handleChange = (value: number) => {
|
||||
setValue(value);
|
||||
onChange(value);
|
||||
};
|
||||
return (
|
||||
<Stack spacing={1} alignItems="flex-start">
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
<RawSliderInput
|
||||
iconLabel={<TextFieldsOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value}
|
||||
setValue={handleChange}
|
||||
units="px"
|
||||
step={1}
|
||||
min={10}
|
||||
max={48}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ToggleButton } from '@mui/material';
|
||||
|
||||
import RadioGroupInput from './RadioGroupInput';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
export default function FontWeightInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<RadioGroupInput
|
||||
label={label}
|
||||
defaultValue={value}
|
||||
onChange={(fontWeight) => {
|
||||
setValue(fontWeight);
|
||||
onChange(fontWeight);
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="normal">Regular</ToggleButton>
|
||||
<ToggleButton value="bold">Bold</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
);
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
AlignHorizontalLeftOutlined,
|
||||
AlignHorizontalRightOutlined,
|
||||
AlignVerticalBottomOutlined,
|
||||
AlignVerticalTopOutlined,
|
||||
} from '@mui/icons-material';
|
||||
import { InputLabel, Stack } from '@mui/material';
|
||||
|
||||
import RawSliderInput from './raw/RawSliderInput';
|
||||
|
||||
type TPaddingValue = {
|
||||
top: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
left: number;
|
||||
};
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: TPaddingValue | null;
|
||||
onChange: (value: TPaddingValue) => void;
|
||||
};
|
||||
export default function PaddingInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(() => {
|
||||
if (defaultValue) {
|
||||
return defaultValue;
|
||||
}
|
||||
return {
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
};
|
||||
});
|
||||
|
||||
function handleChange(internalName: keyof TPaddingValue, nValue: number) {
|
||||
const v = {
|
||||
...value,
|
||||
[internalName]: nValue,
|
||||
};
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={2} alignItems="flex-start" pb={1}>
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignVerticalTopOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.top}
|
||||
setValue={(num) => handleChange('top', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignVerticalBottomOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.bottom}
|
||||
setValue={(num) => handleChange('bottom', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignHorizontalLeftOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.left}
|
||||
setValue={(num) => handleChange('left', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignHorizontalRightOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.right}
|
||||
setValue={(num) => handleChange('right', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { InputLabel, Stack, ToggleButtonGroup } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
label: string | JSX.Element;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
defaultValue: string;
|
||||
onChange: (v: string) => void;
|
||||
};
|
||||
export default function RadioGroupInput({ label, children, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<Stack alignItems="flex-start">
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
<ToggleButtonGroup
|
||||
exclusive
|
||||
fullWidth
|
||||
value={value}
|
||||
size="small"
|
||||
onChange={(_, v: unknown) => {
|
||||
if (typeof v !== 'string') {
|
||||
throw new Error('RadioGroupInput can only receive string values');
|
||||
}
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ToggleButtonGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { InputLabel, Stack } from '@mui/material';
|
||||
|
||||
import RawSliderInput from './raw/RawSliderInput';
|
||||
|
||||
type SliderInputProps = {
|
||||
label: string;
|
||||
iconLabel: JSX.Element;
|
||||
|
||||
step?: number;
|
||||
marks?: boolean;
|
||||
units: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
||||
defaultValue: number;
|
||||
onChange: (v: number) => void;
|
||||
};
|
||||
|
||||
export default function SliderInput({ label, defaultValue, onChange, ...props }: SliderInputProps) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<Stack spacing={1} alignItems="flex-start">
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
<RawSliderInput
|
||||
value={value}
|
||||
setValue={(value: number) => {
|
||||
setValue(value);
|
||||
onChange(value);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { FormatAlignCenterOutlined, FormatAlignLeftOutlined, FormatAlignRightOutlined } from '@mui/icons-material';
|
||||
import { ToggleButton } from '@mui/material';
|
||||
|
||||
import RadioGroupInput from './RadioGroupInput';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: string | null;
|
||||
onChange: (value: string | null) => void;
|
||||
};
|
||||
export default function TextAlignInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue ?? 'left');
|
||||
|
||||
return (
|
||||
<RadioGroupInput
|
||||
label={label}
|
||||
defaultValue={value}
|
||||
onChange={(value) => {
|
||||
setValue(value);
|
||||
onChange(value);
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="left">
|
||||
<FormatAlignLeftOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="center">
|
||||
<FormatAlignCenterOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="right">
|
||||
<FormatAlignRightOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import { TextField, Typography } from '@mui/material';
|
||||
|
||||
type TextDimensionInputProps = {
|
||||
label: string;
|
||||
defaultValue: number | null | undefined;
|
||||
onChange: (v: number | null) => void;
|
||||
};
|
||||
export default function TextDimensionInput({ label, defaultValue, onChange }: TextDimensionInputProps) {
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = parseInt(ev.target.value);
|
||||
onChange(isNaN(value) ? null : value);
|
||||
};
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
onChange={handleChange}
|
||||
defaultValue={defaultValue}
|
||||
label={label}
|
||||
variant="standard"
|
||||
placeholder="auto"
|
||||
size="small"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
px
|
||||
</Typography>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { InputProps, TextField } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
helperText?: string | JSX.Element;
|
||||
InputProps?: InputProps;
|
||||
defaultValue: string;
|
||||
onChange: (v: string) => void;
|
||||
};
|
||||
export default function TextInput({ helperText, label, placeholder, rows, InputProps, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const isMultiline = typeof rows === 'number' && rows > 1;
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline={isMultiline}
|
||||
minRows={rows}
|
||||
variant={isMultiline ? 'outlined' : 'standard'}
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
helperText={helperText}
|
||||
InputProps={InputProps}
|
||||
value={value}
|
||||
onChange={(ev) => {
|
||||
const v = ev.target.value;
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Slider, Stack, Typography } from "@mui/material";
|
||||
|
||||
type SliderInputProps = {
|
||||
iconLabel: JSX.Element;
|
||||
|
||||
step?: number;
|
||||
marks?: boolean;
|
||||
units: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
||||
value: number;
|
||||
setValue: (v: number) => void;
|
||||
};
|
||||
|
||||
export default function RawSliderInput({
|
||||
iconLabel,
|
||||
value,
|
||||
setValue,
|
||||
units,
|
||||
...props
|
||||
}: SliderInputProps) {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
>
|
||||
<Box sx={{ minWidth: 24, lineHeight: 1, flexShrink: 0 }}>{iconLabel}</Box>
|
||||
<Slider
|
||||
{...props}
|
||||
value={value}
|
||||
onChange={(_, value: unknown) => {
|
||||
if (typeof value !== "number") {
|
||||
throw new Error(
|
||||
"RawSliderInput values can only receive numeric values"
|
||||
);
|
||||
}
|
||||
setValue(value);
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ minWidth: 32, textAlign: "right", flexShrink: 0 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ lineHeight: 1 }}
|
||||
>
|
||||
{value}
|
||||
{units}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
|
||||
import { TStyle } from "../../../../../../documents/blocks/helpers/TStyle";
|
||||
|
||||
import SingleStylePropertyPanel from "./SingleStylePropertyPanel";
|
||||
|
||||
type MultiStylePropertyPanelProps = {
|
||||
names: (keyof TStyle)[];
|
||||
value: TStyle | undefined | null;
|
||||
onChange: (style: TStyle) => void;
|
||||
};
|
||||
export default function MultiStylePropertyPanel({
|
||||
names,
|
||||
value,
|
||||
onChange
|
||||
}: MultiStylePropertyPanelProps) {
|
||||
return (
|
||||
<>
|
||||
{names.map((name) => (
|
||||
<SingleStylePropertyPanel
|
||||
key={name}
|
||||
name={name}
|
||||
value={value || {}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
|
||||
import { RoundedCornerOutlined } from "@mui/icons-material";
|
||||
|
||||
import { TStyle } from "../../../../../../documents/blocks/helpers/TStyle";
|
||||
import { NullableColorInput } from "../inputs/ColorInput";
|
||||
import { NullableFontFamily } from "../inputs/FontFamily";
|
||||
import FontSizeInput from "../inputs/FontSizeInput";
|
||||
import FontWeightInput from "../inputs/FontWeightInput";
|
||||
import PaddingInput from "../inputs/PaddingInput";
|
||||
import SliderInput from "../inputs/SliderInput";
|
||||
import TextAlignInput from "../inputs/TextAlignInput";
|
||||
|
||||
type StylePropertyPanelProps = {
|
||||
name: keyof TStyle;
|
||||
value: TStyle;
|
||||
onChange: (style: TStyle) => void;
|
||||
};
|
||||
export default function SingleStylePropertyPanel({
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
}: StylePropertyPanelProps) {
|
||||
const defaultValue = value[name] ?? null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleChange = (v: any) => {
|
||||
onChange({ ...value, [name]: v });
|
||||
};
|
||||
|
||||
switch (name) {
|
||||
case "backgroundColor":
|
||||
return (
|
||||
<NullableColorInput
|
||||
label="Background color"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "borderColor":
|
||||
return (
|
||||
<NullableColorInput
|
||||
label="Border color"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "borderRadius":
|
||||
return (
|
||||
<SliderInput
|
||||
iconLabel={<RoundedCornerOutlined />}
|
||||
units="px"
|
||||
step={4}
|
||||
marks
|
||||
min={0}
|
||||
max={48}
|
||||
label="Border radius"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "color":
|
||||
return (
|
||||
<NullableColorInput
|
||||
label="Text color"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "fontFamily":
|
||||
return (
|
||||
<NullableFontFamily
|
||||
label="Font family"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "fontSize":
|
||||
return (
|
||||
<FontSizeInput
|
||||
label="Font size"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "fontWeight":
|
||||
return (
|
||||
<FontWeightInput
|
||||
label="Font weight"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "textAlign":
|
||||
return (
|
||||
<TextAlignInput
|
||||
label="Alignment"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "padding":
|
||||
return (
|
||||
<PaddingInput
|
||||
label="Padding"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
|
||||
import { setDocument, useDocument } from "../../documents/editor/EditorContext";
|
||||
|
||||
import EmailLayoutSidebarPanel from "./ConfigurationPanel/input-panels/EmailLayoutSidebarPanel";
|
||||
|
||||
export default function StylesPanel() {
|
||||
const block = useDocument().root;
|
||||
if (!block) {
|
||||
return <p>Block not found</p>;
|
||||
}
|
||||
|
||||
const { data, type } = block;
|
||||
if (type !== "EmailLayout") {
|
||||
throw new Error('Expected "root" element to be of type EmailLayout');
|
||||
}
|
||||
|
||||
return (
|
||||
<EmailLayoutSidebarPanel
|
||||
key="root"
|
||||
data={data}
|
||||
setData={(data) => setDocument({ root: { type, data } })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
|
||||
import { AppRegistrationOutlined, LastPageOutlined } from "@mui/icons-material";
|
||||
import { IconButton } from "@mui/material";
|
||||
|
||||
import {
|
||||
toggleInspectorDrawerOpen,
|
||||
useInspectorDrawerOpen
|
||||
} from "../../documents/editor/EditorContext";
|
||||
|
||||
export default function ToggleInspectorPanelButton() {
|
||||
const inspectorDrawerOpen = useInspectorDrawerOpen();
|
||||
|
||||
const handleClick = () => {
|
||||
toggleInspectorDrawerOpen();
|
||||
};
|
||||
if (inspectorDrawerOpen) {
|
||||
return (
|
||||
<IconButton onClick={handleClick}>
|
||||
<LastPageOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<IconButton onClick={handleClick}>
|
||||
<AppRegistrationOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Drawer, Tab, Tabs } from "@mui/material";
|
||||
|
||||
import {
|
||||
setSidebarTab,
|
||||
useInspectorDrawerOpen,
|
||||
useSelectedSidebarTab
|
||||
} from "../../documents/editor/EditorContext";
|
||||
|
||||
import ConfigurationPanel from "./ConfigurationPanel";
|
||||
import StylesPanel from "./StylesPanel";
|
||||
|
||||
export const INSPECTOR_DRAWER_WIDTH = 320;
|
||||
|
||||
export default function InspectorDrawer() {
|
||||
const selectedSidebarTab = useSelectedSidebarTab();
|
||||
const inspectorDrawerOpen = useInspectorDrawerOpen();
|
||||
|
||||
const renderCurrentSidebarPanel = () => {
|
||||
switch (selectedSidebarTab) {
|
||||
case "block-configuration":
|
||||
return <ConfigurationPanel />;
|
||||
case "styles":
|
||||
return <StylesPanel />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
variant="persistent"
|
||||
anchor="right"
|
||||
open={inspectorDrawerOpen}
|
||||
sx={{
|
||||
width: inspectorDrawerOpen ? INSPECTOR_DRAWER_WIDTH : 0
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: INSPECTOR_DRAWER_WIDTH,
|
||||
height: 49,
|
||||
borderBottom: 1,
|
||||
borderColor: "divider"
|
||||
}}
|
||||
>
|
||||
<Box px={2}>
|
||||
<Tabs
|
||||
value={selectedSidebarTab}
|
||||
onChange={(_, v) => setSidebarTab(v)}
|
||||
>
|
||||
<Tab value="styles" label="Styles" />
|
||||
<Tab value="block-configuration" label="Inspect" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: INSPECTOR_DRAWER_WIDTH,
|
||||
height: "calc(100% - 49px)",
|
||||
overflow: "auto"
|
||||
}}
|
||||
>
|
||||
{renderCurrentSidebarPanel()}
|
||||
</Box>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
|
||||
import { Button } from "@mui/material";
|
||||
|
||||
import { resetDocument } from "../../documents/editor/EditorContext";
|
||||
import getConfiguration from "../../getConfiguration";
|
||||
|
||||
export default function SidebarButton({
|
||||
href,
|
||||
children
|
||||
}: {
|
||||
href: string;
|
||||
children: JSX.Element | string;
|
||||
}) {
|
||||
const handleClick = () => {
|
||||
resetDocument(getConfiguration(href));
|
||||
};
|
||||
return (
|
||||
<Button size="small" href={href} onClick={handleClick}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
|
||||
import { FirstPageOutlined, MenuOutlined } from "@mui/icons-material";
|
||||
import { IconButton } from "@mui/material";
|
||||
|
||||
import {
|
||||
toggleSamplesDrawerOpen,
|
||||
useSamplesDrawerOpen
|
||||
} from "../../documents/editor/EditorContext";
|
||||
|
||||
function useIcon() {
|
||||
const samplesDrawerOpen = useSamplesDrawerOpen();
|
||||
if (samplesDrawerOpen) {
|
||||
return <FirstPageOutlined fontSize="small" />;
|
||||
}
|
||||
return <MenuOutlined fontSize="small" />;
|
||||
}
|
||||
|
||||
export default function ToggleSamplesPanelButton() {
|
||||
const icon = useIcon();
|
||||
return <IconButton onClick={toggleSamplesDrawerOpen}>{icon}</IconButton>;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import { Divider, Drawer, Stack, Typography } from "@mui/material";
|
||||
import { useSamplesDrawerOpen } from "../../documents/editor/EditorContext";
|
||||
import SidebarButton from "./SidebarButton";
|
||||
|
||||
export const SAMPLES_DRAWER_WIDTH = 240;
|
||||
|
||||
export default function SamplesDrawer() {
|
||||
const samplesDrawerOpen = useSamplesDrawerOpen();
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
variant="persistent"
|
||||
anchor="left"
|
||||
open={samplesDrawerOpen}
|
||||
sx={{ width: samplesDrawerOpen ? SAMPLES_DRAWER_WIDTH : 0 }}
|
||||
>
|
||||
<Stack
|
||||
spacing={3}
|
||||
py={1}
|
||||
px={2}
|
||||
width={SAMPLES_DRAWER_WIDTH}
|
||||
justifyContent="space-between"
|
||||
height="100%"
|
||||
>
|
||||
<Stack
|
||||
spacing={2}
|
||||
sx={{
|
||||
"& .MuiButtonBase-root": {
|
||||
width: "100%",
|
||||
justifyContent: "flex-start"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="h1" sx={{ p: 0.75 }}>
|
||||
EmailBuilder
|
||||
</Typography>
|
||||
|
||||
<Stack alignItems="flex-start">
|
||||
<SidebarButton href="#">Empty</SidebarButton>
|
||||
<SidebarButton href="#sample/requestemail">
|
||||
Request Email
|
||||
</SidebarButton>
|
||||
<SidebarButton href="#sample/completionemail">
|
||||
Completion Email
|
||||
</SidebarButton>
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
import { FileDownloadOutlined } from "@mui/icons-material";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
|
||||
import { useDocument } from "../../../documents/editor/EditorContext";
|
||||
// import { renderToStaticMarkup } from "@usewaypoint/email-builder";
|
||||
import renderEmailHtml from "../helper/renderEmailHtml";
|
||||
|
||||
export default function DownloadJson() {
|
||||
const doc = useDocument();
|
||||
// const code = useMemo(
|
||||
// () => renderToStaticMarkup(doc, { rootBlockId: "root" }),
|
||||
// [doc]
|
||||
// );
|
||||
|
||||
const code = useMemo(() => renderEmailHtml(doc, "root"), [doc]);
|
||||
|
||||
const href = useMemo(() => {
|
||||
const blob = new Blob([code], { type: "text/html;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
return url;
|
||||
}, [doc]);
|
||||
return (
|
||||
<Tooltip title="Download html of template in txt file">
|
||||
<IconButton href={href} download="emailTemplate.txt">
|
||||
<FileDownloadOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
// import { renderToStaticMarkup } from "@usewaypoint/email-builder";
|
||||
|
||||
import { useDocument } from "../../documents/editor/EditorContext";
|
||||
|
||||
import HighlightedCodePanel from "./helper/HighlightedCodePanel";
|
||||
import renderEmailHtml from "./helper/renderEmailHtml";
|
||||
|
||||
export default function HtmlPanel() {
|
||||
const document = useDocument();
|
||||
// const code = useMemo(
|
||||
// () => renderToStaticMarkup(document, { rootBlockId: "root" }),
|
||||
// [document]
|
||||
// );
|
||||
const code = useMemo(() => renderEmailHtml(document, "root"), [document]);
|
||||
return <HighlightedCodePanel type="html" value={code} />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
import { useDocument } from "../../documents/editor/EditorContext";
|
||||
|
||||
import HighlightedCodePanel from "./helper/HighlightedCodePanel";
|
||||
|
||||
export default function JsonPanel() {
|
||||
const document = useDocument();
|
||||
const code = useMemo(() => JSON.stringify(document, null, " "), [document]);
|
||||
return <HighlightedCodePanel type="json" value={code} />;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
// DataObjectOutlined,
|
||||
import {
|
||||
CodeOutlined,
|
||||
EditOutlined,
|
||||
PreviewOutlined
|
||||
} from "@mui/icons-material";
|
||||
import { Tab, Tabs, Tooltip } from "@mui/material";
|
||||
|
||||
import {
|
||||
setSelectedMainTab,
|
||||
useSelectedMainTab
|
||||
} from "../../documents/editor/EditorContext";
|
||||
|
||||
export default function MainTabsGroup() {
|
||||
const selectedMainTab = useSelectedMainTab();
|
||||
const handleChange = (_: unknown, v: unknown) => {
|
||||
switch (v) {
|
||||
case "json":
|
||||
case "preview":
|
||||
case "editor":
|
||||
case "html":
|
||||
setSelectedMainTab(v);
|
||||
return;
|
||||
default:
|
||||
setSelectedMainTab("editor");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs value={selectedMainTab} onChange={handleChange}>
|
||||
<Tab
|
||||
value="editor"
|
||||
label={
|
||||
<Tooltip title="Edit">
|
||||
<EditOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
value="preview"
|
||||
label={
|
||||
<Tooltip title="Preview">
|
||||
<PreviewOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
value="html"
|
||||
label={
|
||||
<Tooltip title="HTML output">
|
||||
<CodeOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
{/* <Tab
|
||||
value="json"
|
||||
label={
|
||||
<Tooltip title="JSON output">
|
||||
<DataObjectOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
}
|
||||
/> */}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { ContentCopyOutlined } from "@mui/icons-material";
|
||||
import { IconButton, Snackbar, Tooltip } from "@mui/material";
|
||||
|
||||
import { useDocument } from "../../documents/editor/EditorContext";
|
||||
// import { renderToStaticMarkup } from "@usewaypoint/email-builder";
|
||||
import renderEmailHtml from "./helper/renderEmailHtml";
|
||||
|
||||
export default function ShareButton() {
|
||||
const document = useDocument();
|
||||
// const code = useMemo(
|
||||
// () => renderToStaticMarkup(document, { rootBlockId: "root" }),
|
||||
// [document]
|
||||
// );
|
||||
const code = useMemo(() => renderEmailHtml(document, "root"), [document]);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const onClick = async () => {
|
||||
await copyToClipboard(code);
|
||||
setMessage("Your current template is copied");
|
||||
setTimeout(() => {
|
||||
setMessage(null);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
async function copyToClipboard(text: string): Promise<boolean> {
|
||||
// 1) Try modern Clipboard API
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to legacy approach
|
||||
}
|
||||
|
||||
// 2) Fallback: execCommand('copy')
|
||||
try {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
|
||||
// Prevent scrolling to bottom on iOS
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.top = "0";
|
||||
textarea.style.left = "0";
|
||||
textarea.style.opacity = "0";
|
||||
textarea.style.pointerEvents = "none";
|
||||
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
const ok = document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
|
||||
return ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
setMessage(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <IconButton onClick={onClick}>
|
||||
<Tooltip title="Share current template">
|
||||
<IosShareOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
</IconButton> */}
|
||||
<IconButton onClick={onClick} sx={{ color: "text.primary" }}>
|
||||
<Tooltip title="copy" placement="left-start">
|
||||
<ContentCopyOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
open={message !== null}
|
||||
onClose={onClose}
|
||||
message={message}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { html, json } from "./highlighters";
|
||||
|
||||
type TextEditorPanelProps = {
|
||||
type: "json" | "html" | "javascript";
|
||||
value: string;
|
||||
};
|
||||
export default function HighlightedCodePanel({
|
||||
type,
|
||||
value
|
||||
}: TextEditorPanelProps) {
|
||||
const [code, setCode] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
switch (type) {
|
||||
case "html":
|
||||
html(value).then(setCode);
|
||||
return;
|
||||
case "json":
|
||||
json(value).then(setCode);
|
||||
return;
|
||||
}
|
||||
}, [setCode, value, type]);
|
||||
|
||||
if (code === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<pre
|
||||
style={{ margin: 0, padding: 16 }}
|
||||
dangerouslySetInnerHTML={{ __html: code }}
|
||||
onClick={(ev) => {
|
||||
const s = window.getSelection();
|
||||
if (s === null) {
|
||||
return;
|
||||
}
|
||||
s.selectAllChildren(ev.currentTarget);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import hljs from "highlight.js";
|
||||
import jsonHighlighter from "highlight.js/lib/languages/json";
|
||||
import xmlHighlighter from "highlight.js/lib/languages/xml";
|
||||
import prettierPluginBabel from "prettier/plugins/babel";
|
||||
import prettierPluginEstree from "prettier/plugins/estree";
|
||||
import prettierPluginHtml from "prettier/plugins/html";
|
||||
import { format } from "prettier/standalone";
|
||||
|
||||
hljs.registerLanguage("json", jsonHighlighter);
|
||||
hljs.registerLanguage("html", xmlHighlighter);
|
||||
|
||||
export async function html(value: string): Promise<string> {
|
||||
const prettyValue = await format(value, {
|
||||
parser: "html",
|
||||
plugins: [prettierPluginHtml]
|
||||
});
|
||||
return hljs.highlight(prettyValue, { language: "html" }).value;
|
||||
}
|
||||
|
||||
export async function json(value: string): Promise<string> {
|
||||
const prettyValue = await format(value, {
|
||||
parser: "json",
|
||||
printWidth: 0,
|
||||
trailingComma: "all",
|
||||
plugins: [prettierPluginBabel, prettierPluginEstree]
|
||||
});
|
||||
return hljs.highlight(prettyValue, { language: "javascript" }).value;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
renderToStaticMarkup
|
||||
} from "@usewaypoint/email-builder";
|
||||
import { TEditorConfiguration } from "../../../documents/editor/core";
|
||||
|
||||
function overrideCanvasSize(
|
||||
html: string,
|
||||
document: TEditorConfiguration,
|
||||
rootBlockId: string
|
||||
) {
|
||||
const rootBlock = document[rootBlockId];
|
||||
if (!rootBlock || rootBlock.type !== "EmailLayout") {
|
||||
return html;
|
||||
}
|
||||
|
||||
const data = rootBlock.data as { canvasWidth?: number | null };
|
||||
|
||||
const canvasWidth =
|
||||
typeof data?.canvasWidth === "number" && data.canvasWidth > 0
|
||||
? data.canvasWidth
|
||||
: 600;
|
||||
|
||||
let updatedHtml = html.replace(
|
||||
/max-width:\s*600px/gi,
|
||||
`max-width:${canvasWidth}px`
|
||||
);
|
||||
|
||||
return updatedHtml;
|
||||
}
|
||||
|
||||
export default function renderEmailHtml(
|
||||
document: TEditorConfiguration,
|
||||
rootBlockId = "root"
|
||||
) {
|
||||
const html = renderToStaticMarkup(document, { rootBlockId });
|
||||
return overrideCanvasSize(html, document, rootBlockId);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
import { MonitorOutlined, PhoneIphoneOutlined } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Stack,
|
||||
SxProps,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Tooltip
|
||||
} from "@mui/material";
|
||||
import { Reader } from "@usewaypoint/email-builder";
|
||||
|
||||
import EditorBlock from "../../documents/editor/EditorBlock";
|
||||
import {
|
||||
setSelectedScreenSize,
|
||||
useDocument,
|
||||
useSelectedMainTab,
|
||||
useSelectedScreenSize
|
||||
} from "../../documents/editor/EditorContext";
|
||||
import ToggleInspectorPanelButton from "../InspectorDrawer/ToggleInspectorPanelButton";
|
||||
import ToggleSamplesPanelButton from "../SamplesDrawer/ToggleSamplesPanelButton";
|
||||
|
||||
import DownloadJson from "./DownloadJson";
|
||||
import HtmlPanel from "./HtmlPanel";
|
||||
import JsonPanel from "./JsonPanel";
|
||||
import MainTabsGroup from "./MainTabsGroup";
|
||||
import ShareButton from "./ShareButton";
|
||||
import renderEmailHtml from "./helper/renderEmailHtml";
|
||||
|
||||
export default function TemplatePanel() {
|
||||
const document = useDocument();
|
||||
const selectedMainTab = useSelectedMainTab();
|
||||
const selectedScreenSize = useSelectedScreenSize();
|
||||
const previewHtml = useMemo(
|
||||
() => renderEmailHtml(document, "root"),
|
||||
[document]
|
||||
);
|
||||
|
||||
let mainBoxSx: SxProps = { height: "100%" };
|
||||
if (selectedScreenSize === "mobile") {
|
||||
mainBoxSx = {
|
||||
...mainBoxSx,
|
||||
margin: "32px auto",
|
||||
width: 370,
|
||||
height: 800,
|
||||
boxShadow:
|
||||
"rgba(33, 36, 67, 0.04) 0px 10px 20px, rgba(33, 36, 67, 0.04) 0px 2px 6px, rgba(33, 36, 67, 0.04) 0px 0px 1px"
|
||||
};
|
||||
}
|
||||
|
||||
const handleScreenSizeChange = (_: unknown, value: unknown) => {
|
||||
switch (value) {
|
||||
case "mobile":
|
||||
case "desktop":
|
||||
setSelectedScreenSize(value);
|
||||
return;
|
||||
default:
|
||||
setSelectedScreenSize("desktop");
|
||||
}
|
||||
};
|
||||
|
||||
const renderMainPanel = () => {
|
||||
switch (selectedMainTab) {
|
||||
case "editor":
|
||||
return (
|
||||
<Box sx={mainBoxSx}>
|
||||
<EditorBlock id="root" />
|
||||
</Box>
|
||||
);
|
||||
case "preview":
|
||||
return (
|
||||
<Box sx={mainBoxSx}>
|
||||
<iframe
|
||||
title="Email preview"
|
||||
srcDoc={previewHtml}
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox"
|
||||
style={{ width: "100%", height: "100%", border: "none" }}
|
||||
/>
|
||||
{/* <Reader document={document} rootBlockId="root" /> */}
|
||||
</Box>
|
||||
);
|
||||
case "html":
|
||||
return <HtmlPanel />;
|
||||
case "json":
|
||||
return <JsonPanel />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
sx={{
|
||||
height: 49,
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
backgroundColor: "white",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: "appBar",
|
||||
px: 1
|
||||
}}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<ToggleSamplesPanelButton />
|
||||
<Stack
|
||||
px={2}
|
||||
direction="row"
|
||||
gap={2}
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<MainTabsGroup />
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<DownloadJson />
|
||||
<ToggleButtonGroup
|
||||
value={selectedScreenSize}
|
||||
exclusive
|
||||
size="small"
|
||||
onChange={handleScreenSizeChange}
|
||||
>
|
||||
<ToggleButton value="desktop">
|
||||
<Tooltip title="Desktop view">
|
||||
<MonitorOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="mobile">
|
||||
<Tooltip title="Mobile view">
|
||||
<PhoneIphoneOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<ShareButton />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ToggleInspectorPanelButton />
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{ height: "calc(100vh - 49px)", overflow: "auto", minWidth: 370 }}
|
||||
>
|
||||
{renderMainPanel()}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
useState
|
||||
} from "react";
|
||||
import { Stack, useTheme } from "@mui/material";
|
||||
import {
|
||||
useInspectorDrawerOpen,
|
||||
useSamplesDrawerOpen
|
||||
} from "../documents/editor/EditorContext";
|
||||
import InspectorDrawer, { INSPECTOR_DRAWER_WIDTH } from "./InspectorDrawer";
|
||||
import SamplesDrawer, { SAMPLES_DRAWER_WIDTH } from "./SamplesDrawer";
|
||||
import TemplatePanel from "./TemplatePanel";
|
||||
|
||||
function useDrawerTransition(
|
||||
cssProperty: "margin-left" | "margin-right",
|
||||
open: boolean
|
||||
) {
|
||||
const { transitions } = useTheme();
|
||||
return transitions.create(cssProperty, {
|
||||
easing: !open ? transitions.easing.sharp : transitions.easing.easeOut,
|
||||
duration: !open
|
||||
? transitions.duration.leavingScreen
|
||||
: transitions.duration.enteringScreen
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export default function EmailBuilderApp() {
|
||||
const inspectorDrawerOpen = useInspectorDrawerOpen();
|
||||
const samplesDrawerOpen = useSamplesDrawerOpen();
|
||||
|
||||
const marginLeftTransition = useDrawerTransition(
|
||||
"margin-left",
|
||||
samplesDrawerOpen
|
||||
);
|
||||
const marginRightTransition = useDrawerTransition(
|
||||
"margin-right",
|
||||
inspectorDrawerOpen
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InspectorDrawer />
|
||||
<SamplesDrawer />
|
||||
<Stack
|
||||
sx={{
|
||||
marginRight: inspectorDrawerOpen ? `${INSPECTOR_DRAWER_WIDTH}px` : 0,
|
||||
marginLeft: samplesDrawerOpen ? `${SAMPLES_DRAWER_WIDTH}px` : 0,
|
||||
transition: [marginLeftTransition, marginRightTransition].join(", ")
|
||||
}}
|
||||
>
|
||||
<TemplatePanel />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { CssBaseline, ThemeProvider } from "@mui/material";
|
||||
|
||||
import App from "./App";
|
||||
import theme from "./EmailBuildertheme";
|
||||
|
||||
const EmailBuilder = () => {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailBuilder;
|
||||
@@ -0,0 +1,520 @@
|
||||
import { alpha, createTheme, darken, lighten } from "@mui/material/styles";
|
||||
|
||||
const BRAND_NAVY = "#212443";
|
||||
const BRAND_BLUE = "#0079CC";
|
||||
const BRAND_GREEN = "#1F8466";
|
||||
const BRAND_RED = "#E81212";
|
||||
const BRAND_YELLOW = "#F6DC9F";
|
||||
const BRAND_PURPLE = "#6C0E7C";
|
||||
const BRAND_BROWN = "#CC996C";
|
||||
const STANDARD_FONT_FAMILY =
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"';
|
||||
const MONOSPACE_FONT_FAMILY =
|
||||
'ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace';
|
||||
|
||||
const BASE_THEME = createTheme({
|
||||
palette: {
|
||||
background: {
|
||||
default: "#f2f5f7"
|
||||
},
|
||||
text: {
|
||||
primary: "#1F1F21",
|
||||
secondary: "#4F4F4F"
|
||||
}
|
||||
},
|
||||
typography: {
|
||||
fontFamily: STANDARD_FONT_FAMILY
|
||||
}
|
||||
});
|
||||
|
||||
const THEME = createTheme(BASE_THEME, {
|
||||
palette: {
|
||||
brand: {
|
||||
navy: BRAND_NAVY,
|
||||
blue: BRAND_BLUE,
|
||||
red: BRAND_RED,
|
||||
green: BRAND_GREEN,
|
||||
yellow: BRAND_YELLOW,
|
||||
purple: BRAND_PURPLE,
|
||||
brown: BRAND_BROWN
|
||||
},
|
||||
success: {
|
||||
main: BRAND_GREEN,
|
||||
light: lighten(BRAND_GREEN, 0.15),
|
||||
dark: darken(BRAND_GREEN, 0.15)
|
||||
},
|
||||
error: {
|
||||
main: BRAND_RED,
|
||||
light: lighten(BRAND_RED, 0.15),
|
||||
dark: darken(BRAND_RED, 0.15)
|
||||
},
|
||||
cadet: {
|
||||
100: "#F9FAFB",
|
||||
200: "#F2F5F7",
|
||||
300: "#DCE4EA",
|
||||
400: "#A8BBCA",
|
||||
500: "#6A8BA4"
|
||||
},
|
||||
highlight: {
|
||||
100: lighten(BRAND_YELLOW, 0.8),
|
||||
200: lighten(BRAND_YELLOW, 0.6),
|
||||
300: lighten(BRAND_YELLOW, 0.4),
|
||||
400: lighten(BRAND_YELLOW, 0.2),
|
||||
500: BRAND_YELLOW
|
||||
},
|
||||
info: {
|
||||
main: BRAND_BLUE
|
||||
},
|
||||
primary: {
|
||||
main: BRAND_BLUE
|
||||
}
|
||||
},
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: `
|
||||
address {
|
||||
font-style: normal;
|
||||
}
|
||||
fieldset {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
pre {
|
||||
font-family: ${MONOSPACE_FONT_FAMILY}
|
||||
white-space: pre-wrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
`
|
||||
},
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(14)
|
||||
},
|
||||
action: {
|
||||
paddingTop: 0,
|
||||
marginRight: 0
|
||||
},
|
||||
filledSuccess: {
|
||||
backgroundColor: BRAND_GREEN
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiStepLabel: {
|
||||
styleOverrides: {
|
||||
label: {
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiDialog: {
|
||||
defaultProps: {
|
||||
fullWidth: true
|
||||
}
|
||||
},
|
||||
MuiDialogContent: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
paddingTop: BASE_THEME.spacing(1),
|
||||
paddingBottom: BASE_THEME.spacing(2)
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiDialogTitle: {
|
||||
defaultProps: {
|
||||
variant: "h4"
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
paddingTop: BASE_THEME.spacing(3),
|
||||
paddingBottom: BASE_THEME.spacing(1)
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiDialogActions: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderTop: "1px solid",
|
||||
borderTopColor: BASE_THEME.palette.divider,
|
||||
marginTop: BASE_THEME.spacing(2.5),
|
||||
padding: `${BASE_THEME.spacing(1.5)} ${BASE_THEME.spacing(3)}`
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
...BASE_THEME.typography.body2,
|
||||
borderColor: BASE_THEME.palette.grey[200]
|
||||
},
|
||||
head: {
|
||||
...BASE_THEME.typography.overline,
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
letterSpacing: "0.075em",
|
||||
color: BASE_THEME.palette.text.secondary
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiTableRow: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&:last-child td": {
|
||||
borderBottom: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiAvatar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: "uppercase",
|
||||
fontSize: BASE_THEME.typography.pxToRem(14)
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&.MuiChip-filledError, &.MuiChip-filledSuccess": {
|
||||
fill: BASE_THEME.palette.primary.contrastText
|
||||
}
|
||||
},
|
||||
sizeSmall: {
|
||||
borderRadius: BASE_THEME.spacing(0.5),
|
||||
fontSize: 12
|
||||
},
|
||||
iconSmall: {
|
||||
fontSize: 14,
|
||||
marginLeft: BASE_THEME.spacing(1)
|
||||
},
|
||||
colorSecondary: {
|
||||
borderColor: BASE_THEME.palette.grey[400],
|
||||
color: BASE_THEME.palette.text.secondary
|
||||
},
|
||||
label: {
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiDrawer: {
|
||||
defaultProps: {
|
||||
PaperProps: {
|
||||
elevation: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiTooltip: {
|
||||
styleOverrides: {
|
||||
tooltip: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(12),
|
||||
backgroundColor: alpha(BASE_THEME.palette.text.primary, 0.9)
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiSlider: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
height: 1
|
||||
},
|
||||
track: {
|
||||
height: 1,
|
||||
border: "none"
|
||||
},
|
||||
rail: {
|
||||
height: 1,
|
||||
backgroundColor: BASE_THEME.palette.grey[500]
|
||||
},
|
||||
mark: {
|
||||
backgroundColor: BASE_THEME.palette.grey[500]
|
||||
},
|
||||
markActive: {
|
||||
height: 0
|
||||
},
|
||||
thumb: {
|
||||
height: 16,
|
||||
width: 16,
|
||||
cursor: "col-resize",
|
||||
"&:hover, &.Mui-active, &.Mui-focusVisible": {
|
||||
boxShadow: `0 0 0 4px ${alpha(BRAND_BLUE, 0.2)}`
|
||||
},
|
||||
"&:before": {
|
||||
display: "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiPaper: {
|
||||
defaultProps: {
|
||||
elevation: 2,
|
||||
square: true
|
||||
}
|
||||
},
|
||||
MuiButtonBase: {
|
||||
defaultProps: {
|
||||
disableTouchRipple: true,
|
||||
focusRipple: true
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&.MuiButton-containedSecondary.Mui-disabled": {
|
||||
backgroundColor: BASE_THEME.palette.grey[100]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiButtonGroup: {
|
||||
defaultProps: {
|
||||
disableElevation: true
|
||||
}
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
edgeStart: {
|
||||
marginLeft: BASE_THEME.spacing(-1)
|
||||
},
|
||||
colorSecondary: {
|
||||
color: BASE_THEME.palette.grey[500]
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
disableElevation: true
|
||||
},
|
||||
styleOverrides: {
|
||||
textPrimary: {
|
||||
color: BASE_THEME.palette.text.primary
|
||||
},
|
||||
textSecondary: {
|
||||
color: BASE_THEME.palette.text.secondary
|
||||
},
|
||||
outlinedPrimary: {
|
||||
borderColor: BASE_THEME.palette.grey[300],
|
||||
color: BASE_THEME.palette.text.primary,
|
||||
"&:hover, &:active, &:focus": {
|
||||
borderColor: BASE_THEME.palette.grey[500],
|
||||
color: BASE_THEME.palette.text.primary
|
||||
}
|
||||
},
|
||||
containedSecondary: {
|
||||
backgroundColor: BASE_THEME.palette.common.white,
|
||||
border: `1px solid ${BASE_THEME.palette.grey[300]}`,
|
||||
color: BASE_THEME.palette.text.primary,
|
||||
"&:hover, &:active, &:focus": {
|
||||
backgroundColor: BASE_THEME.palette.common.white,
|
||||
borderColor: BASE_THEME.palette.grey[500],
|
||||
color: BASE_THEME.palette.text.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiToggleButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
paddingLeft: BASE_THEME.spacing(1.5),
|
||||
paddingRight: BASE_THEME.spacing(1.5)
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiInputBase: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&:not(.Mui-disabled, .Mui-error):before": {
|
||||
borderBottom: `1px solid ${BASE_THEME.palette.grey[400]}`
|
||||
},
|
||||
"&:hover:not(.Mui-disabled, .Mui-error):before": {
|
||||
borderBottom: `1px solid ${BASE_THEME.palette.grey[500]} !important`
|
||||
},
|
||||
"&:after": {
|
||||
borderBottom: `1px solid ${BASE_THEME.palette.text.primary} !important`
|
||||
},
|
||||
"&.MuiOutlinedInput-root:not(.Mui-error)": {
|
||||
"& fieldset": {
|
||||
borderColor: BASE_THEME.palette.grey[300],
|
||||
transition: "border-color 0.2s"
|
||||
}
|
||||
},
|
||||
"&.MuiOutlinedInput-root:not(.Mui-disabled, .Mui-error)": {
|
||||
"&:hover fieldset": {
|
||||
borderColor: BASE_THEME.palette.grey[400]
|
||||
},
|
||||
"&.Mui-focused fieldset": {
|
||||
borderColor: BASE_THEME.palette.text.secondary,
|
||||
borderWidth: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
input: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||
"&.Mui-disabled": {
|
||||
WebkitTextFillColor: "inherit",
|
||||
color: BASE_THEME.palette.text.secondary
|
||||
}
|
||||
},
|
||||
inputSizeSmall: {}
|
||||
}
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
notchedOutline: {
|
||||
"& legend": {
|
||||
fontSize: "0.85em",
|
||||
maxWidth: "100%"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiInputAdornment: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||
color: BASE_THEME.palette.text.secondary
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiInputLabel: {
|
||||
defaultProps: {
|
||||
shrink: true
|
||||
},
|
||||
styleOverrides: {
|
||||
shrink: {
|
||||
transform: "scale(0.85)",
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
"&.Mui-focused": {
|
||||
color: BASE_THEME.palette.text.primary
|
||||
},
|
||||
"&.MuiInputLabel-standard": {
|
||||
transform: "translate(0, -4px) scale(0.85)",
|
||||
color: "#4F4F4F"
|
||||
},
|
||||
"&.MuiInputLabel-outlined": {
|
||||
transform: "translate(15px, -8px) scale(0.85)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiTabs: {
|
||||
defaultProps: {
|
||||
variant: "scrollable"
|
||||
},
|
||||
styleOverrides: {
|
||||
indicator: {
|
||||
height: 1,
|
||||
backgroundColor: BASE_THEME.palette.text.primary
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: "none",
|
||||
minWidth: BASE_THEME.spacing(2),
|
||||
paddingLeft: BASE_THEME.spacing(1.5),
|
||||
paddingRight: BASE_THEME.spacing(1.5),
|
||||
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
lineHeight: 1.5,
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
transition: "color 0.2s",
|
||||
"&.Mui-selected": {
|
||||
color: BASE_THEME.palette.text.primary
|
||||
},
|
||||
"&:hover": {
|
||||
color: BASE_THEME.palette.text.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiCardHeader: {
|
||||
styleOverrides: {
|
||||
title: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(18),
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
typography: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
h1: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(40),
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: "-0.02em",
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||
},
|
||||
h2: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(32),
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: "-0.02em",
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||
},
|
||||
h3: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(24),
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: "-0.01em",
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||
},
|
||||
h4: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(20),
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: "-0.01em",
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||
},
|
||||
h5: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(18),
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: "-0.01em",
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||
},
|
||||
h6: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(16),
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: "-0.005em",
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||
},
|
||||
body1: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(14)
|
||||
},
|
||||
body2: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(12)
|
||||
},
|
||||
overline: {
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
letterSpacing: "0.05em"
|
||||
},
|
||||
button: {
|
||||
textTransform: "none",
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
lineHeight: 1.5
|
||||
},
|
||||
caption: {
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1.5
|
||||
}
|
||||
},
|
||||
shadows: [
|
||||
"none",
|
||||
"0px 4px 15px rgba(33, 36, 67, 0.04), 0px 0px 2px rgba(33, 36, 67, 0.04), 0px 0px 1px rgba(33, 36, 67, 0.04)",
|
||||
"0px 10px 20px rgba(33, 36, 67, 0.04), 0px 2px 6px rgba(33, 36, 67, 0.04), 0px 0px 1px rgba(33, 36, 67, 0.04)",
|
||||
"0px 16px 24px rgba(33, 36, 67, 0.05), 0px 2px 6px rgba(33, 36, 67, 0.05), 0px 0px 1px rgba(33, 36, 67, 0.05)",
|
||||
"0px 24px 32px rgba(33, 36, 67, 0.06), 0px 16px 24px rgba(33, 36, 67, 0.06), 0px 4px 8px rgba(33, 36, 67, 0.06)",
|
||||
...Array(20).fill("none")
|
||||
]
|
||||
});
|
||||
|
||||
export default THEME;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
|
||||
import { ColumnsContainer as BaseColumnsContainer } from "@usewaypoint/block-columns-container";
|
||||
|
||||
import { useCurrentBlockId } from "../../editor/EditorBlock";
|
||||
import { setDocument, setSelectedBlockId } from "../../editor/EditorContext";
|
||||
import EditorChildrenIds, {
|
||||
EditorChildrenChange
|
||||
} from "../helpers/EditorChildrenIds";
|
||||
|
||||
import ColumnsContainerPropsSchema, {
|
||||
ColumnsContainerProps
|
||||
} from "./ColumnsContainerPropsSchema";
|
||||
|
||||
const EMPTY_COLUMNS = [
|
||||
{ childrenIds: [] },
|
||||
{ childrenIds: [] },
|
||||
{ childrenIds: [] }
|
||||
];
|
||||
|
||||
export default function ColumnsContainerEditor({
|
||||
style,
|
||||
props
|
||||
}: ColumnsContainerProps) {
|
||||
const currentBlockId = useCurrentBlockId();
|
||||
|
||||
const { columns, ...restProps } = props ?? {};
|
||||
const columnsValue = columns ?? EMPTY_COLUMNS;
|
||||
|
||||
const updateColumn = (
|
||||
columnIndex: 0 | 1 | 2,
|
||||
{ block, blockId, childrenIds }: EditorChildrenChange
|
||||
) => {
|
||||
const nColumns = [...columnsValue];
|
||||
nColumns[columnIndex] = { childrenIds };
|
||||
setDocument({
|
||||
[blockId]: block,
|
||||
[currentBlockId]: {
|
||||
type: "ColumnsContainer",
|
||||
data: ColumnsContainerPropsSchema.parse({
|
||||
style,
|
||||
props: {
|
||||
...restProps,
|
||||
columns: nColumns
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
setSelectedBlockId(blockId);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseColumnsContainer
|
||||
props={restProps}
|
||||
style={style}
|
||||
columns={[
|
||||
<EditorChildrenIds
|
||||
childrenIds={columns?.[0]?.childrenIds}
|
||||
onChange={(change) => updateColumn(0, change)}
|
||||
/>,
|
||||
<EditorChildrenIds
|
||||
childrenIds={columns?.[1]?.childrenIds}
|
||||
onChange={(change) => updateColumn(1, change)}
|
||||
/>,
|
||||
<EditorChildrenIds
|
||||
childrenIds={columns?.[2]?.childrenIds}
|
||||
onChange={(change) => updateColumn(2, change)}
|
||||
/>
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ColumnsContainerPropsSchema as BaseColumnsContainerPropsSchema } from "@usewaypoint/block-columns-container";
|
||||
|
||||
const BasePropsShape = BaseColumnsContainerPropsSchema.shape.props
|
||||
.unwrap()
|
||||
.unwrap().shape;
|
||||
|
||||
const ColumnsContainerPropsSchema = z.object({
|
||||
style: BaseColumnsContainerPropsSchema.shape.style,
|
||||
props: z
|
||||
.object({
|
||||
...BasePropsShape,
|
||||
columns: z.tuple([
|
||||
z.object({ childrenIds: z.array(z.string()) }),
|
||||
z.object({ childrenIds: z.array(z.string()) }),
|
||||
z.object({ childrenIds: z.array(z.string()) })
|
||||
])
|
||||
})
|
||||
.optional()
|
||||
.nullable()
|
||||
});
|
||||
|
||||
export type ColumnsContainerProps = z.infer<typeof ColumnsContainerPropsSchema>;
|
||||
export default ColumnsContainerPropsSchema;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
import { Container as BaseContainer } from "@usewaypoint/block-container";
|
||||
|
||||
import { useCurrentBlockId } from "../../editor/EditorBlock";
|
||||
import {
|
||||
setDocument,
|
||||
setSelectedBlockId,
|
||||
useDocument
|
||||
} from "../../editor/EditorContext";
|
||||
import EditorChildrenIds from "../helpers/EditorChildrenIds";
|
||||
|
||||
import { ContainerProps } from "./ContainerPropsSchema";
|
||||
|
||||
export default function ContainerEditor({ style, props }: ContainerProps) {
|
||||
const childrenIds = props?.childrenIds ?? [];
|
||||
|
||||
const document = useDocument();
|
||||
const currentBlockId = useCurrentBlockId();
|
||||
|
||||
return (
|
||||
<BaseContainer style={style}>
|
||||
<EditorChildrenIds
|
||||
childrenIds={childrenIds}
|
||||
onChange={({ block, blockId, childrenIds }) => {
|
||||
setDocument({
|
||||
[blockId]: block,
|
||||
[currentBlockId]: {
|
||||
type: "Container",
|
||||
data: {
|
||||
...document[currentBlockId].data,
|
||||
props: { childrenIds: childrenIds }
|
||||
}
|
||||
}
|
||||
});
|
||||
setSelectedBlockId(blockId);
|
||||
}}
|
||||
/>
|
||||
</BaseContainer>
|
||||
);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ContainerPropsSchema as BaseContainerPropsSchema } from "@usewaypoint/block-container";
|
||||
|
||||
const ContainerPropsSchema = z.object({
|
||||
style: BaseContainerPropsSchema.shape.style,
|
||||
props: z
|
||||
.object({
|
||||
childrenIds: z.array(z.string()).optional().nullable()
|
||||
})
|
||||
.optional()
|
||||
.nullable()
|
||||
});
|
||||
|
||||
export default ContainerPropsSchema;
|
||||
|
||||
export type ContainerProps = z.infer<typeof ContainerPropsSchema>;
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import React from "react";
|
||||
|
||||
import { useCurrentBlockId } from "../../editor/EditorBlock";
|
||||
import {
|
||||
setDocument,
|
||||
setSelectedBlockId,
|
||||
useDocument
|
||||
} from "../../editor/EditorContext";
|
||||
import EditorChildrenIds from "../helpers/EditorChildrenIds";
|
||||
|
||||
import { EmailLayoutProps } from "./EmailLayoutPropsSchema";
|
||||
|
||||
function getFontFamily(fontFamily: EmailLayoutProps["fontFamily"]) {
|
||||
const f = fontFamily ?? "MODERN_SANS";
|
||||
switch (f) {
|
||||
case "MODERN_SANS":
|
||||
return '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif';
|
||||
case "BOOK_SANS":
|
||||
return 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif';
|
||||
case "ORGANIC_SANS":
|
||||
return 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif';
|
||||
case "GEOMETRIC_SANS":
|
||||
return 'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif';
|
||||
case "HEAVY_SANS":
|
||||
return 'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif';
|
||||
case "ROUNDED_SANS":
|
||||
return 'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif';
|
||||
case "MODERN_SERIF":
|
||||
return 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif';
|
||||
case "BOOK_SERIF":
|
||||
return '"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif';
|
||||
case "MONOSPACE":
|
||||
return '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace';
|
||||
}
|
||||
}
|
||||
|
||||
export default function EmailLayoutEditor(props: EmailLayoutProps) {
|
||||
const childrenIds = props.childrenIds ?? [];
|
||||
const canvasWidth = props.canvasWidth ?? 600;
|
||||
const document = useDocument();
|
||||
const currentBlockId = useCurrentBlockId();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
setSelectedBlockId(null);
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: props.backdropColor ?? "#F5F5F5",
|
||||
color: props.textColor ?? "#262626",
|
||||
fontFamily: getFontFamily(props.fontFamily),
|
||||
fontSize: "16px",
|
||||
fontWeight: "400",
|
||||
letterSpacing: "0.15008px",
|
||||
lineHeight: "1.5",
|
||||
margin: "0",
|
||||
padding: "32px 0",
|
||||
width: "100%",
|
||||
minHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
style={{
|
||||
margin: "0 auto",
|
||||
// maxWidth: "600px",
|
||||
maxWidth: `${canvasWidth}px`,
|
||||
backgroundColor: props.canvasColor ?? "#FFFFFF",
|
||||
borderRadius: props.borderRadius ?? undefined,
|
||||
border: (() => {
|
||||
const v = props.borderColor;
|
||||
if (!v) {
|
||||
return undefined;
|
||||
}
|
||||
return `1px solid ${v}`;
|
||||
})()
|
||||
}}
|
||||
role="presentation"
|
||||
cellSpacing="0"
|
||||
cellPadding="0"
|
||||
border={0}
|
||||
>
|
||||
<tbody>
|
||||
<tr style={{ width: "100%" }}>
|
||||
<td>
|
||||
<EditorChildrenIds
|
||||
childrenIds={childrenIds}
|
||||
onChange={({ block, blockId, childrenIds }) => {
|
||||
setDocument({
|
||||
[blockId]: block,
|
||||
[currentBlockId]: {
|
||||
type: "EmailLayout",
|
||||
data: {
|
||||
...document[currentBlockId].data,
|
||||
childrenIds: childrenIds
|
||||
}
|
||||
}
|
||||
});
|
||||
setSelectedBlockId(blockId);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const COLOR_SCHEMA = z
|
||||
.string()
|
||||
.regex(/^#[0-9a-fA-F]{6}$/)
|
||||
.nullable()
|
||||
.optional();
|
||||
|
||||
const FONT_FAMILY_SCHEMA = z
|
||||
.enum([
|
||||
"MODERN_SANS",
|
||||
"BOOK_SANS",
|
||||
"ORGANIC_SANS",
|
||||
"GEOMETRIC_SANS",
|
||||
"HEAVY_SANS",
|
||||
"ROUNDED_SANS",
|
||||
"MODERN_SERIF",
|
||||
"BOOK_SERIF",
|
||||
"MONOSPACE"
|
||||
])
|
||||
.nullable()
|
||||
.optional();
|
||||
|
||||
const EmailLayoutPropsSchema = z.object({
|
||||
backdropColor: COLOR_SCHEMA,
|
||||
borderColor: COLOR_SCHEMA,
|
||||
borderRadius: z.number().optional().nullable(),
|
||||
canvasColor: COLOR_SCHEMA,
|
||||
canvasWidth: z.number().optional().nullable(),
|
||||
textColor: COLOR_SCHEMA,
|
||||
fontFamily: FONT_FAMILY_SCHEMA,
|
||||
childrenIds: z.array(z.string()).optional().nullable()
|
||||
});
|
||||
|
||||
export default EmailLayoutPropsSchema;
|
||||
|
||||
export type EmailLayoutProps = z.infer<typeof EmailLayoutPropsSchema>;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Button, SxProps, Typography } from "@mui/material";
|
||||
|
||||
type BlockMenuButtonProps = {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const BUTTON_SX: SxProps = { p: 1.5, display: "flex", flexDirection: "column" };
|
||||
const ICON_SX: SxProps = {
|
||||
mb: 0.75,
|
||||
width: "100%",
|
||||
bgcolor: "cadet.200",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
p: 1,
|
||||
border: "1px solid",
|
||||
borderColor: "cadet.300"
|
||||
};
|
||||
|
||||
export default function BlockTypeButton({
|
||||
label,
|
||||
icon,
|
||||
onClick
|
||||
}: BlockMenuButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
sx={BUTTON_SX}
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<Box sx={ICON_SX}>{icon}</Box>
|
||||
<Typography variant="body2">{label}</Typography>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Menu } from "@mui/material";
|
||||
|
||||
import { TEditorBlock } from "../../../../editor/core";
|
||||
|
||||
import BlockButton from "./BlockButton";
|
||||
import { BUTTONS } from "./buttons";
|
||||
|
||||
type BlocksMenuProps = {
|
||||
anchorEl: HTMLElement | null;
|
||||
setAnchorEl: (v: HTMLElement | null) => void;
|
||||
onSelect: (block: TEditorBlock) => void;
|
||||
};
|
||||
export default function BlocksMenu({
|
||||
anchorEl,
|
||||
setAnchorEl,
|
||||
onSelect
|
||||
}: BlocksMenuProps) {
|
||||
const onClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const onClick = (block: TEditorBlock) => {
|
||||
onSelect(block);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
if (anchorEl === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
>
|
||||
<Box
|
||||
sx={{ p: 1, display: "grid", gridTemplateColumns: "1fr 1fr 1fr 1fr" }}
|
||||
>
|
||||
{BUTTONS.map((k, i) => (
|
||||
<BlockButton
|
||||
key={i}
|
||||
label={k.label}
|
||||
icon={k.icon}
|
||||
onClick={() => onClick(k.block())}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { AddOutlined } from "@mui/icons-material";
|
||||
import { Fade, IconButton } from "@mui/material";
|
||||
|
||||
type Props = {
|
||||
buttonElement: HTMLElement | null;
|
||||
onClick: () => void;
|
||||
};
|
||||
export default function DividerButton({ buttonElement, onClick }: Props) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function listener({ clientX, clientY }: MouseEvent) {
|
||||
if (!buttonElement) {
|
||||
return;
|
||||
}
|
||||
const rect = buttonElement.getBoundingClientRect();
|
||||
const rectY = rect.y;
|
||||
const bottomX = rect.x;
|
||||
const topX = bottomX + rect.width;
|
||||
|
||||
if (Math.abs(clientY - rectY) < 20) {
|
||||
if (bottomX < clientX && clientX < topX) {
|
||||
setVisible(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setVisible(false);
|
||||
}
|
||||
window.addEventListener("mousemove", listener);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", listener);
|
||||
};
|
||||
}, [buttonElement, setVisible]);
|
||||
|
||||
return (
|
||||
<Fade in={visible}>
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
p: 0.12,
|
||||
position: "absolute",
|
||||
top: "-12px",
|
||||
left: "50%",
|
||||
transform: "translateX(-10px)",
|
||||
bgcolor: "brand.blue",
|
||||
color: "primary.contrastText",
|
||||
zIndex: "fab",
|
||||
"&:hover, &:active, &:focus": {
|
||||
bgcolor: "brand.blue",
|
||||
color: "primary.contrastText"
|
||||
}
|
||||
}}
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<AddOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Fade>
|
||||
);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
|
||||
import { AddOutlined } from "@mui/icons-material";
|
||||
import { ButtonBase } from "@mui/material";
|
||||
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
};
|
||||
export default function PlaceholderButton({ onClick }: Props) {
|
||||
return (
|
||||
<ButtonBase
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignContent: "center",
|
||||
justifyContent: "center",
|
||||
height: 48,
|
||||
width: "100%",
|
||||
bgcolor: "rgba(0,0,0, 0.05)"
|
||||
}}
|
||||
>
|
||||
<AddOutlined
|
||||
sx={{
|
||||
p: 0.12,
|
||||
bgcolor: "brand.blue",
|
||||
borderRadius: 24,
|
||||
color: "primary.contrastText"
|
||||
}}
|
||||
fontSize="small"
|
||||
/>
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
AccountCircleOutlined,
|
||||
Crop32Outlined,
|
||||
HMobiledataOutlined,
|
||||
HorizontalRuleOutlined,
|
||||
HtmlOutlined,
|
||||
ImageOutlined,
|
||||
LibraryAddOutlined,
|
||||
NotesOutlined,
|
||||
SmartButtonOutlined,
|
||||
ViewColumnOutlined
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import { TEditorBlock } from "../../../../editor/core";
|
||||
|
||||
type TButtonProps = {
|
||||
label: string;
|
||||
icon: JSX.Element;
|
||||
block: () => TEditorBlock;
|
||||
};
|
||||
export const BUTTONS: TButtonProps[] = [
|
||||
{
|
||||
label: "Heading",
|
||||
icon: <HMobiledataOutlined />,
|
||||
block: () => ({
|
||||
type: "Heading",
|
||||
data: {
|
||||
props: { text: "Hello friend" },
|
||||
style: {
|
||||
padding: { top: 16, bottom: 16, left: 24, right: 24 }
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Text",
|
||||
icon: <NotesOutlined />,
|
||||
block: () => ({
|
||||
type: "Text",
|
||||
data: {
|
||||
props: { text: "My new text block" },
|
||||
style: {
|
||||
padding: { top: 16, bottom: 16, left: 24, right: 24 },
|
||||
fontWeight: "normal"
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
{
|
||||
label: "Button",
|
||||
icon: <SmartButtonOutlined />,
|
||||
block: () => ({
|
||||
type: "Button",
|
||||
data: {
|
||||
props: { text: "Button", url: window.location.origin },
|
||||
style: { padding: { top: 16, bottom: 16, left: 24, right: 24 } }
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Image",
|
||||
icon: <ImageOutlined />,
|
||||
block: () => ({
|
||||
type: "Image",
|
||||
data: {
|
||||
props: {
|
||||
url: "https://ui-avatars.com/api/?size=128",
|
||||
alt: "Sample product",
|
||||
contentAlignment: "middle",
|
||||
linkHref: null
|
||||
},
|
||||
style: { padding: { top: 16, bottom: 16, left: 24, right: 24 } }
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Avatar",
|
||||
icon: <AccountCircleOutlined />,
|
||||
block: () => ({
|
||||
type: "Avatar",
|
||||
data: {
|
||||
props: {
|
||||
imageUrl: "https://ui-avatars.com/api/?size=128",
|
||||
shape: "circle"
|
||||
},
|
||||
style: { padding: { top: 16, bottom: 16, left: 24, right: 24 } }
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Divider",
|
||||
icon: <HorizontalRuleOutlined />,
|
||||
block: () => ({
|
||||
type: "Divider",
|
||||
data: {
|
||||
style: { padding: { top: 16, right: 0, bottom: 16, left: 0 } },
|
||||
props: {
|
||||
lineColor: "#CCCCCC"
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Spacer",
|
||||
icon: <Crop32Outlined />,
|
||||
block: () => ({
|
||||
type: "Spacer",
|
||||
data: {}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Html",
|
||||
icon: <HtmlOutlined />,
|
||||
block: () => ({
|
||||
type: "Html",
|
||||
data: {
|
||||
props: { contents: "<strong>Hello world</strong>" },
|
||||
style: {
|
||||
fontSize: 16,
|
||||
textAlign: null,
|
||||
padding: { top: 16, bottom: 16, left: 24, right: 24 }
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Columns",
|
||||
icon: <ViewColumnOutlined />,
|
||||
block: () => ({
|
||||
type: "ColumnsContainer",
|
||||
data: {
|
||||
props: {
|
||||
columnsGap: 16,
|
||||
columnsCount: 3,
|
||||
columns: [
|
||||
{ childrenIds: [] },
|
||||
{ childrenIds: [] },
|
||||
{ childrenIds: [] }
|
||||
]
|
||||
},
|
||||
style: { padding: { top: 16, bottom: 16, left: 24, right: 24 } }
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Container",
|
||||
icon: <LibraryAddOutlined />,
|
||||
block: () => ({
|
||||
type: "Container",
|
||||
data: {
|
||||
style: { padding: { top: 16, bottom: 16, left: 24, right: 24 } }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// { label: 'ProgressBar', icon: <ProgressBarOutlined />, block: () => ({}) },
|
||||
// { label: 'LoopContainer', icon: <ViewListOutlined />, block: () => ({}) },
|
||||
];
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { TEditorBlock } from "../../../../editor/core";
|
||||
|
||||
import BlocksMenu from "./BlocksMenu";
|
||||
import DividerButton from "./DividerButton";
|
||||
import PlaceholderButton from "./PlaceholderButton";
|
||||
|
||||
type Props = {
|
||||
placeholder?: boolean;
|
||||
onSelect: (block: TEditorBlock) => void;
|
||||
};
|
||||
export default function AddBlockButton({ onSelect, placeholder }: Props) {
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const [buttonElement, setButtonElement] = useState<HTMLElement | null>(null);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
setMenuAnchorEl(buttonElement);
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
if (placeholder) {
|
||||
return <PlaceholderButton onClick={handleButtonClick} />;
|
||||
} else {
|
||||
return (
|
||||
<DividerButton
|
||||
buttonElement={buttonElement}
|
||||
onClick={handleButtonClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={setButtonElement} style={{ position: "relative" }}>
|
||||
{renderButton()}
|
||||
</div>
|
||||
<BlocksMenu
|
||||
anchorEl={menuAnchorEl}
|
||||
setAnchorEl={setMenuAnchorEl}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
import { TEditorBlock } from "../../../editor/core";
|
||||
import EditorBlock from "../../../editor/EditorBlock";
|
||||
|
||||
import AddBlockButton from "./AddBlockMenu";
|
||||
|
||||
export type EditorChildrenChange = {
|
||||
blockId: string;
|
||||
block: TEditorBlock;
|
||||
childrenIds: string[];
|
||||
};
|
||||
|
||||
function generateId() {
|
||||
return `block-${Date.now()}`;
|
||||
}
|
||||
|
||||
export type EditorChildrenIdsProps = {
|
||||
childrenIds: string[] | null | undefined;
|
||||
onChange: (val: EditorChildrenChange) => void;
|
||||
};
|
||||
export default function EditorChildrenIds({
|
||||
childrenIds,
|
||||
onChange
|
||||
}: EditorChildrenIdsProps) {
|
||||
const appendBlock = (block: TEditorBlock) => {
|
||||
const blockId = generateId();
|
||||
return onChange({
|
||||
blockId,
|
||||
block,
|
||||
childrenIds: [...(childrenIds || []), blockId]
|
||||
});
|
||||
};
|
||||
|
||||
const insertBlock = (block: TEditorBlock, index: number) => {
|
||||
const blockId = generateId();
|
||||
const newChildrenIds = [...(childrenIds || [])];
|
||||
newChildrenIds.splice(index, 0, blockId);
|
||||
return onChange({
|
||||
blockId,
|
||||
block,
|
||||
childrenIds: newChildrenIds
|
||||
});
|
||||
};
|
||||
|
||||
if (!childrenIds || childrenIds.length === 0) {
|
||||
return <AddBlockButton placeholder onSelect={appendBlock} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{childrenIds.map((childId, i) => (
|
||||
<Fragment key={childId}>
|
||||
<AddBlockButton onSelect={(block) => insertBlock(block, i)} />
|
||||
<EditorBlock id={childId} />
|
||||
</Fragment>
|
||||
))}
|
||||
<AddBlockButton onSelect={appendBlock} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export type TStyle = {
|
||||
backgroundColor?: any;
|
||||
borderColor?: any;
|
||||
borderRadius?: any;
|
||||
color?: any;
|
||||
fontFamily?: any;
|
||||
fontSize?: any;
|
||||
fontWeight?: any;
|
||||
padding?: any;
|
||||
textAlign?: any;
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import React, { CSSProperties, useState } from "react";
|
||||
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
import { useCurrentBlockId } from "../../../editor/EditorBlock";
|
||||
import {
|
||||
setSelectedBlockId,
|
||||
useSelectedBlockId
|
||||
} from "../../../editor/EditorContext";
|
||||
|
||||
import TuneMenu from "./TuneMenu";
|
||||
|
||||
type TEditorBlockWrapperProps = {
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export default function EditorBlockWrapper({
|
||||
children
|
||||
}: TEditorBlockWrapperProps) {
|
||||
const selectedBlockId = useSelectedBlockId();
|
||||
const [mouseInside, setMouseInside] = useState(false);
|
||||
const blockId = useCurrentBlockId();
|
||||
|
||||
let outline: CSSProperties["outline"];
|
||||
if (selectedBlockId === blockId) {
|
||||
outline = "2px solid rgba(0,121,204, 1)";
|
||||
} else if (mouseInside) {
|
||||
outline = "2px solid rgba(0,121,204, 0.3)";
|
||||
}
|
||||
|
||||
const renderMenu = () => {
|
||||
if (selectedBlockId !== blockId) {
|
||||
return null;
|
||||
}
|
||||
return <TuneMenu blockId={blockId} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
maxWidth: "100%",
|
||||
outlineOffset: "-1px",
|
||||
outline
|
||||
}}
|
||||
onMouseEnter={(ev) => {
|
||||
setMouseInside(true);
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setMouseInside(false);
|
||||
}}
|
||||
onClick={(ev) => {
|
||||
setSelectedBlockId(blockId);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
{renderMenu()}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import React, { CSSProperties } from "react";
|
||||
|
||||
import { TStyle } from "../TStyle";
|
||||
|
||||
type TReaderBlockWrapperProps = {
|
||||
style: TStyle;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export default function ReaderBlockWrapper({
|
||||
style,
|
||||
children
|
||||
}: TReaderBlockWrapperProps) {
|
||||
const { padding, borderColor, ...restStyle } = style;
|
||||
const cssStyle: CSSProperties = {
|
||||
...restStyle
|
||||
};
|
||||
|
||||
if (padding) {
|
||||
const { top, bottom, left, right } = padding;
|
||||
cssStyle.padding = `${top}px ${right}px ${bottom}px ${left}px`;
|
||||
}
|
||||
|
||||
if (borderColor) {
|
||||
cssStyle.border = `1px solid ${borderColor}`;
|
||||
}
|
||||
|
||||
return <div style={{ maxWidth: "100%", ...cssStyle }}>{children}</div>;
|
||||
}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
ArrowDownwardOutlined,
|
||||
ArrowUpwardOutlined,
|
||||
ContentCopyOutlined,
|
||||
DeleteOutlined
|
||||
} from "@mui/icons-material";
|
||||
import { IconButton, Paper, Stack, SxProps, Tooltip } from "@mui/material";
|
||||
|
||||
import { TEditorBlock, TEditorConfiguration } from "../../../editor/core";
|
||||
import {
|
||||
resetDocument,
|
||||
setSelectedBlockId,
|
||||
useDocument
|
||||
} from "../../../editor/EditorContext";
|
||||
import { ColumnsContainerProps } from "../../ColumnsContainer/ColumnsContainerPropsSchema";
|
||||
import cloneDocumentBlock from "../cloneDocumentBlock";
|
||||
|
||||
const sx: SxProps = {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: -56,
|
||||
borderRadius: 64,
|
||||
paddingX: 0.5,
|
||||
paddingY: 1,
|
||||
zIndex: "fab"
|
||||
};
|
||||
|
||||
function findParentBlockId(blockId: string, document: TEditorConfiguration) {
|
||||
for (const [id, b] of Object.entries(document)) {
|
||||
if (id === blockId) {
|
||||
continue;
|
||||
}
|
||||
const block = b as TEditorBlock;
|
||||
switch (block.type) {
|
||||
case "EmailLayout":
|
||||
if (block.data.childrenIds?.includes(blockId)) {
|
||||
return id;
|
||||
}
|
||||
break;
|
||||
case "Container":
|
||||
if (block.data.props?.childrenIds?.includes(blockId)) {
|
||||
return id;
|
||||
}
|
||||
break;
|
||||
case "ColumnsContainer":
|
||||
if (
|
||||
block.data.props?.columns?.some((col) =>
|
||||
col.childrenIds?.includes(blockId)
|
||||
)
|
||||
) {
|
||||
return id;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
blockId: string;
|
||||
};
|
||||
export default function TuneMenu({ blockId }: Props) {
|
||||
const document = useDocument();
|
||||
|
||||
const handleDuplicateClick = () => {
|
||||
const parentBlockId = findParentBlockId(blockId, document);
|
||||
|
||||
const { document: newDocument, blockId: newBlockId } = cloneDocumentBlock(
|
||||
document,
|
||||
blockId
|
||||
);
|
||||
|
||||
if (parentBlockId) {
|
||||
const parentBlock = newDocument[parentBlockId];
|
||||
switch (parentBlock.type) {
|
||||
case "EmailLayout": {
|
||||
if (!parentBlock.data.childrenIds) {
|
||||
parentBlock.data.childrenIds = [];
|
||||
}
|
||||
const index = parentBlock.data.childrenIds.indexOf(blockId);
|
||||
parentBlock.data.childrenIds.splice(index + 1, 0, newBlockId);
|
||||
break;
|
||||
}
|
||||
case "Container": {
|
||||
if (!parentBlock.data.props) {
|
||||
parentBlock.data.props = {};
|
||||
}
|
||||
if (!parentBlock.data.props.childrenIds) {
|
||||
parentBlock.data.props.childrenIds = [];
|
||||
}
|
||||
const index = parentBlock.data.props.childrenIds.indexOf(blockId);
|
||||
parentBlock.data.props.childrenIds.splice(index + 1, 0, newBlockId);
|
||||
break;
|
||||
}
|
||||
case "ColumnsContainer":
|
||||
if (!parentBlock.data.props) {
|
||||
parentBlock.data.props = {
|
||||
columns: [
|
||||
{ childrenIds: [] },
|
||||
{ childrenIds: [] },
|
||||
{ childrenIds: [] }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
for (const column of parentBlock.data.props.columns) {
|
||||
if (column.childrenIds.includes(blockId)) {
|
||||
const index = column.childrenIds.indexOf(blockId);
|
||||
column.childrenIds.splice(index + 1, 0, newBlockId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
resetDocument(newDocument);
|
||||
setSelectedBlockId(newBlockId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
const filterChildrenIds = (childrenIds: string[] | null | undefined) => {
|
||||
if (!childrenIds) {
|
||||
return childrenIds;
|
||||
}
|
||||
return childrenIds.filter((f) => f !== blockId);
|
||||
};
|
||||
const nDocument: typeof document = { ...document };
|
||||
for (const [id, b] of Object.entries(nDocument)) {
|
||||
const block = b as TEditorBlock;
|
||||
if (id === blockId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (block.type) {
|
||||
case "EmailLayout":
|
||||
nDocument[id] = {
|
||||
...block,
|
||||
data: {
|
||||
...block.data,
|
||||
childrenIds: filterChildrenIds(block.data.childrenIds)
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "Container":
|
||||
nDocument[id] = {
|
||||
...block,
|
||||
data: {
|
||||
...block.data,
|
||||
props: {
|
||||
...block.data.props,
|
||||
childrenIds: filterChildrenIds(block.data.props?.childrenIds)
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "ColumnsContainer":
|
||||
nDocument[id] = {
|
||||
type: "ColumnsContainer",
|
||||
data: {
|
||||
style: block.data.style,
|
||||
props: {
|
||||
...block.data.props,
|
||||
columns: block.data.props?.columns?.map((c) => ({
|
||||
childrenIds: filterChildrenIds(c.childrenIds)
|
||||
}))
|
||||
}
|
||||
} as ColumnsContainerProps
|
||||
};
|
||||
break;
|
||||
default:
|
||||
nDocument[id] = block;
|
||||
}
|
||||
}
|
||||
delete nDocument[blockId];
|
||||
resetDocument(nDocument);
|
||||
};
|
||||
|
||||
const handleMoveClick = (direction: "up" | "down") => {
|
||||
const moveChildrenIds = (ids: string[] | null | undefined) => {
|
||||
if (!ids) {
|
||||
return ids;
|
||||
}
|
||||
const index = ids.indexOf(blockId);
|
||||
if (index < 0) {
|
||||
return ids;
|
||||
}
|
||||
|
||||
const childrenIds = [...ids];
|
||||
if (direction === "up" && index > 0) {
|
||||
[childrenIds[index], childrenIds[index - 1]] = [
|
||||
childrenIds[index - 1],
|
||||
childrenIds[index]
|
||||
];
|
||||
} else if (direction === "down" && index < childrenIds.length - 1) {
|
||||
[childrenIds[index], childrenIds[index + 1]] = [
|
||||
childrenIds[index + 1],
|
||||
childrenIds[index]
|
||||
];
|
||||
}
|
||||
return childrenIds;
|
||||
};
|
||||
|
||||
const nDocument: typeof document = { ...document };
|
||||
for (const [id, b] of Object.entries(nDocument)) {
|
||||
const block = b as TEditorBlock;
|
||||
if (id === blockId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (block.type) {
|
||||
case "EmailLayout":
|
||||
nDocument[id] = {
|
||||
...block,
|
||||
data: {
|
||||
...block.data,
|
||||
childrenIds: moveChildrenIds(block.data.childrenIds)
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "Container":
|
||||
nDocument[id] = {
|
||||
...block,
|
||||
data: {
|
||||
...block.data,
|
||||
props: {
|
||||
...block.data.props,
|
||||
childrenIds: moveChildrenIds(block.data.props?.childrenIds)
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "ColumnsContainer":
|
||||
nDocument[id] = {
|
||||
type: "ColumnsContainer",
|
||||
data: {
|
||||
style: block.data.style,
|
||||
props: {
|
||||
...block.data.props,
|
||||
columns: block.data.props?.columns?.map((c) => ({
|
||||
childrenIds: moveChildrenIds(c.childrenIds)
|
||||
}))
|
||||
}
|
||||
} as ColumnsContainerProps
|
||||
};
|
||||
break;
|
||||
default:
|
||||
nDocument[id] = block;
|
||||
}
|
||||
}
|
||||
|
||||
resetDocument(nDocument);
|
||||
setSelectedBlockId(blockId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper sx={sx} onClick={(ev) => ev.stopPropagation()}>
|
||||
<Stack>
|
||||
<Tooltip title="Move up" placement="left-start">
|
||||
<IconButton
|
||||
onClick={() => handleMoveClick("up")}
|
||||
sx={{ color: "text.primary" }}
|
||||
>
|
||||
<ArrowUpwardOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Move down" placement="left-start">
|
||||
<IconButton
|
||||
onClick={() => handleMoveClick("down")}
|
||||
sx={{ color: "text.primary" }}
|
||||
>
|
||||
<ArrowDownwardOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Duplicate" placement="left-start">
|
||||
<IconButton
|
||||
onClick={handleDuplicateClick}
|
||||
sx={{ color: "text.primary" }}
|
||||
>
|
||||
<ContentCopyOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete" placement="left-start">
|
||||
<IconButton
|
||||
onClick={handleDeleteClick}
|
||||
sx={{ color: "text.primary" }}
|
||||
>
|
||||
<DeleteOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { TEditorBlock, TEditorConfiguration } from "../../editor/core";
|
||||
|
||||
type TResult = {
|
||||
document: TEditorConfiguration;
|
||||
blockId: string;
|
||||
};
|
||||
|
||||
function cloneChildrenIds(
|
||||
document: TEditorConfiguration,
|
||||
blockIds: string[]
|
||||
): string[] {
|
||||
return blockIds.map((blockId) => {
|
||||
const newBlock = cloneBlock(document, blockId);
|
||||
const newBlockId = `block-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
||||
document[newBlockId] = newBlock;
|
||||
return newBlockId;
|
||||
});
|
||||
}
|
||||
|
||||
function cloneBlock(
|
||||
document: TEditorConfiguration,
|
||||
blockId: string
|
||||
): TEditorBlock {
|
||||
const clone = structuredClone(document[blockId]);
|
||||
switch (clone.type) {
|
||||
case "EmailLayout":
|
||||
throw new Error("Cloning EmailLayout blocks is not supported.");
|
||||
case "Avatar":
|
||||
case "Button":
|
||||
case "Divider":
|
||||
case "Heading":
|
||||
case "Html":
|
||||
case "Image":
|
||||
case "Spacer":
|
||||
case "Text":
|
||||
return clone;
|
||||
case "Container":
|
||||
if (clone.data?.props?.childrenIds) {
|
||||
clone.data.props.childrenIds = cloneChildrenIds(
|
||||
document,
|
||||
clone.data.props.childrenIds
|
||||
);
|
||||
}
|
||||
return clone;
|
||||
case "ColumnsContainer":
|
||||
if (clone.data?.props?.columns) {
|
||||
clone.data.props.columns[0].childrenIds = cloneChildrenIds(
|
||||
document,
|
||||
clone.data.props.columns[0].childrenIds
|
||||
);
|
||||
clone.data.props.columns[1].childrenIds = cloneChildrenIds(
|
||||
document,
|
||||
clone.data.props.columns[1].childrenIds
|
||||
);
|
||||
clone.data.props.columns[2].childrenIds = cloneChildrenIds(
|
||||
document,
|
||||
clone.data.props.columns[2].childrenIds
|
||||
);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
export default function cloneDocumentBlock(
|
||||
originalDocument: TEditorConfiguration,
|
||||
originalBlockId: string
|
||||
): TResult {
|
||||
const document = { ...originalDocument };
|
||||
const blockId = `block-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
||||
document[blockId] = cloneBlock(document, originalBlockId);
|
||||
return {
|
||||
document,
|
||||
blockId
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
export const FONT_FAMILIES = [
|
||||
{
|
||||
key: "MODERN_SANS",
|
||||
label: "Modern sans",
|
||||
value: '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif'
|
||||
},
|
||||
{
|
||||
key: "BOOK_SANS",
|
||||
label: "Book sans",
|
||||
value: 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif'
|
||||
},
|
||||
{
|
||||
key: "ORGANIC_SANS",
|
||||
label: "Organic sans",
|
||||
value:
|
||||
'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif'
|
||||
},
|
||||
{
|
||||
key: "GEOMETRIC_SANS",
|
||||
label: "Geometric sans",
|
||||
value:
|
||||
'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif'
|
||||
},
|
||||
{
|
||||
key: "HEAVY_SANS",
|
||||
label: "Heavy sans",
|
||||
value:
|
||||
'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif'
|
||||
},
|
||||
{
|
||||
key: "ROUNDED_SANS",
|
||||
label: "Rounded sans",
|
||||
value:
|
||||
'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif'
|
||||
},
|
||||
{
|
||||
key: "MODERN_SERIF",
|
||||
label: "Modern serif",
|
||||
value: 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif'
|
||||
},
|
||||
{
|
||||
key: "BOOK_SERIF",
|
||||
label: "Book serif",
|
||||
value:
|
||||
'"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif'
|
||||
},
|
||||
{
|
||||
key: "MONOSPACE",
|
||||
label: "Monospace",
|
||||
value: '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace'
|
||||
}
|
||||
];
|
||||
|
||||
export const FONT_FAMILY_NAMES = [
|
||||
"MODERN_SANS",
|
||||
"BOOK_SANS",
|
||||
"ORGANIC_SANS",
|
||||
"GEOMETRIC_SANS",
|
||||
"HEAVY_SANS",
|
||||
"ROUNDED_SANS",
|
||||
"MODERN_SERIF",
|
||||
"BOOK_SERIF",
|
||||
"MONOSPACE"
|
||||
] as const;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { FONT_FAMILY_NAMES } from "./fontFamily";
|
||||
|
||||
export function zColor() {
|
||||
return z.string().regex(/^#[0-9a-fA-F]{6}$/);
|
||||
}
|
||||
|
||||
export function zFontFamily() {
|
||||
return z.enum(FONT_FAMILY_NAMES);
|
||||
}
|
||||
|
||||
export function zFontWeight() {
|
||||
return z.enum(["bold", "normal"]);
|
||||
}
|
||||
|
||||
export function zTextAlign() {
|
||||
return z.enum(["left", "center", "right"]);
|
||||
}
|
||||
|
||||
export function zPadding() {
|
||||
return z.object({
|
||||
top: z.number(),
|
||||
bottom: z.number(),
|
||||
right: z.number(),
|
||||
left: z.number()
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { createContext, useContext } from "react";
|
||||
|
||||
import { EditorBlock as CoreEditorBlock } from "./core";
|
||||
import { useDocument } from "./EditorContext";
|
||||
|
||||
const EditorBlockContext = createContext<string | null>(null);
|
||||
export const useCurrentBlockId = () => useContext(EditorBlockContext)!;
|
||||
|
||||
type EditorBlockProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id - Block id
|
||||
* @returns EditorBlock component that loads data from the EditorDocumentContext
|
||||
*/
|
||||
export default function EditorBlock({ id }: EditorBlockProps) {
|
||||
const document = useDocument();
|
||||
const block = document[id];
|
||||
if (!block) {
|
||||
throw new Error("Could not find block");
|
||||
}
|
||||
return (
|
||||
<EditorBlockContext.Provider value={id}>
|
||||
<CoreEditorBlock {...block} />
|
||||
</EditorBlockContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
import getConfiguration from "../../getConfiguration";
|
||||
|
||||
import { TEditorConfiguration } from "./core";
|
||||
|
||||
type TValue = {
|
||||
document: TEditorConfiguration;
|
||||
|
||||
selectedBlockId: string | null;
|
||||
selectedSidebarTab: "block-configuration" | "styles";
|
||||
selectedMainTab: "editor" | "preview" | "json" | "html";
|
||||
selectedScreenSize: "desktop" | "mobile";
|
||||
|
||||
inspectorDrawerOpen: boolean;
|
||||
samplesDrawerOpen: boolean;
|
||||
};
|
||||
|
||||
const editorStateStore = create<TValue>(() => ({
|
||||
document: getConfiguration(window.location.hash),
|
||||
selectedBlockId: null,
|
||||
selectedSidebarTab: "styles",
|
||||
selectedMainTab: "editor",
|
||||
selectedScreenSize: "desktop",
|
||||
|
||||
inspectorDrawerOpen: true,
|
||||
samplesDrawerOpen: true
|
||||
}));
|
||||
|
||||
export function useDocument() {
|
||||
return editorStateStore((s) => s.document);
|
||||
}
|
||||
|
||||
export function useSelectedBlockId() {
|
||||
return editorStateStore((s) => s.selectedBlockId);
|
||||
}
|
||||
|
||||
export function useSelectedScreenSize() {
|
||||
return editorStateStore((s) => s.selectedScreenSize);
|
||||
}
|
||||
|
||||
export function useSelectedMainTab() {
|
||||
return editorStateStore((s) => s.selectedMainTab);
|
||||
}
|
||||
|
||||
export function setSelectedMainTab(selectedMainTab: TValue["selectedMainTab"]) {
|
||||
return editorStateStore.setState({ selectedMainTab });
|
||||
}
|
||||
|
||||
export function useSelectedSidebarTab() {
|
||||
return editorStateStore((s) => s.selectedSidebarTab);
|
||||
}
|
||||
|
||||
export function useInspectorDrawerOpen() {
|
||||
return editorStateStore((s) => s.inspectorDrawerOpen);
|
||||
}
|
||||
|
||||
export function useSamplesDrawerOpen() {
|
||||
return editorStateStore((s) => s.samplesDrawerOpen);
|
||||
}
|
||||
|
||||
export function setSelectedBlockId(selectedBlockId: TValue["selectedBlockId"]) {
|
||||
const selectedSidebarTab =
|
||||
selectedBlockId === null ? "styles" : "block-configuration";
|
||||
const options: Partial<TValue> = {};
|
||||
if (selectedBlockId !== null) {
|
||||
options.inspectorDrawerOpen = true;
|
||||
}
|
||||
return editorStateStore.setState({
|
||||
selectedBlockId,
|
||||
selectedSidebarTab,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
export function setSidebarTab(
|
||||
selectedSidebarTab: TValue["selectedSidebarTab"]
|
||||
) {
|
||||
return editorStateStore.setState({ selectedSidebarTab });
|
||||
}
|
||||
|
||||
export function resetDocument(document: TValue["document"]) {
|
||||
return editorStateStore.setState({
|
||||
document,
|
||||
selectedSidebarTab: "styles",
|
||||
selectedBlockId: null
|
||||
});
|
||||
}
|
||||
|
||||
export function setDocument(document: TValue["document"]) {
|
||||
const originalDocument = editorStateStore.getState().document;
|
||||
return editorStateStore.setState({
|
||||
document: {
|
||||
...originalDocument,
|
||||
...document
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleInspectorDrawerOpen() {
|
||||
const inspectorDrawerOpen = !editorStateStore.getState().inspectorDrawerOpen;
|
||||
return editorStateStore.setState({ inspectorDrawerOpen });
|
||||
}
|
||||
|
||||
export function toggleSamplesDrawerOpen() {
|
||||
const samplesDrawerOpen = !editorStateStore.getState().samplesDrawerOpen;
|
||||
return editorStateStore.setState({ samplesDrawerOpen });
|
||||
}
|
||||
|
||||
export function setSelectedScreenSize(
|
||||
selectedScreenSize: TValue["selectedScreenSize"]
|
||||
) {
|
||||
return editorStateStore.setState({ selectedScreenSize });
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user