Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fff699202b |
@@ -241,15 +241,6 @@
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ehowe",
|
||||
"name": "Eugene Howe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/756984?v=4",
|
||||
"profile": "https://github.com/ehowe",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,46 +21,34 @@ jobs:
|
||||
dockerfile: apps/OpenSign/Dockerhubfile
|
||||
- image: opensign/opensignserver
|
||||
dockerfile: apps/OpenSignServer/Dockerhubfile
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# 👇 Enable emulation so we can build for arm64 on amd64 runners
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
# 👇 Enable Buildx for multi-arch builds
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
-
|
||||
name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ matrix.image }}
|
||||
|
||||
- name: Debug - List files opensign
|
||||
-
|
||||
name: Debug - List files opensign
|
||||
run: ls -R apps/OpenSign/ # Adjust the path as needed
|
||||
|
||||
- name: Debug - List files opensignserver
|
||||
-
|
||||
name: Debug - List files opensignserver
|
||||
run: ls -R apps/OpenSignServer/ # Adjust the path as needed
|
||||
|
||||
- name: Build and push (multi-arch)
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
push: true
|
||||
# 👇 Build for both AMD64 & ARM64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -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/v1.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/opensign-api-v-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'>
|
||||
@@ -165,7 +165,6 @@ We would like to thank all our contributors and users for their support and feed
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/daniel-mutwiri"><img src="https://avatars.githubusercontent.com/u/8936960?v=4?s=100" width="100px;" alt="Daniel Mutwiri"/><br /><sub><b>Daniel Mutwiri</b></sub></a><br /><a href="#code-daniel-mutwiri" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zathiel"><img src="https://avatars.githubusercontent.com/u/26553418?v=4?s=100" width="100px;" alt="Zathiel"/><br /><sub><b>Zathiel</b></sub></a><br /><a href="#code-Zathiel" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1024mb"><img src="https://avatars.githubusercontent.com/u/9301204?v=4?s=100" width="100px;" alt="1024mb"/><br /><sub><b>1024mb</b></sub></a><br /><a href="#translation-1024mb" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ehowe"><img src="https://avatars.githubusercontent.com/u/756984?v=4?s=100" width="100px;" alt="Eugene Howe"/><br /><sub><b>Eugene Howe</b></sub></a><br /><a href="#code-ehowe" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -21,7 +21,9 @@ 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
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="The fastest way to sign PDFs & request signatures from others" />
|
||||
<!-- <link rel="apple-touch-icon" href="/logo192.png" /> -->
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
|
||||
<title>...Loading</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root" style="touch-action:pan-x pan-y;"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,88 +1,67 @@
|
||||
{
|
||||
"name": "open_sign",
|
||||
"version": "2.37.0",
|
||||
"version": "2.21.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@imgly/background-removal": "^1.7.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.19.0",
|
||||
"@mui/icons-material": "^5.15.10",
|
||||
"@mui/material": "^5.15.10",
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@imgly/background-removal": "^1.6.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.14.2",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@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.18.0",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"axios": "^1.10.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dompurify": "^3.4.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"highlight.js": "^11.11.1",
|
||||
"i18next": "^26.0.8",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-http-backend": "^3.0.6",
|
||||
"i18next": "^25.3.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"juice": "^11.1.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"parse": "^8.1.0",
|
||||
"parse": "^6.1.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pkijs": "^3.4.0",
|
||||
"prettier": "^3.8.3",
|
||||
"pkijs": "^3.0.8",
|
||||
"print-js": "^1.6.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"process": "^0.11.10",
|
||||
"quill-html-edit-button": "^3.0.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.5",
|
||||
"radix-ui": "^1.4.2",
|
||||
"react": "^18.3.1",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-confetti": "^6.4.0",
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-datepicker": "^8.4.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dnd-multi-backend": "^9.0.0",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-dnd-touch-backend": "^16.0.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-gtm-module": "^2.0.11",
|
||||
"react-i18next": "^17.0.6",
|
||||
"react-konva": "^19.2.3",
|
||||
"react-pdf": "^10.4.1",
|
||||
"react-quill-new": "^3.8.3",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-pdf": "^9.2.1",
|
||||
"react-quill-new": "^3.4.6",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-rnd": "^10.5.3",
|
||||
"react-router": "^7.18.2",
|
||||
"react-rnd": "^10.5.2",
|
||||
"react-router": "^7.6.3",
|
||||
"react-scrollbars-custom": "^4.1.1",
|
||||
"react-select": "^5.10.2",
|
||||
"react-select": "^5.10.1",
|
||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||
"react-timezone-select": "^3.3.3",
|
||||
"react-to-print": "^3.3.0",
|
||||
"react-tooltip": "^5.30.1",
|
||||
"react-timezone-select": "^3.2.8",
|
||||
"react-tooltip": "^5.29.1",
|
||||
"reactour": "^1.19.4",
|
||||
"redux": "^5.0.1",
|
||||
"regex-parser": "^2.3.1",
|
||||
"web-vitals": "^5.2.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.5.1"
|
||||
"serve": "^14.2.4",
|
||||
"styled-components": "^5.3.11",
|
||||
"web-vitals": "^5.0.3",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||
"start-dev": "vite",
|
||||
"dev": "vite",
|
||||
"preview": "vite preview",
|
||||
"start": "node server.cjs",
|
||||
"start": "serve -s build",
|
||||
"version": "curl -s https://api.github.com/repos/opensignlabs/opensign/releases/latest | grep '\"tag_name\":' | awk -F '\"' '{print $4}' > ./public/version.txt",
|
||||
"version-win": "powershell -Command \"Invoke-RestMethod -Uri 'https://api.github.com/repos/opensignlabs/opensign/releases/latest' | Select-Object -ExpandProperty tag_name | Out-File -FilePath ./public/version.txt\"",
|
||||
"build-win": "npm run version-win && vite build",
|
||||
@@ -115,49 +94,38 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.2",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/runtime-corejs2": "^7.29.2",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@babel/core": "^7.27.7",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/runtime-corejs2": "^7.27.6",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@vitest/ui": "^4.1.5",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"babel-loader": "^9.2.1",
|
||||
"@types/react": "^18.3.23",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-loader": "^10.0.0",
|
||||
"commitizen": "^4.3.1",
|
||||
"concurrently": "^10.0.4",
|
||||
"css-loader": "^7.1.4",
|
||||
"concurrently": "^9.2.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"daisyui": "^4.12.24",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"dotenv": "^16.6.1",
|
||||
"eslint": "^9.30.0",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jsdom": "^29.0.2",
|
||||
"lint-staged": "^16.4.0",
|
||||
"postcss": "^8.5.23",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.2",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"tmp": "^0.2.7",
|
||||
"vite": ">=8.0.16",
|
||||
"vite-plugin-svgr": "^5.2.0",
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vitest": "^4.1.5"
|
||||
},
|
||||
"overrides": {
|
||||
"brace-expansion": ">=2.1.2",
|
||||
"tmp": "$tmp",
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom",
|
||||
"form-data": ">=4.0.6",
|
||||
"js-yaml": "^3.15.0",
|
||||
"undici": "^7.29.0",
|
||||
"fast-uri": "^3.1.5",
|
||||
"nanoid": ">=3.3.16",
|
||||
"ws": ">=8.21.0",
|
||||
"protobufjs": ">=7.6.5"
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || 22"
|
||||
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -26,8 +26,6 @@
|
||||
"Name": "नाम",
|
||||
"Date": "दिनांक"
|
||||
},
|
||||
"folder": "फ़ोल्डर",
|
||||
"pdf": "पीडीएफ़",
|
||||
"context-menu": {
|
||||
"Download": "डाउनलोड करें",
|
||||
"Rename": "नाम बदलें",
|
||||
@@ -46,10 +44,6 @@
|
||||
"contact-now": "अभी संपर्क करें",
|
||||
"upgrade-to": "इसमें अपग्रेड करें",
|
||||
"plan": "योजना",
|
||||
"connect": "कनेक्ट करें",
|
||||
"connect-to-g-drive": "Google Drive से कनेक्ट करें",
|
||||
"reconnect-to-g-drive": "Google Drive से फिर से कनेक्ट करें",
|
||||
"gdrive-info-connect": "जब Google Drive जुड़ा होता है, तो पूर्ण दस्तावेज़ {{appName}} फ़ोल्डर में Google Drive पर सहेजा जाएगा।",
|
||||
"subscription-renew-warning": "आपकी सदस्यता {{remainingDays}} दिनों में समाप्त हो जाएगी। कृपया अपनी सदस्यता नवीनीकृत करें।",
|
||||
"subscribe-card-teamplan": "सहयोग की पूरी शक्ति को अनलॉक करें! असीमित संगठन, टीम और पदानुक्रम बनाएं। टीमों में टेम्पलेट को निर्बाध रूप से साझा करें और कस्टम उपयोगकर्ता भूमिकाएँ असाइन करें। आज ही अपने वर्कफ़्लो को उन्नत करें!",
|
||||
"subscribe-card-plan": "केवल {{premiumPrice}}/माह से शुरू होने वाली प्रीमियम सुविधाओं को अनलॉक करें। अपने शामिल प्रीमियम क्रेडिट के बाद प्रति अतिरिक्त क्रेडिट केवल {{addonPrice}} पर बढ़ी हुई प्रदर्शन का आनंद लें।",
|
||||
@@ -64,18 +58,17 @@
|
||||
"welcome": "वापसी पर स्वागत है!",
|
||||
"Login-to-your-account": "अपने खाते में प्रवेश करे",
|
||||
"password": "पासवर्ड",
|
||||
"forgot-password": "पासवर्ड भूल गए",
|
||||
"forgot-password": "पासवर्ड भूल गए?",
|
||||
"loading": "लोड हो रहा है...",
|
||||
"of": "का",
|
||||
"sign-SSO": "SSO के साथ साइन इन करें",
|
||||
"login-page": "लॉगिन पृष्ठ",
|
||||
"or": "या",
|
||||
"signup-page": "साइनअप पृष्ठ",
|
||||
"password-match-length": "नया पासवर्ड और पुष्टि पासवर्ड मेल खाना चाहिए",
|
||||
"password-length": "पासवर्ड 8 वर्ण लंबा होना चाहिए",
|
||||
"password-case": "पासवर्ड में अपरकेस अक्षर, लोअरकेस अक्षर, अंक होना चाहिए",
|
||||
"password-special-char": " पासवर्ड में विशेष वर्ण होना चाहिए",
|
||||
"agree": "मैं सहमत हूँ",
|
||||
"agreee": "मैं सहमत हूँ",
|
||||
"term": "सेवा की शर्तें",
|
||||
"subscribe-to-opensign": "OpenSign न्यूज़लेटर की सदस्यता लें",
|
||||
"register": "पंजीकृत करें",
|
||||
@@ -121,8 +114,7 @@
|
||||
"Teams": "टीमें",
|
||||
"Team-update-successfully": "टीम सफलतापूर्वक अपडेट की गई।",
|
||||
"Users": "उपयोगकर्ता"
|
||||
},
|
||||
"Global SMTP": "ग्लोबल SMTP"
|
||||
}
|
||||
},
|
||||
"dashboard-card": {
|
||||
"Need your Signature": "आपके हस्ताक्षर की आवश्यकता है",
|
||||
@@ -167,11 +159,7 @@
|
||||
"daily-mail-quota": "दैनिक ईमेल कोटा",
|
||||
"Save as template": "टेम्पलेट के रूप में सहेजें",
|
||||
"Fix & resend": "ठीक करें और पुनः भेजें",
|
||||
"Kiosk Mode": "कियोस्क मोड",
|
||||
"Reset password": "पासवर्ड रीसेट करें",
|
||||
"View formdata": "फॉर्म डेटा देखें",
|
||||
"Download all formdata": "सभी फॉर्म डेटा डाउनलोड करें",
|
||||
"API credits": "API क्रेडिट्स"
|
||||
"Kiosk Mode": "कियोस्क मोड"
|
||||
},
|
||||
"report-heading": {
|
||||
"Sr.No": "क्रमांक",
|
||||
@@ -193,21 +181,7 @@
|
||||
"created-date": "निर्माण तिथि",
|
||||
"Type": "प्रकार",
|
||||
"Logs": "लॉग",
|
||||
"Expiry-date": "समाप्ति तिथि",
|
||||
"Company": "कंपनी",
|
||||
"JobTitle": "पद",
|
||||
"Time to complete (Days)": "पूरा करने का समय (दिन)",
|
||||
"Auto reminder": "स्वचालित अनुस्मारक",
|
||||
"Remind once in every (Days)": "हर (दिन) में एक बार याद दिलाएं",
|
||||
"Enable OTP verification": "OTP सत्यापन सक्षम करें",
|
||||
"Enable Tour": "टूर सक्षम करें",
|
||||
"Notify on signatures": "हस्ताक्षर पर सूचित करें",
|
||||
"Allow modifications": "संशोधन की अनुमति दें",
|
||||
"Redirect url": "रीडायरेक्ट URL",
|
||||
"Created Date": "निर्माण तिथि",
|
||||
"Updated Date": "अद्यतन तिथि",
|
||||
"Expiry Date": "समाप्ति तिथि",
|
||||
"Sent Date": "भेजे जाने की तिथि"
|
||||
"Expiry-date": "समाप्ति तिथि"
|
||||
},
|
||||
"report-help": {
|
||||
"Draft Documents": "ये वे दस्तावेज़ हैं जिन्हें आपने शुरू तो किया है लेकिन भेजने के लिए अंतिम रूप नहीं दिया है।",
|
||||
@@ -219,15 +193,16 @@
|
||||
"Contactbook": "यह आपके द्वारा जोड़े गए संपर्कों/हस्ताक्षरकर्ताओं की सूची है। जब आप किसी नए दस्तावेज़ में हस्ताक्षरकर्ताओं को जोड़ने का प्रयास करेंगे तो ये सुझावों के रूप में दिखाई देंगे।",
|
||||
"Templates": "यह उन टेम्पलेट्स की सूची है जो दस्तावेज़ बनाने के लिए आपके लिए उपलब्ध हैं। आप टेम्पलेट का उपयोग करके एक नया दस्तावेज़ बनाने के लिए 'उपयोग करें' बटन पर क्लिक कर सकते हैं, दस्तावेज़ को संशोधित कर सकते हैं और अगले चरण में हस्ताक्षरकर्ताओं को जोड़ सकते हैं।"
|
||||
},
|
||||
"Sign Yourself": "स्वयं हस्ताक्षर करें",
|
||||
"Request Signatures": "हस्ताक्षर का अनुरोध करें",
|
||||
"New Template": "नया टेम्पलेट",
|
||||
"form-name": {
|
||||
"Sign Yourself": "स्वयं हस्ताक्षर करें",
|
||||
"Request Signatures": "हस्ताक्षर का अनुरोध करें",
|
||||
"New Template": "नया टेम्पलेट"
|
||||
},
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"files-selected": "फ़ाइलें चयनित",
|
||||
"file-selected": "फ़ाइल चयनित",
|
||||
"template-title": "टेम्पलेट शीर्षक",
|
||||
"document-title": "दस्तावेज़ शीर्षक",
|
||||
"title": "शीर्षक",
|
||||
"description": "विवरण",
|
||||
"time-to-complete": "पूरा करने का समय (दिन)",
|
||||
"send-in-order": "क्रम से भेजें",
|
||||
@@ -237,12 +212,11 @@
|
||||
"p3": "इस विकल्प का चयन करने पर हस्ताक्षर लिंक सभी हस्ताक्षरकर्ताओं को एक साथ भेजे जाएंगे। प्रत्येक हस्ताक्षरकर्ता अपनी सुविधानुसार दस्तावेज़ पर हस्ताक्षर कर सकता है, भले ही अन्य हस्ताक्षरकर्ताओं ने अपने हस्ताक्षर पूरे कर लिए हों या नहीं। यह विधि तेज़ है लेकिन प्रतिभागियों के बीच किसी भी हस्ताक्षर क्रम को लागू नहीं करती है।",
|
||||
"p4": "वह विकल्प चुनें जो आपके दस्तावेज़ प्रसंस्करण की आवश्यकताओं के लिए सबसे उपयुक्त हो।"
|
||||
},
|
||||
"strict-order": "सख्त क्रम लागू करें",
|
||||
"strict-order-help": "जब सक्षम होता है, तो कोई भी हस्ताक्षरकर्ता तब तक हस्ताक्षर या अनुमोदन नहीं कर सकता जब तक कि पिछले सभी हस्ताक्षरकर्ता/अनुमोदक अपनी कार्रवाई पूरी न कर लें।",
|
||||
"no": "नहीं",
|
||||
"auto-reminder": "स्वतः अनुस्मारक",
|
||||
"remind-once": " प्रत्येक (दिन) में एक बार याद दिलाएं",
|
||||
"next": "अगला",
|
||||
"select-folder": "फ़ोल्डर चुनें",
|
||||
"OpenSign-drive": "{{appName}} ड्राइव",
|
||||
"drive-document-status": {
|
||||
"Completed": "पूरा हुआ",
|
||||
@@ -262,7 +236,6 @@
|
||||
"signers": "हस्ताक्षरकर्ता",
|
||||
"signers-help": "अपने सहेजे गए संपर्कों से सुझाए गए हस्ताक्षरकर्ताओं को देखने के लिए किसी संपर्क का नाम टाइप करना शुरू करें या नए जोड़ें। वांछित क्रम में हस्ताक्षरकर्ताओं को जोड़कर हस्ताक्षर क्रम की व्यवस्था करें। हस्ताक्षरकर्ताओं को शामिल करने के लिए '+' बटन और उन्हें हटाने के लिए 'x' का उपयोग करें। प्रत्येक हस्ताक्षरकर्ता को सूचीबद्ध क्रम में दस्तावेज़ पर हस्ताक्षर करने के लिए एक ईमेल संकेत प्राप्त होगा।",
|
||||
"bcc-help": "अपने सहेजे गए संपर्कों से सुझाव देखने के लिए किसी संपर्क का नाम टाइप करना शुरू करें या नए जोड़ें। किसी उपयोगकर्ता को जोड़ने के लिए '+' बटन और उसे हटाने के लिए 'x' का उपयोग करें। चयनित उपयोगकर्ता का ईमेल Bcc (ब्लाइंड कार्बन कॉपी) के रूप में जोड़ा जाएगा। दस्तावेज़ पूरा होने के बाद प्रत्येक उपयोगकर्ता को एक ईमेल सूचना प्राप्त होगी।",
|
||||
"cc-help": "सुझाव देखने के लिए अपने सेव किए गए कॉन्टैक्ट्स में से किसी कॉन्टैक्ट का नाम टाइप करना शुरू करें या नए जोड़ें। यूज़र जोड़ने के लिए '+' बटन और हटाने के लिए 'x' का उपयोग करें। चुने गए यूज़र का ईमेल CC (कार्बन कॉपी) के रूप में जोड़ा जाएगा। दस्तावेज़ पूर्ण होने पर प्रत्येक यूज़र को एक ईमेल सूचना मिलेगी।",
|
||||
"add-signer": "हस्ताक्षरकर्ता जोड़ें",
|
||||
"contact-not-found": "संपर्क नहीं मिला",
|
||||
"add-yourself": "स्वयं को जोड़ें",
|
||||
@@ -272,7 +245,7 @@
|
||||
"signature": "हस्ताक्षर",
|
||||
"upload-image": " छवि अपलोड करें",
|
||||
"clear": "स्पष्ट",
|
||||
"upload-signature/Image": "कृपया हस्ताक्षर/छवि अपलोड करें/मोहर",
|
||||
"upload-signature/Image": "कृपया हस्ताक्षर/छवि अपलोड करें",
|
||||
"initials": "हस्ताक्षर",
|
||||
"API": "एपीआई",
|
||||
"api-token": "एपीआई टोकन",
|
||||
@@ -284,15 +257,13 @@
|
||||
"yes": "हाँ",
|
||||
"copied": "कॉपी किया गया",
|
||||
"something-went-wrong-mssg": "कुछ गलत हो गया, इस पृष्ठ को रीफ्रेश करने से यह समस्या हल हो सकती है।",
|
||||
"attachments-docx-upload-failed": "फिलहाल एक समस्या आ रही है। कृपया पुनः प्रयास करें।",
|
||||
"attachments-docx-upload-limit": "बड़ी DOCX फ़ाइलों के लिए अपलोड सीमा को पार कर लिया गया है। कृपया 4 मिनट तक प्रतीक्षा करें और फिर प्रयास करें।",
|
||||
"token-generated": "टोकन सफलतापूर्वक उत्पन्न हुआ।",
|
||||
"webhook": "वेबहूक",
|
||||
"update-webhook": "वेबहूक अपडेट करें",
|
||||
"add-webhook": "वेबहूक जोड़ें",
|
||||
"webhook-added": "वेबहूक सफलतापूर्वक जोड़ा गया।",
|
||||
"team-disabled": "टीम अक्षम",
|
||||
"team-enabled": "टीम सक्षम",
|
||||
"Team enabled": "टीम सक्षम",
|
||||
"are-you-sure": "क्या आप वाकई चाहते हैं",
|
||||
"disable": "अक्षम करें",
|
||||
"enable": "सक्षम करें",
|
||||
@@ -300,7 +271,7 @@
|
||||
"edit-team": "टीम संपादित करें",
|
||||
"name-of-team": "टीम का नाम",
|
||||
"prev": "पिछला",
|
||||
"no-data-available": "कोई डेटा उपलब्ध नहीं है",
|
||||
"no-data-avaliable": "कोई डेटा उपलब्ध नहीं है",
|
||||
"add-team": "टीम जोड़ें",
|
||||
"page-not-found": "पृष्ठ नहीं मिला",
|
||||
"users-from-teams": "टीमों से उपयोगकर्ता",
|
||||
@@ -311,37 +282,12 @@
|
||||
"delete-user": "उपयोगकर्ता हटाएं",
|
||||
"delete": "हटाएं",
|
||||
"add-user": "उपयोगकर्ता जोड़ें",
|
||||
"password-generated": "पासवर्ड केवल एक बार उत्पन्न होगा; इसे कॉपी करना सुनिश्चित करें।",
|
||||
"password-generateed": "पासवर्ड केवल एक बार उत्पन्न होगा; इसे कॉपी करना सुनिश्चित करें।",
|
||||
"Team status": "टीम स्थिति",
|
||||
"user-deactivated": "उपयोगकर्ता निष्क्रिय।",
|
||||
"user-activated": "उपयोगकर्ता सक्रिय।",
|
||||
"public": "सार्वजनिक",
|
||||
"download": "डाउनलोड करें",
|
||||
"sign-offline": "ऑफ़लाइन हस्ताक्षर करें",
|
||||
"sign-offline-instructions": "दस्तावेज़ डाउनलोड करें, इसे किसी बाहरी टूल से हस्ताक्षरित करें, फिर यहाँ हस्ताक्षरित प्रति अपलोड करें। दस्तावेज़ का मालिक आपकी सबमिशन की समीक्षा करके उसे स्वीकृत या अस्वीकार करेगा।",
|
||||
"sign-offline-step-1": "बिना हस्ताक्षर का PDF डाउनलोड करें।",
|
||||
"sign-offline-step-2": "अपने पसंदीदा टूल से PDF हस्ताक्षरित करें।",
|
||||
"sign-offline-step-3": "मालिक की स्वीकृति के लिए हस्ताक्षरित PDF अपलोड करें।",
|
||||
"sign-offline-upload-hint": "स्वीकृति के लिए सबमिट करने हेतु हस्ताक्षरित PDF चुनें।",
|
||||
"sign-offline-submitted-title": "स्वीकृति के लिए सबमिट किया गया",
|
||||
"sign-offline-submitted-body": "दस्तावेज़ के मालिक को आपकी ऑफ़लाइन हस्ताक्षरित प्रति की समीक्षा करने के लिए ईमेल द्वारा सूचित किया गया है।",
|
||||
"offline-already-submitted-title": "दस्तावेज़ ऑफ़लाइन सबमिट किया गया",
|
||||
"offline-already-submitted-body": "आपने यह दस्तावेज़ पहले ही ऑफ़लाइन हस्ताक्षर समीक्षा के लिए सबमिट कर दिया है। स्वीकृति या अस्वीकृति होने पर आपको सूचित किया जाएगा।",
|
||||
"offline-action-approve-title": "ऑफ़लाइन हस्ताक्षर स्वीकृत करें",
|
||||
"offline-action-approve-confirm": "स्वीकृत करें",
|
||||
"offline-action-reject-title": "ऑफ़लाइन हस्ताक्षर अस्वीकार करें",
|
||||
"offline-action-reject-hint": "वैकल्पिक रूप से कारण बताएं। हस्ताक्षरकर्ता को ईमेल द्वारा सूचित किया जाएगा।",
|
||||
"offline-action-reject-reason-placeholder": "कारण (वैकल्पिक)",
|
||||
"offline-action-reject-confirm": "अस्वीकृति की पुष्टि करें",
|
||||
"offline-action-review-title": "ऑफ़लाइन हस्ताक्षर की समीक्षा करें",
|
||||
"offline-action-review-hint": "हस्ताक्षरकर्ता द्वारा अपलोड किए गए हस्ताक्षरित PDF का पूर्वावलोकन करें, फिर उसे स्वीकृत या अस्वीकार करें। पुष्टि करने तक कोई कार्रवाई नहीं की जाएगी।",
|
||||
"offline-action-submitted-at": "सबमिट किया गया",
|
||||
"offline-action-no-preview": "इस सबमिशन के लिए कोई पूर्वावलोकन उपलब्ध नहीं है।",
|
||||
"offline-action-open-in-new-tab": "PDF नए टैब में खोलें",
|
||||
"offline-action-invalid-link": "यह समीक्षा लिंक अमान्य है या समाप्त हो गई है।",
|
||||
"offline-action-success": "कार्रवाई पूर्ण हुई।",
|
||||
"reject": "अस्वीकार करें",
|
||||
"document": "दस्तावेज़",
|
||||
"view": "देखें",
|
||||
"make-template-public": "टेम्पलेट को सार्वजनिक करें",
|
||||
"make-template-private": "टेम्पलेट को निजी बनाएं",
|
||||
@@ -366,7 +312,7 @@
|
||||
"quick-send-alert-1": "इस दस्तावेज़ में सभी भूमिकाएँ वर्तमान में संपर्कों से जुड़ी हुई हैं। इस टेम्पलेट की प्रतियां कई हस्ताक्षरकर्ताओं को त्वरित रूप से भेजने के लिए, कृपया सुनिश्चित करें कि कम से कम एक भूमिका किसी भी संपर्क से जुड़ी नहीं है।",
|
||||
"quick-send-alert-2": "कृपया सुनिश्चित करें कि सभी प्राप्तकर्ताओं के लिए कम से कम एक हस्ताक्षर विजेट जोड़ा गया है।",
|
||||
"quick-send-alert-3": "कृपया इस टेम्पलेट में कम से कम एक भूमिका जोड़ें।",
|
||||
"quick-send-alert-4": "{{flow}} प्रति बैच अधिकतम {{count}} साइनरों का समर्थन करता है। अपनी सूची विभाजित करें और पुनः प्रयास करें।",
|
||||
"quick-send-alert-4": "त्वरित भेजें सीमा तक पहुँच गया।",
|
||||
"copy-link": "लिंक कॉपी करें",
|
||||
"copy": "कॉपी करें",
|
||||
"revoke-document": "दस्तावेज़ रद्द करें",
|
||||
@@ -421,12 +367,7 @@
|
||||
"dropdown": "ड्रॉपडाउन",
|
||||
"radio button": "रेडियो बटन",
|
||||
"image": "छवि",
|
||||
"email": "ईमेल",
|
||||
"number": "संख्या",
|
||||
"draw": "आरेखित करें",
|
||||
"attachments": "संलग्नक",
|
||||
"attachments-add": "संलग्नक अपलोड करने के लिए क्लिक करें",
|
||||
"attachments-count": "{{count}} संलग्नक"
|
||||
"email": "ईमेल"
|
||||
},
|
||||
"fields": "क्षेत्र",
|
||||
"recipients": "प्राप्तकर्ता",
|
||||
@@ -438,14 +379,6 @@
|
||||
"certificate": "प्रमाण पत्र",
|
||||
"decline": "अस्वीकार करें",
|
||||
"finish": "समाप्त करें",
|
||||
"approve": "स्वीकृत करें",
|
||||
"approve-mssg": " क्या आप वाकई दस्तावेज़ को स्वीकृत करना चाहते हैं?",
|
||||
"signer-role-label": "प्राप्तकर्ता की भूमिका",
|
||||
"signer-role-signer": "हस्ताक्षरकर्ता",
|
||||
"signer-role-viewer": "दर्शक",
|
||||
"signer-role-approver": "अनुमोदक",
|
||||
"signer-role-hint": "हस्ताक्षरकर्ता को हस्ताक्षर करना होगा। अनुमोदक बिना हस्ताक्षर किए अनुमोदन करता है। दर्शक केवल देखता है।",
|
||||
"signer-role-upgrade-msg": "दर्शक और अनुमोदक भूमिकाएँ केवल सशुल्क योजनाओं में उपलब्ध हैं। प्राप्तकर्ताओं को ये भूमिकाएँ सौंपने के लिए अपनी योजना अपग्रेड करें।",
|
||||
"done": "हो गया",
|
||||
"mail": "मेल",
|
||||
"sign-now": "अभी हस्ताक्षर करें",
|
||||
@@ -456,10 +389,6 @@
|
||||
"Email-verified-alert-1": "ईमेल सत्यापित है।",
|
||||
"Email-verified-alert-2": "ईमेल पहले से ही सत्यापित है।",
|
||||
"upload-stamp-image": "मोहर छवि अपलोड करें",
|
||||
"draw-signature": "हस्ताक्षर बनाएं",
|
||||
"draw-initials": "इनिशियल्स बनाएं",
|
||||
"enter-text": "पाठ दर्ज करें",
|
||||
"enter-widgettype": "{{widgetType}} दर्ज करें",
|
||||
"draw": "आरेखित करें",
|
||||
"type": "टाइप",
|
||||
"color-type": {
|
||||
@@ -478,47 +407,19 @@
|
||||
"reset-password-alert-1": "पासवर्ड रीसेट लिंक आपके ईमेल आईडी पर भेज दिया गया है",
|
||||
"reset-password-alert-2": "कृपया ईमेल एडॉप्टर सेटअप करें ",
|
||||
"reset-password-alert-3": "अपना पासवर्ड रीसेट करें",
|
||||
"failed-animation": "एनिमेशन लोड करने में विफल",
|
||||
"faild-animation": "एनिमेशन लोड करने में विफल",
|
||||
"apply": "लागू करें",
|
||||
"select-columns": "स्तंभ चुनें",
|
||||
"copy-type": {
|
||||
"All pages": "सभी पृष्ठ",
|
||||
"All pages but last": "अंतिम को छोड़कर सभी पृष्ठ",
|
||||
"All pages but first": "पहले को छोड़कर सभी पृष्ठ",
|
||||
"Next to current widget": "वर्तमान विजेट के आगे",
|
||||
"Page range": "पृष्ठ सीमा"
|
||||
"Next to current widget": "वर्तमान विजेट के आगे"
|
||||
},
|
||||
"options": "विकल्प",
|
||||
"minimum-check": "न्यूनतम जाँच",
|
||||
"minimun-check": "न्यूनतम जाँच",
|
||||
"maximum-check": "अधिकतम जाँच",
|
||||
"cell-count": "सेल्स संख्या",
|
||||
"default-value": "डिफ़ॉल्ट मान",
|
||||
"formula": "सूत्र",
|
||||
"formula-placeholder": "संख्या विजेट्स का उपयोग करके एक सूत्र बनाएं",
|
||||
"formula-no-widgets": "इस हस्ताक्षरकर्ता को अभी तक कोई अन्य संख्या विजेट असाइन नहीं किया गया है।",
|
||||
"supported-operators": "समर्थित ऑपरेटर:",
|
||||
"formula-helper": "सूची में से संख्या विजेट चिप पर क्लिक करके उसे जोड़ें (उदा. {{number-1}} + {{number-2}})।",
|
||||
"formula-error-letter": "विजेट नामों को दोहरी घुंघराली ब्रैकेट में रखें (उदा. {{number-1}})।",
|
||||
"formula-error-characters": "ब्रैकेट के बाहर केवल संख्याएँ और +, -, *, /, (, ) की अनुमति है।",
|
||||
"formula-error-braces": "हर खुलने वाली {{ के लिए एक मिलान करने वाली }} होना चाहिए।",
|
||||
"formula-error-operators": "ऑपरेटर या कोष्ठक का उपयोग करते समय कम से कम दो मान जोड़ें।",
|
||||
"formula-error-leading-operator": "सूत्र ऑपरेटर से शुरू नहीं हो सकते।",
|
||||
"formula-error-trailing-operator": "सूत्र किसी ऑपरेटर पर समाप्त नहीं हो सकते।",
|
||||
"formula-error-adjacent-placeholders": "दो प्लेसहोल्डर बिना बीच में ऑपरेटर के साथ-साथ नहीं लिखे जा सकते।",
|
||||
"formula-error-continuous-operators": "दो ऑपरेटर लगातार साथ में उपयोग नहीं किए जा सकते।",
|
||||
"formula-error-empty-parentheses": "कोष्ठक (parentheses) खाली नहीं हो सकते।",
|
||||
"formula-error-invalid-parentheses-content": "कोष्ठक किसी ऑपरेटर से शुरू या समाप्त नहीं हो सकते।",
|
||||
"formula-error-before-open-parenthesis": "'(' से पहले ऑपरेटर गायब है। ओपनिंग कोष्ठक से पहले एक ऑपरेटर जोड़ें।",
|
||||
"formula-error-after-close-parenthesis": "')' के बाद ऑपरेटर गायब है। क्लोज़िंग कोष्ठक के बाद '+' या '*' जैसा ऑपरेटर जोड़ें।",
|
||||
"formula-error-between-parentheses": "')' और '(' के बीच ऑपरेटर गायब है। दोनों समूहों के बीच '*' जैसा ऑपरेटर जोड़ें।",
|
||||
"formula-error-division-by-zero": "शून्य से भाग देना अनुमति नहीं है। '0' को किसी मान्य गैर-शून्य संख्या या वेरिएबल से बदलें।",
|
||||
"formula-error-invalid-number-format": "अमान्य संख्या फ़ॉर्मेट मिला। संख्याएँ '1', '3.14' जैसे मान्य फ़ॉर्मेट में होनी चाहिए, '.5', '1.' या '1.2.3' नहीं।",
|
||||
"formula-error-unclosed-parenthesis": "बिना बंद किया हुआ '(' मिला। हर ओपनिंग '(' के लिए एक मैचिंग क्लोज़िंग ')' होना चाहिए।",
|
||||
"formula-error-unexpected-closing-parenthesis": "बिना मैचिंग ओपनिंग '(' के अप्रत्याशित क्लोज़िंग ')' मिला।",
|
||||
"formula-error-empty-placeholder": "खाली प्लेसहोल्डर '{{}}' अनुमति नहीं है। ब्रेसेज़ के अंदर वेरिएबल का नाम दें।",
|
||||
"decimal-places": "दशमलव स्थान",
|
||||
"number-of-decimal-places": "दशमलव स्थानों की संख्या",
|
||||
"decimal-places-helper": "पूर्णांक के लिए 0 पर सेट करें",
|
||||
"select": "चुनें",
|
||||
"read-only": "सिर्फ पढ़ने के लिए है",
|
||||
"hide-labels": "लेबल छिपाएँ",
|
||||
@@ -565,26 +466,25 @@
|
||||
"add-recipients": "प्राप्तकर्ता जोड़ें",
|
||||
"loading-mssg": "इसमें कुछ समय लग सकता है",
|
||||
"send-mail": "मेल भेजें",
|
||||
"signature-field-widget": "प्रत्येक उपयोगकर्ता के पास कम से कम एक हस्ताक्षर विजेट होना चाहिए। आपने {{signersName}} के लिए हस्ताक्षर विजेट नहीं जोड़ा है।",
|
||||
"signature-field-widget": "प्रत्येक उपयोगकर्ता के लिए कम से कम एक हस्ताक्षर फ़ील्ड जोड़ा जाना चाहिए। आपने {{signersName}} के लिए हस्ताक्षर फ़ील्ड नहीं जोड़े हैं",
|
||||
"placeholder-alert-1": "कृपया सुनिश्चित करें कि सभी प्राप्तकर्ताओं के लिए कम से कम एक हस्ताक्षर विजेट जोड़ा गया है।",
|
||||
"placeholder-alert-2": "कृपया पुष्टि करें कि आपने टेक्स्ट फ़ील्ड भर दिया है।",
|
||||
"placeholder-alert-3": " क्या आप वाकई इस दस्तावेज़ को हस्ताक्षर के लिए भेजना चाहते हैं?",
|
||||
"placeholder-alert-4": "आपने सभी प्राप्तकर्ताओं को सफलतापूर्वक मेल भेज दिए हैं!",
|
||||
"placeholder-mail-alert": "आपने {{name}} को सफलतापूर्वक ईमेल भेज दिया है। {{name}} द्वारा दस्तावेज़ पर हस्ताक्षर करने के बाद बाद के हस्ताक्षरकर्ताओं को ईमेल प्राप्त होंगे",
|
||||
"placeholder-mail-alert-you": "आपके दस्तावेज़ पर हस्ताक्षर करने के बाद अगले हस्ताक्षरकर्ता को ईमेल प्राप्त होगा।",
|
||||
"placeholder-alert-5": "आप अभी हस्ताक्षर कर सकते हैं या इसे बाद में पूरा करने के लिए अपने ईमेल पर भेज सकते हैं।",
|
||||
"placeholder-mail-alert-you": "आपके द्वारा दस्तावेज़ पर हस्ताक्षर करने के बाद बाद के हस्ताक्षरकर्ताओं को ईमेल प्राप्त होंगे।",
|
||||
"placeholder-alert-5": "क्या आप अभी दस्तावेज़ पर हस्ताक्षर करना चाहते हैं?",
|
||||
"placeholder-alert-6": "मेल भेजने के लिए कृपया मेल एडॉप्टर सेटअप करें!",
|
||||
"placeholder-alert-7": "प्लेसहोल्डर जोड़ने के लिए कृपया हस्ताक्षरकर्ता चुनें!",
|
||||
"email-subject": "ईमेल विषय",
|
||||
"email-body": "ईमेल मुख्य भाग",
|
||||
"email-placeholder": "ईमेल का मुख्य भाग जोड़ें",
|
||||
"reset-to-default": "डिफ़ॉल्ट पर रीसेट करें",
|
||||
"customize-email": "ईमेल अनुकूलित करें",
|
||||
"cutomize-email": "ईमेल अनुकूलित करें",
|
||||
"upgrade-to-customize-email": "ईमेल अनुकूलित करने के लिए अपग्रेड करें",
|
||||
"sign-url": "यूआरएल पर हस्ताक्षर करें",
|
||||
"mails-sent": "मेल भेजे गए",
|
||||
"mail-not-delivered": "मेल वितरित नहीं किया गया",
|
||||
"email-not-verified-head": "ईमेल सत्यापित नहीं है",
|
||||
"document-alert": "दस्तावेज़ चेतावनी",
|
||||
"owner-subscription-expired": "मालिक की सदस्यता समाप्त हो गई है।",
|
||||
"subscription-expired": "सदस्यता समाप्त",
|
||||
@@ -645,7 +545,7 @@
|
||||
"provide-your-details": "अपना विवरण प्रदान करें",
|
||||
"provide-email": "कृपया ईमेल प्रदान करें।",
|
||||
"additional-info": "अतिरिक्त जानकारी",
|
||||
"signature-saved-alert": "सफलतापूर्वक सहेजा गया।",
|
||||
"signature-saved-alert": "हस्ताक्षर सफलतापूर्वक सहेजा गया।",
|
||||
"saved-successfully": "सफलतापूर्वक सहेजा गया।",
|
||||
"fill-field": "कृपया इस फ़ील्ड को भरें",
|
||||
"error-template": "त्रुटि: टेम्पलेट नहीं मिला!",
|
||||
@@ -660,9 +560,6 @@
|
||||
"document-signed-alert-7": "यह दस्तावेज़ अब उपलब्ध नहीं है।",
|
||||
"document-signed-alert-8": "दस्तावेज़ पर हस्ताक्षर के लिए पहले ही भेज दिया गया है।",
|
||||
"yet-to-sign": "अभी हस्ताक्षर करना बाकी है",
|
||||
"viewers": "दर्शक",
|
||||
"viewer-no-widgets": "दर्शकों को दस्तावेज़ की केवल-पढ़ने योग्य प्रति मिलती है और उन्हें विजेट असाइन नहीं किए जा सकते।",
|
||||
"viewer-confirm-widget-removal": "इस प्राप्तकर्ता को दर्शक में बदलने पर उन्हें पहले से असाइन किए गए सभी विजेट हटा दिए जाएंगे। जारी रखें?",
|
||||
"validation-alert-1": "इनपुट नियमित अभिव्यक्ति द्वारा निर्धारित मानदंडों को पूरा नहीं करता है।",
|
||||
"account-already-exists": "खाता पहले से मौजूद है!",
|
||||
"choose-password": "पासवर्ड चुनें",
|
||||
@@ -685,7 +582,7 @@
|
||||
"date": "दिनांक",
|
||||
"report-not-found": "रिपोर्ट नहीं मिली",
|
||||
"please-wait": "कृपया प्रतीक्षा करें",
|
||||
"select-folder": "फ़ोल्डर चुनें",
|
||||
"select-foler": "फ़ोल्डर चुनें",
|
||||
"generated-on": "पर उत्पन्न",
|
||||
"text-field": "पाठ क्षेत्र",
|
||||
"font-size": "फ़ॉन्ट आकार",
|
||||
@@ -704,8 +601,7 @@
|
||||
"tour-mssg": {
|
||||
"home-layout-1": "आप सफलतापूर्वक लॉग इन हो गए हैं! आइए एक नज़र डालते हैं।",
|
||||
"home-layout-2": "स्व-हस्ताक्षर के लिए दस्तावेज़ अपलोड करने या दूसरों के हस्ताक्षर का अनुरोध करने के लिए, बस संबंधित बटन चुनें।",
|
||||
"home-layout-3": "आप {{appName}} का उपयोग शुरू करने के लिए तैयार हैं!",
|
||||
"home-layout-4": "हमें स्टार दें",
|
||||
"home-layout-3": "आप {{appName}} का उपयोग शुरू करने के लिए तैयार हैं! यदि आपको सहायता की आवश्यकता है तो हमसे संपर्क करने में संकोच न करें।",
|
||||
"generate-token": "उत्पादन एपीआई टोकन उत्पन्न करने के लिए अभी अपग्रेड करें।",
|
||||
"opensign-drive-1": "फ़ोल्डर पदानुक्रम के माध्यम से आसानी से नेविगेट करने और प्रत्येक फ़ोल्डर के भीतर दस्तावेज़ देखने के लिए ब्रेडक्रंब लिंक पर क्लिक करें।",
|
||||
"opensign-drive-2": "नया फ़ोल्डर या दस्तावेज़ बनाने के लिए जोड़ें बटन पर क्लिक करें।",
|
||||
@@ -715,42 +611,44 @@
|
||||
"opensign-drive-6": "डाउनलोड, नाम बदलें, स्थानांतरित करें और हटाएं जैसे विकल्प देखने के लिए किसी दस्तावेज़ पर राइट-क्लिक करें। इसे खोलने के लिए दस्तावेज़ पर क्लिक करें।",
|
||||
"opensign-drive-7": "विकल्प देखने के लिए किसी भी फ़ोल्डर पर राइट-क्लिक करें। फ़ोल्डर का नाम बदलने के लिए 'नाम बदलें' चुनें या इसकी सामग्री के माध्यम से नेविगेट करने के लिए फ़ोल्डर पर क्लिक करें।",
|
||||
"pdf-request-file-1": "उन हस्ताक्षरकर्ताओं की सूची जिन्हें अभी भी दस्तावेज़ पर हस्ताक्षर करने की आवश्यकता है।",
|
||||
"pdf-request-file-2": "शुरू करने के लिए दस्तावेज़ में दिखाई देने वाले किसी भी फ़ील्ड पर क्लिक करें। उसके बाद आप आवश्यक जानकारी भर सकेंगे।",
|
||||
"pdf-request-file-3": "एक बार जब आप सभी आवश्यक फ़ील्ड भर लें, तो 'समाप्त करें' पर क्लिक करें। उसके बाद आप हस्ताक्षरित दस्तावेज़ डाउनलोड कर सकेंगे। प्रेषक की सेटिंग्स के आधार पर, जब सभी प्राप्तकर्ता हस्ताक्षर कर लेंगे, तो आपको पूर्ण दस्तावेज़ की एक प्रति और पूर्णता प्रमाणपत्र प्राप्त हो सकता है।",
|
||||
"pdf-request-file-2": "हस्ताक्षर करने के लिए दस्तावेज़ पर दिखाई देने वाले किसी भी प्लेसहोल्डर पर क्लिक करें। फिर आपको अपना हस्ताक्षर खींचने, टाइप करने या एक छवि अपलोड करने के विकल्प दिखाई देंगे।",
|
||||
"pdf-request-file-3": "अपने दस्तावेज़ को नेविगेट करने के लिए अस्वीकार करें, या समाप्त करें बटन पर क्लिक करें। डाउनलोड बटन सहित अतिरिक्त विकल्पों के लिए दीर्घवृत्त मेनू का उपयोग करें।",
|
||||
"pdf-request-file-4": "उन हस्ताक्षरकर्ताओं की सूची जिन्होंने पहले ही दस्तावेज़ पर हस्ताक्षर कर दिए हैं।",
|
||||
"pdf-request-file-5": "आप उन सभी स्थानों पर स्वचालित रूप से हस्ताक्षर करने के लिए 'सभी पर स्वतः हस्ताक्षर करें' पर क्लिक कर सकते हैं, जिन पर आपके द्वारा हस्ताक्षर किए जाने हैं। सुनिश्चित करें कि आप इस बटन पर क्लिक करने से पहले दस्तावेज़ की ठीक से समीक्षा कर लें।",
|
||||
"pdf-request-file-6": "कृपया पृष्ठ संख्या {{pagenumbers}} पर फ़ील्ड पूरी करें, सभी आसान पहचान के लिए एक ही रंग में हाइलाइट किए गए हैं।",
|
||||
"placeholder-sign-1": "इस सूची से एक प्राप्तकर्ता चुनें ताकि विजेट जोड़ सकें। दस्तावेज़ पर रखने के बाद विजेट उसी रंग में दिखाई देगा जो प्राप्तकर्ता के नाम का है।",
|
||||
"placeholder-sign-1": "एक प्लेस-होल्डर जोड़ने के लिए इस सूची से एक प्राप्तकर्ता का चयन करें जहां उसे हस्ताक्षर करना है। प्लेसहोल्डर दस्तावेज़ पर छोड़ने के बाद प्राप्तकर्ता के नाम के समान रंग में दिखाई देगा।",
|
||||
"placeholder-sign-2": "'प्राप्तकर्ता जोड़ें' बटन पर क्लिक करने से आप अधिक हस्ताक्षरकर्ताओं को जोड़ सकेंगे।",
|
||||
"placeholder-sign-3": "दस्तावेज़ में अधिक प्राप्तकर्ताओं/हस्ताक्षरकर्ताओं को जोड़ने के लिए इस बटन पर क्लिक करें।",
|
||||
"placeholder-sign-4": "दस्तावेज़ में जोड़ने के लिए विजेट को खींचें या उस पर क्लिक करें।",
|
||||
"placeholder-sign-5": "PDF सामग्री क्षेत्र पहले से ही टेम्पलेट के मौजूदा विजेट दिखाता है। आपकी सुविधा के लिए, ये विजेट प्राप्तकर्ता के नाम के रंग से मेल खाएँगे ताकि वे आसानी से पहचाने जा सकें।",
|
||||
"placeholder-sign-4": "इसे दस्तावेज़ में जोड़ने के लिए किसी फ़ील्ड पर खींचें या क्लिक करें।",
|
||||
"placeholder-sign-5": "पीडीएफ सामग्री क्षेत्र पहले से ही टेम्पलेट के मौजूदा प्लेसहोल्डर प्रदर्शित करता है। आपकी सुविधा के लिए, ये प्लेसहोल्डर प्राप्तकर्ता के नाम के रंग से मेल खाएंगे, जिससे उन्हें आसानी से पहचाना जा सकेगा।",
|
||||
"placeholder-sign-6": "'अगला' पर क्लिक करने से दस्तावेज़ सहेजा जाएगा। अगले चरण में आप प्राप्तकर्ताओं को भेजे जाने वाले ईमेल को अनुकूलित कर सकते हैं या हस्ताक्षर लिंक कॉपी कर सकते हैं और उन्हें स्वयं प्राप्तकर्ताओं के साथ साझा कर सकते हैं।",
|
||||
"report-1": "नया टेम्पलेट बनाने के लिए 'जोड़ें' बटन पर क्लिक करें। टेम्पलेट पुन: प्रयोज्य दस्तावेज़ हैं जिन्हें समान संरचना और अलग-अलग हस्ताक्षरकर्ताओं के साथ नए दस्तावेज़ों को शीघ्रता से उत्पन्न करने के लिए डिज़ाइन किया गया है। उदाहरण के लिए, ऑनबोर्डिंग के लिए एक मानव संसाधन टेम्पलेट में 'मानव संसाधन प्रबंधक' और 'नया कर्मचारी' जैसी पूर्वनिर्धारित भूमिकाएँ हो सकती हैं। हर बार जब आप टेम्पलेट का उपयोग करते हैं, तो आप विभिन्न आने वाले कर्मचारियों को 'नया कर्मचारी' भूमिका सौंप सकते हैं, जबकि 'मानव संसाधन प्रबंधक' भूमिका स्थिर रहती है, जिससे प्रत्येक भर्ती के लिए एक सहज ऑनबोर्डिंग प्रक्रिया सुविधाजनक होती है। ",
|
||||
"redirect": "किसी मौजूदा टेम्पलेट से नया दस्तावेज़ बनाने के लिए 'उपयोग करें' बटन पर क्लिक करें।",
|
||||
"bulksend": "किसी मौजूदा टेम्पलेट का उपयोग करके कई दस्तावेज़ जल्दी भेजने के लिए, केवल प्राप्तकर्ताओं के ईमेल पते दर्ज करें और 'बल्क सेंड' बटन पर क्लिक करें। आप अधिकतम 50 प्राप्तकर्ताओं को भेज सकते हैं।",
|
||||
"option": "इस मेनू में और विकल्प दिखाई देंगे जैसे संपादित करें, हटाएं, नाम बदलें, डुप्लिकेट करें, साझा करें आदि। <1>यहां क्लिक करें</1> सभी उपलब्ध विकल्पों के बारे में और पढ़ने के लिए। <3>नोट: किसी मौजूदा टेम्पलेट में किए गए बदलाव उस टेम्पलेट से बनाए गए सभी भविष्य के दस्तावेज़ों पर लागू होंगे, लेकिन पहले से भेजे गए दस्तावेज़ों पर प्रभाव नहीं डालेंगे।</3>",
|
||||
"signyour-self-1": "हस्ताक्षर करने से पहले अपने दस्तावेज़ को अनुकूलित करने के लिए अपनी पसंद के विजेट चुनें या उन्हें PDF पर खींचें। अपनी आवश्यकता के अनुसार प्रत्येक विजेट को सही स्थान पर रखें।",
|
||||
"bulksend": "केवल प्राप्तकर्ता ईमेल पते बनाकर किसी मौजूदा टेम्पलेट का उपयोग करके कई दस्तावेज़ों को शीघ्रता से भेजने के लिए, 'थोक भेजें' बटन पर क्लिक करें।",
|
||||
"option": "यह मेनू संपादन और हटाएं जैसे अधिक विकल्प प्रकट करता है। हस्ताक्षरकर्ता भूमिकाएं जोड़ने, फ़ील्ड संशोधित करने और अपने टेम्पलेट को अपडेट करने के लिए 'संपादित करें' बटन का उपयोग करें। परिवर्तन इस टेम्पलेट से बनाए गए सभी भविष्य के दस्तावेज़ों पर लागू होंगे लेकिन मौजूदा दस्तावेज़ों को प्रभावित नहीं करेंगे। हटाएं बटन का उपयोग करके आप टेम्पलेट हटा सकते हैं। ",
|
||||
"signyour-self-1": "हस्ताक्षर करने से पहले अपने दस्तावेज़ को अनुकूलित करने के लिए अपने पसंदीदा विजेट चुनें और उन्हें पीडीएफ पर खींचें। अपनी आवश्यकताओं के अनुसार दस्तावेज़ को अनुकूलित करने के लिए प्रत्येक संशोधन के लिए सही स्थान चुनें।",
|
||||
"signyour-self-2": "इस क्षेत्र में कहीं भी खींचें और छोड़ें। आप इसे बाद में आकार बदल सकते हैं और स्थानांतरित कर सकते हैं।",
|
||||
"template-placeholder-1": "'भूमिका जोड़ें' बटन पर क्लिक करने से आप विभिन्न हस्ताक्षरकर्ता भूमिकाएँ जोड़ सकेंगे। आप बाद के चरणों में प्रत्येक भूमिका में उपयोगकर्ताओं को संलग्न कर सकते हैं।",
|
||||
"template-placeholder-2": "भूमिकाएँ जोड़ने के बाद, सूची में से एक चुनें ताकि उस प्राप्तकर्ता के लिए विजेट रखा जा सके। आप विजेट पर क्लिक कर सकते हैं या उसे दस्तावेज़ पर खींच सकते हैं। एक बार रखे जाने के बाद, विजेट चयनित भूमिका के समान रंग में दिखाई देगा।",
|
||||
"template-placeholder-3": "'अगला' पर क्लिक करने से वर्तमान टेम्पलेट संग्रहीत हो जाएगा। सहेजने के बाद, यदि आप चाहें तो इस टेम्पलेट से एक नया दस्तावेज़ बनाने के लिए आपको संकेत दिया जाएगा।",
|
||||
"template-placeholder-2": "भूमिकाएँ जोड़ने के बाद, सूची से एक भूमिका चुनें ताकि एक प्लेस-होल्डर जोड़ा जा सके जहाँ उसे हस्ताक्षर करना है। प्लेसहोल्डर दस्तावेज़ पर छोड़ने के बाद भूमिका के नाम के समान रंग में दिखाई देगा।",
|
||||
"template-placeholder-3": "इसे दस्तावेज़ में जोड़ने के लिए किसी फ़ील्ड पर खींचें या क्लिक करें।",
|
||||
"template-placeholder-4": "किसी भूमिका के लिए प्लेसहोल्डर को दस्तावेज़ पर कहीं भी खींचें। याद रखें, यह आसान संदर्भ के लिए प्राप्तकर्ता के नाम के समान रंग में दिखाई देगा।",
|
||||
"template-placeholder-5": "'अगला' पर क्लिक करने से वर्तमान टेम्पलेट संग्रहीत हो जाएगा। सहेजने के बाद, यदि आप चाहें तो इस टेम्पलेट से एक नया दस्तावेज़ बनाने के लिए आपको संकेत दिया जाएगा।",
|
||||
"webhook-1": "वेबहूक सेट करने के लिए अभी अपग्रेड करें",
|
||||
"Need your Signature": "इस कार्ड पर क्लिक करने से आप अपनी समीक्षा की प्रतीक्षा कर रहे दस्तावेज़ों की सूची पर पहुंच जाएंगे।",
|
||||
"Out for signatures": "इस कार्ड पर क्लिक करने से आप हस्ताक्षर की प्रतीक्षा कर रहे दस्तावेज़ों की सूची पर पहुंच जाएंगे।",
|
||||
"Recent signature requests": "यह उन दस्तावेज़ों की सूची है जो आपके हस्ताक्षर की प्रतीक्षा कर रहे हैं।",
|
||||
"Recently sent for signatures": "यह उन दस्तावेज़ों की सूची है जिन्हें आपने हस्ताक्षर के लिए अन्य पक्षों को भेजा है।",
|
||||
"Drafts": "ये वे दस्तावेज़ हैं जिन्हें आपने शुरू किया है लेकिन भेजने के लिए अंतिम रूप नहीं दिया है।",
|
||||
"Drafts": "ये वे दस्तावेज़ हैं जिन्हें आपने शुरू तो किया है लेकिन भेजने के लिए अंतिम रूप नहीं दिया है।",
|
||||
"public-template": "यह वीडियो दर्शाता है कि आप अपना व्यक्तिगत सार्वजनिक प्रोफ़ाइल कैसे सेट अप कर सकते हैं, जैसे 'https://opensign.me/your-username'। आप यह भी सीखेंगे कि अपनी टैगलाइन को कैसे अनुकूलित करें और अपने टेम्पलेट्स को सार्वजनिक हस्ताक्षर के लिए उपलब्ध कराएं।",
|
||||
"allowModify-widgets": "आप इन फ़ील्ड में से किसी को भी दस्तावेज़ पर खींच और छोड़ सकते हैं, इसके अतिरिक्त दस्तावेज़ निर्माता द्वारा आपके लिए पहले से निर्दिष्ट फ़ील्ड के। "
|
||||
},
|
||||
"enter-email-placeholder": "एक ईमेल पता जोड़ें और एंटर दबाएं",
|
||||
"enter-email-plaholder": "एक ईमेल पता जोड़ें और एंटर दबाएं",
|
||||
"success-email-alert": "ईमेल सफलतापूर्वक भेजा गया!",
|
||||
"expired-doc-title": "समाप्त दस्तावेज़",
|
||||
"expired-on-mssg": "यह दस्तावेज़ {{expiredDate}} को समाप्त हो गया है और अब हस्ताक्षर के लिए उपलब्ध नहीं है।",
|
||||
"signature-validate-alert": "कृपया पुष्टि करें कि आपने कम से कम {{minRequiredCount}} चेकबॉक्स चुने हैं।",
|
||||
"signature-validate-alert-2": "सुनिश्चित करें कि यह फ़ील्ड सटीक रूप से भरा गया है और सभी आवश्यकताओं को पूरा करता है।",
|
||||
"remaining-users": "शेष उपयोगकर्ता",
|
||||
"remaing-users": "शेष उपयोगकर्ता",
|
||||
"Role": "भूमिका",
|
||||
"additional-users": "कृपया ऐड-ऑन उपयोगकर्ता खरीदें।",
|
||||
"Quantity-of-users": "उपयोगकर्ताओं की मात्रा",
|
||||
@@ -764,7 +662,7 @@
|
||||
"select-date-format": "एक दिनांक प्रारूप चुनें",
|
||||
"quantity-of-credits": "प्रीमियम क्रेडिट की मात्रा",
|
||||
"remaining-credits": "प्रीमियम क्रेडिट उपलब्ध:",
|
||||
"remaining-credits-help": "API दस्तावेज़ साइनिंग, बल्क सेंडिंग या अपनी वेबसाइट पर OpenSign इंटीग्रेशन एम्बेड करने के लिए प्रीमियम क्रेडिट का उपयोग करें। आपके पास {{allowedcredits}} शामिल क्रेडिट और {{addoncredits}} अतिरिक्त खरीदे हुए क्रेडिट शेष हैं। यदि आप Teams या Enterprise प्लान पर हैं, तो ये क्रेडिट आपकी कंपनी के सभी उपयोगकर्ताओं के बीच साझा किए जा सकते हैं, जब तक कि आपके खाते के लिए अकाउंट एडमिन ने कोई सीमा निर्धारित न की हो।",
|
||||
"remaining-credits-help": "अपनी वेबसाइट पर एपीआई दस्तावेज़ हस्ताक्षर, थोक भेजने या OpenSign एकीकरण को एम्बेड करने के लिए प्रीमियम क्रेडिट का उपयोग करें। आपके पास {{allowedcredits}} शामिल क्रेडिट और {{addoncredits}} अतिरिक्त खरीदे गए क्रेडिट शेष हैं।",
|
||||
"additional-credits": "कृपया प्रीमियम क्रेडिट खरीदें",
|
||||
"quota-err-quicksend": "कोटा पहुँच गया, आपके पास पर्याप्त क्रेडिट नहीं हैं।",
|
||||
"buy-credits": "प्रीमियम क्रेडिट खरीदें",
|
||||
@@ -772,7 +670,7 @@
|
||||
"rotate-left": "बाईं ओर घुमाएँ",
|
||||
"rotate-alert-mssg": "इस पृष्ठ पर सभी विजेट खो जाएंगे। क्या आप वाकई आगे बढ़ना चाहते हैं?",
|
||||
"templateid": "टेम्पलेट-आईडी",
|
||||
"bulk-send-subscription-alert": "थोक भेजने का उपयोग करने के लिए कृपया व्यावसायिक या टीम योजना में अपग्रेड करें।",
|
||||
"bulk-send-subcription-alert": "थोक भेजने का उपयोग करने के लिए कृपया व्यावसायिक या टीम योजना में अपग्रेड करें।",
|
||||
"generate-test-token": "परीक्षण टोकन उत्पन्न करें",
|
||||
"regenerate-test-token": "परीक्षण टोकन पुनः उत्पन्न करें",
|
||||
"help-test-token": "इस टोकन का उपयोग https://sandbox.opensignlabs.com/api/v1 समापन बिंदु पर एपीआई का परीक्षण करने के लिए किया जा सकता है, जिससे आप असीमित दस्तावेज़ हस्ताक्षर कर सकते हैं। कृपया ध्यान दें कि सैंडबॉक्स एपीआई आपके दस्तावेज़ों पर स्व-हस्ताक्षरित प्रमाणपत्रों के साथ हस्ताक्षर करेगा, जिन्हें एडोब द्वारा मान्य के रूप में मान्यता नहीं दी जा सकती है। एक बार जब आप अपना परीक्षण पूरा कर लेते हैं, तो आप उत्पादन टोकन उत्पन्न करने के लिए हमारी किसी भुगतान योजना में अपग्रेड कर सकते हैं।",
|
||||
@@ -792,16 +690,14 @@
|
||||
"copied-code": "कॉपी किया गया",
|
||||
"Installation": "इंस्टॉलेशन",
|
||||
"Usage": "उपयोग",
|
||||
"insufficient-credits": "हस्ताक्षर क्रेडिट अपर्याप्त हैं। कृपया {{ownerEmail}} से संपर्क करें",
|
||||
"insufficient-credits-title": "अपर्याप्त हस्ताक्षर क्रेडिट",
|
||||
"insufficient-credits-mssg": "इस दस्तावेज़ के स्वामी के पास वर्तमान में आपके हस्ताक्षर करने के लिए आवश्यक {{appName}} क्रेडिट की कमी है। यदि आपको अधिक विवरण चाहिए तो कृपया स्वामी से संपर्क करें।",
|
||||
"insufficient-credits": "अपर्याप्त हस्ताक्षर क्रेडिट",
|
||||
"insufficient-credits-mssg": "इस दस्तावेज़ के स्वामी के पास वर्तमान में आपके हस्ताक्षर करने के लिए आवश्यक OpenSign क्रेडिट की कमी है। यदि आपको अधिक विवरण चाहिए तो कृपया स्वामी से संपर्क करें।",
|
||||
"angular-npm-mssg-1": "OpenSign को अपने Angular प्रोजेक्ट में एकीकृत करने के लिए, बस निम्नलिखित कमांड चलाएँ:",
|
||||
"quota-mail-info-head": "मासिक ईमेल सीमा",
|
||||
"quota-mail-info": "सेवा की गुणवत्ता बनाए रखने और स्पैम को रोकने के लिए, OpenSign निःशुल्क योजना पर प्रति माह 15 ईमेल तक की अनुमति देता है। असीमित ईमेल भेजने के लिए अभी अपग्रेड करें।",
|
||||
"quota-mail-reset": "आपके हस्ताक्षर अनुरोध ईमेल क्रेडिट इस पर रीसेट हो जाएंगे",
|
||||
"quota-mail": "आपने इस महीने के लिए 15 हस्ताक्षर अनुरोध ईमेल की अपनी सीमा पूरी कर ली है। सीधे ईमेल भेजना जारी रखने के लिए अभी अपग्रेड करें।",
|
||||
"quota-mail-tip": "सुझाव: आप अभी भी हस्ताक्षर अनुरोध लिंक को मैन्युअल रूप से साझा करके <1>असीमित दस्तावेज़ों</1> पर हस्ताक्षर कर सकते हैं।",
|
||||
"quota-mail-limit-reached": "आपने 15 ईमेल क्रेडिट की अपनी मासिक सीमा पूरी कर ली है। सीधे ईमेल भेजना जारी रखने के लिए कृपया अपना प्लान अपग्रेड करें।\nआपके ईमेल क्रेडिट {{resetDate}} को रीसेट हो जाएंगे।",
|
||||
"quota-mail-head": "कोटा पहुँच गया",
|
||||
"unauthorized-modal": "आपको यह कार्रवाई करने की अनुमति नहीं है, कृपया {{adminEmail}} से संपर्क करें।",
|
||||
"sent-this-month": "इस महीने भेजा गया",
|
||||
@@ -814,13 +710,6 @@
|
||||
"p3": "इस विकल्प का चयन करने पर ओटीपी सत्यापन अक्षम हो जाएगा, जिससे उपयोगकर्ता अतिरिक्त चरणों के बिना सीधे दस्तावेज़ पर हस्ताक्षर कर सकेंगे।",
|
||||
"p4": "कृपया वह विकल्प चुनें जो आपके दस्तावेज़ हस्ताक्षर आवश्यकताओं के लिए सबसे उपयुक्त हो।"
|
||||
},
|
||||
"allow-offline-sign": "ऑफ़लाइन हस्ताक्षर की अनुमति दें",
|
||||
"allow-offline-sign-help": {
|
||||
"p1": "चुनें कि क्या हस्ताक्षरकर्ता बाहरी टूल से हस्ताक्षरित इस दस्तावेज़ की एक प्रति स्वामी की समीक्षा के लिए जमा कर सकते हैं।",
|
||||
"p2": "हस्ताक्षरकर्ताओं को ऑफ़लाइन हस्ताक्षर विकल्प दिखाई देगा और वे बाहरी रूप से हस्ताक्षरित PDF स्वीकृति के लिए अपलोड कर सकते हैं।",
|
||||
"p3": "हस्ताक्षरकर्ता केवल एप के भीतर ही हस्ताक्षर कर सकते हैं। ऑफ़लाइन हस्ताक्षर विकल्प छिपा दिया जाएगा।",
|
||||
"p4": "यह सेटिंग डिफ़ॉल्ट रूप से आपकी खाता प्राथमिकता का उपयोग करती है और भुगतान योजनाओं पर उपलब्ध है।"
|
||||
},
|
||||
"advanced-options": "उन्नत विकल्प",
|
||||
"hide-advanced-options": "उन्नत विकल्प छिपाएँ",
|
||||
"download-files": "फ़ाइलें डाउनलोड करें",
|
||||
@@ -840,7 +729,7 @@
|
||||
"public-tour-message": "साझा करने योग्य लिंक उत्पन्न करने से पहले टेम्पलेट को सार्वजनिक होना चाहिए।",
|
||||
"add-user-template": "इसके लिए फ़ील्ड जोड़ने से पहले आपको एक भूमिका जोड़ने की आवश्यकता है।",
|
||||
"pdf-uncompatible": "यह पीडीएफ संगत नहीं है, कृपया {{appName}} से संपर्क करें",
|
||||
"text-field-tour": "दस्तावेज़ सबमिट करने से पहले 'प्रीफ़िल' फ़ील्ड को पहले से भरना होगा। यदि आपको हस्ताक्षरकर्ताओं से इनपुट की आवश्यकता है, तो इसके बजाय हस्ताक्षरकर्ता फ़ील्ड का उपयोग करें।",
|
||||
"text-field-tour": "दस्तावेज़ भेजे जाने से पहले 'टेक्स्ट' प्रकार के फ़ील्ड पहले से भरे जाने चाहिए। यदि आपको हस्ताक्षरकर्ताओं से इनपुट प्रदान करने की आवश्यकता है, तो इसके बजाय 'टेक्स्ट इनपुट' फ़ील्ड का उपयोग करें।",
|
||||
"attach-signer-tour": "आपको प्रत्येक भूमिका में एक हस्ताक्षरकर्ता संलग्न करने की आवश्यकता है। आप इस आइकन पर क्लिक करके ऐसा कर सकते हैं। एक बार जब आप एक हस्ताक्षरकर्ता का चयन कर लेते हैं तो यह उस भूमिका से जुड़े सभी फ़ील्ड से जुड़ जाएगा जो एक ही रंग में दिखाई देते हैं।",
|
||||
"allowed-signature-types": "अनुमत हस्ताक्षर प्रकार",
|
||||
"at-least-one-signature-type": "कम से कम एक हस्ताक्षर प्रकार सक्षम होना चाहिए।",
|
||||
@@ -883,13 +772,12 @@
|
||||
"delete-folder-alert-1": "हटाने में असमर्थ: फ़ोल्डर खाली होना चाहिए।",
|
||||
"defaultSign-alert": "{{widgetsType}} विजेट स्वतः हस्ताक्षर के लिए उपलब्ध नहीं हैं।",
|
||||
"embed-heading": "{{appName}} टेम्पलेट एम्बेड करें",
|
||||
"other-apps-by-opensign": "OpenSign टीम के अन्य ऐप्स",
|
||||
"public-template-edit-title": "सार्वजनिक टेम्पलेट संपादित करें",
|
||||
"public-template-edit-mssg": "आप सीधे सार्वजनिक टेम्पलेट संपादित नहीं कर सकते। परिवर्तन करने के लिए, टेम्पलेट को निजी पर सेट किया जाएगा, और आपको बाद में इसे फिर से सार्वजनिक करना होगा। क्या आप जारी रखना चाहते हैं?",
|
||||
"agree-p1": "मैं पुष्टि करता हूं कि मैंने पढ़ लिया है और समझ लिया है ",
|
||||
"agree-p2": "इलेक्ट्रॉनिक रिकॉर्ड और हस्ताक्षर प्रकटीकरण",
|
||||
"agree-p3": "और इलेक्ट्रॉनिक रिकॉर्ड और हस्ताक्षर का उपयोग करने के लिए सहमति।",
|
||||
"agrre-button": "मैं पुष्टि करता हूँ और आगे बढ़ने के लिए सहमत हूँ",
|
||||
"agrre-button": " सहमत हूँ और जारी रखें",
|
||||
"term-cond-title": "नियम और शर्तें",
|
||||
"term-cond-h": "इलेक्ट्रॉनिक रिकॉर्ड और हस्ताक्षर प्रकटीकरण",
|
||||
"term-cond-p1": "यह इलेक्ट्रॉनिक रिकॉर्ड और हस्ताक्षर प्रकटीकरण ('प्रकटीकरण') दस्तावेज़ निर्माता ('प्रेषक') और हस्ताक्षरकर्ता ('आप') के बीच एक समझौता है, जिसे {{appName}} प्लेटफ़ॉर्म ('प्लेटफ़ॉर्म') के माध्यम से सुगम बनाया गया है। {{appName}} के माध्यम से दस्तावेज़ों पर हस्ताक्षर करके, आप इस प्रकटीकरण में उल्लिखित शर्तों से सहमत होते हैं। कृपया आगे बढ़ने से पहले इसे ध्यान से पढ़ें।",
|
||||
@@ -935,6 +823,7 @@
|
||||
"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": "कृपया एपीआई परीक्षण टोकन उत्पन्न करें",
|
||||
@@ -954,7 +843,6 @@
|
||||
"p3": "इस विकल्प का चयन करने से हस्ताक्षरकर्ताओं को हस्ताक्षर प्रक्रिया के दौरान दस्तावेज़ में कोई भी সংযোজন करने से प्रतिबंधित किया जाता है।"
|
||||
},
|
||||
"signature-setting": "हस्ताक्षर सेटिंग्स",
|
||||
"rotation": "घुमाव",
|
||||
"activation-key": "सक्रियण कुंजी",
|
||||
"paid-selfhost-help": "अधिक जानकारी के लिए कृपया {{appName}} से संपर्क करें ",
|
||||
"remaining-credits-selfhost-help": "अपनी वेबसाइट पर एपीआई दस्तावेज़ हस्ताक्षर, थोक भेजने {{appName}} एकीकरण के लिए प्रीमियम क्रेडिट का उपयोग करें। आपके पास {{allowedcredits}} शामिल क्रेडिट और {{addoncredits}} अतिरिक्त खरीदे गए क्रेडिट शेष हैं।",
|
||||
@@ -983,8 +871,6 @@
|
||||
"visit-below-link": "अधिक जानने के लिए नीचे दिए गए लिंक पर जाएं -",
|
||||
"storage-help": "BYOC सक्षम करने से आप अपना स्वयं का S3 संग्रहण कनेक्ट कर सकते हैं ताकि आपकी फ़ाइलें पूरी तरह से आपके नियंत्रण में रहें - कोई बाहरी प्रतियां बरकरार न रहें। यदि डेटा स्वायत्तता आपके लिए मायने रखती है, तो इस सुविधा को अनलॉक करने के लिए टीमों में अपग्रेड करने पर विचार करें।",
|
||||
"daily-quota-reached": "आपने अपना दैनिक कोटा पूरा कर लिया है। सहायता के लिए, कृपया quotas@opensignlabs.com पर संपर्क करें।",
|
||||
"email-not-verified-send": "ईमेल भेजने से पहले कृपया अपना ईमेल पता सत्यापित करें। ईमेल सत्यापन पूरा करने के लिए अपनी <1>प्रोफ़ाइल सेटिंग्स</1> में जाएँ।",
|
||||
"email-not-verified-send-plain": "ईमेल भेजने से पहले कृपया अपना ईमेल पता सत्यापित करें। ईमेल सत्यापन पूरा करने के लिए अपनी प्रोफ़ाइल सेटिंग्स में जाएँ।",
|
||||
"enabled-signature-type": "सक्षम हस्ताक्षर प्रकार",
|
||||
"enabled-signature-type-help": "'सक्षम हस्ताक्षर प्रकार' सेटिंग यह निर्धारित करती है कि आपके संगठन में कौन से हस्ताक्षर विकल्प उपलब्ध हैं। उदाहरण के लिए, यदि आप 'ड्रा' विकल्प को अक्षम करते हैं, तो आपके संगठन के सदस्य इसे हस्ताक्षर विजेट में नहीं देख पाएंगे, जबकि अन्य तीन विकल्प पहुंच योग्य रहेंगे।",
|
||||
"indexing-public-profile": "खोज इंजन द्वारा सार्वजनिक प्रोफ़ाइल को अनुक्रमित करने की अनुमति दें",
|
||||
@@ -994,10 +880,6 @@
|
||||
"thanks-for-feedback": "आपकी प्रतिक्रिया के लिए धन्यवाद 🙏",
|
||||
"share-your-feedback": "अपनी प्रतिक्रिया साझा करें",
|
||||
"share-your-review": "अपनी समीक्षा साझा करें",
|
||||
"please-select-rating": "कृपया एक रेटिंग चुनें",
|
||||
"feedback-optional": "प्रतिक्रिया (वैकल्पिक)",
|
||||
"feedback-saved": "प्रतिक्रिया सहेज ली गई है।",
|
||||
"feedback-save-error": "प्रतिक्रिया सहेजी नहीं जा सकी, कृपया पुनः प्रयास करें।",
|
||||
"date-format": "दिनांक प्रारूप",
|
||||
"document-deleted": "दस्तावेज़ हटा दिया गया है या आपके पास पहुंच नहीं है। कृपया प्रेषक से संपर्क करें।",
|
||||
"save-as-template-?": "क्या आप वाकई इस दस्तावेज़ को टेम्पलेट के रूप में सहेजना चाहते हैं?",
|
||||
@@ -1012,7 +894,6 @@
|
||||
"please-provide-templateid": "कृपया टेम्पलेट आईडी प्रदान करें",
|
||||
"this-template-is-not-public": "यह टेम्पलेट सार्वजनिक नहीं है",
|
||||
"invalid-templateid": "अमान्य टेम्पलेट आईडी",
|
||||
"kiosk-no-active-role": "इस टेम्पलेट में कियोस्क हस्ताक्षर के लिए कोई हस्ताक्षरकर्ता या अनुमोदक भूमिका उपलब्ध नहीं है।",
|
||||
"contact-billing-at-opensign": "अधिक सीटें जोड़ने के लिए, कृपया सहायता के लिए <1>billing@opensignlabs.com</1> पर OpenSign™ से संपर्क करें",
|
||||
"title-length-alert": "शीर्षक अधिकतम 250 वर्ण लंबा होना चाहिए।",
|
||||
"note-length-alert": "टिप्पणी अधिकतम 200 वर्ण लंबी होनी चाहिए।",
|
||||
@@ -1102,7 +983,7 @@
|
||||
},
|
||||
"signer-already-present": "हस्ताक्षरकर्ता पहले से मौजूद है",
|
||||
"kiosk-sign": "कियोस्क साइन",
|
||||
"do-not-have-access-to-template": "टेम्पलेट हटा दिया गया है या आपके पास पहुंच नहीं है। कृपया प्रेषक से संपर्क करें।",
|
||||
"dont-have-access-to-template": "टेम्पलेट हटा दिया गया है या आपके पास पहुंच नहीं है। कृपया प्रेषक से संपर्क करें।",
|
||||
"kiosk-info": "कियोस्क मोड आपको व्यक्तिगत रूप से हस्ताक्षर शीघ्रता और कुशलता से एकत्र करने देता है। ट्रेड शो, कार्यक्रमों या वॉक-इन परिदृश्यों के लिए आदर्श जहां सभी हस्ताक्षरकर्ता शारीरिक रूप से मौजूद हों। ",
|
||||
"learn-more": "और जानें",
|
||||
"finish-mssg": " क्या आप वाकई दस्तावेज़ समाप्त करना चाहते हैं?",
|
||||
@@ -1152,466 +1033,14 @@
|
||||
"not-calculated": "गणना नहीं की गई",
|
||||
"not-found-in-signature": "हस्ताक्षर में नहीं मिला",
|
||||
"readonly-error": "रीड-ओनली {{widgetName}} विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।",
|
||||
"choose-one": "एक का चयन",
|
||||
"choose-one":"एक का चयन",
|
||||
"search-templates": "टेम्पलेट खोजें…",
|
||||
"search-documents": "दस्तावेज़ खोजें…",
|
||||
"search-contacts": "संपर्क खोजें…",
|
||||
"add-role-alert": "कृपया कम से कम एक भूमिका जोड़ें",
|
||||
"edit-draft": "ड्राफ्ट संपादित करें",
|
||||
"invalid-email-found": "अमान्य ईमेल पाया गया: {{email}}",
|
||||
"duplicate-email-found": "डुप्लिकेट ईमेल पाया गया: {{email}}",
|
||||
"vertical": "वर्टिकल",
|
||||
"horizontal": "हॉरिज़ॉन्टल",
|
||||
"billing": "बिलिंग",
|
||||
"console": "कंसोल",
|
||||
"prefill-widget": "पूर्व भराव विजेट्स",
|
||||
"action-prohibited": "यह क्रिया आपके ईमेल डोमेन के लिए अनुमत नहीं है। कृपया सहायता के लिए अपने व्यवस्थापक से संपर्क करें।",
|
||||
"must-have-at-least-one-vacant-role": "किसी टेम्पलेट को 'public' पर सेट करने से पहले कम से कम एक भूमिका खाली होनी चाहिए।",
|
||||
"remove-duplicate": "कृपया डुप्लिकेट विकल्प हटाएं",
|
||||
"prefill-bulk-error": "जब प्रीफ़िल विजेट जोड़े जाते हैं तो बल्क भेजना अनुमति नहीं है। कृपया आगे बढ़ने के लिए प्रीफ़िल विजेट हटा दें।",
|
||||
"session-expired-title": "सत्र समाप्त हो गया",
|
||||
"access-denied": "पहुंच अस्वीकृत",
|
||||
"upgrade": "अपग्रेड",
|
||||
"do-not-access-app": "आपको इस एप्लिकेशन तक पहुंच नहीं है।",
|
||||
"do-not-have-access": "आपको पहुंच नहीं है।",
|
||||
"valid-email-alert": "कृपया एक मान्य ईमेल पता दर्ज करें।",
|
||||
"otp-not-validate": "ओटीपी मान्य नहीं है।",
|
||||
"domain-not-allowed": "यह डोमेन अनुमति नहीं है",
|
||||
"atleast-one-recipient-alert": "कृपया कम से कम एक प्राप्तकर्ता जोड़ें!",
|
||||
"incorrect-password-or-decryption-failed": "गलत पासवर्ड या डिक्रिप्शन विफल।",
|
||||
"incorrect-password-for-file": "फ़ाइल के लिए गलत पासवर्ड: {{file}}",
|
||||
"error-uploading-pdf": "PDF अपलोड करते समय त्रुटि।",
|
||||
"provide-password": "कृपया पासवर्ड प्रदान करें।",
|
||||
"only-pdf-allowed": "केवल PDF फ़ाइलें अनुमत हैं।",
|
||||
"invalid-username-password-region": "अमान्य उपयोगकर्ता नाम/पासवर्ड या क्षेत्र।",
|
||||
"pfx-extension-alert": ".pfx एक्सटेंशन वाली फ़ाइल अपलोड करें।",
|
||||
"email-already-exist": "ईमेल पहले से मौजूद है",
|
||||
"branding": "ब्रांडिंग",
|
||||
"branding-help": "ब्रांडिंग आपकी ऐप में वाइट लेबलिंग की सुविधा देती है",
|
||||
"custom-sub-domain": "कस्टम सब-डोमेन",
|
||||
"app-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": "लोगो",
|
||||
"prefill-unfilled-widget": "निम्न अनिवार्य फ़ील्ड खाली नहीं छोड़े जा सकते: {{emptyWidget}}। कृपया आगे बढ़ने के लिए इन्हें भरें।",
|
||||
"Dashboard": "डैशबोर्ड",
|
||||
"Analytics": "विश्लेषिकी",
|
||||
"Templates": "टेम्पलेट्स",
|
||||
"Need your sign": "आपके हस्ताक्षर की आवश्यकता है",
|
||||
"In Progress": "प्रगति में है",
|
||||
"Completed": "पूरा हुआ",
|
||||
"Drafts": "ड्राफ्ट",
|
||||
"Declined": "अस्वीकृत",
|
||||
"Expired": "समाप्त",
|
||||
"Contactbook": "संपर्क पुस्तिका",
|
||||
"My Signature": "मेरा हस्ताक्षर",
|
||||
"API Token": "एपीआई टोकन",
|
||||
"Webhook": "वेबहूक",
|
||||
"Preferences": "वरीयताएँ",
|
||||
"Teams": "टीमें",
|
||||
"Users": "उपयोगकर्ता",
|
||||
"Drive": "ड्राइव",
|
||||
"Branding": "ब्रांडिंग",
|
||||
"Mail": "मेल",
|
||||
"Storage": "भंडारण",
|
||||
"Signing certificate": "हस्ताक्षर प्रमाण पत्र",
|
||||
"General": "सामान्य",
|
||||
"Organizations": "संगठन",
|
||||
"OrgAdmins": "संगठन व्यवस्थापक",
|
||||
"Debug Pdf": "पीडीएफ़ डीबग करें",
|
||||
"New Document": "नया दस्तावेज़",
|
||||
"subscription": "सब्सक्रिप्शन",
|
||||
"Draft document": "ड्राफ्ट दस्तावेज़",
|
||||
"Draft template": "ड्राफ्ट टेम्पलेट",
|
||||
"Public sign": "पब्लिक साइन",
|
||||
"Signup": "साइन अप करें",
|
||||
"delete-contact": "संपर्क हटाएं",
|
||||
"total-records-found": "कुल रिकॉर्ड मिले: {{count}}",
|
||||
"Invalid-records-found": "अमान्य रिकॉर्ड मिले: {{records}}",
|
||||
"previous": "पिछला",
|
||||
"page-n-of-n": "पृष्ठ {{currentPage}} का {{totalPages}}",
|
||||
"import": "आयात करें",
|
||||
"search": "खोजें",
|
||||
"viewed-on": "देखा गया: {{ViewedOn}}",
|
||||
"signed-on": "हस्ताक्षर किया गया: {{SignedOn}}",
|
||||
"hide": "छिपाएं",
|
||||
"show-more": "और दिखाएं",
|
||||
"browse-or-drag-to-replace-existing-file": "नई फ़ाइल ब्राउज़ करें या खींचकर छोड़ें ताकि मौजूदा फ़ाइल को बदला जा सके",
|
||||
"optional-details": "वैकल्पिक विवरण",
|
||||
"hide-optional-details": "वैकल्पिक विवरण छिपाएं",
|
||||
"mail-adapter-subscription-alert": "कस्टम SMTP सेटअप करने के लिए कृपया प्रोफेशनल या टीम प्लान में अपग्रेड करें।",
|
||||
"connect-to-mail": "Gmail से कनेक्ट करें",
|
||||
"custom-smtp": "कस्टम SMTP",
|
||||
"default-smtp": "{{appName}} डिफ़ॉल्ट SMTP",
|
||||
"host": "होस्ट",
|
||||
"port": "पोर्ट",
|
||||
"sender-email": "प्रेषक ईमेल",
|
||||
"username": "यूज़रनेम",
|
||||
"use-default-mail-adapter": "क्या आप वाकई {{appName}} के डिफ़ॉल्ट मेल सर्वर का उपयोग करके सिग्नेचर अनुरोध भेजना चाहते हैं? बेहतर इनबॉक्स डिलीवरी के लिए हम अपने Gmail या SMTP सर्वर का उपयोग करने की सिफारिश करते हैं।",
|
||||
"verification-code-sent-registered-email": "आपके पंजीकृत ईमेल <1>{{useremail}}</1> पर एक सत्यापन कोड भेजा गया है। कृपया अपनी सेटिंग की पुष्टि के लिए नीचे कोड दर्ज करें।",
|
||||
"smpt-credentials": "SMTP क्रेडेंशियल्स",
|
||||
"delete-account": "खाता हटाएं",
|
||||
"delete-account-que": "क्या आप वाकई अपना खाता हटाना चाहते हैं?",
|
||||
"delete-account-que-user": "आप इस उपयोगकर्ता और सभी संबंधित डेटा को स्थायी रूप से हटाने वाले हैं। इस क्रिया को पूर्ववत नहीं किया जा सकता।",
|
||||
"user-deleted-successfully": "उपयोगकर्ता और सभी संबंधित डेटा सफलतापूर्वक हटाए गए।",
|
||||
"account-deletion-request-sent-via-mail": "हमने आपको एक पुष्टि लिंक ईमेल किया है। अपने खाते को हटाने की प्रक्रिया पूरी करने के लिए अनुरोध को स्वीकृत करें।",
|
||||
"type-exact-email-delete": "हटाने को सक्षम करने के लिए सटीक ईमेल टाइप करें",
|
||||
"email-does-not-match": "ईमेल मेल नहीं खाता।",
|
||||
"please-type-to-confirm": "कृपया पुष्टि करने के लिए <1>{{userEmail}}</1> टाइप करें:",
|
||||
"email-settings": "ईमेल सेटिंग्स",
|
||||
"email-settings-help": "अपने हस्ताक्षर अनुरोध ईमेल की बेहतर इनबॉक्स डिलीवरी सुनिश्चित करने के लिए, आप अपने स्वयं के ईमेल प्रदाता को कनेक्ट कर सकते हैं। निम्न विकल्पों में से एक चुनें:",
|
||||
"connect-to-gmail": "Gmail से कनेक्ट करें: ",
|
||||
"connect-to-gmail-help": "अपने Gmail खाते का उपयोग करके हस्ताक्षर अनुरोध ईमेल सीधे अपने इनबॉक्स से भेजें, जिससे डिलीवरी दर और विश्वसनीयता में सुधार होगा।",
|
||||
"connect-to-smtp": "कस्टम SMTP: ",
|
||||
"connect-to-smtp-help": "अपने डोमेन के माध्यम से ईमेल भेजने के लिए अपने कस्टम SMTP सर्वर का उपयोग करें। यह विकल्प आपको अपने ईमेल इंफ्रास्ट्रक्चर पर पूरा नियंत्रण देता है, जिससे डिलीवरी और ब्रांड की निरंतरता में सुधार होता है।",
|
||||
"connect-to-default": "यदि आप चाहें, तो आप {{appName}} के डिफ़ॉल्ट मेल सर्वर का भी उपयोग कर सकते हैं, हालांकि सर्वोत्तम परिणामों के लिए हम अपने सर्वर का उपयोग करने की सलाह देते हैं।",
|
||||
"email-settings-redirect-message": "यह सेटिंग कंसोल से मुख्य मेनू सेटिंग्स → प्रेफ़रेंस में स्थानांतरित कर दी गई है। यह पेज आने वाले संस्करणों में हटा दिया जाएगा।",
|
||||
"go-to-preferences-menu": "प्रेफ़रेंस मेनू पर जाएं",
|
||||
"document-download-filename-format": "दस्तावेज़ डाउनलोड फ़ाइलनाम प्रारूप",
|
||||
"preview": "पूर्वावलोकन: ",
|
||||
"download-filename-format-help": "डाउनलोड की गई PDF फ़ाइलों का नाम कैसे रखा जाए, चुनें। आपका चयन आपके प्रोफ़ाइल में सहेजा जाएगा और पूरे ऐप में उपयोग किया जाएगा।",
|
||||
"delete-action-prohibited": "यह कार्रवाई अनुमत नहीं है। कृपया अपना खाता हटाने के लिए अपने व्यवस्थापक से संपर्क करें।",
|
||||
"not-verified": "सत्यापित नहीं",
|
||||
"verified": "सत्यापित",
|
||||
"expires": "समाप्ति",
|
||||
"fix-resend-error": "आप इस दस्तावेज़ को ठीक करके पुनः नहीं भेज सकते क्योंकि इसमें प्रीफ़िल विजेट्स हैं।",
|
||||
"duplicate-template-error": "आप इस टेम्पलेट को डुप्लिकेट नहीं कर सकते क्योंकि इसमें प्रीफ़िल विजेट्स हैं।",
|
||||
"save-as-template-error": "इस दस्तावेज़ को टेम्पलेट के रूप में सहेजा नहीं जा सकता क्योंकि इसमें प्रीफ़िल विजेट्स हैं।",
|
||||
"redirecting-you-in": "{{redirectTimeLeft}} सेकंड में आपको रीडायरेक्ट किया जा रहा है...",
|
||||
"pdf-tools-tour": "पृष्ठ जोड़ने, हटाने, पुनः व्यवस्थित करने, घुमाने और ज़ूम करने के लिए इन बटनों पर क्लिक करें।",
|
||||
"widgets": "विजेट्स",
|
||||
"prefill-tour": "दस्तावेज़ को प्राप्तकर्ताओं को भेजने से पहले अग्रिम रूप से जानकारी दर्ज करने के लिए इस विकल्प का उपयोग करें।",
|
||||
"empty-prefill-error": "निम्न आवश्यक फ़ील्ड खाली नहीं छोड़े जा सकते:",
|
||||
"please-fill-out": "कृपया आगे बढ़ने के लिए इन्हें भरें।",
|
||||
"custom-signing-certificate": "कस्टम साइनिंग प्रमाणपत्र",
|
||||
"signing-certificate-help": "आप अपना स्वयं का दस्तावेज़ हस्ताक्षर प्रमाणपत्र अपलोड कर सकते हैं, जिसका उपयोग आपके सभी दस्तावेज़ों और पूर्णता प्रमाणपत्रों पर हस्ताक्षर करने के लिए किया जाएगा। प्रमाणपत्र फ़ाइल PFX फॉर्मेट में P12 प्रमाणपत्र होनी चाहिए।",
|
||||
"certificate-file-p12-in-PFX-format": "प्रमाणपत्र फ़ाइल (PFX फॉर्मेट में p12 प्रमाणपत्र)",
|
||||
"password-of-pfx-file": "pfx फ़ाइल का पासवर्ड दर्ज करें",
|
||||
"update": "अपडेट करें",
|
||||
"use-default-certificate": "डिफ़ॉल्ट {{appName}} प्रमाणपत्र का उपयोग करें",
|
||||
"upgrade-to-team-plan": "टीम प्लान में अपग्रेड करें",
|
||||
"setup-file-storage": "फ़ाइल स्टोरेज सेटअप करें",
|
||||
"save-and-activate": "सहेजें और सक्रिय करें",
|
||||
"logging-out-to-apply-settings": "नई सेटिंग लागू करने के लिए आपको लॉग आउट किया जा रहा है",
|
||||
"reconnect-to-default": "{{appName}} से फिर से कनेक्ट करें",
|
||||
"active-file-adapter": "सक्रिय फ़ाइल एडेप्टर",
|
||||
"file-adapter-unique-name": "फ़ाइल एडेप्टर का यूनिक नाम",
|
||||
"unique-name-for-file-adapter": "फ़ाइल एडेप्टर के लिए यूनिक नाम दर्ज करें",
|
||||
"storage-provider": "स्टोरेज प्रदाता",
|
||||
"enter-bucket-name": "बकेट का नाम दर्ज करें",
|
||||
"enter-space-name": "स्पेस का नाम दर्ज करें",
|
||||
"enter-region-of-bucket": "बकेट का क्षेत्र दर्ज करें",
|
||||
"enter-region-of-space": "स्पेस का क्षेत्र दर्ज करें",
|
||||
"enter-access-key": "एक्सेस की दर्ज करें",
|
||||
"enter-secret-access-key": "सीक्रेट एक्सेस की दर्ज करें",
|
||||
"otp-email": "हमने एक सत्यापन कोड भेजा है",
|
||||
"save-as-temp-warn": "नोट: इस दस्तावेज़ में पहले से भरे हुए विजेट शामिल हैं, जिन्हें स्वचालित रूप से हटा दिया जाएगा क्योंकि वे पहले से ही मूल दस्तावेज़ में सम्मिलित हैं।",
|
||||
"edit-document": "दस्तावेज़ संपादित करें",
|
||||
"modify": "संशोधित करें",
|
||||
"merge-certificate-to-pdf": "प्रमाणपत्र को PDF में मिलाएँ",
|
||||
"merge-cc-to-pdf-help": {
|
||||
"p1": "यह सुनिश्चित करेगा कि पूर्णता प्रमाणपत्र अंतिम PDF दस्तावेज़ में शामिल हो। हालाँकि, कृपया ध्यान दें कि एक बार मिलाने के बाद, प्रमाणपत्र को मुख्य दस्तावेज़ से अलग नहीं किया जा सकता।",
|
||||
"p2": "यदि आप मिलाना नहीं चुनते हैं, तो पूर्णता प्रमाणपत्र हस्ताक्षरित दस्तावेज़ के साथ एक अलग PDF फ़ाइल के रूप में प्रदान किया जाएगा।"
|
||||
},
|
||||
"read-only-date-error": "रीड-ओनली दिनांक विजेट में एक डिफ़ॉल्ट दिनांक होना आवश्यक है।",
|
||||
"set-date": "दिनांक सेट करें",
|
||||
"default-date": "डिफ़ॉल्ट तिथि",
|
||||
"set-today": "हस्ताक्षर की तिथि",
|
||||
"enter-name": "नाम दर्ज करें",
|
||||
"enter-email": "ईमेल दर्ज करें",
|
||||
"subscribe-to-opensign-msg": "{{appName}} की सदस्यता लें और असीमित निःशुल्क डिजिटल हस्ताक्षरों का आनंद लें।",
|
||||
"duplicate-subscribe-msg": "डुप्लिकेट करने से आप चयनित टेम्पलेट की एक सटीक प्रति बना सकते हैं, जिसे आप पुनः उपयोग या संशोधित कर सकते हैं बिना मूल को प्रभावित किए।",
|
||||
"save-as-template-msg": "इस दस्तावेज़ को एक पुन: प्रयोज्य टेम्पलेट के रूप में सहेजें जिसे आप भविष्य के दस्तावेज़ों के लिए फिर से उपयोग कर सकते हैं।",
|
||||
"public-credit-alert": "इस लिंक के माध्यम से हस्ताक्षर करने पर आपके निःशुल्क ईमेल क्रेडिट का उपभोग होगा। एक निःशुल्क उपयोगकर्ता के रूप में, आपको स्पैमर्स को हमारी प्रणाली का दुरुपयोग करने से रोकने के लिए प्रति माह 15 ईमेल क्रेडिट आवंटित किए जाते हैं। उच्च सीमा और निर्बाध पहुँच का आनंद लेने के लिए, <1>OpenSign™ पेड प्लान</1> की सदस्यता लें।",
|
||||
"know-more-about": "इसके बारे में और जानें",
|
||||
"free-unlimited-signatures": "मुफ़्त असीमित हस्ताक्षर",
|
||||
"email-quota-updated": "दैनिक ईमेल कोटा सफलतापूर्वक अपडेट किया गया।",
|
||||
"password-has-been-reset": "पासवर्ड रीसेट कर दिया गया है।",
|
||||
"Unauthorized to reset your own password.": "अपना पासवर्ड रीसेट करने के लिए अधिकृत नहीं है।",
|
||||
"User not found or not allowed.": "उपयोगकर्ता नहीं मिला या अनुमति नहीं है।",
|
||||
"User not found.": "उपयोगकर्ता नहीं मिला।",
|
||||
"Admin user tenant not found.": "एडमिन उपयोगकर्ता टेनेंट नहीं मिला।",
|
||||
"User is not authenticated.": "उपयोगकर्ता प्रमाणीकृत नहीं है।",
|
||||
"Please provide required parameters.": "कृपया आवश्यक पैरामीटर प्रदान करें।",
|
||||
"reset-password": "उपयोगकर्ता पासवर्ड रीसेट करें",
|
||||
"enter-strong-password": "एक मज़बूत पासवर्ड दर्ज करें या स्वचालित रूप से उत्पन्न करें।",
|
||||
"autogenerate": "स्वतः उत्पन्न करें",
|
||||
"extend": "बढ़ाएँ",
|
||||
"console-general-help": "व्यक्तिगत उपयोगकर्ताओं को अपनी स्वयं की ईमेल टेम्पलेट्स अनुकूलित करने की अनुमति देने के लिए, जाएँ",
|
||||
"console-mail-help": "कंपनी-व्यापी ईमेल टेम्पलेट्स अपडेट करने के लिए, जाएँ",
|
||||
"admin-setting-disabled": "यह सेटिंग अक्षम है। सहायता के लिए कृपया व्यवस्थापक से संपर्क करें।",
|
||||
"company-email-settings": "कंपनी ईमेल सेटिंग्स",
|
||||
"allow-email-template-individually": "उपयोगकर्ताओं को व्यक्तिगत रूप से ईमेल टेम्पलेट अनुकूलित करने की अनुमति दें ",
|
||||
"user-email-template-general-help": "इसे निष्क्रिय करने पर उपयोगकर्ता अपनी ईमेल टेम्पलेट्स को अनुकूलित नहीं कर पाएंगे।",
|
||||
"disable-individual-user-smtp": "व्यक्तिगत उपयोगकर्ता SMTP सेटिंग्स अक्षम करें",
|
||||
"enable-individual-user-smtp": "व्यक्तिगत उपयोगकर्ता SMTP सेटिंग्स सक्षम करें",
|
||||
"user-smtp-general-help": "जब यह निष्क्रिय हो, तो उपयोगकर्ता अपनी स्वयं की SMTP सेटिंग्स कॉन्फ़िगर नहीं कर पाएंगे। सभी ईमेल ग्लोबल SMTP कॉन्फ़िगरेशन का उपयोग करेंगे।",
|
||||
"global-smtp-settings": "ग्लोबल SMTP सेटिंग्स",
|
||||
"global-smtp-settings-help-p1": "ये सेटिंग्स आपके पूरे {{appName}} खाते में वैश्विक रूप से लागू होती हैं। यदि किसी उपयोगकर्ता ने अपनी स्वयं की ईमेल डिलीवरी सेटिंग्स कॉन्फ़िगर नहीं की हैं, तो सिस्टम स्वचालित रूप से यहाँ परिभाषित ग्लोबल SMTP सेटिंग्स का उपयोग करेगा।",
|
||||
"global-smtp-settings-help-p2": "सभी हस्ताक्षर अनुरोध ईमेल की बेहतर इनबॉक्स डिलीवरी सुनिश्चित करने के लिए, आप अपना पसंदीदा ईमेल प्रदाता जोड़ सकते हैं। निम्न विकल्पों में से एक चुनें:",
|
||||
"global-connect-to-gmail-help": "अपनी संस्था की इनबॉक्स से सीधे हस्ताक्षर अनुरोध ईमेल भेजने के लिए Gmail खाते का उपयोग करें। यह डिलीवरी दर, प्रतिष्ठा और विश्वसनीयता में सुधार करता है।",
|
||||
"global-connect-to-smtp-help": "अपने डोमेन के माध्यम से ईमेल भेजने के लिए अपना स्वयं का SMTP सर्वर उपयोग करें। यह आपकी ईमेल संरचना पर पूर्ण नियंत्रण प्रदान करता है, डिलीवरी को बेहतर बनाता है और ब्रांड की निरंतरता बनाए रखता है।",
|
||||
"global-smtp-settings-help-p3": "यदि कोई व्यक्तिगत उपयोगकर्ता-स्तरीय सेटिंग्स कॉन्फ़िगर नहीं की गई हैं, तो ये वैश्विक सेटिंग्स स्वचालित रूप से लागू होंगी।",
|
||||
"hide-text-with-asterisks": "टेक्स्ट को तारांकन (*) के साथ छुपाएँ",
|
||||
"hide-text-with-asterisks-help": "हस्ताक्षरित दस्तावेज़ पर टेक्स्ट दृश्य से छिपा रहेगा, लेकिन दस्तावेज़ भेजने वाले (मालिक) के लिए उनके {{appName}} खाते से सुलभ रहेगा।",
|
||||
"document-id": "दस्तावेज़ ID",
|
||||
"sent-date": "भेजे जाने की तिथि",
|
||||
"status": "स्थिति",
|
||||
"signer-form-data-for": "हस्ताक्षरकर्ता के लिए फ़ॉर्म डेटा: {{signerName}} <{{signerEmail}}>",
|
||||
"widget-name": "विजेट नाम",
|
||||
"widget-responses": "विजेट प्रतिक्रियाएँ",
|
||||
"formdata-download-note": "नोट: हस्ताक्षर, स्टैम्प, प्रारंभिक अक्षर और छवियाँ निर्यात नहीं की जाएँगी। ऑफ़लाइन हस्ताक्षरकर्ताओं का डेटा उपलब्ध नहीं होगा।",
|
||||
"export-all-formdata-error": "आप फ़ॉर्म डेटा निर्यात नहीं कर सकते क्योंकि टेम्पलेट में डुप्लिकेट विजेट नाम हैं। कृपया नया टेम्पलेट बनाएं ताकि आप फ़ॉर्म डेटा निर्यात कर सकें।",
|
||||
"export-formdata-error": "आप फ़ॉर्म डेटा निर्यात नहीं कर सकते क्योंकि दस्तावेज़ में डुप्लिकेट विजेट नाम हैं। कृपया नया दस्तावेज़ बनाएं जिसमें डुप्लिकेट विजेट नाम न हों।",
|
||||
"duplicate-widget-name-error": "यह विजेट नाम पहले से ही किसी अन्य साइनर को सौंपा गया है। कृपया कोई दूसरा नाम चुनें।",
|
||||
"duplicate-template-widget-error": "टेम्पलेट को डुप्लिकेट नहीं किया जा सकता क्योंकि कुछ विजेट नाम अलग-अलग भूमिकाओं में डुप्लिकेट हैं। डुप्लिकेट विजेट्स का नाम बदलें और फिर से प्रयास करें।",
|
||||
"credit-alert": "क्रेडिट अलर्ट",
|
||||
"document-form-data": "दस्तावेज़ फ़ॉर्म डेटा",
|
||||
"view-formdata-paid-title-error": "'फ़ॉर्म डेटा देखें' सुविधा आपको हस्ताक्षर करते समय साइनरों द्वारा भरे गए विजेट डेटा को प्रिंट और डाउनलोड करने की सुविधा देती है.",
|
||||
"view-formdata-paid-error": "नोट: 'फ़ॉर्म डेटा देखें' सुविधा केवल उन दस्तावेज़ों के लिए उपलब्ध है जो v2.32.0 या उसके बाद बनाए गए हों और जिनमें डुप्लिकेट विजेट नाम न हों.",
|
||||
"download-all-formdata-paid-title-error": "'सभी फ़ॉर्म डेटा डाउनलोड करें' सुविधा आपको टेम्पलेट से बनाए गए सभी दस्तावेज़ों के लिए साइनरों द्वारा भरे गए विजेट डेटा को डाउनलोड करने देती है.",
|
||||
"download-all-formdata-paid-error": "नोट: 'सभी फ़ॉर्म डेटा डाउनलोड करें' सुविधा केवल उन टेम्पलेट्स के लिए उपलब्ध है जो v2.32.0 या उसके बाद बनाए गए हों और जिनमें डुप्लिकेट विजेट नाम न हों.",
|
||||
"api-token-not-found": "API टोकन नहीं मिला।",
|
||||
"sub-head-api-credits": "इस उपयोगकर्ता द्वारा उपभोग किए जाने वाले API क्रेडिट की अधिकतम संख्या निर्धारित करें।",
|
||||
"consumed-credits": "उपयोग किए गए क्रेडिट",
|
||||
"allowed-api-credits": "अनुमत API क्रेडिट",
|
||||
"allowed-api-credits-help": "अनुमत क्रेडिट उपयोग किए गए क्रेडिट के बराबर या उससे अधिक होने चाहिए।",
|
||||
"Enter-allowed-api-credits": "अनुमत API क्रेडिट दर्ज करें",
|
||||
"login-to-sandbox": "सैंडबॉक्स में लॉगिन करें",
|
||||
"your-credits-usage": "आपके क्रेडिट उपयोग",
|
||||
"remaining": "शेष",
|
||||
"used": "उपयोग किए गए",
|
||||
"your-credits-usage-help": "यह आपके API कुंजी द्वारा उपभोग किए गए क्रेडिट की संख्या है।",
|
||||
"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}} के लिए अमान्य मान पाया गया।",
|
||||
"all-signer-status": "सभी हस्ताक्षरकर्ताओं की स्थिति",
|
||||
"viewed": "देखा गया",
|
||||
"signed": "हस्ताक्षरित",
|
||||
"switch-to-advanced": "एडवांस्ड पर स्विच करें",
|
||||
"switch-to-basic": "बेसिक पर स्विच करें",
|
||||
"use-name-as-sender": "प्रेषक के रूप में प्रोफ़ाइल नाम का उपयोग करें",
|
||||
"use-name-as-sender-help": "सक्षम होने पर, {{appName}} हस्ताक्षर अनुरोध और पूर्णता ईमेल के लिए आपके प्रोफ़ाइल नाम को प्रेषक के रूप में उपयोग करता है।",
|
||||
"acro-fields-detected": "फॉर्म फ़ील्ड मिले",
|
||||
"acro-fields-detected-msg": "अपलोड किए गए PDF में {{count}} फॉर्म फ़ील्ड पाए गए। क्या आप स्वचालित रूप से संबंधित विजेट सम्मिलित करना चाहेंगे?",
|
||||
"documents-count": "दस्तावेज़ों की संख्या",
|
||||
"templates-count": "टेम्पलेट्स की संख्या",
|
||||
"emails-sent": "भेजे गए ईमेल",
|
||||
"storage-used": "उपयोग किया गया स्टोरेज",
|
||||
"docs-count-team-help": "आपकी संगठनों में सभी उपयोगकर्ताओं द्वारा बनाए गए दस्तावेज़ों की कुल संख्या।",
|
||||
"docs-count-help": "आपके द्वारा बनाए गए दस्तावेज़ों की कुल संख्या।",
|
||||
"templates-count-team-help": "आपकी संगठनों में सभी उपयोगकर्ताओं द्वारा बनाए गए टेम्पलेट्स की कुल संख्या।",
|
||||
"templates-count-help": "आपके द्वारा बनाए गए टेम्पलेट्स की कुल संख्या।",
|
||||
"emails-sent-team-help": "आपकी संगठनों में भेजी गई ईमेल सूचनाओं की कुल संख्या।",
|
||||
"emails-sent-help": "आपके द्वारा भेजी गई ईमेल सूचनाओं की कुल संख्या।",
|
||||
"storage-used-team-help": "आपकी संगठनों में सभी उपयोगकर्ताओं द्वारा उपयोग किया गया कुल स्टोरेज स्पेस।",
|
||||
"storage-used-help": "आपके द्वारा उपयोग किया गया कुल स्टोरेज स्पेस।",
|
||||
"cloud": "क्लाउड",
|
||||
"on-premises": "ऑन-प्रिमाइसेस",
|
||||
"plan-name-free": "OPENSIGN™ FREE",
|
||||
"plan-name-professional": "OPENSIGN™ PROFESSIONAL",
|
||||
"plan-name-teams": "OPENSIGN™ TEAMS",
|
||||
"plan-name-enterprise": "OPENSIGN™ ENTERPRISE",
|
||||
"plan-name-self-hosted": "OPENSIGN™ SELF-HOSTED",
|
||||
"plan-subtitle-free": "निःशुल्क असीमित ई-हस्ताक्षर, हमेशा के लिए।",
|
||||
"plan-subtitle-professional": "उन्नत सुविधाओं तक विशेष पहुँच।",
|
||||
"plan-subtitle-scalable": "प्राथमिकता समर्थन के साथ स्केलेबल सुविधाएँ।",
|
||||
"subscribe": "सदस्यता लें",
|
||||
"contact-us": "हमसे संपर्क करें",
|
||||
"benefit-unlimited-digital-signatures": "असीमित डिजिटल हस्ताक्षर",
|
||||
"benefit-sign-docs-yourself": "स्वयं दस्तावेज़ पर हस्ताक्षर करें",
|
||||
"benefit-request-signatures": "दूसरों से हस्ताक्षर का अनुरोध करें",
|
||||
"benefit-unlimited-templates": "असीमित टेम्पलेट",
|
||||
"benefit-14-field-types": "14 फ़ील्ड प्रकार",
|
||||
"benefit-automatic-e-signatures": "स्वचालित ई-हस्ताक्षर",
|
||||
"benefit-completion-certificates": "समापन प्रमाण पत्र",
|
||||
"benefit-send-in-order": "क्रम में भेजें",
|
||||
"benefit-organize-docs": "OpenSign™ Drive में दस्तावेज़ व्यवस्थित करें",
|
||||
"benefit-document-templates": "दस्तावेज़ टेम्पलेट",
|
||||
"benefit-import-from-dropbox": "Dropbox से आयात करें",
|
||||
"benefit-contact-book": "संपर्क पुस्तक",
|
||||
"benefit-document-expiry": "दस्तावेज़ समाप्ति समर्थन",
|
||||
"benefit-decline-document": "दस्तावेज़ अस्वीकृति समर्थन",
|
||||
"benefit-email-notifications": "ईमेल सूचनाएं",
|
||||
"benefit-public-profiles": "सार्वजनिक प्रोफ़ाइल",
|
||||
"benefit-and-much-more": "और बहुत कुछ",
|
||||
"benefit-everything-free": "OpenSign™ Free में सब कुछ",
|
||||
"benefit-field-validations": "फ़ील्ड सत्यापन",
|
||||
"benefit-regex-validations": "नियमित अभिव्यक्ति सत्यापन",
|
||||
"benefit-webhooks": "वेबहुक",
|
||||
"benefit-zapier": "Zapier एकीकरण",
|
||||
"benefit-api-access": "API एक्सेस",
|
||||
"benefit-api-signatures-100": "100 तक API हस्ताक्षर",
|
||||
"benefit-api-signatures-240": "240 तक API हस्ताक्षर",
|
||||
"benefit-custom-email-templates": "कस्टम ईमेल टेम्पलेट",
|
||||
"benefit-custom-smtp": "ईमेल भेजने के लिए अपना Gmail या SMTP खाता कनेक्ट करें",
|
||||
"benefit-auto-reminders": "स्वचालित अनुस्मारक",
|
||||
"benefit-bulk-send-100": "बल्क भेजें (100 दस्तावेज़ तक)",
|
||||
"benefit-bulk-send-240": "बल्क भेजें (240 दस्तावेज़ तक)",
|
||||
"benefit-premium-profiles": "प्रीमियम सार्वजनिक प्रोफ़ाइल उपयोगकर्ता नाम",
|
||||
"benefit-email-verification": "हस्ताक्षरकर्ता की पहचान की पुष्टि के लिए ईमेल-आधारित सत्यापन लागू करें",
|
||||
"benefit-embedded-signing": "एम्बेडेड हस्ताक्षर",
|
||||
"benefit-everything-professional": "OpenSign™ Professional में सब कुछ",
|
||||
"benefit-teams-organizations": "टीम और संगठन",
|
||||
"benefit-share-templates-teams": "टीमों के साथ टेम्पलेट साझा करें",
|
||||
"benefit-share-templates-individuals": "व्यक्तियों के साथ टेम्पलेट साझा करें",
|
||||
"benefit-byoc": "BYOC - अपने दस्तावेज़ अपने क्लाउड स्टोरेज में संग्रहीत करें",
|
||||
"benefit-doc-id-removal": "हस्ताक्षरित दस्तावेज़ों से DocumentId हटाना",
|
||||
"benefit-request-payments": "भुगतान का अनुरोध (जल्द आ रहा है)",
|
||||
"benefit-mobile-app": "मोबाइल ऐप (जल्द आ रहा है)",
|
||||
"benefit-api-signatures-500": "500 तक API हस्ताक्षर",
|
||||
"benefit-bulk-send-500": "बल्क भेजें (500 दस्तावेज़ तक)",
|
||||
"benefit-all-features": "सभी सुविधाएँ",
|
||||
"benefit-custom-domain": "कस्टम डोमेन",
|
||||
"benefit-custom-branding": "कस्टम ब्रांडिंग",
|
||||
"benefit-uptime-sla": "अपटाइम SLA",
|
||||
"benefit-sso-support": "SSO समर्थन",
|
||||
"benefit-priority-support": "प्राथमिकता समर्थन",
|
||||
"benefit-3-users-1500-credits": "3 उपयोगकर्ता और 1500 प्रीमियम क्रेडिट शामिल हैं",
|
||||
"benefit-public-links": "सार्वजनिक लिंक",
|
||||
"benefit-kiosk-mode": "कियोस्क मोड",
|
||||
"benefit-bulk-send-1500": "बल्क भेजें (1500 दस्तावेज़ तक)",
|
||||
"benefit-connect-gmail-smtp": "अपना Gmail या SMTP कनेक्ट करें",
|
||||
"benefit-analytics": "विश्लेषण",
|
||||
"benefit-bulksend-api-pro-month-help": "इस प्लान में प्रति माह 100 प्रीमियम क्रेडिट मिलते हैं, जिनसे आप API के जरिए दस्तावेज़ भेज सकते हैं, पब्लिक साइनिंग लिंक शेयर कर सकते हैं, बल्क सिग्नेचर रिक्वेस्ट चला सकते हैं, इन-पर्सन साइनिंग के लिए Kiosk मोड का उपयोग कर सकते हैं, या अपनी वेबसाइट पर सीधे एम्बेड किए गए दस्तावेज़ों से सिग्नेचर एकत्र कर सकते हैं। हर बार जब आप किसी दस्तावेज़ को शुरू (initiate) करते हैं या भेजते हैं, 1 क्रेडिट उपयोग होता है — और उस एक दस्तावेज़ में अधिकतम 10 साइनर हो सकते हैं।",
|
||||
"benefit-bulksend-api-pro-year-help": "इस प्लान में प्रति वर्ष 240 प्रीमियम क्रेडिट मिलते हैं, जिनसे आप API के जरिए दस्तावेज़ भेज सकते हैं, पब्लिक साइनिंग लिंक शेयर कर सकते हैं, बल्क सिग्नेचर रिक्वेस्ट चला सकते हैं, इन-पर्सन साइनिंग के लिए Kiosk मोड का उपयोग कर सकते हैं, या अपनी वेबसाइट पर सीधे एम्बेड किए गए दस्तावेज़ों से सिग्नेचर एकत्र कर सकते हैं। हर बार जब आप किसी दस्तावेज़ को शुरू (initiate) करते हैं या भेजते हैं, 1 क्रेडिट उपयोग होता है — और उस एक दस्तावेज़ में अधिकतम 10 साइनर हो सकते हैं।",
|
||||
"benefit-bulksend-api-team-month-help": "इस प्लान में प्रति माह 100 प्रीमियम क्रेडिट मिलते हैं, जिनसे आप API के जरिए दस्तावेज़ भेज सकते हैं, पब्लिक साइनिंग लिंक शेयर कर सकते हैं, बल्क सिग्नेचर रिक्वेस्ट चला सकते हैं, इन-पर्सन साइनिंग के लिए Kiosk मोड का उपयोग कर सकते हैं, या अपनी वेबसाइट पर सीधे एम्बेड किए गए दस्तावेज़ों से सिग्नेचर एकत्र कर सकते हैं। हर बार जब आप किसी दस्तावेज़ को शुरू (initiate) करते हैं या भेजते हैं, 1 क्रेडिट उपयोग होता है — और उस एक दस्तावेज़ में अधिकतम 10 साइनर हो सकते हैं।",
|
||||
"benefit-bulksend-api-team-year-help": "इस प्लान में प्रति वर्ष 500 प्रीमियम क्रेडिट मिलते हैं, जिनसे आप API के जरिए दस्तावेज़ भेज सकते हैं, पब्लिक साइनिंग लिंक शेयर कर सकते हैं, बल्क सिग्नेचर रिक्वेस्ट चला सकते हैं, इन-पर्सन साइनिंग के लिए Kiosk मोड का उपयोग कर सकते हैं, या अपनी वेबसाइट पर सीधे एम्बेड किए गए दस्तावेज़ों से सिग्नेचर एकत्र कर सकते हैं। हर बार जब आप किसी दस्तावेज़ को शुरू (initiate) करते हैं या भेजते हैं, 1 क्रेडिट उपयोग होता है — और उस एक दस्तावेज़ में अधिकतम 10 साइनर हो सकते हैं।",
|
||||
"failed-pdf-for-prefill-processing": "प्रीफिल प्रोसेसिंग के लिए PDF लोड करने में विफल।",
|
||||
"attach-all-role-to-signer": "कृपया सभी भूमिकाएँ साइनर से जोड़ें।",
|
||||
"upgrade-to-edit": "एडिट करने के लिए अपग्रेड करें",
|
||||
"hide-live-chat-for-signers": "साइनर के लिए लाइव चैट छुपाएं",
|
||||
"hide-live-chat-for-signers-help": "सक्षम होने पर, साइनिंग पेजों पर साइनर के लिए लाइव चैट विजेट छुपाए जाएंगे।",
|
||||
"attachments-creator-hint": "हस्ताक्षरकर्ता ऐसी फ़ाइलें अपलोड कर सकते हैं जो दस्तावेज़ के अंत में जोड़ी जाएँगी।",
|
||||
"attachments-drop-hint": "फ़ाइलें यहाँ छोड़ें या चुनने के लिए क्लिक करें",
|
||||
"attachments-limits": "PDF, DOCX, PNG, JPG. अधिकतम {{maxFiles}} फ़ाइलें, प्रत्येक {{maxSizeMB}} MB।",
|
||||
"attachments-invalid-type": "असमर्थित फ़ाइल प्रकार: {{name}}",
|
||||
"attachments-max-files": "अधिकतम {{maxFiles}} संलग्नक अनुमत हैं।",
|
||||
"attachments-remove-confirm": "हटाने से केवल मेटाडेटा हटेगा। पहले से जोड़े गए पृष्ठ दस्तावेज़ में बने रहेंगे। जारी रखें?",
|
||||
"conditional-logic": "सशर्त तर्क",
|
||||
"enabled": "सक्षम",
|
||||
"disabled": "अक्षम",
|
||||
"logic-no-sources": "इस हस्ताक्षरकर्ता का कोई अन्य फ़ील्ड स्रोत के रूप में उपयोग नहीं किया जा सकता।",
|
||||
"logic-action": "कार्रवाई",
|
||||
"logic-when": "जब",
|
||||
"logic-all": "सभी",
|
||||
"logic-any": "कोई भी",
|
||||
"logic-of-following": "निम्न शर्तें सही हों",
|
||||
"logic-add-rule": "नियम जोड़ें",
|
||||
"logic-pick-value": "मान चुनें",
|
||||
"logic-value-placeholder": "मान दर्ज करें",
|
||||
"logic-action-show": "इस फ़ील्ड को दिखाएँ",
|
||||
"logic-action-hide": "इस फ़ील्ड को छिपाएँ",
|
||||
"logic-action-require": "अनिवार्य करें",
|
||||
"logic-action-optional": "वैकल्पिक करें",
|
||||
"logic-op-equals": "बराबर है",
|
||||
"logic-op-notEquals": "बराबर नहीं है",
|
||||
"logic-op-checked": "चेक किया गया है",
|
||||
"logic-op-unchecked": "चेक नहीं किया गया है",
|
||||
"widget-hidden-due-to-logic": "यह विजेट सशर्त तर्क के कारण छिपा हुआ है।",
|
||||
"remove": "हटाएँ",
|
||||
"attachement-note": "नोट: यह विजेट हस्ताक्षरकर्ताओं को साइनिंग प्रक्रिया के दौरान फ़ाइल अपलोड करने की अनुमति देता है। अपलोड की गई फ़ाइल मौजूदा PDF के अंत में अतिरिक्त पेज के रूप में जोड़ दी जाएगी।",
|
||||
"attachments-docx-not-supported": "हम वर्तमान में DOCX फ़ाइल अपलोड में एक समस्या का सामना कर रहे हैं। कृपया दस्तावेज़ को किसी अन्य समर्थित प्रारूप में अपलोड करने का प्रयास करें, या बाद में पुनः प्रयास करें।",
|
||||
"access-code": "एक्सेस कोड",
|
||||
"access-code-set": "एक्सेस कोड सेट किया गया",
|
||||
"access-code-add": "एक्सेस कोड जोड़ें",
|
||||
"access-code-help": "वैकल्पिक गुप्त कोड जो केवल इस हस्ताक्षरकर्ता को पता है। दस्तावेज़ देखने या हस्ताक्षर करने से पहले उन्हें इसे दर्ज करना होगा। इसे प्राप्तकर्ता के साथ किसी अन्य माध्यम (फ़ोन, व्यक्तिगत रूप से) से साझा करें।",
|
||||
"access-code-placeholder": "कम से कम 6 अंक",
|
||||
"access-code-clear": "साफ़ करें",
|
||||
"access-code-must-be-numeric": "एक्सेस कोड में केवल 0-9 अंक होने चाहिए।",
|
||||
"access-code-save": "सहेजें",
|
||||
"access-code-required": "प्रेषक ने इस प्राप्तकर्ता को एक एक्सेस कोड दिया है। जारी रखने के लिए इसे दर्ज करें।",
|
||||
"access-code-invalid": "गलत एक्सेस कोड। कृपया प्रेषक से जांचें।",
|
||||
"access-code-too-short": "एक्सेस कोड कम से कम 6 अंक का होना चाहिए।",
|
||||
"access-code-verify": "एक्सेस कोड सत्यापित करें",
|
||||
"access-code-verified": "एक्सेस कोड सत्यापित हो गया।",
|
||||
"access-code-warning": "एक बार दस्तावेज़ भेज दिए जाने के बाद इस एक्सेस कोड को बदला या पुनः प्राप्त नहीं किया जा सकता।",
|
||||
"copy-access-code": "एक्सेस कोड कॉपी करें",
|
||||
"access-code-upgrade-msg": "एक्सेस कोड केवल भुगतान योजनाओं पर उपलब्ध हैं। प्राप्तकर्ताओं के लिए एक्सेस कोड जोड़ने के लिए अपनी योजना को अपग्रेड करें।",
|
||||
"show-access-code": "एक्सेस कोड दिखाएं",
|
||||
"hide-access-code": "एक्सेस कोड छिपाएं",
|
||||
"already-copied-to-clipboard": "पहले से क्लिपबोर्ड में कॉपी किया गया है"
|
||||
"console": "कंसोल"
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -1,4 +1,4 @@
|
||||
Name,Email,Phone,Company,JobTitle
|
||||
John Doe,john@example.com,1234567890,abc firm,dev
|
||||
Jane Smith,jane@example.com,9876543210,xyz firm,manager
|
||||
Foo Bar,foo@example.com,5555555555,xyz firm,director
|
||||
Name,Email,Phone
|
||||
John Doe,john@example.com,1234567890
|
||||
Jane Smith,jane@example.com,9876543210
|
||||
Foo Bar,foo@example.com,5555555555
|
||||
|
||||
|
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{ "source": "/:path((?!\\.well-known/).*)", "destination": "/index.html" }
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 86 KiB |
@@ -1,160 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-env node */
|
||||
/* eslint-disable no-console */
|
||||
// Tiny static file server for the production build.
|
||||
// - Serves real files from ./build (including dotfile dirs like .well-known)
|
||||
// - Falls back to /index.html only when the requested path does not exist
|
||||
// (SPA client-side routing).
|
||||
const http = require("node:http");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const root = path.join(__dirname, "build");
|
||||
const port = Number(process.env.PORT) || 3000;
|
||||
const host = process.env.HOST || "0.0.0.0";
|
||||
|
||||
const mime = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "application/javascript; charset=utf-8",
|
||||
".mjs": "application/javascript; charset=utf-8",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".map": "application/json; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".ico": "image/x-icon",
|
||||
".webp": "image/webp",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".ttf": "font/ttf",
|
||||
".otf": "font/otf",
|
||||
".txt": "text/plain; charset=utf-8",
|
||||
".pdf": "application/pdf",
|
||||
".csv": "text/csv; charset=utf-8",
|
||||
".wasm": "application/wasm",
|
||||
".xml": "application/xml; charset=utf-8"
|
||||
};
|
||||
|
||||
function contentType(filePath) {
|
||||
return mime[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
||||
}
|
||||
|
||||
|
||||
function safeJoin(reqPath) {
|
||||
let decoded;
|
||||
try {
|
||||
decoded = decodeURIComponent(reqPath.split("?")[0].split("#")[0]);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const resolved = path.normalize(path.join(root, decoded));
|
||||
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function cacheControl(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const relativePath = path.relative(root, filePath);
|
||||
const isBuildAsset =
|
||||
relativePath === "assets" ||
|
||||
relativePath.startsWith(`assets${path.sep}`);
|
||||
|
||||
if (ext === ".html" || ext === "") {
|
||||
return "no-cache";
|
||||
}
|
||||
|
||||
return isBuildAsset
|
||||
? "public, max-age=31536000, immutable"
|
||||
: "no-cache";
|
||||
}
|
||||
|
||||
function streamFile(req, res, filePath, stats) {
|
||||
const headers = {
|
||||
"Content-Type": contentType(filePath),
|
||||
"Content-Length": stats.size,
|
||||
"Last-Modified": stats.mtime.toUTCString(),
|
||||
"Cache-Control": cacheControl(filePath)
|
||||
};
|
||||
if (req.method === "HEAD") {
|
||||
res.writeHead(200, headers);
|
||||
return res.end();
|
||||
}
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.on("error", (err) => {
|
||||
console.error("Stream error:", err);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||
res.end("Internal Server Error");
|
||||
} else {
|
||||
res.destroy();
|
||||
}
|
||||
});
|
||||
res.on("close", () => {
|
||||
if (!stream.destroyed) stream.destroy();
|
||||
});
|
||||
res.on("error", (err) => {
|
||||
console.error("Response error:", err);
|
||||
if (!stream.destroyed) stream.destroy();
|
||||
});
|
||||
res.writeHead(200, headers);
|
||||
stream.pipe(res);
|
||||
}
|
||||
|
||||
function sendIndex(req, res) {
|
||||
const indexPath = path.join(root, "index.html");
|
||||
fs.stat(indexPath, (err, stats) => {
|
||||
if (err || !stats || !stats.isFile()) {
|
||||
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||
return res.end("index.html not found");
|
||||
}
|
||||
streamFile(req, res, indexPath, stats);
|
||||
});
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.method === "OPTIONS") {
|
||||
const filePath = safeJoin(req.url || "/");
|
||||
res.writeHead(204);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
res.writeHead(405, { Allow: "GET, HEAD, OPTIONS" });
|
||||
return res.end();
|
||||
}
|
||||
|
||||
const reqUrl = req.url || "/";
|
||||
const filePath = safeJoin(reqUrl);
|
||||
if (!filePath) {
|
||||
res.writeHead(400);
|
||||
return res.end("Bad Request");
|
||||
}
|
||||
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if (err) {
|
||||
// No file at this path → SPA fallback to index.html
|
||||
return sendIndex(req, res);
|
||||
}
|
||||
if (stats.isFile()) {
|
||||
return streamFile(req, res, filePath, stats);
|
||||
}
|
||||
if (stats.isDirectory()) {
|
||||
const indexInDir = path.join(filePath, "index.html");
|
||||
return fs.stat(indexInDir, (dirErr, dirStats) => {
|
||||
if (!dirErr && dirStats && dirStats.isFile()) {
|
||||
return streamFile(req, res, indexInDir, dirStats);
|
||||
}
|
||||
return sendIndex(req, res);
|
||||
});
|
||||
}
|
||||
// Path exists but is neither file nor directory → SPA fallback
|
||||
return sendIndex(req, res);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, host, () => {
|
||||
console.log(`Serving ${root} on http://${host}:${port}`);
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { lazyWithRetry, hideUpgradeProgress } from "./utils";
|
||||
import { useState, useEffect, lazy } from "react";
|
||||
import { Routes, Route, BrowserRouter } from "react-router";
|
||||
import { pdfjs } from "react-pdf";
|
||||
import Form from "./pages/Form";
|
||||
@@ -14,29 +13,24 @@ import SignYourSelf from "./pages/SignyourselfPdf";
|
||||
import DraftDocument from "./components/pdf/DraftDocument";
|
||||
import PlaceHolderSign from "./pages/PlaceHolderSign";
|
||||
import PdfRequestFiles from "./pages/PdfRequestFiles";
|
||||
import Lazy from "./primitives/LazyPage";
|
||||
import LazyPage from "./primitives/LazyPage";
|
||||
import Loader from "./primitives/Loader";
|
||||
import UserList from "./pages/UserList";
|
||||
import { serverUrl_fn } from "./constant/appinfo";
|
||||
import DocSuccessPage from "./pages/DocSuccessPage";
|
||||
import DragProvider from "./components/DragProivder";
|
||||
import Title from "./components/Title";
|
||||
const DebugPdf = lazyWithRetry(() => import("./pages/DebugPdf"));
|
||||
const ForgetPassword = lazyWithRetry(() => import("./pages/ForgetPassword"));
|
||||
const GuestLogin = lazyWithRetry(() => import("./pages/GuestLogin"));
|
||||
const ChangePassword = lazyWithRetry(() => import("./pages/ChangePassword"));
|
||||
const UserProfile = lazyWithRetry(() => import("./pages/UserProfile"));
|
||||
const Opensigndrive = lazyWithRetry(() => import("./pages/Opensigndrive"));
|
||||
const ManageSign = lazyWithRetry(() => import("./pages/Managesign"));
|
||||
const AddAdmin = lazyWithRetry(() => import("./pages/AddAdmin"));
|
||||
const UpdateExistUserAdmin = lazyWithRetry(
|
||||
() => import("./pages/UpdateExistUserAdmin")
|
||||
);
|
||||
const Preferences = lazyWithRetry(() => import("./pages/Preferences"));
|
||||
const Login = lazyWithRetry(() => import("./pages/Login"));
|
||||
const VerifyDocument = lazyWithRetry(() => import("./pages/VerifyDocument"));
|
||||
const EmailBuilder = lazyWithRetry(() => import("./pages/EmailBuilder"));
|
||||
|
||||
import ValidateSession from "./primitives/ValidateSession";
|
||||
const DebugPdf = lazy(() => import("./pages/DebugPdf"));
|
||||
const ForgetPassword = lazy(() => import("./pages/ForgetPassword"));
|
||||
const GuestLogin = lazy(() => import("./pages/GuestLogin"));
|
||||
const ChangePassword = lazy(() => import("./pages/ChangePassword"));
|
||||
const UserProfile = lazy(() => import("./pages/UserProfile"));
|
||||
const Opensigndrive = lazy(() => import("./pages/Opensigndrive"));
|
||||
const ManageSign = lazy(() => import("./pages/Managesign"));
|
||||
const AddAdmin = lazy(() => import("./pages/AddAdmin"));
|
||||
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
|
||||
const Preferences = lazy(() => import("./pages/Preferences"));
|
||||
const Login = lazy(() => import("./pages/Login"));
|
||||
const VerifyDocument = lazy(() => import("./pages/VerifyDocument"));
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
|
||||
const AppLoader = () => {
|
||||
return (
|
||||
@@ -48,98 +42,140 @@ const AppLoader = () => {
|
||||
function App() {
|
||||
const [isloading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
// initialize creds
|
||||
const id = process.env.REACT_APP_APPID ?? "opensign";
|
||||
localStorage.setItem("parseAppId", id);
|
||||
localStorage.setItem("baseUrl", `${serverUrl_fn()}/`);
|
||||
hideUpgradeProgress();
|
||||
localStorage.removeItem("showUpgradeProgress");
|
||||
setIsLoading(false);
|
||||
handleCredentials();
|
||||
}, []);
|
||||
|
||||
const handleCredentials = () => {
|
||||
const appId = process.env.REACT_APP_APPID
|
||||
? process.env.REACT_APP_APPID
|
||||
: "opensign";
|
||||
const baseurl = serverUrl_fn();
|
||||
try {
|
||||
localStorage.setItem("baseUrl", `${baseurl}/`);
|
||||
localStorage.setItem("parseAppId", appId);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log("err ", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-base-200">
|
||||
{isloading ? (
|
||||
<AppLoader />
|
||||
) : (
|
||||
<BrowserRouter>
|
||||
<Title />
|
||||
<Routes>
|
||||
<Route element={<ValidateRoute />}>
|
||||
<Route exact path="/" element={<Lazy Page={Login} />} />
|
||||
<Route path="/addadmin" element={<Lazy Page={AddAdmin} />} />
|
||||
<Route exact path="/" element={<LazyPage Page={Login} />} />
|
||||
<Route
|
||||
path="/addadmin"
|
||||
element={<LazyPage Page={AddAdmin} />}
|
||||
/>
|
||||
<Route
|
||||
path="/upgrade-2.1"
|
||||
element={<Lazy Page={UpdateExistUserAdmin} />}
|
||||
element={<LazyPage Page={UpdateExistUserAdmin} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route element={<Validate />}>
|
||||
<Route
|
||||
path="/load/template/:templateId"
|
||||
element={<TemplatePlaceholder />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/load/placeholdersign/:docId"
|
||||
element={<PlaceHolderSign />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/load/recipientSignPdf/:docId/:contactBookId"
|
||||
element={<DragProvider Page={PdfRequestFiles} />}
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="/login/:base64url"
|
||||
element={<Lazy Page={GuestLogin} />}
|
||||
path="/loadmf/signmicroapp/login/:id/:userMail/:contactBookId/:serverUrl"
|
||||
element={<LazyPage Page={GuestLogin} />}
|
||||
/>
|
||||
<Route path="/debugpdf" element={<Lazy Page={DebugPdf} />} />
|
||||
<Route
|
||||
path="/login/:id/:userMail/:contactBookId/:serverUrl"
|
||||
element={<LazyPage Page={GuestLogin} />}
|
||||
/>
|
||||
<Route
|
||||
path="/login/:base64url"
|
||||
element={<LazyPage Page={GuestLogin} />}
|
||||
/>
|
||||
<Route path="/debugpdf" element={<LazyPage Page={DebugPdf} />} />
|
||||
<Route
|
||||
path="/forgetpassword"
|
||||
element={<Lazy Page={ForgetPassword} />}
|
||||
element={<LazyPage Page={ForgetPassword} />}
|
||||
/>
|
||||
<Route element={<HomeLayout />}>
|
||||
<Route path="/users" element={<UserList />} />
|
||||
<Route
|
||||
path="/changepassword"
|
||||
element={<Lazy Page={ChangePassword} />}
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<ValidateSession>
|
||||
<HomeLayout />
|
||||
</ValidateSession>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
path="/changepassword"
|
||||
element={<LazyPage Page={ChangePassword} />}
|
||||
/>
|
||||
<Route path="/form/:id" element={<Form />} />
|
||||
<Route path="/report/:id" element={<Report />} />
|
||||
<Route path="/dashboard/:id" element={<Dashboard />} />
|
||||
<Route path="/profile" element={<Lazy Page={UserProfile} />} />
|
||||
<Route path="/drive" element={<Lazy Page={Opensigndrive} />} />
|
||||
<Route path="/managesign" element={<Lazy Page={ManageSign} />} />
|
||||
<Route
|
||||
path="/profile"
|
||||
element={<LazyPage Page={UserProfile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/drive"
|
||||
element={<LazyPage Page={Opensigndrive} />}
|
||||
/>
|
||||
<Route
|
||||
path="/managesign"
|
||||
element={<LazyPage Page={ManageSign} />}
|
||||
/>
|
||||
<Route
|
||||
path="/template/:templateId"
|
||||
element={<DragProvider Page={TemplatePlaceholder} />}
|
||||
element={<TemplatePlaceholder />}
|
||||
/>
|
||||
{/* signyouself route with no rowlevel data using docId from url */}
|
||||
<Route
|
||||
path="/signaturePdf/:docId"
|
||||
element={<DragProvider Page={SignYourSelf} />}
|
||||
/>
|
||||
<Route path="/signaturePdf/:docId" element={<SignYourSelf />} />
|
||||
{/* draft document route to handle and navigate route page according to document status */}
|
||||
<Route
|
||||
path="/draftDocument"
|
||||
element={<DragProvider Page={DraftDocument} />}
|
||||
/>
|
||||
<Route path="/draftDocument" element={<DraftDocument />} />
|
||||
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
|
||||
<Route
|
||||
path="/placeHolderSign/:docId"
|
||||
element={<DragProvider Page={PlaceHolderSign} />}
|
||||
element={<PlaceHolderSign />}
|
||||
/>
|
||||
{/* for user signature (need your sign route) with row level data */}
|
||||
<Route path="/pdfRequestFiles" element={<PdfRequestFiles />} />
|
||||
{/* for user signature (need your sign route) with no row level data */}
|
||||
<Route
|
||||
path="/pdfRequestFiles/:docId"
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
{/* recipient signature route with no rowlevel data using docId from url */}
|
||||
<Route
|
||||
path="/recipientSignPdf/:docId/:contactBookId"
|
||||
element={<DragProvider Page={PdfRequestFiles} />}
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
<Route
|
||||
path="/recipientSignPdf/:docId"
|
||||
element={<DragProvider Page={PdfRequestFiles} />}
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
<Route path="/users" element={<UserList />} />
|
||||
<Route
|
||||
path="/verify-document"
|
||||
element={<Lazy Page={VerifyDocument} />}
|
||||
element={<LazyPage Page={VerifyDocument} />}
|
||||
/>
|
||||
<Route
|
||||
path="/preferences"
|
||||
element={<Lazy Page={Preferences} />}
|
||||
element={<LazyPage Page={Preferences} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/success" element={<DocSuccessPage />} />
|
||||
<Route path="/emailbuilder" element={<EmailBuilder />} />
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,300 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Parse from "parse";
|
||||
import Loader from "../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { emailRegex } from "../constant/const";
|
||||
|
||||
const AddSigner = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [addYourself, setAddYourself] = useState(false);
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [isUserExist, setIsUserExist] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkUserExist();
|
||||
}, []);
|
||||
// Load user details from localStorage when the component mounts
|
||||
useEffect(() => {
|
||||
const savedUserDetails = JSON.parse(
|
||||
localStorage.getItem("UserInformation")
|
||||
);
|
||||
if (savedUserDetails && addYourself) {
|
||||
setName(savedUserDetails.name);
|
||||
setPhone(savedUserDetails?.phone || "");
|
||||
setEmail(savedUserDetails.email?.toLowerCase()?.replace(/\s/g, ""));
|
||||
}
|
||||
}, [addYourself]);
|
||||
|
||||
const checkUserExist = async () => {
|
||||
const user = Parse.User.current();
|
||||
try {
|
||||
const query = new Parse.Query("contracts_Contactbook");
|
||||
query.equalTo("CreatedBy", user);
|
||||
query.notEqualTo("IsDeleted", true);
|
||||
query.equalTo("Email", user.getEmail());
|
||||
const res = await query.first();
|
||||
// console.log(res);
|
||||
if (!res) {
|
||||
setIsUserExist(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
}
|
||||
};
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!emailRegex.test(email)) {
|
||||
alert("Please enter a valid email address.");
|
||||
} else {
|
||||
setIsLoader(true);
|
||||
if (localStorage.getItem("TenantId")) {
|
||||
try {
|
||||
const user = Parse.User.current();
|
||||
const query = new Parse.Query("contracts_Contactbook");
|
||||
query.equalTo("CreatedBy", user);
|
||||
query.notEqualTo("IsDeleted", true);
|
||||
query.equalTo("Email", email);
|
||||
const res = await query.first();
|
||||
// console.log(res);
|
||||
if (!res) {
|
||||
const contactQuery = new Parse.Object("contracts_Contactbook");
|
||||
contactQuery.set("Name", name);
|
||||
if (phone) {
|
||||
contactQuery.set("Phone", phone);
|
||||
}
|
||||
contactQuery.set("Email", email);
|
||||
contactQuery.set("UserRole", "contracts_Guest");
|
||||
|
||||
contactQuery.set("TenantId", {
|
||||
__type: "Pointer",
|
||||
className: "partners_Tenant",
|
||||
objectId: localStorage.getItem("TenantId")
|
||||
});
|
||||
|
||||
try {
|
||||
const _users = Parse.Object.extend("User");
|
||||
const _user = new _users();
|
||||
_user.set("name", name);
|
||||
_user.set("username", email);
|
||||
_user.set("email", email);
|
||||
_user.set("password", email);
|
||||
if (phone) {
|
||||
_user.set("phone", phone);
|
||||
}
|
||||
const user = await _user.save();
|
||||
if (user) {
|
||||
const currentUser = Parse.User.current();
|
||||
contactQuery.set(
|
||||
"CreatedBy",
|
||||
Parse.User.createWithoutData(currentUser.id)
|
||||
);
|
||||
|
||||
contactQuery.set("UserId", user);
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
acl.setReadAccess(currentUser.id, true);
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
|
||||
contactQuery.setACL(acl);
|
||||
|
||||
const res = await contactQuery.save();
|
||||
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
if (props.details) {
|
||||
props.details({
|
||||
value: parseData[props.valueKey],
|
||||
label: parseData[props.displayKey],
|
||||
email: parseData?.Email
|
||||
});
|
||||
}
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
if (props.handleUserData) {
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
|
||||
setIsLoader(false);
|
||||
// Reset the form fields
|
||||
setAddYourself(false);
|
||||
setName("");
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err ", err);
|
||||
if (err.code === 202) {
|
||||
const params = { email: email };
|
||||
const userRes = await Parse.Cloud.run("getUserId", params);
|
||||
const currentUser = Parse.User.current();
|
||||
contactQuery.set(
|
||||
"CreatedBy",
|
||||
Parse.User.createWithoutData(currentUser.id)
|
||||
);
|
||||
|
||||
contactQuery.set("UserId", {
|
||||
__type: "Pointer",
|
||||
className: "_User",
|
||||
objectId: userRes.id
|
||||
});
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
acl.setReadAccess(currentUser.id, true);
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
|
||||
contactQuery.setACL(acl);
|
||||
const res = await contactQuery.save();
|
||||
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
if (props.details) {
|
||||
props.details({
|
||||
value: parseData[props.valueKey],
|
||||
label: parseData[props.displayKey],
|
||||
email: parseData?.Email
|
||||
});
|
||||
}
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
if (props.handleUserData) {
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
setIsLoader(false);
|
||||
// Reset the form fields
|
||||
setAddYourself(false);
|
||||
setName("");
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert(t("add-signer-alert"));
|
||||
setIsLoader(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err in fetch contact", err);
|
||||
setIsLoader(false);
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define a function to handle the "add yourself" checkbox
|
||||
const handleAddYourselfChange = () => {
|
||||
if (addYourself) {
|
||||
setAddYourself(false);
|
||||
setName("");
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
} else {
|
||||
setAddYourself(true);
|
||||
}
|
||||
};
|
||||
const handleReset = () => {
|
||||
setAddYourself(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full p-[20px]">
|
||||
{isLoader && (
|
||||
<div className="fixed inset-0 flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full mx-auto p-2 text-base-content">
|
||||
<p className="mb-3 text-xs text-gray-500">{t("add-signer-note")}</p>
|
||||
{isUserExist && (
|
||||
<div className="mb-3 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="addYourself"
|
||||
checked={addYourself}
|
||||
onChange={handleAddYourselfChange}
|
||||
className="op-checkbox op-checkbox-sm"
|
||||
/>
|
||||
<label htmlFor="addYourself" className="ml-2 mb-0">
|
||||
{t("add-yourself")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="name" className="block text-xs font-semibold">
|
||||
{t("name")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
disabled={addYourself}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="block text-xs font-semibold">
|
||||
{t("email")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) =>
|
||||
setEmail(e.target.value?.toLowerCase()?.replace(/\s/g, ""))
|
||||
}
|
||||
required
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
disabled={addYourself}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="phone" className="block text-xs font-semibold">
|
||||
{t("phone")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
disabled={addYourself}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("phone-optional")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-x-2 justify-start">
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
{t("submit")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReset()}
|
||||
className="op-btn op-btn-secondary"
|
||||
>
|
||||
{t("reset")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddSigner;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Parse from "parse";
|
||||
import Title from "./Title";
|
||||
import Loader from "../primitives/Loader";
|
||||
import {
|
||||
copytoData,
|
||||
@@ -11,8 +12,6 @@ import {
|
||||
import {
|
||||
useTranslation
|
||||
} from "react-i18next";
|
||||
import { withSessionValidation } from "../utils";
|
||||
|
||||
function generatePassword(length) {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
@@ -69,11 +68,11 @@ const AddUser = (props) => {
|
||||
}
|
||||
};
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = withSessionValidation(async (e) => {
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!emailRegex.test(formdata.email)) {
|
||||
alert(t("valid-email-alert"));
|
||||
alert("Please enter a valid email address.");
|
||||
} else {
|
||||
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||
setIsFormLoader(true);
|
||||
@@ -101,6 +100,7 @@ const AddUser = (props) => {
|
||||
};
|
||||
const res = await Parse.Cloud.run("adduser", params);
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
console.log("parseData ", parseData);
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
@@ -132,7 +132,7 @@ const AddUser = (props) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Define a function to handle the "add yourself" checkbox
|
||||
const handleReset = () => {
|
||||
@@ -155,6 +155,7 @@ const AddUser = (props) => {
|
||||
};
|
||||
return (
|
||||
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
|
||||
<Title title={t("add-user")} />
|
||||
{isFormLoader && (
|
||||
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
|
||||
<Loader />
|
||||
@@ -165,7 +166,7 @@ const AddUser = (props) => {
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-xs font-semibold"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
{t("name")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
@@ -181,13 +182,12 @@ const AddUser = (props) => {
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-xs font-semibold"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
{t("email")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
@@ -203,11 +203,10 @@ const AddUser = (props) => {
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-email")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="block text-xs font-semibold">
|
||||
<label className="block text-xs text-gray-700 font-semibold">
|
||||
{t("password")}
|
||||
</label>
|
||||
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
|
||||
@@ -218,15 +217,16 @@ const AddUser = (props) => {
|
||||
></i>
|
||||
</div>
|
||||
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
|
||||
{t("password-generated")}
|
||||
{t("password-generateed")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-xs font-semibold"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
{t("phone")}
|
||||
{/* <span className="text-[red] text-[13px]"> *</span> */}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -240,7 +240,7 @@ const AddUser = (props) => {
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-xs font-semibold"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
{t("Role")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
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([]);
|
||||
useEffect(() => {
|
||||
signatureExist();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
//function to check at least one signature field exist
|
||||
const signatureExist = async () => {
|
||||
setIsDisableBulkSend(false);
|
||||
const getPlaceholder = props?.Placeholders;
|
||||
const checkIsSignatureExistt = getPlaceholder?.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>
|
||||
)}
|
||||
{!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,62 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import ModalUi from "../primitives/ModalUi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const ColumnSelector = ({
|
||||
isOpen,
|
||||
allColumns = [],
|
||||
visibleColumns = [],
|
||||
columnLabels = {},
|
||||
onApply,
|
||||
onClose
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selected, setSelected] = useState(visibleColumns);
|
||||
const [names, setNames] = useState(columnLabels);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(visibleColumns);
|
||||
setNames(columnLabels);
|
||||
}, [visibleColumns, columnLabels]);
|
||||
|
||||
const handleChange = (col) => {
|
||||
setSelected((prev) =>
|
||||
prev.includes(col) ? prev.filter((c) => c !== col) : [...prev, col]
|
||||
);
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
onApply && onApply(selected, names);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalUi isOpen={isOpen} title={t("select-columns")} handleClose={onClose}>
|
||||
<div className="p-[20px] flex flex-col gap-2">
|
||||
{allColumns.map((col, i) => (
|
||||
<div key={col} className="flex justify-between items-center gap-2">
|
||||
<span className="flex justify-center items-center h-full">
|
||||
<input
|
||||
id={col + "_" + i}
|
||||
type="checkbox"
|
||||
checked={selected.includes(col)}
|
||||
onChange={() => handleChange(col)}
|
||||
className="mb-0 cursor-pointer"
|
||||
/>
|
||||
<span className="whitespace-nowrap ml-1">
|
||||
{t(`report-heading.${col}`, { defaultValue: col })}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-start mt-2">
|
||||
<button onClick={handleApply} className="op-btn op-btn-primary">
|
||||
{t("apply")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalUi>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColumnSelector;
|
||||
@@ -1,35 +0,0 @@
|
||||
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;
|
||||
@@ -1,95 +0,0 @@
|
||||
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;
|
||||
@@ -1,27 +0,0 @@
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import {
|
||||
DndProvider,
|
||||
MouseTransition
|
||||
} from "react-dnd-multi-backend";
|
||||
import LazyPage from "../primitives/LazyPage";
|
||||
import { GuidelinesProvider } from "../context/GuidelinesContext";
|
||||
|
||||
const HTML5toTouch = {
|
||||
backends: [
|
||||
{
|
||||
id: "html5",
|
||||
backend: HTML5Backend,
|
||||
transition: MouseTransition
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default function DragProvider({ Page, lazy = false }) {
|
||||
return (
|
||||
<DndProvider options={HTML5toTouch}>
|
||||
<GuidelinesProvider>
|
||||
{lazy ? <LazyPage Page={Page} /> : <Page />}
|
||||
</GuidelinesProvider>
|
||||
</DndProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Loader from "../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Parse from "parse";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
|
||||
const EditContactForm = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
Name: "",
|
||||
Email: "",
|
||||
Phone: "",
|
||||
Company: "",
|
||||
JobTitle: ""
|
||||
});
|
||||
const [formData, setFormData] = useState({ Name: "", Email: "", Phone: "" });
|
||||
useEffect(() => {
|
||||
if (props.contact?.Email) {
|
||||
setFormData((prev) => ({ ...prev, ...props.contact }));
|
||||
@@ -29,10 +21,9 @@ const EditContactForm = (props) => {
|
||||
setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
}
|
||||
};
|
||||
const handleSubmit = withSessionValidation(async (e) => {
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (props.handleEditContact) {
|
||||
try {
|
||||
setIsLoader(true);
|
||||
@@ -40,9 +31,7 @@ const EditContactForm = (props) => {
|
||||
contactId: props.contact.objectId,
|
||||
name: formData.Name,
|
||||
email: formData.Email,
|
||||
phone: formData?.Phone,
|
||||
company: formData?.Company,
|
||||
jobTitle: formData?.JobTitle,
|
||||
phone: formData.Phone,
|
||||
tenantId: localStorage.getItem("TenantId")
|
||||
};
|
||||
const res = await Parse.Cloud.run("editcontact", params);
|
||||
@@ -50,9 +39,7 @@ const EditContactForm = (props) => {
|
||||
...res,
|
||||
Name: formData.Name,
|
||||
Email: formData.Email,
|
||||
Phone: formData?.Phone,
|
||||
Company: formData?.Company,
|
||||
JobTitle: formData?.JobTitle
|
||||
Phone: formData.Phone
|
||||
};
|
||||
props.handleEditContact(updateContact);
|
||||
} catch (err) {
|
||||
@@ -67,7 +54,7 @@ const EditContactForm = (props) => {
|
||||
props.handleClose && props.handleClose();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="h-full p-[20px]">
|
||||
{isLoader && (
|
||||
@@ -91,7 +78,6 @@ const EditContactForm = (props) => {
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-name")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -109,7 +95,6 @@ const EditContactForm = (props) => {
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-email")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -127,34 +112,6 @@ const EditContactForm = (props) => {
|
||||
placeholder={t("phone-optional")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="Company" className="block text-xs font-semibold">
|
||||
{t("company")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="Company"
|
||||
name="Company"
|
||||
value={formData.Company}
|
||||
onChange={(e) => handleChange(e)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("phone-optional")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="JobTitle" className="block text-xs font-semibold">
|
||||
{t("job-title")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="JobTitle"
|
||||
name="JobTitle"
|
||||
value={formData.JobTitle}
|
||||
onChange={(e) => handleChange(e)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("phone-optional")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-x-2 justify-start">
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
{t("submit")}
|
||||
@@ -1,130 +0,0 @@
|
||||
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;
|
||||
@@ -12,44 +12,32 @@ import {
|
||||
} from "../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { appInfo } from "../constant/appinfo";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { toggleSidebar } from "../redux/reducers/sidebarReducer";
|
||||
import { sessionStatus } from "../redux/reducers/userReducer";
|
||||
|
||||
const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { width } = useWindowSize();
|
||||
const dispatch = useDispatch();
|
||||
const username = localStorage.getItem("username") || "";
|
||||
const image = localStorage.getItem("profileImg") || dp;
|
||||
const isAdmin =
|
||||
localStorage.getItem("Extand_Class") &&
|
||||
JSON.parse(localStorage.getItem("Extand_Class"))?.[0]?.UserRole ===
|
||||
"contracts_Admin";
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [applogo, setAppLogo] = useState("");
|
||||
const [isDarkTheme, setIsDarkTheme] = useState();
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen(!isOpen);
|
||||
closeSidebar();
|
||||
};
|
||||
const closeSidebar = () => {
|
||||
if (width && width <= 768) {
|
||||
dispatch(toggleSidebar(false));
|
||||
if (width <= 768) {
|
||||
setIsMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initializeHead();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
closeSidebar();
|
||||
}, [width]);
|
||||
|
||||
const showSidebar = () => {
|
||||
dispatch(toggleSidebar());
|
||||
};
|
||||
|
||||
|
||||
async function initializeHead() {
|
||||
const applogo = await getAppLogo();
|
||||
@@ -60,15 +48,13 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
setAppLogo(logo);
|
||||
}
|
||||
}
|
||||
const handleLogout = async () => {
|
||||
|
||||
const closeDropdown = async () => {
|
||||
setIsOpen(false);
|
||||
setIsLoggingOut(true);
|
||||
try {
|
||||
await Parse.User.logOut();
|
||||
} catch (err) {
|
||||
console.log("Err while logging out", err);
|
||||
} finally {
|
||||
dispatch(sessionStatus(true));
|
||||
}
|
||||
let appdata = localStorage.getItem("userSettings");
|
||||
let applogo = localStorage.getItem("appLogo");
|
||||
@@ -76,7 +62,6 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
let PageLanding = localStorage.getItem("PageLanding");
|
||||
let baseUrl = localStorage.getItem("baseUrl");
|
||||
let appid = localStorage.getItem("parseAppId");
|
||||
let favicon = localStorage.getItem("favicon");
|
||||
|
||||
localStorage.clear();
|
||||
saveLanguageInLocal(i18n);
|
||||
@@ -86,8 +71,7 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
localStorage.setItem("userSettings", appdata);
|
||||
localStorage.setItem("baseUrl", baseUrl);
|
||||
localStorage.setItem("parseAppId", appid);
|
||||
localStorage.setItem("favicon", favicon);
|
||||
setIsLoggingOut(false);
|
||||
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
@@ -129,7 +113,7 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="op-navbar bg-base-100 shadow touch-none">
|
||||
<div className="flex-none">
|
||||
<button
|
||||
@@ -140,10 +124,7 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 ml-2">
|
||||
<div
|
||||
onClick={() => navigate("/dashboard/35KBoSgoAK")}
|
||||
className="h-[25px] md:h-[40px] w-auto overflow-hidden cursor-pointer"
|
||||
>
|
||||
<div className="h-[25px] md:h-[40px] w-auto overflow-hidden">
|
||||
{applogo && (
|
||||
<img
|
||||
className="object-contain h-full w-auto"
|
||||
@@ -159,7 +140,7 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
</div>
|
||||
<div id="profile-menu" className="flex-none gap-2">
|
||||
<div>
|
||||
<FullScreenButton />
|
||||
<FullScreenButton />
|
||||
</div>
|
||||
{width >= 768 && (
|
||||
<div
|
||||
@@ -197,7 +178,7 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
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 ${
|
||||
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 ${
|
||||
isOpen ? "" : "hidden"
|
||||
}`}
|
||||
>
|
||||
@@ -256,7 +237,7 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
<li onClick={handleLogout}>
|
||||
<li onClick={closeDropdown}>
|
||||
<span>
|
||||
<i className="fa-light fa-arrow-right-from-bracket"></i>{" "}
|
||||
{t("log-out")}
|
||||
@@ -266,7 +247,7 @@ const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ function RotateAlert(props) {
|
||||
props.setShowRotateAlert({ status: false, degree: 0 })
|
||||
}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content shadow-md"
|
||||
className="op-btn op-btn-ghost shadow-md"
|
||||
>
|
||||
{t("no")}
|
||||
</button>
|
||||
|
||||
@@ -1,81 +1,20 @@
|
||||
import { useLocation, matchPath } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
import { useManifestUrl } from "../hook/useManifestUrl";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
const TITLE_MAP = {
|
||||
"/": "login",
|
||||
// Homelayout
|
||||
"/dashboard/35KBoSgoAK": "Dashboard",
|
||||
"/form/sHAnZphf69": "Sign Yourself",
|
||||
"/form/8mZzFxbG1z": "Request Signatures",
|
||||
"/form/template": "New Template",
|
||||
"/report/6TeaPr321t": "Templates",
|
||||
"/report/4Hhwbp482K": "Need your sign",
|
||||
"/report/1MwEuxLEkF": "In Progress",
|
||||
"/report/kQUoW4hUXz": "Completed",
|
||||
"/report/ByHuevtCFY": "Drafts",
|
||||
"/report/UPr2Fm5WY3": "Declined",
|
||||
"/report/zNqBHXHsYH": "Expired",
|
||||
"/report/contacts": "Contactbook",
|
||||
"/drive": "Drive",
|
||||
"/managesign": "My Signature",
|
||||
"/preferences": "Preferences",
|
||||
"/users": "Users",
|
||||
"/profile": "profile",
|
||||
"/changepassword": "change-password",
|
||||
"/verify-document": "verify-document",
|
||||
|
||||
"/signaturePdf/:docId": "Sign Yourself",
|
||||
"/placeHolderSign/:docId": "Request Signatures",
|
||||
"/template/:templateId": "New Template",
|
||||
"/recipientSignPdf/:docId": "Request Signatures",
|
||||
"/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
|
||||
"/load/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
|
||||
|
||||
// alone
|
||||
"/debugpdf": "Debug Pdf",
|
||||
"/forgetpassword": "forgot-password",
|
||||
"/success": "success",
|
||||
"/addadmin": "add-admin",
|
||||
"/upgrade-2.1": "add-admin",
|
||||
"/draftDocument": "New Document",
|
||||
"/login/:base64url": "Request Signatures",
|
||||
|
||||
};
|
||||
|
||||
function resolveTitle(pathname, override) {
|
||||
if (override) return override;
|
||||
for (let [pattern, label] of Object.entries(TITLE_MAP)) {
|
||||
if (matchPath({ path: pattern, end: true }, pathname)) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export default function Title() {
|
||||
const { pathname, state } = useLocation();
|
||||
const { t } = useTranslation();
|
||||
function Title({ title, drive }) {
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const logo = useMemo(() => localStorage.getItem("favicon"), []);
|
||||
const prefix = useMemo(
|
||||
() => resolveTitle(pathname, state?.title),
|
||||
[pathname, state?.title]
|
||||
);
|
||||
const title = useMemo(
|
||||
() => (prefix ? `${t(prefix)} - ${appName}` : appName),
|
||||
[t, prefix, appName]
|
||||
);
|
||||
const manifestUrl = useManifestUrl(appName, logo);
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={title} />
|
||||
{logo && <link rel="icon" type="image/png" href={logo} />}
|
||||
<link rel="manifest" href={manifestUrl} />
|
||||
</>
|
||||
<Helmet>
|
||||
<title>{drive ? title : `${title} - ${appName}`}</title>
|
||||
<meta name="description" content={`${title} - ${appName}`} />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href={localStorage.getItem("fev_Icon")}
|
||||
sizes="40x40"
|
||||
/>
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
|
||||
export default Title;
|
||||
|
||||
@@ -1,650 +0,0 @@
|
||||
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,
|
||||
hasSignatureWidget,
|
||||
} 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 reservedEmails = (
|
||||
props?.Placeholders?.filter((p) => !!p?.signerObjId) || []
|
||||
)
|
||||
.map(
|
||||
(role) =>
|
||||
normalizeKey(role?.email) || normalizeKey(role?.signerPtr?.Email)
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
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
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
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(reservedEmails);
|
||||
|
||||
// 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");
|
||||
|
||||
// Viewer/approver roles do not require a signature widget; only
|
||||
// signer-role placeholders must have one.
|
||||
const isSignExist =
|
||||
filtered?.length > 0 && filtered.every((p) => hasSignatureWidget(p));
|
||||
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()) {
|
||||
const rowEmails = new Set();
|
||||
const rowNumber = rowIndex + 1;
|
||||
const filterFields = item?.fields || [];
|
||||
for (const field of filterFields) {
|
||||
// skip prefill
|
||||
if (field?.label === "prefill") continue;
|
||||
const normalizedEmail = normalizeKey(field?.email);
|
||||
const params = { email: field.email, row: rowNumber };
|
||||
if (!normalizedEmail) {
|
||||
alert(t("email-not-found-in-row", { ...params }));
|
||||
return false;
|
||||
} else if (!emailRegex.test(normalizedEmail)) {
|
||||
alert(t("invalid-email-found-in-row", { ...params }));
|
||||
return false;
|
||||
} else if (
|
||||
rowEmails.has(normalizedEmail) ||
|
||||
emails?.includes(normalizedEmail)
|
||||
) {
|
||||
alert(t("duplicate-email-found-in-row", { ...params }));
|
||||
return false;
|
||||
} else {
|
||||
rowEmails.add(normalizedEmail);
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -1,68 +0,0 @@
|
||||
import RenderWidgets from "./RenderWidgets";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const PrefillWidgets = ({ prefills = [], setPrefills, onNext }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleWidgetDetails = (value, widgetLabel) => {
|
||||
setPrefills((prev) => {
|
||||
const widgets = [...(prev ?? [])];
|
||||
const index = widgets.findIndex((w) => w?.label === widgetLabel);
|
||||
if (index === -1) return prev;
|
||||
widgets[index] = {
|
||||
...widgets[index],
|
||||
options: { ...widgets[index].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]
|
||||
.sort((a, b) =>
|
||||
a.pageNumber !== b.pageNumber
|
||||
? a.pageNumber - b.pageNumber
|
||||
: (a.yPosition ?? 0) - (b.yPosition ?? 0)
|
||||
)
|
||||
.map((widget) => (
|
||||
<RenderWidgets
|
||||
key={widget.key}
|
||||
showLabel
|
||||
widget={widget}
|
||||
handleWidgetDetails={(value) =>
|
||||
handleWidgetDetails(value, widget.label)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</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;
|
||||
@@ -1,630 +0,0 @@
|
||||
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 { 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 hint = widget?.options?.hint;
|
||||
const regExpression = inputValidation(serverRegex, inputType);
|
||||
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
|
||||
useEffect(() => {
|
||||
const response =
|
||||
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
|
||||
setWord(String(response || ""));
|
||||
}, []);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const value = e.target.value;
|
||||
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">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={
|
||||
hint || t("enter-value", { value: widget?.options?.name })
|
||||
}
|
||||
value={word ?? ""}
|
||||
onChange={(e) => handleChange(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("");
|
||||
}}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none text-base-content hover:border-base-content w-full text-xs"
|
||||
maxLength={count}
|
||||
/>
|
||||
</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);
|
||||
@@ -1,33 +0,0 @@
|
||||
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;
|
||||
@@ -1,104 +0,0 @@
|
||||
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;
|
||||
@@ -1,74 +0,0 @@
|
||||
/* ---------------- 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;
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Parse from "parse";
|
||||
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();
|
||||
@@ -15,7 +14,7 @@ const DashboardCard = (props) => {
|
||||
const [response, setresponse] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const renderData = withSessionValidation(async () => {
|
||||
const renderData = async () => {
|
||||
if (props.Data.queryType === "function") {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -169,27 +168,31 @@ const DashboardCard = (props) => {
|
||||
}
|
||||
}
|
||||
setresponse(arr.length);
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
await axios.get(url, { headers: headers }).then((res) => {
|
||||
if (res?.data?.[props.Data.key]) {
|
||||
setresponse(parseInt(res.data[props.Data.key]));
|
||||
} else if (res?.data?.results?.length > 0) {
|
||||
setresponse(res.data.results.length);
|
||||
if (res.data.results.length > 0) {
|
||||
setLoading(false);
|
||||
if (props.Data.key !== "count") {
|
||||
setresponse(res.data.results[0][props.Data.key]);
|
||||
} else {
|
||||
setresponse(res.data[props.Data.key]);
|
||||
}
|
||||
} else {
|
||||
setresponse(0);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Problem", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const filterRender = withSessionValidation(async () => {
|
||||
const filterRender = async () => {
|
||||
if (props.FilterData && props.FilterData.queryType === "function") {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -267,7 +270,7 @@ const DashboardCard = (props) => {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setFormat = (val) => {
|
||||
switch (props.Format) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import Parse from "parse";
|
||||
import DocumentsReport from "../../reports/document/DocumentsReport";
|
||||
import ReportTable from "../../primitives/GetReportDisplay";
|
||||
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();
|
||||
// console.log("props ", props)
|
||||
const [List, setList] = useState([]);
|
||||
const [isLoader, setIsLoader] = useState(true);
|
||||
const [reportName, setReportName] = useState("");
|
||||
@@ -47,7 +47,7 @@ function DashboardReport(props) {
|
||||
// eslint-disable-next-line
|
||||
}, [isNextRecord]);
|
||||
|
||||
const handleSearchChange = withSessionValidation(async (e) => {
|
||||
const handleSearchChange = async (e) => {
|
||||
const term = e.target.value.toLowerCase();
|
||||
setSearchTerm(term);
|
||||
if (debounceTimer.current) {
|
||||
@@ -83,7 +83,7 @@ function DashboardReport(props) {
|
||||
}
|
||||
}, 300);
|
||||
setIsSearchResult(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearchPaste = (e) => {
|
||||
setTimeout(() => {
|
||||
@@ -99,95 +99,96 @@ function DashboardReport(props) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
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 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 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
|
||||
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
|
||||
);
|
||||
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 (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
|
||||
);
|
||||
}
|
||||
if (arr.length === docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} 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
|
||||
);
|
||||
}
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setIsLoader(false);
|
||||
} catch (err) {
|
||||
const isCancel = axios.isCancel(err);
|
||||
if (!isCancel) {
|
||||
console.log("err ", err);
|
||||
setIsLoader(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) {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
}
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isLoader ? (
|
||||
@@ -197,7 +198,7 @@ function DashboardReport(props) {
|
||||
) : (
|
||||
<>
|
||||
{reportName ? (
|
||||
<DocumentsReport
|
||||
<ReportTable
|
||||
ReportName={reportName}
|
||||
List={List}
|
||||
setList={setList}
|
||||
@@ -214,9 +215,9 @@ function DashboardReport(props) {
|
||||
isSearchResult={isSearchResult}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded-box">
|
||||
<div className="text-center text-xl text-base-content">
|
||||
{t("report-not-found")}
|
||||
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded">
|
||||
<div className="text-center">
|
||||
<p className="text-xl text-base-content">{t("report-not-found")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useState, useEffect, Suspense } from "react";
|
||||
import { lazyWithRetry } from "../../utils";
|
||||
import React, { Suspense, lazy } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const DashboardButton = lazyWithRetry(() => import("./DashboardButton"));
|
||||
const DashboardCard = lazyWithRetry(() => import("./DashboardCard"));
|
||||
const DashboardReport = lazyWithRetry(() => import("./DashboardReport"));
|
||||
const DashboardButton = lazy(() => import("./DashboardButton"));
|
||||
const DashboardCard = lazy(() => import("./DashboardCard"));
|
||||
const DashboardReport = lazy(() => import("./DashboardReport"));
|
||||
const buttonList = [
|
||||
{
|
||||
label: "Sign yourself",
|
||||
@@ -20,7 +19,6 @@ const buttonList = [
|
||||
];
|
||||
const GetDashboard = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const Button = ({ label, redirectId, redirectType, icon }) => (
|
||||
<DashboardButton
|
||||
Icon={icon}
|
||||
@@ -60,9 +58,7 @@ const GetDashboard = (props) => {
|
||||
<div data-tut={col.widget.data.tourSection}>
|
||||
<Suspense fallback={<div>please wait</div>}>
|
||||
<div className="mb-3 md:mb-0">
|
||||
<DashboardReport
|
||||
Record={col.widget}
|
||||
/>
|
||||
<DashboardReport Record={col.widget} />
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
@@ -96,9 +92,7 @@ const GetDashboard = (props) => {
|
||||
return (
|
||||
<Suspense fallback={<div>please wait</div>}>
|
||||
<div className="mb-3 md:mb-0">
|
||||
<DashboardReport
|
||||
Record={col.widget}
|
||||
/>
|
||||
<DashboardReport Record={col.widget} />
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
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>;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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 />;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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 } })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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>;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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} />;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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} />;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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;
|
||||