Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
348ff6de70 | ||
|
|
a773c50774 | ||
|
|
7c414157dd | ||
|
|
7b4785461c | ||
|
|
7f114dc124 | ||
|
|
50cc654166 | ||
|
|
dc8555dfec | ||
|
|
43e911dfdc | ||
|
|
05f472ace8 | ||
|
|
3729564878 | ||
|
|
f72624fa26 | ||
|
|
8140588898 | ||
|
|
f2ada47302 | ||
|
|
b63d609a70 | ||
|
|
d97b353a81 | ||
|
|
80ccd5d022 | ||
|
|
e948fdc60b | ||
|
|
4bdfe9d725 | ||
|
|
be0c4a2cbf | ||
|
|
b417754a0c | ||
|
|
6271fedcbf | ||
|
|
d3cb8d575c | ||
|
|
1e228c86c3 | ||
|
|
0c295961c3 | ||
|
|
5fbd1fc6f2 | ||
|
|
3f4007d564 | ||
|
|
b49dc6bc6a | ||
|
|
b325095d60 | ||
|
|
56a119a901 | ||
|
|
bea891691b | ||
|
|
b9a309fa36 | ||
|
|
197c00dd79 | ||
|
|
ef81a0409b | ||
|
|
dd499ba0d0 | ||
|
|
a56b87a702 | ||
|
|
acb0f3083c | ||
|
|
606eb6a93b | ||
|
|
2e2e535b70 | ||
|
|
336cfb3482 | ||
|
|
f1278d32a8 | ||
|
|
79d184cf88 | ||
|
|
cd2904b9f0 | ||
|
|
fe8ab56d47 | ||
|
|
fce20a165b | ||
|
|
3a737ea1b7 | ||
|
|
e035592e94 | ||
|
|
4c5a3509cd | ||
|
|
0891cf55fc | ||
|
|
5d328b51c3 | ||
|
|
f751a97f24 | ||
|
|
336114f6ec | ||
|
|
eb8e7b089b | ||
|
|
e4ecb5dbfd | ||
|
|
bce0e4971e | ||
|
|
4fb8aa9a16 | ||
|
|
2297d6fc22 | ||
|
|
8a2329dd60 | ||
|
|
75a40fbf43 | ||
|
|
8922b61ecd | ||
|
|
334edb3c0c | ||
|
|
6465aa7c2c | ||
|
|
4593ce7fec | ||
|
|
b4ffdf0dc9 | ||
|
|
db4b2d58d6 | ||
|
|
3c8b8835c7 | ||
|
|
20752161d3 | ||
|
|
7017930709 | ||
|
|
002b0d5277 | ||
|
|
47348b990e | ||
|
|
ac86b6ef61 | ||
|
|
810037e3c8 | ||
|
|
7894a6edb8 | ||
|
|
dac8957904 | ||
|
|
f8987a57d9 | ||
|
|
ef686aec6a | ||
|
|
f31155e0b4 | ||
|
|
78e4857d1a | ||
|
|
c3144366d9 | ||
|
|
f00a7c9e50 | ||
|
|
803d25e9d1 | ||
|
|
ca1b87309f | ||
|
|
0b4548130d | ||
|
|
f3d20391fa | ||
|
|
e8b3c7fed8 | ||
|
|
2ce43ec2c8 | ||
|
|
6594ba03a3 | ||
|
|
d5af1e8a17 | ||
|
|
fab7998e22 | ||
|
|
9c2a388f66 | ||
|
|
4a4db6851e | ||
|
|
870973e7f3 | ||
|
|
8c49e1ac18 | ||
|
|
4146680c1e | ||
|
|
d9fcc69ca4 | ||
|
|
a878ca1a96 | ||
|
|
e1bf2ed94b | ||
|
|
d1c405912b | ||
|
|
745aca1754 | ||
|
|
797fcbd624 | ||
|
|
672eeea851 | ||
|
|
98efff65c9 | ||
|
|
847657dc76 | ||
|
|
fe21978538 | ||
|
|
ca4ccfd889 | ||
|
|
3a361cc21e | ||
|
|
6cb2d69566 | ||
|
|
98b9ebb228 | ||
|
|
1496c1b1da | ||
|
|
1c2f453496 | ||
|
|
b01d8125d9 | ||
|
|
85bc2fbbb0 | ||
|
|
e857724502 | ||
|
|
086d8ee5be | ||
|
|
04543e826a | ||
|
|
04f6e38778 | ||
|
|
1b5c85fd5f | ||
|
|
123425d7d1 | ||
|
|
a5b9d8178c | ||
|
|
aefd2ff933 | ||
|
|
fe93be8be7 | ||
|
|
369e3660fc | ||
|
|
378c0f057b | ||
|
|
4f3c034cec | ||
|
|
e7d3453744 | ||
|
|
47cc0ef580 | ||
|
|
04f71be6a2 | ||
|
|
07603f4bc2 | ||
|
|
ea16180551 | ||
|
|
75ad566d54 | ||
|
|
fda9fb534b | ||
|
|
cb3e31ebbf | ||
|
|
400525debb | ||
|
|
d87fa38976 | ||
|
|
1cc017bb05 | ||
|
|
531dd11185 | ||
|
|
1e80754750 | ||
|
|
0d286938c7 | ||
|
|
360c13df25 | ||
|
|
71b9bd6367 | ||
|
|
4a5b35e147 | ||
|
|
7930a8f439 | ||
|
|
df4630aa31 | ||
|
|
d8a0d9137e | ||
|
|
1b2528ac1d | ||
|
|
9b357acbdd | ||
|
|
ff4fa3a15f | ||
|
|
f104b0ca86 | ||
|
|
31affb53f1 | ||
|
|
338b76029f | ||
|
|
646e202b91 | ||
|
|
8a34917343 | ||
|
|
9b8c9403d1 | ||
|
|
40198e1bd8 | ||
|
|
0e6220e16c | ||
|
|
fc3a5ad1bf | ||
|
|
4919d6c3d3 | ||
|
|
91e89bb0e2 | ||
|
|
f942b2571a | ||
|
|
ae2c15660d | ||
|
|
721d52d539 | ||
|
|
b03e27cf70 | ||
|
|
4eae44c9b4 | ||
|
|
79234441db |
@@ -241,6 +241,15 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"translation"
|
"translation"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "ehowe",
|
||||||
|
"name": "Eugene Howe",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/756984?v=4",
|
||||||
|
"profile": "https://github.com/ehowe",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,34 +21,46 @@ jobs:
|
|||||||
dockerfile: apps/OpenSign/Dockerhubfile
|
dockerfile: apps/OpenSign/Dockerhubfile
|
||||||
- image: opensign/opensignserver
|
- image: opensign/opensignserver
|
||||||
dockerfile: apps/OpenSignServer/Dockerhubfile
|
dockerfile: apps/OpenSignServer/Dockerhubfile
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Checkout
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
-
|
|
||||||
name: Login to Docker Hub
|
# 👇 Enable emulation so we can build for arm64 on amd64 runners
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
# 👇 Enable Buildx for multi-arch builds
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
-
|
|
||||||
name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: ${{ matrix.image }}
|
images: ${{ matrix.image }}
|
||||||
-
|
|
||||||
name: Debug - List files opensign
|
- name: Debug - List files opensign
|
||||||
run: ls -R apps/OpenSign/ # Adjust the path as needed
|
run: ls -R apps/OpenSign/ # Adjust the path as needed
|
||||||
-
|
|
||||||
name: Debug - List files opensignserver
|
- name: Debug - List files opensignserver
|
||||||
run: ls -R apps/OpenSignServer/ # Adjust the path as needed
|
run: ls -R apps/OpenSignServer/ # Adjust the path as needed
|
||||||
-
|
|
||||||
name: Build and push
|
- name: Build and push (multi-arch)
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ${{ matrix.dockerfile }}
|
file: ${{ matrix.dockerfile }}
|
||||||
push: true
|
push: true
|
||||||
|
# 👇 Build for both AMD64 & ARM64
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ Welcome to OpenSign, the premier open source docusign alternative - document e-s
|
|||||||
- **PDF Template Creation:** OpenSign™ allows you to create and store PDF document templates for repeated use thereby saving you a lot of time & collect e-signatures seamlessly.
|
- **PDF Template Creation:** OpenSign™ allows you to create and store PDF document templates for repeated use thereby saving you a lot of time & collect e-signatures seamlessly.
|
||||||
- **OpenSign™ Drive:** It is a centralised secure vault for your digital documents that makes storing, signing, organizing, sharing & archieving your docs a breeze.
|
- **OpenSign™ Drive:** It is a centralised secure vault for your digital documents that makes storing, signing, organizing, sharing & archieving your docs a breeze.
|
||||||
- **Audit Trails & completion certificate:** Being a security focused solution, OpenSign™ makes it a top priority to save detailed logs for tracking document activities along with time-stamps, IP addresses, email IDs & phone numbers. A completion certificate is generated as soon as document is completed which contains all the document related logs for added safety.
|
- **Audit Trails & completion certificate:** Being a security focused solution, OpenSign™ makes it a top priority to save detailed logs for tracking document activities along with time-stamps, IP addresses, email IDs & phone numbers. A completion certificate is generated as soon as document is completed which contains all the document related logs for added safety.
|
||||||
- **API Support:** OpenSign™ API allows seamless integration into existing systems and software. You can generate an API key from the app and refer the [official API docs](https://docs.opensignlabs.com/docs/API-docs/opensign-api-v-1) to start integrating it in your existing applications.
|
- **API Support:** OpenSign™ API allows seamless integration into existing systems and software. You can generate an API key from the app and refer the [official API docs](https://docs.opensignlabs.com/docs/API-docs/v1.1) to start integrating it in your existing applications.
|
||||||
- **Integrations:** The open source document signing experience becomes even more seamless because of integrations with various Cloud storage systems, CRMs & enterprise platforms. We also have a Zapier integration that allows you to integrate it with virtually any application.
|
- **Integrations:** The open source document signing experience becomes even more seamless because of integrations with various Cloud storage systems, CRMs & enterprise platforms. We also have a Zapier integration that allows you to integrate it with virtually any application.
|
||||||
<img alt="Login page" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/8016edb6-7d6e-4185-ab72-82d2c4a16032" height='200' >
|
<img alt="Login page" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/8016edb6-7d6e-4185-ab72-82d2c4a16032" height='200' >
|
||||||
<img alt="Dash_board" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/6e7b96f7-3154-4c25-bc1e-caf33925cc47" height='200'>
|
<img alt="Dash_board" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/6e7b96f7-3154-4c25-bc1e-caf33925cc47" height='200'>
|
||||||
@@ -165,6 +165,7 @@ We would like to thank all our contributors and users for their support and feed
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/daniel-mutwiri"><img src="https://avatars.githubusercontent.com/u/8936960?v=4?s=100" width="100px;" alt="Daniel Mutwiri"/><br /><sub><b>Daniel Mutwiri</b></sub></a><br /><a href="#code-daniel-mutwiri" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/daniel-mutwiri"><img src="https://avatars.githubusercontent.com/u/8936960?v=4?s=100" width="100px;" alt="Daniel Mutwiri"/><br /><sub><b>Daniel Mutwiri</b></sub></a><br /><a href="#code-daniel-mutwiri" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zathiel"><img src="https://avatars.githubusercontent.com/u/26553418?v=4?s=100" width="100px;" alt="Zathiel"/><br /><sub><b>Zathiel</b></sub></a><br /><a href="#code-Zathiel" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zathiel"><img src="https://avatars.githubusercontent.com/u/26553418?v=4?s=100" width="100px;" alt="Zathiel"/><br /><sub><b>Zathiel</b></sub></a><br /><a href="#code-Zathiel" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1024mb"><img src="https://avatars.githubusercontent.com/u/9301204?v=4?s=100" width="100px;" alt="1024mb"/><br /><sub><b>1024mb</b></sub></a><br /><a href="#translation-1024mb" title="Translation">🌍</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1024mb"><img src="https://avatars.githubusercontent.com/u/9301204?v=4?s=100" width="100px;" alt="1024mb"/><br /><sub><b>1024mb</b></sub></a><br /><a href="#translation-1024mb" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ehowe"><img src="https://avatars.githubusercontent.com/u/756984?v=4?s=100" width="100px;" alt="Eugene Howe"/><br /><sub><b>Eugene Howe</b></sub></a><br /><a href="#code-ehowe" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ const DisabledButtonExamples = () => {
|
|||||||
|
|
||||||
{/* Option C: Conditional styling */}
|
{/* Option C: Conditional styling */}
|
||||||
<button
|
<button
|
||||||
className={`op-btn op-btn-primary ${
|
className={`op-btn op-btn-primary ${isDisabled ? "op-btn-vscode-disabled" : ""}`}
|
||||||
isDisabled ? "op-btn-vscode-disabled" : ""
|
|
||||||
}`}
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
>
|
>
|
||||||
Conditional Button
|
Conditional Button
|
||||||
|
|||||||
@@ -3,20 +3,20 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<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="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="The fastest way to sign PDFs & request signatures from others" />
|
<meta name="description" content="The fastest way to sign PDFs & request signatures from others" />
|
||||||
<!-- <link rel="apple-touch-icon" href="/logo192.png" /> -->
|
<!-- <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"
|
<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" />
|
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" />
|
||||||
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
|
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
|
||||||
<title>...Loading</title>
|
<title>...Loading</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root" style="touch-action:pan-x pan-y;"></div>
|
<div id="root" style="touch-action:pan-x pan-y;"></div>
|
||||||
<script type="module" src="/src/index.jsx"></script>
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
@@ -1,67 +1,88 @@
|
|||||||
{
|
{
|
||||||
"name": "open_sign",
|
"name": "open_sign",
|
||||||
"version": "2.21.1",
|
"version": "2.37.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/auto-animate": "^0.8.2",
|
"@emotion/react": "^11.14.0",
|
||||||
"@imgly/background-removal": "^1.6.0",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@lottiefiles/dotlottie-react": "^0.14.0",
|
"@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",
|
||||||
"@pdf-lib/fontkit": "^1.1.1",
|
"@pdf-lib/fontkit": "^1.1.1",
|
||||||
"@radix-ui/themes": "^3.2.1",
|
"@radix-ui/themes": "^3.3.0",
|
||||||
"@reduxjs/toolkit": "^2.8.2",
|
"@reactour/tour": "^3.8.0",
|
||||||
"axios": "^1.9.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",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
|
"dompurify": "^3.4.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^23.16.8",
|
"highlight.js": "^11.11.1",
|
||||||
"i18next-browser-languagedetector": "^8.1.0",
|
"i18next": "^26.0.8",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
|
"i18next-http-backend": "^3.0.6",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
"juice": "^11.1.1",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"parse": "^6.1.1",
|
"parse": "^8.1.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pkijs": "^3.0.8",
|
"pkijs": "^3.4.0",
|
||||||
|
"prettier": "^3.8.3",
|
||||||
"print-js": "^1.6.0",
|
"print-js": "^1.6.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
"quill-html-edit-button": "^3.0.0",
|
"quill-html-edit-button": "^3.0.0",
|
||||||
"radix-ui": "^1.4.2",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^18.3.1",
|
"react": "^19.2.5",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-confetti": "^6.4.0",
|
"react-confetti": "^6.4.0",
|
||||||
"react-datepicker": "^8.4.0",
|
"react-datepicker": "^9.1.0",
|
||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dnd-multi-backend": "^9.0.0",
|
"react-dnd-multi-backend": "^9.0.0",
|
||||||
"react-dnd-touch-backend": "^16.0.1",
|
"react-dom": "^19.2.5",
|
||||||
"react-dom": "^18.3.1",
|
|
||||||
"react-gtm-module": "^2.0.11",
|
"react-gtm-module": "^2.0.11",
|
||||||
"react-helmet": "^6.1.0",
|
"react-i18next": "^17.0.6",
|
||||||
"react-i18next": "^15.5.2",
|
"react-konva": "^19.2.3",
|
||||||
"react-konva": "^18.2.10",
|
"react-pdf": "^10.4.1",
|
||||||
"react-pdf": "^9.2.1",
|
"react-quill-new": "^3.8.3",
|
||||||
"react-quill-new": "^3.4.6",
|
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-rnd": "^10.5.2",
|
"react-rnd": "^10.5.3",
|
||||||
"react-router": "^7.6.1",
|
"react-router": "^7.18.2",
|
||||||
"react-scrollbars-custom": "^4.1.1",
|
"react-scrollbars-custom": "^4.1.1",
|
||||||
"react-select": "^5.10.1",
|
"react-select": "^5.10.2",
|
||||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||||
"react-timezone-select": "^3.2.8",
|
"react-timezone-select": "^3.3.3",
|
||||||
"react-tooltip": "^5.28.1",
|
"react-to-print": "^3.3.0",
|
||||||
"reactour": "^1.19.4",
|
"react-tooltip": "^5.30.1",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"regex-parser": "^2.3.1",
|
"regex-parser": "^2.3.1",
|
||||||
"serve": "^14.2.4",
|
"web-vitals": "^5.2.0",
|
||||||
"styled-components": "^5.3.11",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"web-vitals": "^5.0.2",
|
"zod": "^3.22.4",
|
||||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
"zustand": "^4.5.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||||
"start-dev": "vite",
|
"start-dev": "vite",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "serve -s build",
|
"start": "node server.cjs",
|
||||||
"version": "curl -s https://api.github.com/repos/opensignlabs/opensign/releases/latest | grep '\"tag_name\":' | awk -F '\"' '{print $4}' > ./public/version.txt",
|
"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\"",
|
"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",
|
"build-win": "npm run version-win && vite build",
|
||||||
@@ -94,38 +115,49 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.27.4",
|
"@babel/core": "^7.29.0",
|
||||||
"@babel/preset-env": "^7.27.2",
|
"@babel/preset-env": "^7.29.2",
|
||||||
"@babel/preset-react": "^7.27.1",
|
"@babel/preset-react": "^7.28.5",
|
||||||
"@babel/runtime-corejs2": "^7.27.4",
|
"@babel/runtime-corejs2": "^7.29.2",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/react": "^18.3.23",
|
"@types/react": "^19.2.14",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.1",
|
"@vitest/ui": "^4.1.5",
|
||||||
"@vitest/ui": "^3.2.0",
|
"autoprefixer": "^10.5.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"babel-loader": "^9.2.1",
|
||||||
"babel-loader": "^10.0.0",
|
|
||||||
"commitizen": "^4.3.1",
|
"commitizen": "^4.3.1",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^10.0.4",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.4",
|
||||||
"daisyui": "^4.12.24",
|
"daisyui": "^4.12.24",
|
||||||
"dotenv": "^16.5.0",
|
"eslint": "^9.39.4",
|
||||||
"eslint": "^9.28.0",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"eslint-plugin-prettier": "^5.4.1",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^29.0.2",
|
||||||
"lint-staged": "^16.1.0",
|
"lint-staged": "^16.4.0",
|
||||||
"postcss": "^8.5.4",
|
"postcss": "^8.5.23",
|
||||||
"prettier": "^3.5.3",
|
|
||||||
"pretty-quick": "^4.2.2",
|
"pretty-quick": "^4.2.2",
|
||||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.19",
|
||||||
"vite": "^6.3.5",
|
"tmp": "^0.2.7",
|
||||||
"vite-plugin-svgr": "^4.3.0",
|
"vite": ">=8.0.16",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-plugin-svgr": "^5.2.0",
|
||||||
"vitest": "^3.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"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18 || 20 || 22"
|
"node": "18 || 20 || 22"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -0,0 +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
|
||||||
|
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"rewrites": [
|
||||||
|
{ "source": "/:path((?!\\.well-known/).*)", "destination": "/index.html" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 86 KiB |
@@ -0,0 +1,160 @@
|
|||||||
|
#!/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,4 +1,5 @@
|
|||||||
import { useState, useEffect, lazy } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { lazyWithRetry, hideUpgradeProgress } from "./utils";
|
||||||
import { Routes, Route, BrowserRouter } from "react-router";
|
import { Routes, Route, BrowserRouter } from "react-router";
|
||||||
import { pdfjs } from "react-pdf";
|
import { pdfjs } from "react-pdf";
|
||||||
import Form from "./pages/Form";
|
import Form from "./pages/Form";
|
||||||
@@ -13,24 +14,29 @@ import SignYourSelf from "./pages/SignyourselfPdf";
|
|||||||
import DraftDocument from "./components/pdf/DraftDocument";
|
import DraftDocument from "./components/pdf/DraftDocument";
|
||||||
import PlaceHolderSign from "./pages/PlaceHolderSign";
|
import PlaceHolderSign from "./pages/PlaceHolderSign";
|
||||||
import PdfRequestFiles from "./pages/PdfRequestFiles";
|
import PdfRequestFiles from "./pages/PdfRequestFiles";
|
||||||
import LazyPage from "./primitives/LazyPage";
|
import Lazy from "./primitives/LazyPage";
|
||||||
import Loader from "./primitives/Loader";
|
import Loader from "./primitives/Loader";
|
||||||
import UserList from "./pages/UserList";
|
import UserList from "./pages/UserList";
|
||||||
import { serverUrl_fn } from "./constant/appinfo";
|
import { serverUrl_fn } from "./constant/appinfo";
|
||||||
import DocSuccessPage from "./pages/DocSuccessPage";
|
import DocSuccessPage from "./pages/DocSuccessPage";
|
||||||
import ValidateSession from "./primitives/ValidateSession";
|
import DragProvider from "./components/DragProivder";
|
||||||
const DebugPdf = lazy(() => import("./pages/DebugPdf"));
|
import Title from "./components/Title";
|
||||||
const ForgetPassword = lazy(() => import("./pages/ForgetPassword"));
|
const DebugPdf = lazyWithRetry(() => import("./pages/DebugPdf"));
|
||||||
const GuestLogin = lazy(() => import("./pages/GuestLogin"));
|
const ForgetPassword = lazyWithRetry(() => import("./pages/ForgetPassword"));
|
||||||
const ChangePassword = lazy(() => import("./pages/ChangePassword"));
|
const GuestLogin = lazyWithRetry(() => import("./pages/GuestLogin"));
|
||||||
const UserProfile = lazy(() => import("./pages/UserProfile"));
|
const ChangePassword = lazyWithRetry(() => import("./pages/ChangePassword"));
|
||||||
const Opensigndrive = lazy(() => import("./pages/Opensigndrive"));
|
const UserProfile = lazyWithRetry(() => import("./pages/UserProfile"));
|
||||||
const ManageSign = lazy(() => import("./pages/Managesign"));
|
const Opensigndrive = lazyWithRetry(() => import("./pages/Opensigndrive"));
|
||||||
const AddAdmin = lazy(() => import("./pages/AddAdmin"));
|
const ManageSign = lazyWithRetry(() => import("./pages/Managesign"));
|
||||||
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
|
const AddAdmin = lazyWithRetry(() => import("./pages/AddAdmin"));
|
||||||
const Preferences = lazy(() => import("./pages/Preferences"));
|
const UpdateExistUserAdmin = lazyWithRetry(
|
||||||
const Login = lazy(() => import("./pages/Login"));
|
() => import("./pages/UpdateExistUserAdmin")
|
||||||
const VerifyDocument = lazy(() => import("./pages/VerifyDocument"));
|
);
|
||||||
|
const Preferences = lazyWithRetry(() => import("./pages/Preferences"));
|
||||||
|
const Login = lazyWithRetry(() => import("./pages/Login"));
|
||||||
|
const VerifyDocument = lazyWithRetry(() => import("./pages/VerifyDocument"));
|
||||||
|
const EmailBuilder = lazyWithRetry(() => import("./pages/EmailBuilder"));
|
||||||
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
|
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
|
||||||
const AppLoader = () => {
|
const AppLoader = () => {
|
||||||
return (
|
return (
|
||||||
@@ -42,140 +48,98 @@ const AppLoader = () => {
|
|||||||
function App() {
|
function App() {
|
||||||
const [isloading, setIsLoading] = useState(true);
|
const [isloading, setIsLoading] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleCredentials();
|
// 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);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className="bg-base-200">
|
<div className="bg-base-200">
|
||||||
{isloading ? (
|
{isloading ? (
|
||||||
<AppLoader />
|
<AppLoader />
|
||||||
) : (
|
) : (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<Title />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<ValidateRoute />}>
|
<Route element={<ValidateRoute />}>
|
||||||
<Route exact path="/" element={<LazyPage Page={Login} />} />
|
<Route exact path="/" element={<Lazy Page={Login} />} />
|
||||||
<Route
|
<Route path="/addadmin" element={<Lazy Page={AddAdmin} />} />
|
||||||
path="/addadmin"
|
|
||||||
element={<LazyPage Page={AddAdmin} />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/upgrade-2.1"
|
path="/upgrade-2.1"
|
||||||
element={<LazyPage Page={UpdateExistUserAdmin} />}
|
element={<Lazy Page={UpdateExistUserAdmin} />}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<Validate />}>
|
<Route element={<Validate />}>
|
||||||
<Route
|
|
||||||
path="/load/template/:templateId"
|
|
||||||
element={<TemplatePlaceholder />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/load/placeholdersign/:docId"
|
|
||||||
element={<PlaceHolderSign />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/load/recipientSignPdf/:docId/:contactBookId"
|
path="/load/recipientSignPdf/:docId/:contactBookId"
|
||||||
element={<PdfRequestFiles />}
|
element={<DragProvider Page={PdfRequestFiles} />}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
|
||||||
path="/loadmf/signmicroapp/login/:id/:userMail/:contactBookId/:serverUrl"
|
|
||||||
element={<LazyPage Page={GuestLogin} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/login/:id/:userMail/:contactBookId/:serverUrl"
|
|
||||||
element={<LazyPage Page={GuestLogin} />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/login/:base64url"
|
path="/login/:base64url"
|
||||||
element={<LazyPage Page={GuestLogin} />}
|
element={<Lazy Page={GuestLogin} />}
|
||||||
/>
|
/>
|
||||||
<Route path="/debugpdf" element={<LazyPage Page={DebugPdf} />} />
|
<Route path="/debugpdf" element={<Lazy Page={DebugPdf} />} />
|
||||||
<Route
|
<Route
|
||||||
path="/forgetpassword"
|
path="/forgetpassword"
|
||||||
element={<LazyPage Page={ForgetPassword} />}
|
element={<Lazy Page={ForgetPassword} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route element={<HomeLayout />}>
|
||||||
element={
|
<Route path="/users" element={<UserList />} />
|
||||||
<ValidateSession>
|
<Route
|
||||||
<HomeLayout />
|
path="/changepassword"
|
||||||
</ValidateSession>
|
element={<Lazy Page={ChangePassword} />}
|
||||||
}
|
/>
|
||||||
>
|
|
||||||
<Route
|
|
||||||
path="/changepassword"
|
|
||||||
element={<LazyPage Page={ChangePassword} />}
|
|
||||||
/>
|
|
||||||
<Route path="/form/:id" element={<Form />} />
|
<Route path="/form/:id" element={<Form />} />
|
||||||
<Route path="/report/:id" element={<Report />} />
|
<Route path="/report/:id" element={<Report />} />
|
||||||
<Route path="/dashboard/:id" element={<Dashboard />} />
|
<Route path="/dashboard/:id" element={<Dashboard />} />
|
||||||
<Route
|
<Route path="/profile" element={<Lazy Page={UserProfile} />} />
|
||||||
path="/profile"
|
<Route path="/drive" element={<Lazy Page={Opensigndrive} />} />
|
||||||
element={<LazyPage Page={UserProfile} />}
|
<Route path="/managesign" element={<Lazy Page={ManageSign} />} />
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/drive"
|
|
||||||
element={<LazyPage Page={Opensigndrive} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/managesign"
|
|
||||||
element={<LazyPage Page={ManageSign} />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/template/:templateId"
|
path="/template/:templateId"
|
||||||
element={<TemplatePlaceholder />}
|
element={<DragProvider Page={TemplatePlaceholder} />}
|
||||||
/>
|
/>
|
||||||
{/* signyouself route with no rowlevel data using docId from url */}
|
{/* signyouself route with no rowlevel data using docId from url */}
|
||||||
<Route path="/signaturePdf/:docId" element={<SignYourSelf />} />
|
<Route
|
||||||
|
path="/signaturePdf/:docId"
|
||||||
|
element={<DragProvider Page={SignYourSelf} />}
|
||||||
|
/>
|
||||||
{/* draft document route to handle and navigate route page according to document status */}
|
{/* draft document route to handle and navigate route page according to document status */}
|
||||||
<Route path="/draftDocument" element={<DraftDocument />} />
|
<Route
|
||||||
|
path="/draftDocument"
|
||||||
|
element={<DragProvider Page={DraftDocument} />}
|
||||||
|
/>
|
||||||
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
|
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
|
||||||
<Route
|
<Route
|
||||||
path="/placeHolderSign/:docId"
|
path="/placeHolderSign/:docId"
|
||||||
element={<PlaceHolderSign />}
|
element={<DragProvider Page={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 */}
|
{/* recipient signature route with no rowlevel data using docId from url */}
|
||||||
<Route
|
<Route
|
||||||
path="/recipientSignPdf/:docId/:contactBookId"
|
path="/recipientSignPdf/:docId/:contactBookId"
|
||||||
element={<PdfRequestFiles />}
|
element={<DragProvider Page={PdfRequestFiles} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/recipientSignPdf/:docId"
|
path="/recipientSignPdf/:docId"
|
||||||
element={<PdfRequestFiles />}
|
element={<DragProvider Page={PdfRequestFiles} />}
|
||||||
/>
|
/>
|
||||||
<Route path="/users" element={<UserList />} />
|
|
||||||
<Route
|
<Route
|
||||||
path="/verify-document"
|
path="/verify-document"
|
||||||
element={<LazyPage Page={VerifyDocument} />}
|
element={<Lazy Page={VerifyDocument} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/preferences"
|
path="/preferences"
|
||||||
element={<LazyPage Page={Preferences} />}
|
element={<Lazy Page={Preferences} />}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/success" element={<DocSuccessPage />} />
|
<Route path="/success" element={<DocSuccessPage />} />
|
||||||
|
<Route path="/emailbuilder" element={<EmailBuilder />} />
|
||||||
<Route path="*" element={<PageNotFound />} />
|
<Route path="*" element={<PageNotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
@@ -1,300 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import Parse from "parse";
|
|
||||||
import Loader from "../primitives/Loader";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { emailRegex } from "../constant/const";
|
|
||||||
|
|
||||||
const AddSigner = (props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [phone, setPhone] = useState("");
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [addYourself, setAddYourself] = useState(false);
|
|
||||||
const [isLoader, setIsLoader] = useState(false);
|
|
||||||
const [isUserExist, setIsUserExist] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
checkUserExist();
|
|
||||||
}, []);
|
|
||||||
// Load user details from localStorage when the component mounts
|
|
||||||
useEffect(() => {
|
|
||||||
const savedUserDetails = JSON.parse(
|
|
||||||
localStorage.getItem("UserInformation")
|
|
||||||
);
|
|
||||||
if (savedUserDetails && addYourself) {
|
|
||||||
setName(savedUserDetails.name);
|
|
||||||
setPhone(savedUserDetails?.phone || "");
|
|
||||||
setEmail(savedUserDetails.email?.toLowerCase()?.replace(/\s/g, ""));
|
|
||||||
}
|
|
||||||
}, [addYourself]);
|
|
||||||
|
|
||||||
const checkUserExist = async () => {
|
|
||||||
const user = Parse.User.current();
|
|
||||||
try {
|
|
||||||
const query = new Parse.Query("contracts_Contactbook");
|
|
||||||
query.equalTo("CreatedBy", user);
|
|
||||||
query.notEqualTo("IsDeleted", true);
|
|
||||||
query.equalTo("Email", user.getEmail());
|
|
||||||
const res = await query.first();
|
|
||||||
// console.log(res);
|
|
||||||
if (!res) {
|
|
||||||
setIsUserExist(true);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log("err", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Define a function to handle form submission
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!emailRegex.test(email)) {
|
|
||||||
alert("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,6 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Parse from "parse";
|
import Parse from "parse";
|
||||||
import Title from "./Title";
|
|
||||||
import Loader from "../primitives/Loader";
|
import Loader from "../primitives/Loader";
|
||||||
import {
|
import {
|
||||||
copytoData,
|
copytoData,
|
||||||
@@ -12,6 +11,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
useTranslation
|
useTranslation
|
||||||
} from "react-i18next";
|
} from "react-i18next";
|
||||||
|
import { withSessionValidation } from "../utils";
|
||||||
|
|
||||||
function generatePassword(length) {
|
function generatePassword(length) {
|
||||||
const characters =
|
const characters =
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
@@ -68,11 +69,11 @@ const AddUser = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Define a function to handle form submission
|
// Define a function to handle form submission
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = withSessionValidation(async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!emailRegex.test(formdata.email)) {
|
if (!emailRegex.test(formdata.email)) {
|
||||||
alert("Please enter a valid email address.");
|
alert(t("valid-email-alert"));
|
||||||
} else {
|
} else {
|
||||||
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||||
setIsFormLoader(true);
|
setIsFormLoader(true);
|
||||||
@@ -100,7 +101,6 @@ const AddUser = (props) => {
|
|||||||
};
|
};
|
||||||
const res = await Parse.Cloud.run("adduser", params);
|
const res = await Parse.Cloud.run("adduser", params);
|
||||||
const parseData = JSON.parse(JSON.stringify(res));
|
const parseData = JSON.parse(JSON.stringify(res));
|
||||||
console.log("parseData ", parseData);
|
|
||||||
if (props.closePopup) {
|
if (props.closePopup) {
|
||||||
props.closePopup();
|
props.closePopup();
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ const AddUser = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// Define a function to handle the "add yourself" checkbox
|
// Define a function to handle the "add yourself" checkbox
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
@@ -155,7 +155,6 @@ const AddUser = (props) => {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
|
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
|
||||||
<Title title={t("add-user")} />
|
|
||||||
{isFormLoader && (
|
{isFormLoader && (
|
||||||
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
|
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
|
||||||
<Loader />
|
<Loader />
|
||||||
@@ -166,7 +165,7 @@ const AddUser = (props) => {
|
|||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
className="block text-xs text-gray-700 font-semibold"
|
className="block text-xs font-semibold"
|
||||||
>
|
>
|
||||||
{t("name")}
|
{t("name")}
|
||||||
<span className="text-[red] text-[13px]"> *</span>
|
<span className="text-[red] text-[13px]"> *</span>
|
||||||
@@ -182,12 +181,13 @@ const AddUser = (props) => {
|
|||||||
onInput={(e) => e.target.setCustomValidity("")}
|
onInput={(e) => e.target.setCustomValidity("")}
|
||||||
required
|
required
|
||||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||||
|
placeholder={t("enter-name")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
className="block text-xs text-gray-700 font-semibold"
|
className="block text-xs font-semibold"
|
||||||
>
|
>
|
||||||
{t("email")}
|
{t("email")}
|
||||||
<span className="text-[red] text-[13px]"> *</span>
|
<span className="text-[red] text-[13px]"> *</span>
|
||||||
@@ -203,10 +203,11 @@ const AddUser = (props) => {
|
|||||||
}
|
}
|
||||||
onInput={(e) => e.target.setCustomValidity("")}
|
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"
|
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>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="block text-xs text-gray-700 font-semibold">
|
<label className="block text-xs font-semibold">
|
||||||
{t("password")}
|
{t("password")}
|
||||||
</label>
|
</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]">
|
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
|
||||||
@@ -217,16 +218,15 @@ const AddUser = (props) => {
|
|||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
|
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
|
||||||
{t("password-generateed")}
|
{t("password-generated")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label
|
<label
|
||||||
htmlFor="phone"
|
htmlFor="phone"
|
||||||
className="block text-xs text-gray-700 font-semibold"
|
className="block text-xs font-semibold"
|
||||||
>
|
>
|
||||||
{t("phone")}
|
{t("phone")}
|
||||||
{/* <span className="text-[red] text-[13px]"> *</span> */}
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -240,7 +240,7 @@ const AddUser = (props) => {
|
|||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label
|
<label
|
||||||
htmlFor="phone"
|
htmlFor="phone"
|
||||||
className="block text-xs text-gray-700 font-semibold"
|
className="block text-xs font-semibold"
|
||||||
>
|
>
|
||||||
{t("Role")}
|
{t("Role")}
|
||||||
<span className="text-[red] text-[13px]"> *</span>
|
<span className="text-[red] text-[13px]"> *</span>
|
||||||
|
|||||||
@@ -1,289 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
|
||||||
import axios from "axios";
|
|
||||||
import SuggestionInput from "./shared/fields/SuggestionInput";
|
|
||||||
import Loader from "../primitives/Loader";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
emailRegex,
|
|
||||||
} from "../constant/const";
|
|
||||||
const BulkSendUi = (props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [forms, setForms] = useState([]);
|
|
||||||
const formRef = useRef(null);
|
|
||||||
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
|
|
||||||
const [isSubmit, setIsSubmit] = useState(false);
|
|
||||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
|
||||||
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
|
|
||||||
const [isLoader, setIsLoader] = useState(false);
|
|
||||||
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 = [];
|
|
||||||
props?.Placeholders?.forEach((element) => {
|
|
||||||
if (!element.signerObjId) {
|
|
||||||
users = [
|
|
||||||
...users,
|
|
||||||
{
|
|
||||||
fieldId: element.Id,
|
|
||||||
email: "",
|
|
||||||
label: element.Role,
|
|
||||||
signer: {}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
// 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) {
|
|
||||||
for (const field of item.fields) {
|
|
||||||
if (!emailRegex.test(field.email)) {
|
|
||||||
alert(`Invalid email found: ${field.email}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: props.item.Signers
|
|
||||||
? [...props.item.Signers, ...existSigner]
|
|
||||||
: [...existSigner]
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Documents.push({
|
|
||||||
...props.item,
|
|
||||||
Placeholders: updatedPlaceholders,
|
|
||||||
SignatureType: props.signatureType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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;
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const DateFormat = ({ selectDate, dateFormatList, handleChangeFormat }) => {
|
||||||
|
const selectedFormatIndex = dateFormatList?.findIndex(
|
||||||
|
(item) => item.format === selectDate?.format
|
||||||
|
);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col md:items-center md:flex-row gap-y-3 my-2">
|
||||||
|
<span className="capitalize">{t("format")}: </span>
|
||||||
|
<select
|
||||||
|
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-2"
|
||||||
|
value={selectedFormatIndex >= 0 ? selectedFormatIndex : ""}
|
||||||
|
onChange={(e) => handleChangeFormat(e)}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
{t("select-date-format")}
|
||||||
|
</option>
|
||||||
|
{dateFormatList.map((data, ind) => {
|
||||||
|
return (
|
||||||
|
<option className="text-[13px]" value={ind} key={ind}>
|
||||||
|
{data?.date ? data?.date : "nodata"}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
<span className="text-xs text-gray-400 ml-1 uppercase">
|
||||||
|
{selectDate.format}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DateFormat;
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
import ReactDatePicker from "react-datepicker";
|
||||||
|
import {
|
||||||
|
getDefaultDate,
|
||||||
|
getMonth,
|
||||||
|
getYear,
|
||||||
|
months,
|
||||||
|
years
|
||||||
|
} from "../constant/Utils";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const DatePicker = ({
|
||||||
|
selectDate,
|
||||||
|
format,
|
||||||
|
minDate,
|
||||||
|
maxDate,
|
||||||
|
onChange,
|
||||||
|
handleClear,
|
||||||
|
label,
|
||||||
|
showLabel = true,
|
||||||
|
showClear = true,
|
||||||
|
dateClassName = ""
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const CustomInput = forwardRef(({ value, onClick }, ref) => (
|
||||||
|
<div
|
||||||
|
className="w-full border-gray-400 rounded-[50px] border-[1px] px-3 text-xs py-2 focus:outline-none hover:border-base-content flex items-center justify-between"
|
||||||
|
onClick={onClick}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<span className={`${dateClassName} truncate`}>{value}</span>
|
||||||
|
<i className={`fa-light fa-calendar`}></i>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
CustomInput.displayName = "CustomInput";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showLabel && (
|
||||||
|
<span className="flex-shrink-0">{label || t("default-date")}: </span>
|
||||||
|
)}
|
||||||
|
<ReactDatePicker
|
||||||
|
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||||
|
<div className="flex justify-start md:ml-2">
|
||||||
|
<select
|
||||||
|
className="bg-transparent outline-none"
|
||||||
|
value={months[getMonth(date)]}
|
||||||
|
onChange={({ target: { value } }) =>
|
||||||
|
changeMonth(months.indexOf(value))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{months.map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className="bg-transparent outline-none"
|
||||||
|
value={getYear(date)}
|
||||||
|
onChange={({ target: { value } }) => changeYear(value)}
|
||||||
|
>
|
||||||
|
{years.map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
wrapperClassName="w-full"
|
||||||
|
closeOnScroll={true}
|
||||||
|
selected={getDefaultDate(selectDate?.date, selectDate?.format)}
|
||||||
|
popperPlacement="top-end"
|
||||||
|
customInput={<CustomInput />}
|
||||||
|
onChange={(date) => onChange(date)}
|
||||||
|
dateFormat={
|
||||||
|
selectDate ? selectDate?.format : format ? format : "MM/dd/yyyy"
|
||||||
|
}
|
||||||
|
portalId="root-portal"
|
||||||
|
/>
|
||||||
|
{showClear && handleClear && (
|
||||||
|
<span
|
||||||
|
onClick={() => handleClear()}
|
||||||
|
className="underline text-blue-500 cursor-pointer ml-2"
|
||||||
|
>
|
||||||
|
{t("clear")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatePicker;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
import juice from "juice";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const EmailBodyEditor = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
smallscreen = false,
|
||||||
|
bodyName,
|
||||||
|
isReset = false,
|
||||||
|
isTemplateLoaded
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [inputHtml, setInputHtml] = useState("");
|
||||||
|
const [cleanPreview, setCleanPreview] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initProcessContent();
|
||||||
|
}, [isTemplateLoaded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isReset) {
|
||||||
|
initProcessContent(value);
|
||||||
|
}
|
||||||
|
}, [isReset]);
|
||||||
|
|
||||||
|
const initProcessContent = () => {
|
||||||
|
// 1. Sanitize immediately to strip scripts/malicious tags
|
||||||
|
const sanitized = DOMPurify.sanitize(value, {
|
||||||
|
USE_PROFILES: { html: true }, // Ensures basic HTML structure
|
||||||
|
ADD_ATTR: ["target"] // Allow links to open in new tabs
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Inline CSS for email compatibility
|
||||||
|
// Note: Juice is fast, but for huge files, you might debounce this
|
||||||
|
try {
|
||||||
|
const inlined = juice(sanitized);
|
||||||
|
setInputHtml(inlined);
|
||||||
|
onChange?.(inlined);
|
||||||
|
setCleanPreview(inlined);
|
||||||
|
} catch (err) {
|
||||||
|
onChange?.(sanitized);
|
||||||
|
setInputHtml(sanitized); // Fallback if juice fails
|
||||||
|
setCleanPreview(sanitized);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processContent = (value) => {
|
||||||
|
// 1. Sanitize immediately to strip scripts/malicious tags
|
||||||
|
const sanitized = DOMPurify.sanitize(value, {
|
||||||
|
USE_PROFILES: { html: true }, // Ensures basic HTML structure
|
||||||
|
ADD_ATTR: ["target"] // Allow links to open in new tabs
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Inline CSS for email compatibility
|
||||||
|
// Note: Juice is fast, but for huge files, you might debounce this
|
||||||
|
try {
|
||||||
|
const inlined = juice(sanitized);
|
||||||
|
setCleanPreview(inlined);
|
||||||
|
onChange?.(inlined);
|
||||||
|
} catch (err) {
|
||||||
|
setCleanPreview(sanitized); // Fallback if juice fails
|
||||||
|
onChange?.(sanitized);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setInputHtml(value);
|
||||||
|
processContent(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const screen = smallscreen ? "flex-col" : "flex-col md:flex-row ";
|
||||||
|
const template =
|
||||||
|
bodyName === "request"
|
||||||
|
? "#sample/requestemail"
|
||||||
|
: bodyName === "completion"
|
||||||
|
? "#sample/completionemail"
|
||||||
|
: "#";
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="text-sm ">
|
||||||
|
<Trans i18nKey={"open-email-builder"}>
|
||||||
|
{"You can create email template using "}
|
||||||
|
<a
|
||||||
|
href={`/emailbuilder${template}`}
|
||||||
|
target="_blank"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
className="op-link op-link-primary font-medium"
|
||||||
|
>
|
||||||
|
email builder
|
||||||
|
</a>
|
||||||
|
{" platform and copy html code."}
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
<div className={`flex ${screen} gap-5`}>
|
||||||
|
{/* Editor Pane */}
|
||||||
|
<div className="flex flex-1 flex-col mt-2">
|
||||||
|
<label>{t("paste-html-here")}:</label>
|
||||||
|
<div className="flex-1 mt-1 p-3 text-xs op-textarea op-textarea-bordered min-h-[70vh]">
|
||||||
|
<textarea
|
||||||
|
className="w-full min-h-[70vh] focus:outline-none"
|
||||||
|
value={inputHtml}
|
||||||
|
onChange={(e) => handleChange(e)}
|
||||||
|
placeholder="<html><body><h1>Hello!</h1></body></html>"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Live Preview Pane */}
|
||||||
|
<div className="flex flex-1 flex-col mt-2">
|
||||||
|
<label>{t("preview")}</label>
|
||||||
|
<div className="flex-1 mt-1 bg-white border-[1px] op-textarea border-[#ccc] min-h-[70vh]">
|
||||||
|
{cleanPreview && (
|
||||||
|
<iframe
|
||||||
|
title="Safe Preview"
|
||||||
|
srcDoc={cleanPreview}
|
||||||
|
// SECURITY: sandbox prevents scripts from running even if they slip through
|
||||||
|
sandbox="allow-popups allow-popups-to-escape-sandbox"
|
||||||
|
className="w-full min-h-[70vh]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailBodyEditor;
|
||||||
@@ -12,27 +12,44 @@ import {
|
|||||||
} from "../constant/Utils";
|
} from "../constant/Utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { appInfo } from "../constant/appinfo";
|
import { appInfo } from "../constant/appinfo";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { toggleSidebar } from "../redux/reducers/sidebarReducer";
|
||||||
|
import { sessionStatus } from "../redux/reducers/userReducer";
|
||||||
|
|
||||||
const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { width } = useWindowSize();
|
const { width } = useWindowSize();
|
||||||
|
const dispatch = useDispatch();
|
||||||
const username = localStorage.getItem("username") || "";
|
const username = localStorage.getItem("username") || "";
|
||||||
const image = localStorage.getItem("profileImg") || dp;
|
const image = localStorage.getItem("profileImg") || dp;
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [applogo, setAppLogo] = useState("");
|
const [applogo, setAppLogo] = useState("");
|
||||||
|
const [isDarkTheme, setIsDarkTheme] = useState();
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
if (width <= 768) {
|
closeSidebar();
|
||||||
setIsMenu(false);
|
};
|
||||||
|
const closeSidebar = () => {
|
||||||
|
if (width && width <= 768) {
|
||||||
|
dispatch(toggleSidebar(false));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeHead();
|
initializeHead();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
closeSidebar();
|
||||||
|
}, [width]);
|
||||||
|
|
||||||
|
const showSidebar = () => {
|
||||||
|
dispatch(toggleSidebar());
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
async function initializeHead() {
|
async function initializeHead() {
|
||||||
const applogo = await getAppLogo();
|
const applogo = await getAppLogo();
|
||||||
@@ -43,13 +60,15 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
setAppLogo(logo);
|
setAppLogo(logo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const handleLogout = async () => {
|
||||||
const closeDropdown = async () => {
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
setIsLoggingOut(true);
|
||||||
try {
|
try {
|
||||||
await Parse.User.logOut();
|
await Parse.User.logOut();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Err while logging out", err);
|
console.log("Err while logging out", err);
|
||||||
|
} finally {
|
||||||
|
dispatch(sessionStatus(true));
|
||||||
}
|
}
|
||||||
let appdata = localStorage.getItem("userSettings");
|
let appdata = localStorage.getItem("userSettings");
|
||||||
let applogo = localStorage.getItem("appLogo");
|
let applogo = localStorage.getItem("appLogo");
|
||||||
@@ -57,6 +76,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
let PageLanding = localStorage.getItem("PageLanding");
|
let PageLanding = localStorage.getItem("PageLanding");
|
||||||
let baseUrl = localStorage.getItem("baseUrl");
|
let baseUrl = localStorage.getItem("baseUrl");
|
||||||
let appid = localStorage.getItem("parseAppId");
|
let appid = localStorage.getItem("parseAppId");
|
||||||
|
let favicon = localStorage.getItem("favicon");
|
||||||
|
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
saveLanguageInLocal(i18n);
|
saveLanguageInLocal(i18n);
|
||||||
@@ -66,7 +86,8 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
localStorage.setItem("userSettings", appdata);
|
localStorage.setItem("userSettings", appdata);
|
||||||
localStorage.setItem("baseUrl", baseUrl);
|
localStorage.setItem("baseUrl", baseUrl);
|
||||||
localStorage.setItem("parseAppId", appid);
|
localStorage.setItem("parseAppId", appid);
|
||||||
|
localStorage.setItem("favicon", favicon);
|
||||||
|
setIsLoggingOut(false);
|
||||||
navigate("/");
|
navigate("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,22 +109,15 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateLogoForTheme = () => {
|
const updateThemeStatus = () => {
|
||||||
const isDarkMode =
|
const isDarkTheme =
|
||||||
document.documentElement.getAttribute("data-theme") === "opensigndark";
|
document.documentElement.getAttribute("data-theme") === "opensigndark";
|
||||||
const logo = isDarkMode
|
setIsDarkTheme(isDarkTheme);
|
||||||
? "/static/js/assets/images/logo-dark.png" // Path to the dark mode logo
|
|
||||||
: appInfo.applogo; // Use current logo for light mode
|
|
||||||
if (applogo !== logo) {
|
|
||||||
setAppLogo(logo);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
updateThemeStatus();
|
||||||
// Set the logo immediately based on the current theme
|
|
||||||
updateLogoForTheme();
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
updateLogoForTheme();
|
updateThemeStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(document.documentElement, {
|
observer.observe(document.documentElement, {
|
||||||
@@ -112,11 +126,11 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [applogo]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<div className="op-navbar bg-base-100 shadow">
|
<div className="op-navbar bg-base-100 shadow touch-none">
|
||||||
<div className="flex-none">
|
<div className="flex-none">
|
||||||
<button
|
<button
|
||||||
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
|
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
|
||||||
@@ -126,11 +140,18 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 ml-2">
|
<div className="flex-1 ml-2">
|
||||||
<div className="h-[25px] md:h-[40px] w-auto overflow-hidden">
|
<div
|
||||||
|
onClick={() => navigate("/dashboard/35KBoSgoAK")}
|
||||||
|
className="h-[25px] md:h-[40px] w-auto overflow-hidden cursor-pointer"
|
||||||
|
>
|
||||||
{applogo && (
|
{applogo && (
|
||||||
<img
|
<img
|
||||||
className="object-contain h-full w-auto"
|
className="object-contain h-full w-auto"
|
||||||
src={applogo}
|
src={
|
||||||
|
isDarkTheme
|
||||||
|
? "/static/js/assets/images/logo-dark.png"
|
||||||
|
: applogo
|
||||||
|
}
|
||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -138,7 +159,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div id="profile-menu" className="flex-none gap-2">
|
<div id="profile-menu" className="flex-none gap-2">
|
||||||
<div>
|
<div>
|
||||||
<FullScreenButton />
|
<FullScreenButton />
|
||||||
</div>
|
</div>
|
||||||
{width >= 768 && (
|
{width >= 768 && (
|
||||||
<div
|
<div
|
||||||
@@ -176,7 +197,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
|
className={`mt-4 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
|
||||||
isOpen ? "" : "hidden"
|
isOpen ? "" : "hidden"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -235,7 +256,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<li onClick={closeDropdown}>
|
<li onClick={handleLogout}>
|
||||||
<span>
|
<span>
|
||||||
<i className="fa-light fa-arrow-right-from-bracket"></i>{" "}
|
<i className="fa-light fa-arrow-right-from-bracket"></i>{" "}
|
||||||
{t("log-out")}
|
{t("log-out")}
|
||||||
@@ -245,7 +266,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function RotateAlert(props) {
|
|||||||
props.setShowRotateAlert({ status: false, degree: 0 })
|
props.setShowRotateAlert({ status: false, degree: 0 })
|
||||||
}
|
}
|
||||||
type="button"
|
type="button"
|
||||||
className="op-btn op-btn-ghost shadow-md"
|
className="op-btn op-btn-ghost text-base-content shadow-md"
|
||||||
>
|
>
|
||||||
{t("no")}
|
{t("no")}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,20 +1,81 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { useLocation, matchPath } from "react-router";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useManifestUrl } from "../hook/useManifestUrl";
|
||||||
|
|
||||||
function Title({ title, drive }) {
|
const TITLE_MAP = {
|
||||||
const appName =
|
"/": "login",
|
||||||
"OpenSign™";
|
// Homelayout
|
||||||
return (
|
"/dashboard/35KBoSgoAK": "Dashboard",
|
||||||
<Helmet>
|
"/form/sHAnZphf69": "Sign Yourself",
|
||||||
<title>{drive ? title : `${title} - ${appName}`}</title>
|
"/form/8mZzFxbG1z": "Request Signatures",
|
||||||
<meta name="description" content={`${title} - ${appName}`} />
|
"/form/template": "New Template",
|
||||||
<link
|
"/report/6TeaPr321t": "Templates",
|
||||||
rel="icon"
|
"/report/4Hhwbp482K": "Need your sign",
|
||||||
type="image/png"
|
"/report/1MwEuxLEkF": "In Progress",
|
||||||
href={localStorage.getItem("fev_Icon")}
|
"/report/kQUoW4hUXz": "Completed",
|
||||||
sizes="40x40"
|
"/report/ByHuevtCFY": "Drafts",
|
||||||
/>
|
"/report/UPr2Fm5WY3": "Declined",
|
||||||
</Helmet>
|
"/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 Title;
|
export default function Title() {
|
||||||
|
const { pathname, state } = useLocation();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,650 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,630 @@
|
|||||||
|
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);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
function ResponseTab({
|
||||||
|
prefillCount = 0,
|
||||||
|
documentCount = 0,
|
||||||
|
message = { status: "", message: "" }
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { status, message: msg } = message;
|
||||||
|
return (
|
||||||
|
<div className="rounded-box border p-4 m-3 md:m-6 ">
|
||||||
|
<div className="font-medium">{t("summary")}</div>
|
||||||
|
<ul className="mt-2 list-disc pl-5 text-base-content/80">
|
||||||
|
<li>
|
||||||
|
{t("prefill-fields")}: {prefillCount}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t("documents")}: {documentCount}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t("status")}: {status}
|
||||||
|
</li>
|
||||||
|
{msg && (
|
||||||
|
<li>
|
||||||
|
{t("message")}: {msg}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResponseTab;
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
useState
|
||||||
|
} from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import SuggestionInput from "../../shared/fields/SuggestionInput";
|
||||||
|
import RenderWidgets from "./RenderWidgets";
|
||||||
|
|
||||||
|
const requiredAsteriskCls = (isRequired = false) => {
|
||||||
|
return isRequired ? "after:content-['_*'] after:text-red-500" : "";
|
||||||
|
};
|
||||||
|
const Table = ({
|
||||||
|
headers = [],
|
||||||
|
rowData = [],
|
||||||
|
handleInputChange,
|
||||||
|
handleWidgetDetails,
|
||||||
|
initialPerPage = 25,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [itemsPerPage, setItemsPerPage] = useState(initialPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const currentData = rowData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div
|
||||||
|
className="overflow-x-auto border border-gray-300 rounded-lg"
|
||||||
|
>
|
||||||
|
<table className="op-table border-collapse w-full">
|
||||||
|
<thead className="text-[13px] text-center">
|
||||||
|
<tr className="border-y-[1px]">
|
||||||
|
{headers?.map((header) => (
|
||||||
|
<th
|
||||||
|
key={header.label}
|
||||||
|
className={`${requiredAsteriskCls(header?.isRequired)} p-2`}
|
||||||
|
>
|
||||||
|
{header.label}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
{rowData.length > 1 && <th className="p-2">{t("action")}</th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{currentData.length > 0 &&
|
||||||
|
currentData.map((form, formIndex) => {
|
||||||
|
const globalIndex = startIndex + formIndex;
|
||||||
|
const fields = form?.fields ?? [];
|
||||||
|
|
||||||
|
const emailFields = fields.filter((f) => f.label !== "prefill");
|
||||||
|
|
||||||
|
const widgets = fields.flatMap((f, fieldIndex) =>
|
||||||
|
(f.widgets ?? []).map((widget, widgetIndex) => ({
|
||||||
|
widget,
|
||||||
|
fieldIndex, // keep the fieldIndex that produced this widget
|
||||||
|
widgetIndex
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<tr key={form.Id} id={`table-row-${formIndex}`}>
|
||||||
|
{emailFields.map((field, fieldIndex) => (
|
||||||
|
<td key={field.fieldId} className="p-2">
|
||||||
|
<SuggestionInput
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
value={field.email ?? ""}
|
||||||
|
index={fieldIndex}
|
||||||
|
onChange={(signer) =>
|
||||||
|
handleInputChange(globalIndex, signer, field.label)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{widgets.map(({ widget, fieldIndex, widgetIndex }) => (
|
||||||
|
<td key={`${form.Id}-${widget.key}`} className="p-2">
|
||||||
|
<RenderWidgets
|
||||||
|
widget={widget}
|
||||||
|
handleWidgetDetails={(value) =>
|
||||||
|
handleWidgetDetails(
|
||||||
|
value,
|
||||||
|
globalIndex, // formIndex,
|
||||||
|
fieldIndex,
|
||||||
|
widgetIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Table;
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/* ---------------- Wizard Header ---------------- */
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function WizardHeader({ steps, step, onStepClick }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col md:flex-row flex-wrap items-center justify-center gap-0 md:gap-2">
|
||||||
|
{steps.map((s, i) => {
|
||||||
|
const isActive = i === step;
|
||||||
|
const isDone = i < step;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={s.key}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onStepClick?.(i)}
|
||||||
|
className={[
|
||||||
|
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-xs lg:text-sm font-medium border focus:outline-none",
|
||||||
|
isActive
|
||||||
|
? "op-bg-primary text-primary-content border-primary"
|
||||||
|
: isDone
|
||||||
|
? "bg-base-100 text-base-content border-base-300 hover:bg-base-200"
|
||||||
|
: "bg-base-100 text-base-content/70 border-base-300"
|
||||||
|
].join(" ")}
|
||||||
|
title={s.label}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"inline-flex h-5 w-5 items-center justify-center rounded-full text-xs",
|
||||||
|
isActive
|
||||||
|
? "op-bg-primary-content/15 text-primary-content"
|
||||||
|
: isDone
|
||||||
|
? "bg-base-200 text-base-content"
|
||||||
|
: "bg-base-200 text-base-content/70"
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="whitespace-nowrap">{s.label}</span>
|
||||||
|
|
||||||
|
{/* info icon like screenshot */}
|
||||||
|
<a data-tooltip-id={`${s.key}-tooltip`} className="ml-1">
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"inline-flex h-[1.10rem] w-[1.10rem] items-center justify-center border-[1px] rounded-full",
|
||||||
|
!isActive
|
||||||
|
? "border-[#33bbff] text-[#33bbff]"
|
||||||
|
: "border border-base-300 text-base-300"
|
||||||
|
].join(" ")}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<i className="fa-light fa-question rounded-full text-[13px]"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<span className="text-left">{s.help}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{i < steps.length - 1 && (
|
||||||
|
<span
|
||||||
|
className="mx-1 select-none text-base-content/40 text-center text-2xl md:mb-2 rotate-90 md:rotate-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WizardHeader;
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Parse from "parse";
|
import Parse from "parse";
|
||||||
import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
|
import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import Tooltip from "../../primitives/Tooltip";
|
import Tooltip from "../../primitives/Tooltip";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { withSessionValidation } from "../../utils";
|
||||||
|
|
||||||
const DashboardCard = (props) => {
|
const DashboardCard = (props) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -14,7 +15,7 @@ const DashboardCard = (props) => {
|
|||||||
const [response, setresponse] = useState("");
|
const [response, setresponse] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const renderData = async () => {
|
const renderData = withSessionValidation(async () => {
|
||||||
if (props.Data.queryType === "function") {
|
if (props.Data.queryType === "function") {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -148,7 +149,7 @@ const DashboardCard = (props) => {
|
|||||||
);
|
);
|
||||||
let arr = [];
|
let arr = [];
|
||||||
for (const obj of listData) {
|
for (const obj of listData) {
|
||||||
const isSigner = obj.Signers.some(
|
const isSigner = obj.Signers?.some(
|
||||||
(item) => item.UserId.objectId === currentUser.id
|
(item) => item.UserId.objectId === currentUser.id
|
||||||
);
|
);
|
||||||
if (isSigner) {
|
if (isSigner) {
|
||||||
@@ -168,31 +169,27 @@ const DashboardCard = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setresponse(arr.length);
|
setresponse(arr.length);
|
||||||
setLoading(false);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await axios.get(url, { headers: headers }).then((res) => {
|
await axios.get(url, { headers: headers }).then((res) => {
|
||||||
if (res.data.results.length > 0) {
|
if (res?.data?.[props.Data.key]) {
|
||||||
setLoading(false);
|
setresponse(parseInt(res.data[props.Data.key]));
|
||||||
if (props.Data.key !== "count") {
|
} else if (res?.data?.results?.length > 0) {
|
||||||
setresponse(res.data.results[0][props.Data.key]);
|
setresponse(res.data.results.length);
|
||||||
} else {
|
|
||||||
setresponse(res.data[props.Data.key]);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setresponse(0);
|
setresponse(0);
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Problem", e);
|
console.error("Problem", e);
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const filterRender = async () => {
|
const filterRender = withSessionValidation(async () => {
|
||||||
if (props.FilterData && props.FilterData.queryType === "function") {
|
if (props.FilterData && props.FilterData.queryType === "function") {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -270,7 +267,7 @@ const DashboardCard = (props) => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const setFormat = (val) => {
|
const setFormat = (val) => {
|
||||||
switch (props.Format) {
|
switch (props.Format) {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import Parse from "parse";
|
import Parse from "parse";
|
||||||
import ReportTable from "../../primitives/GetReportDisplay";
|
import DocumentsReport from "../../reports/document/DocumentsReport";
|
||||||
import reportJson from "../../json/ReportJson";
|
import reportJson from "../../json/ReportJson";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Loader from "../../primitives/Loader";
|
import Loader from "../../primitives/Loader";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { withSessionValidation } from "../../utils";
|
||||||
function DashboardReport(props) {
|
function DashboardReport(props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// console.log("props ", props)
|
|
||||||
const [List, setList] = useState([]);
|
const [List, setList] = useState([]);
|
||||||
const [isLoader, setIsLoader] = useState(true);
|
const [isLoader, setIsLoader] = useState(true);
|
||||||
const [reportName, setReportName] = useState("");
|
const [reportName, setReportName] = useState("");
|
||||||
@@ -47,7 +47,7 @@ function DashboardReport(props) {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [isNextRecord]);
|
}, [isNextRecord]);
|
||||||
|
|
||||||
const handleSearchChange = async (e) => {
|
const handleSearchChange = withSessionValidation(async (e) => {
|
||||||
const term = e.target.value.toLowerCase();
|
const term = e.target.value.toLowerCase();
|
||||||
setSearchTerm(term);
|
setSearchTerm(term);
|
||||||
if (debounceTimer.current) {
|
if (debounceTimer.current) {
|
||||||
@@ -83,7 +83,7 @@ function DashboardReport(props) {
|
|||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
setIsSearchResult(false);
|
setIsSearchResult(false);
|
||||||
};
|
});
|
||||||
|
|
||||||
const handleSearchPaste = (e) => {
|
const handleSearchPaste = (e) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -99,96 +99,95 @@ function DashboardReport(props) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getReportData = async (
|
const getReportData = withSessionValidation(
|
||||||
id,
|
async (id, skipUserRecord = 0, limit = 20, term = searchTerm) => {
|
||||||
skipUserRecord = 0,
|
setIsLoader(true);
|
||||||
limit = 20,
|
const json = reportJson(id);
|
||||||
term = searchTerm
|
if (json) {
|
||||||
) => {
|
setActions(json.actions);
|
||||||
setIsLoader(true);
|
setReportName(json.reportName);
|
||||||
const json = reportJson(id);
|
setHeading(json.heading);
|
||||||
if (json) {
|
const currentUser = Parse.User.current().id;
|
||||||
setActions(json.actions);
|
|
||||||
setReportName(json.reportName);
|
|
||||||
setHeading(json.heading);
|
|
||||||
const currentUser = Parse.User.current().id;
|
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||||
sessiontoken: localStorage.getItem("accesstoken")
|
sessiontoken: localStorage.getItem("accesstoken")
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
||||||
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
||||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||||
if (term) {
|
if (term) {
|
||||||
params.searchTerm = term;
|
params.searchTerm = term;
|
||||||
}
|
}
|
||||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||||
const res = await axios.post(url, params, {
|
const res = await axios.post(url, params, {
|
||||||
headers: headers,
|
headers: headers,
|
||||||
signal: abortController.signal // is used to cancel fetch query
|
signal: abortController.signal // is used to cancel fetch query
|
||||||
});
|
});
|
||||||
if (id === "5Go51Q7T8r") {
|
if (id === "5Go51Q7T8r") {
|
||||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
const listData = res.data?.result.filter(
|
||||||
let arr = [];
|
(x) => x.Signers.length > 0
|
||||||
for (const obj of listData) {
|
|
||||||
const isSigner = obj.Signers.some(
|
|
||||||
(item) => item.UserId.objectId === currentUser
|
|
||||||
);
|
);
|
||||||
if (isSigner) {
|
let arr = [];
|
||||||
let isRecord;
|
for (const obj of listData) {
|
||||||
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
const isSigner = obj.Signers?.some(
|
||||||
isRecord = obj?.AuditTrail.some(
|
(item) => item.UserId.objectId === currentUser
|
||||||
(item) =>
|
);
|
||||||
item?.UserPtr?.UserId?.objectId === currentUser &&
|
if (isSigner) {
|
||||||
item.Activity === "Signed"
|
let isRecord;
|
||||||
);
|
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
||||||
} else {
|
isRecord = obj?.AuditTrail.some(
|
||||||
isRecord = false;
|
(item) =>
|
||||||
}
|
item?.UserPtr?.UserId?.objectId === currentUser &&
|
||||||
if (isRecord === false) {
|
item.Activity === "Signed"
|
||||||
arr.push(obj);
|
);
|
||||||
|
} else {
|
||||||
|
isRecord = false;
|
||||||
|
}
|
||||||
|
if (isRecord === false) {
|
||||||
|
arr.push(obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (arr.length === docPerPage) {
|
||||||
if (arr.length === docPerPage) {
|
setIsMoreDocs(true);
|
||||||
setIsMoreDocs(true);
|
} else {
|
||||||
} else {
|
setIsMoreDocs(false);
|
||||||
setIsMoreDocs(false);
|
}
|
||||||
}
|
|
||||||
setList((prevRecord) =>
|
|
||||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (res.data.result.length >= docPerPage) {
|
|
||||||
setIsMoreDocs(true);
|
|
||||||
} else {
|
|
||||||
setIsMoreDocs(false);
|
|
||||||
}
|
|
||||||
setIsNextRecord(false);
|
|
||||||
if (!res.data.result.error) {
|
|
||||||
setList((prevRecord) =>
|
setList((prevRecord) =>
|
||||||
prevRecord.length > 0
|
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||||
? [...prevRecord, ...res.data.result]
|
|
||||||
: res.data.result
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
if (res.data.result.length >= docPerPage) {
|
||||||
|
setIsMoreDocs(true);
|
||||||
|
} else {
|
||||||
|
setIsMoreDocs(false);
|
||||||
|
}
|
||||||
|
setIsNextRecord(false);
|
||||||
|
if (!res.data.result.error) {
|
||||||
|
setList((prevRecord) =>
|
||||||
|
prevRecord.length > 0
|
||||||
|
? [...prevRecord, ...res.data.result]
|
||||||
|
: res.data.result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsLoader(false);
|
||||||
|
} catch (err) {
|
||||||
|
const isCancel = axios.isCancel(err);
|
||||||
|
if (!isCancel) {
|
||||||
|
console.log("err ", err);
|
||||||
|
setIsLoader(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
setIsLoader(false);
|
|
||||||
} catch (err) {
|
|
||||||
const isCancel = axios.isCancel(err);
|
|
||||||
if (!isCancel) {
|
|
||||||
console.log("err ", err);
|
|
||||||
setIsLoader(false);
|
setIsLoader(false);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
setIsLoader(false);
|
setIsLoader(false);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
setIsLoader(false);
|
|
||||||
}
|
}
|
||||||
};
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoader ? (
|
{isLoader ? (
|
||||||
@@ -198,7 +197,7 @@ function DashboardReport(props) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{reportName ? (
|
{reportName ? (
|
||||||
<ReportTable
|
<DocumentsReport
|
||||||
ReportName={reportName}
|
ReportName={reportName}
|
||||||
List={List}
|
List={List}
|
||||||
setList={setList}
|
setList={setList}
|
||||||
@@ -215,9 +214,9 @@ function DashboardReport(props) {
|
|||||||
isSearchResult={isSearchResult}
|
isSearchResult={isSearchResult}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded">
|
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded-box">
|
||||||
<div className="text-center">
|
<div className="text-center text-xl text-base-content">
|
||||||
<p className="text-xl text-base-content">{t("report-not-found")}</p>
|
{t("report-not-found")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { Suspense, lazy } from "react";
|
import { useState, useEffect, Suspense } from "react";
|
||||||
|
import { lazyWithRetry } from "../../utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
const DashboardButton = lazy(() => import("./DashboardButton"));
|
const DashboardButton = lazyWithRetry(() => import("./DashboardButton"));
|
||||||
const DashboardCard = lazy(() => import("./DashboardCard"));
|
const DashboardCard = lazyWithRetry(() => import("./DashboardCard"));
|
||||||
const DashboardReport = lazy(() => import("./DashboardReport"));
|
const DashboardReport = lazyWithRetry(() => import("./DashboardReport"));
|
||||||
const buttonList = [
|
const buttonList = [
|
||||||
{
|
{
|
||||||
label: "Sign yourself",
|
label: "Sign yourself",
|
||||||
@@ -19,6 +20,7 @@ const buttonList = [
|
|||||||
];
|
];
|
||||||
const GetDashboard = (props) => {
|
const GetDashboard = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const Button = ({ label, redirectId, redirectType, icon }) => (
|
const Button = ({ label, redirectId, redirectType, icon }) => (
|
||||||
<DashboardButton
|
<DashboardButton
|
||||||
Icon={icon}
|
Icon={icon}
|
||||||
@@ -58,7 +60,9 @@ const GetDashboard = (props) => {
|
|||||||
<div data-tut={col.widget.data.tourSection}>
|
<div data-tut={col.widget.data.tourSection}>
|
||||||
<Suspense fallback={<div>please wait</div>}>
|
<Suspense fallback={<div>please wait</div>}>
|
||||||
<div className="mb-3 md:mb-0">
|
<div className="mb-3 md:mb-0">
|
||||||
<DashboardReport Record={col.widget} />
|
<DashboardReport
|
||||||
|
Record={col.widget}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +96,9 @@ const GetDashboard = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>please wait</div>}>
|
<Suspense fallback={<div>please wait</div>}>
|
||||||
<div className="mb-3 md:mb-0">
|
<div className="mb-3 md:mb-0">
|
||||||
<DashboardReport Record={col.widget} />
|
<DashboardReport
|
||||||
|
Record={col.widget}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
import { TEditorBlock } from '../../../documents/editor/core';
|
||||||
|
import { setDocument, useDocument, useSelectedBlockId } from '../../../documents/editor/EditorContext';
|
||||||
|
|
||||||
|
import AvatarSidebarPanel from './input-panels/AvatarSidebarPanel';
|
||||||
|
import ButtonSidebarPanel from './input-panels/ButtonSidebarPanel';
|
||||||
|
import ColumnsContainerSidebarPanel from './input-panels/ColumnsContainerSidebarPanel';
|
||||||
|
import ContainerSidebarPanel from './input-panels/ContainerSidebarPanel';
|
||||||
|
import DividerSidebarPanel from './input-panels/DividerSidebarPanel';
|
||||||
|
import EmailLayoutSidebarPanel from './input-panels/EmailLayoutSidebarPanel';
|
||||||
|
import HeadingSidebarPanel from './input-panels/HeadingSidebarPanel';
|
||||||
|
import HtmlSidebarPanel from './input-panels/HtmlSidebarPanel';
|
||||||
|
import ImageSidebarPanel from './input-panels/ImageSidebarPanel';
|
||||||
|
import SpacerSidebarPanel from './input-panels/SpacerSidebarPanel';
|
||||||
|
import TextSidebarPanel from './input-panels/TextSidebarPanel';
|
||||||
|
|
||||||
|
function renderMessage(val: string) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ m: 3, p: 1, border: '1px dashed', borderColor: 'divider' }}>
|
||||||
|
<Typography color="text.secondary">{val}</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConfigurationPanel() {
|
||||||
|
const document = useDocument();
|
||||||
|
const selectedBlockId = useSelectedBlockId();
|
||||||
|
|
||||||
|
if (!selectedBlockId) {
|
||||||
|
return renderMessage('Click on a block to inspect.');
|
||||||
|
}
|
||||||
|
const block = document[selectedBlockId];
|
||||||
|
if (!block) {
|
||||||
|
return renderMessage(`Block with id ${selectedBlockId} was not found. Click on a block to reset.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setBlock = (conf: TEditorBlock) => setDocument({ [selectedBlockId]: conf });
|
||||||
|
const { data, type } = block;
|
||||||
|
switch (type) {
|
||||||
|
case 'Avatar':
|
||||||
|
return <AvatarSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||||
|
case 'Button':
|
||||||
|
return <ButtonSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||||
|
case 'ColumnsContainer':
|
||||||
|
return (
|
||||||
|
<ColumnsContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />
|
||||||
|
);
|
||||||
|
case 'Container':
|
||||||
|
return <ContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||||
|
case 'Divider':
|
||||||
|
return <DividerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||||
|
case 'Heading':
|
||||||
|
return <HeadingSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||||
|
case 'Html':
|
||||||
|
return <HtmlSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||||
|
case 'Image':
|
||||||
|
return <ImageSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||||
|
case 'EmailLayout':
|
||||||
|
return <EmailLayoutSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||||
|
case 'Spacer':
|
||||||
|
return <SpacerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||||
|
case 'Text':
|
||||||
|
return <TextSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||||
|
default:
|
||||||
|
return <pre>{JSON.stringify(block, null, ' ')}</pre>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { AspectRatioOutlined } from "@mui/icons-material";
|
||||||
|
import { ToggleButton } from "@mui/material";
|
||||||
|
import {
|
||||||
|
AvatarProps,
|
||||||
|
AvatarPropsDefaults,
|
||||||
|
AvatarPropsSchema
|
||||||
|
} from "@usewaypoint/block-avatar";
|
||||||
|
|
||||||
|
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||||
|
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||||
|
import SliderInput from "./helpers/inputs/SliderInput";
|
||||||
|
import TextInput from "./helpers/inputs/TextInput";
|
||||||
|
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||||
|
|
||||||
|
type AvatarSidebarPanelProps = {
|
||||||
|
data: AvatarProps;
|
||||||
|
setData: (v: AvatarProps) => void;
|
||||||
|
};
|
||||||
|
export default function AvatarSidebarPanel({
|
||||||
|
data,
|
||||||
|
setData
|
||||||
|
}: AvatarSidebarPanelProps) {
|
||||||
|
const [, setErrors] = useState<ZodError | null>(null);
|
||||||
|
const updateData = (d: unknown) => {
|
||||||
|
const res = AvatarPropsSchema.safeParse(d);
|
||||||
|
if (res.success) {
|
||||||
|
setData(res.data);
|
||||||
|
setErrors(null);
|
||||||
|
} else {
|
||||||
|
setErrors(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const size = data.props?.size ?? AvatarPropsDefaults.size;
|
||||||
|
const imageUrl = data.props?.imageUrl ?? AvatarPropsDefaults.imageUrl;
|
||||||
|
const alt = data.props?.alt ?? AvatarPropsDefaults.alt;
|
||||||
|
const shape = data.props?.shape ?? AvatarPropsDefaults.shape;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseSidebarPanel title="Avatar block">
|
||||||
|
<SliderInput
|
||||||
|
label="Size"
|
||||||
|
iconLabel={<AspectRatioOutlined sx={{ color: "text.secondary" }} />}
|
||||||
|
units="px"
|
||||||
|
step={3}
|
||||||
|
min={32}
|
||||||
|
max={256}
|
||||||
|
defaultValue={size}
|
||||||
|
onChange={(size) => {
|
||||||
|
updateData({ ...data, props: { ...data.props, size } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<RadioGroupInput
|
||||||
|
label="Shape"
|
||||||
|
defaultValue={shape}
|
||||||
|
onChange={(shape) => {
|
||||||
|
updateData({ ...data, props: { ...data.props, shape } });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="circle">Circle</ToggleButton>
|
||||||
|
<ToggleButton value="square">Square</ToggleButton>
|
||||||
|
<ToggleButton value="rounded">Rounded</ToggleButton>
|
||||||
|
</RadioGroupInput>
|
||||||
|
<TextInput
|
||||||
|
label="Image URL"
|
||||||
|
defaultValue={imageUrl}
|
||||||
|
onChange={(imageUrl) => {
|
||||||
|
updateData({ ...data, props: { ...data.props, imageUrl } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Alt text"
|
||||||
|
defaultValue={alt}
|
||||||
|
onChange={(alt) => {
|
||||||
|
updateData({ ...data, props: { ...data.props, alt } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MultiStylePropertyPanel
|
||||||
|
names={["textAlign", "padding"]}
|
||||||
|
value={data.style}
|
||||||
|
onChange={(style) => updateData({ ...data, style })}
|
||||||
|
/>
|
||||||
|
</BaseSidebarPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { ToggleButton } from "@mui/material";
|
||||||
|
import {
|
||||||
|
ButtonProps,
|
||||||
|
ButtonPropsDefaults,
|
||||||
|
ButtonPropsSchema
|
||||||
|
} from "@usewaypoint/block-button";
|
||||||
|
|
||||||
|
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||||
|
import ColorInput from "./helpers/inputs/ColorInput";
|
||||||
|
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||||
|
import TextInput from "./helpers/inputs/TextInput";
|
||||||
|
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||||
|
|
||||||
|
type ButtonSidebarPanelProps = {
|
||||||
|
data: ButtonProps;
|
||||||
|
setData: (v: ButtonProps) => void;
|
||||||
|
};
|
||||||
|
export default function ButtonSidebarPanel({
|
||||||
|
data,
|
||||||
|
setData
|
||||||
|
}: ButtonSidebarPanelProps) {
|
||||||
|
const [, setErrors] = useState<ZodError | null>(null);
|
||||||
|
|
||||||
|
const updateData = (d: unknown) => {
|
||||||
|
const res = ButtonPropsSchema.safeParse(d);
|
||||||
|
if (res.success) {
|
||||||
|
setData(res.data);
|
||||||
|
setErrors(null);
|
||||||
|
} else {
|
||||||
|
setErrors(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const text = data.props?.text ?? ButtonPropsDefaults.text;
|
||||||
|
const url = data.props?.url ?? ButtonPropsDefaults.url;
|
||||||
|
const fullWidth = data.props?.fullWidth ?? ButtonPropsDefaults.fullWidth;
|
||||||
|
const size = data.props?.size ?? ButtonPropsDefaults.size;
|
||||||
|
const buttonStyle =
|
||||||
|
data.props?.buttonStyle ?? ButtonPropsDefaults.buttonStyle;
|
||||||
|
const buttonTextColor =
|
||||||
|
data.props?.buttonTextColor ?? ButtonPropsDefaults.buttonTextColor;
|
||||||
|
const buttonBackgroundColor =
|
||||||
|
data.props?.buttonBackgroundColor ??
|
||||||
|
ButtonPropsDefaults.buttonBackgroundColor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseSidebarPanel title="Button block">
|
||||||
|
<TextInput
|
||||||
|
label="Text"
|
||||||
|
defaultValue={text}
|
||||||
|
onChange={(text) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, text } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Url"
|
||||||
|
defaultValue={url}
|
||||||
|
onChange={(url) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, url } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<RadioGroupInput
|
||||||
|
label="Width"
|
||||||
|
defaultValue={fullWidth ? "FULL_WIDTH" : "AUTO"}
|
||||||
|
onChange={(v) =>
|
||||||
|
updateData({
|
||||||
|
...data,
|
||||||
|
props: { ...data.props, fullWidth: v === "FULL_WIDTH" }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ToggleButton value="FULL_WIDTH">Full</ToggleButton>
|
||||||
|
<ToggleButton value="AUTO">Auto</ToggleButton>
|
||||||
|
</RadioGroupInput>
|
||||||
|
<RadioGroupInput
|
||||||
|
label="Size"
|
||||||
|
defaultValue={size}
|
||||||
|
onChange={(size) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, size } })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ToggleButton value="x-small">Xs</ToggleButton>
|
||||||
|
<ToggleButton value="small">Sm</ToggleButton>
|
||||||
|
<ToggleButton value="medium">Md</ToggleButton>
|
||||||
|
<ToggleButton value="large">Lg</ToggleButton>
|
||||||
|
</RadioGroupInput>
|
||||||
|
<RadioGroupInput
|
||||||
|
label="Style"
|
||||||
|
defaultValue={buttonStyle}
|
||||||
|
onChange={(buttonStyle) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, buttonStyle } })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ToggleButton value="rectangle">Rectangle</ToggleButton>
|
||||||
|
<ToggleButton value="rounded">Rounded</ToggleButton>
|
||||||
|
<ToggleButton value="pill">Pill</ToggleButton>
|
||||||
|
</RadioGroupInput>
|
||||||
|
<ColorInput
|
||||||
|
label="Text color"
|
||||||
|
defaultValue={buttonTextColor}
|
||||||
|
onChange={(buttonTextColor) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, buttonTextColor } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
label="Button color"
|
||||||
|
defaultValue={buttonBackgroundColor}
|
||||||
|
onChange={(buttonBackgroundColor) =>
|
||||||
|
updateData({
|
||||||
|
...data,
|
||||||
|
props: { ...data.props, buttonBackgroundColor }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<MultiStylePropertyPanel
|
||||||
|
names={[
|
||||||
|
"backgroundColor",
|
||||||
|
"fontFamily",
|
||||||
|
"fontSize",
|
||||||
|
"fontWeight",
|
||||||
|
"textAlign",
|
||||||
|
"padding"
|
||||||
|
]}
|
||||||
|
value={data.style}
|
||||||
|
onChange={(style) => updateData({ ...data, style })}
|
||||||
|
/>
|
||||||
|
</BaseSidebarPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SpaceBarOutlined,
|
||||||
|
VerticalAlignBottomOutlined,
|
||||||
|
VerticalAlignCenterOutlined,
|
||||||
|
VerticalAlignTopOutlined
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import { ToggleButton } from "@mui/material";
|
||||||
|
|
||||||
|
import ColumnsContainerPropsSchema, {
|
||||||
|
ColumnsContainerProps
|
||||||
|
} from "../../../../documents/blocks/ColumnsContainer/ColumnsContainerPropsSchema";
|
||||||
|
|
||||||
|
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||||
|
import ColumnWidthsInput from "./helpers/inputs/ColumnWidthsInput";
|
||||||
|
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||||
|
import SliderInput from "./helpers/inputs/SliderInput";
|
||||||
|
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||||
|
|
||||||
|
type ColumnsContainerPanelProps = {
|
||||||
|
data: ColumnsContainerProps;
|
||||||
|
setData: (v: ColumnsContainerProps) => void;
|
||||||
|
};
|
||||||
|
export default function ColumnsContainerPanel({
|
||||||
|
data,
|
||||||
|
setData
|
||||||
|
}: ColumnsContainerPanelProps) {
|
||||||
|
const [, setErrors] = useState<ZodError | null>(null);
|
||||||
|
const updateData = (d: unknown) => {
|
||||||
|
const res = ColumnsContainerPropsSchema.safeParse(d);
|
||||||
|
if (res.success) {
|
||||||
|
setData(res.data);
|
||||||
|
setErrors(null);
|
||||||
|
} else {
|
||||||
|
setErrors(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseSidebarPanel title="Columns block">
|
||||||
|
<RadioGroupInput
|
||||||
|
label="Number of columns"
|
||||||
|
defaultValue={data.props?.columnsCount === 2 ? "2" : "3"}
|
||||||
|
onChange={(v) => {
|
||||||
|
updateData({
|
||||||
|
...data,
|
||||||
|
props: { ...data.props, columnsCount: v === "2" ? 2 : 3 }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="2">2</ToggleButton>
|
||||||
|
<ToggleButton value="3">3</ToggleButton>
|
||||||
|
</RadioGroupInput>
|
||||||
|
<ColumnWidthsInput
|
||||||
|
defaultValue={data.props?.fixedWidths}
|
||||||
|
onChange={(fixedWidths) => {
|
||||||
|
updateData({ ...data, props: { ...data.props, fixedWidths } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SliderInput
|
||||||
|
label="Columns gap"
|
||||||
|
iconLabel={<SpaceBarOutlined sx={{ color: "text.secondary" }} />}
|
||||||
|
units="px"
|
||||||
|
step={4}
|
||||||
|
marks
|
||||||
|
min={0}
|
||||||
|
max={80}
|
||||||
|
defaultValue={data.props?.columnsGap ?? 0}
|
||||||
|
onChange={(columnsGap) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, columnsGap } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<RadioGroupInput
|
||||||
|
label="Alignment"
|
||||||
|
defaultValue={data.props?.contentAlignment ?? "middle"}
|
||||||
|
onChange={(contentAlignment) => {
|
||||||
|
updateData({ ...data, props: { ...data.props, contentAlignment } });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="top">
|
||||||
|
<VerticalAlignTopOutlined fontSize="small" />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="middle">
|
||||||
|
<VerticalAlignCenterOutlined fontSize="small" />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="bottom">
|
||||||
|
<VerticalAlignBottomOutlined fontSize="small" />
|
||||||
|
</ToggleButton>
|
||||||
|
</RadioGroupInput>
|
||||||
|
|
||||||
|
<MultiStylePropertyPanel
|
||||||
|
names={["backgroundColor", "padding"]}
|
||||||
|
value={data.style}
|
||||||
|
onChange={(style) => updateData({ ...data, style })}
|
||||||
|
/>
|
||||||
|
</BaseSidebarPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import type { ZodError } from "zod";
|
||||||
|
|
||||||
|
import ContainerPropsSchema, {
|
||||||
|
ContainerProps
|
||||||
|
} from "../../../../documents/blocks/Container/ContainerPropsSchema";
|
||||||
|
|
||||||
|
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||||
|
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||||
|
|
||||||
|
type ContainerSidebarPanelProps = {
|
||||||
|
data: ContainerProps;
|
||||||
|
setData: (v: ContainerProps) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ContainerSidebarPanel({
|
||||||
|
data,
|
||||||
|
setData
|
||||||
|
}: ContainerSidebarPanelProps) {
|
||||||
|
const [, setErrors] = useState<ZodError | null>(null);
|
||||||
|
const updateData = (d: unknown) => {
|
||||||
|
const res = ContainerPropsSchema.safeParse(d);
|
||||||
|
if (res.success) {
|
||||||
|
setData(res.data);
|
||||||
|
setErrors(null);
|
||||||
|
} else {
|
||||||
|
setErrors(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<BaseSidebarPanel title="Container block">
|
||||||
|
<MultiStylePropertyPanel
|
||||||
|
names={["backgroundColor", "borderColor", "borderRadius", "padding"]}
|
||||||
|
value={data.style}
|
||||||
|
onChange={(style) => updateData({ ...data, style })}
|
||||||
|
/>
|
||||||
|
</BaseSidebarPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { HeightOutlined } from "@mui/icons-material";
|
||||||
|
import {
|
||||||
|
DividerProps,
|
||||||
|
DividerPropsDefaults,
|
||||||
|
DividerPropsSchema
|
||||||
|
} from "@usewaypoint/block-divider";
|
||||||
|
|
||||||
|
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||||
|
import ColorInput from "./helpers/inputs/ColorInput";
|
||||||
|
import SliderInput from "./helpers/inputs/SliderInput";
|
||||||
|
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||||
|
|
||||||
|
type DividerSidebarPanelProps = {
|
||||||
|
data: DividerProps;
|
||||||
|
setData: (v: DividerProps) => void;
|
||||||
|
};
|
||||||
|
export default function DividerSidebarPanel({
|
||||||
|
data,
|
||||||
|
setData
|
||||||
|
}: DividerSidebarPanelProps) {
|
||||||
|
const [, setErrors] = useState<ZodError | null>(null);
|
||||||
|
const updateData = (d: unknown) => {
|
||||||
|
const res = DividerPropsSchema.safeParse(d);
|
||||||
|
if (res.success) {
|
||||||
|
setData(res.data);
|
||||||
|
setErrors(null);
|
||||||
|
} else {
|
||||||
|
setErrors(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineColor = data.props?.lineColor ?? DividerPropsDefaults.lineColor;
|
||||||
|
const lineHeight = data.props?.lineHeight ?? DividerPropsDefaults.lineHeight;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseSidebarPanel title="Divider block">
|
||||||
|
<ColorInput
|
||||||
|
label="Color"
|
||||||
|
defaultValue={lineColor}
|
||||||
|
onChange={(lineColor) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, lineColor } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SliderInput
|
||||||
|
label="Height"
|
||||||
|
iconLabel={<HeightOutlined sx={{ color: "text.secondary" }} />}
|
||||||
|
units="px"
|
||||||
|
step={1}
|
||||||
|
min={1}
|
||||||
|
max={24}
|
||||||
|
defaultValue={lineHeight}
|
||||||
|
onChange={(lineHeight) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, lineHeight } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<MultiStylePropertyPanel
|
||||||
|
names={["backgroundColor", "padding"]}
|
||||||
|
value={data.style}
|
||||||
|
onChange={(style) => updateData({ ...data, style })}
|
||||||
|
/>
|
||||||
|
</BaseSidebarPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { WidthWide, RoundedCornerOutlined } from "@mui/icons-material";
|
||||||
|
import EmailLayoutPropsSchema, {
|
||||||
|
EmailLayoutProps
|
||||||
|
} from "../../../../documents/blocks/EmailLayout/EmailLayoutPropsSchema";
|
||||||
|
|
||||||
|
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||||
|
import ColorInput, { NullableColorInput } from "./helpers/inputs/ColorInput";
|
||||||
|
import { NullableFontFamily } from "./helpers/inputs/FontFamily";
|
||||||
|
import SliderInput from "./helpers/inputs/SliderInput";
|
||||||
|
|
||||||
|
type EmailLayoutSidebarFieldsProps = {
|
||||||
|
data: EmailLayoutProps;
|
||||||
|
setData: (v: EmailLayoutProps) => void;
|
||||||
|
};
|
||||||
|
export default function EmailLayoutSidebarFields({
|
||||||
|
data,
|
||||||
|
setData
|
||||||
|
}: EmailLayoutSidebarFieldsProps) {
|
||||||
|
const [, setErrors] = useState<ZodError | null>(null);
|
||||||
|
|
||||||
|
const updateData = (d: unknown) => {
|
||||||
|
const res = EmailLayoutPropsSchema.safeParse(d);
|
||||||
|
if (res.success) {
|
||||||
|
setData(res.data);
|
||||||
|
setErrors(null);
|
||||||
|
} else {
|
||||||
|
setErrors(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseSidebarPanel title="Global">
|
||||||
|
<ColorInput
|
||||||
|
label="Backdrop color"
|
||||||
|
defaultValue={data.backdropColor ?? "#F5F5F5"}
|
||||||
|
onChange={(backdropColor) => updateData({ ...data, backdropColor })}
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
label="Canvas color"
|
||||||
|
defaultValue={data.canvasColor ?? "#FFFFFF"}
|
||||||
|
onChange={(canvasColor) => updateData({ ...data, canvasColor })}
|
||||||
|
/>
|
||||||
|
<NullableColorInput
|
||||||
|
label="Canvas border color"
|
||||||
|
defaultValue={data.borderColor ?? null}
|
||||||
|
onChange={(borderColor) => updateData({ ...data, borderColor })}
|
||||||
|
/>
|
||||||
|
<SliderInput
|
||||||
|
iconLabel={<WidthWide />}
|
||||||
|
units="px"
|
||||||
|
step={10}
|
||||||
|
min={320}
|
||||||
|
max={1200}
|
||||||
|
label="Canvas width"
|
||||||
|
defaultValue={data.canvasWidth ?? 600}
|
||||||
|
onChange={(canvasWidth) => updateData({ ...data, canvasWidth })}
|
||||||
|
/>
|
||||||
|
<SliderInput
|
||||||
|
iconLabel={<RoundedCornerOutlined />}
|
||||||
|
units="px"
|
||||||
|
step={4}
|
||||||
|
marks
|
||||||
|
min={0}
|
||||||
|
max={48}
|
||||||
|
label="Canvas border radius"
|
||||||
|
defaultValue={data.borderRadius ?? 0}
|
||||||
|
onChange={(borderRadius) => updateData({ ...data, borderRadius })}
|
||||||
|
/>
|
||||||
|
<NullableFontFamily
|
||||||
|
label="Font family"
|
||||||
|
defaultValue="MODERN_SANS"
|
||||||
|
onChange={(fontFamily) => updateData({ ...data, fontFamily })}
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
label="Text color"
|
||||||
|
defaultValue={data.textColor ?? "#262626"}
|
||||||
|
onChange={(textColor) => updateData({ ...data, textColor })}
|
||||||
|
/>
|
||||||
|
</BaseSidebarPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { ToggleButton } from "@mui/material";
|
||||||
|
import {
|
||||||
|
HeadingProps,
|
||||||
|
HeadingPropsDefaults,
|
||||||
|
HeadingPropsSchema
|
||||||
|
} from "@usewaypoint/block-heading";
|
||||||
|
|
||||||
|
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||||
|
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||||
|
import TextInput from "./helpers/inputs/TextInput";
|
||||||
|
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||||
|
|
||||||
|
type HeadingSidebarPanelProps = {
|
||||||
|
data: HeadingProps;
|
||||||
|
setData: (v: HeadingProps) => void;
|
||||||
|
};
|
||||||
|
export default function HeadingSidebarPanel({
|
||||||
|
data,
|
||||||
|
setData
|
||||||
|
}: HeadingSidebarPanelProps) {
|
||||||
|
const [, setErrors] = useState<ZodError | null>(null);
|
||||||
|
|
||||||
|
const updateData = (d: unknown) => {
|
||||||
|
const res = HeadingPropsSchema.safeParse(d);
|
||||||
|
if (res.success) {
|
||||||
|
setData(res.data);
|
||||||
|
setErrors(null);
|
||||||
|
} else {
|
||||||
|
setErrors(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseSidebarPanel title="Heading block">
|
||||||
|
<TextInput
|
||||||
|
label="Content"
|
||||||
|
rows={3}
|
||||||
|
defaultValue={data.props?.text ?? HeadingPropsDefaults.text}
|
||||||
|
onChange={(text) => {
|
||||||
|
updateData({ ...data, props: { ...data.props, text } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<RadioGroupInput
|
||||||
|
label="Level"
|
||||||
|
defaultValue={data.props?.level ?? HeadingPropsDefaults.level}
|
||||||
|
onChange={(level) => {
|
||||||
|
updateData({ ...data, props: { ...data.props, level } });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="h1">H1</ToggleButton>
|
||||||
|
<ToggleButton value="h2">H2</ToggleButton>
|
||||||
|
<ToggleButton value="h3">H3</ToggleButton>
|
||||||
|
</RadioGroupInput>
|
||||||
|
<MultiStylePropertyPanel
|
||||||
|
names={[
|
||||||
|
"color",
|
||||||
|
"backgroundColor",
|
||||||
|
"fontFamily",
|
||||||
|
"fontWeight",
|
||||||
|
"textAlign",
|
||||||
|
"padding"
|
||||||
|
]}
|
||||||
|
value={data.style}
|
||||||
|
onChange={(style) => updateData({ ...data, style })}
|
||||||
|
/>
|
||||||
|
</BaseSidebarPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { HtmlProps, HtmlPropsSchema } from "@usewaypoint/block-html";
|
||||||
|
|
||||||
|
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||||
|
import TextInput from "./helpers/inputs/TextInput";
|
||||||
|
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||||
|
|
||||||
|
type HtmlSidebarPanelProps = {
|
||||||
|
data: HtmlProps;
|
||||||
|
setData: (v: HtmlProps) => void;
|
||||||
|
};
|
||||||
|
export default function HtmlSidebarPanel({
|
||||||
|
data,
|
||||||
|
setData
|
||||||
|
}: HtmlSidebarPanelProps) {
|
||||||
|
const [, setErrors] = useState<ZodError | null>(null);
|
||||||
|
|
||||||
|
const updateData = (d: unknown) => {
|
||||||
|
const res = HtmlPropsSchema.safeParse(d);
|
||||||
|
if (res.success) {
|
||||||
|
setData(res.data);
|
||||||
|
setErrors(null);
|
||||||
|
} else {
|
||||||
|
setErrors(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseSidebarPanel title="Html block">
|
||||||
|
<TextInput
|
||||||
|
label="Content"
|
||||||
|
rows={5}
|
||||||
|
defaultValue={data.props?.contents ?? ""}
|
||||||
|
onChange={(contents) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, contents } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<MultiStylePropertyPanel
|
||||||
|
names={[
|
||||||
|
"color",
|
||||||
|
"backgroundColor",
|
||||||
|
"fontFamily",
|
||||||
|
"fontSize",
|
||||||
|
"textAlign",
|
||||||
|
"padding"
|
||||||
|
]}
|
||||||
|
value={data.style}
|
||||||
|
onChange={(style) => updateData({ ...data, style })}
|
||||||
|
/>
|
||||||
|
</BaseSidebarPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
VerticalAlignBottomOutlined,
|
||||||
|
VerticalAlignCenterOutlined,
|
||||||
|
VerticalAlignTopOutlined
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import { Stack, ToggleButton } from "@mui/material";
|
||||||
|
import { ImageProps, ImagePropsSchema } from "@usewaypoint/block-image";
|
||||||
|
|
||||||
|
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||||
|
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||||
|
import TextDimensionInput from "./helpers/inputs/TextDimensionInput";
|
||||||
|
import TextInput from "./helpers/inputs/TextInput";
|
||||||
|
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||||
|
|
||||||
|
type ImageSidebarPanelProps = {
|
||||||
|
data: ImageProps;
|
||||||
|
setData: (v: ImageProps) => void;
|
||||||
|
};
|
||||||
|
export default function ImageSidebarPanel({
|
||||||
|
data,
|
||||||
|
setData
|
||||||
|
}: ImageSidebarPanelProps) {
|
||||||
|
const [, setErrors] = useState<ZodError | null>(null);
|
||||||
|
|
||||||
|
const updateData = (d: unknown) => {
|
||||||
|
const res = ImagePropsSchema.safeParse(d);
|
||||||
|
if (res.success) {
|
||||||
|
setData(res.data);
|
||||||
|
setErrors(null);
|
||||||
|
} else {
|
||||||
|
setErrors(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseSidebarPanel title="Image block">
|
||||||
|
<TextInput
|
||||||
|
label="Source URL"
|
||||||
|
defaultValue={data.props?.url ?? ""}
|
||||||
|
onChange={(v) => {
|
||||||
|
const url = v.trim().length === 0 ? null : v.trim();
|
||||||
|
updateData({ ...data, props: { ...data.props, url } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Alt text"
|
||||||
|
defaultValue={data.props?.alt ?? ""}
|
||||||
|
onChange={(alt) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, alt } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Click through URL"
|
||||||
|
defaultValue={data.props?.linkHref ?? ""}
|
||||||
|
onChange={(v) => {
|
||||||
|
const linkHref = v.trim().length === 0 ? null : v.trim();
|
||||||
|
updateData({ ...data, props: { ...data.props, linkHref } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<TextDimensionInput
|
||||||
|
label="Width"
|
||||||
|
defaultValue={data.props?.width}
|
||||||
|
onChange={(width) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, width } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextDimensionInput
|
||||||
|
label="Height"
|
||||||
|
defaultValue={data.props?.height}
|
||||||
|
onChange={(height) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, height } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<RadioGroupInput
|
||||||
|
label="Alignment"
|
||||||
|
defaultValue={data.props?.contentAlignment ?? "middle"}
|
||||||
|
onChange={(contentAlignment) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, contentAlignment } })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ToggleButton value="top">
|
||||||
|
<VerticalAlignTopOutlined fontSize="small" />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="middle">
|
||||||
|
<VerticalAlignCenterOutlined fontSize="small" />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="bottom">
|
||||||
|
<VerticalAlignBottomOutlined fontSize="small" />
|
||||||
|
</ToggleButton>
|
||||||
|
</RadioGroupInput>
|
||||||
|
|
||||||
|
<MultiStylePropertyPanel
|
||||||
|
names={["backgroundColor", "textAlign", "padding"]}
|
||||||
|
value={data.style}
|
||||||
|
onChange={(style) => updateData({ ...data, style })}
|
||||||
|
/>
|
||||||
|
</BaseSidebarPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { HeightOutlined } from "@mui/icons-material";
|
||||||
|
import {
|
||||||
|
SpacerProps,
|
||||||
|
SpacerPropsDefaults,
|
||||||
|
SpacerPropsSchema
|
||||||
|
} from "@usewaypoint/block-spacer";
|
||||||
|
|
||||||
|
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||||
|
import SliderInput from "./helpers/inputs/SliderInput";
|
||||||
|
|
||||||
|
type SpacerSidebarPanelProps = {
|
||||||
|
data: SpacerProps;
|
||||||
|
setData: (v: SpacerProps) => void;
|
||||||
|
};
|
||||||
|
export default function SpacerSidebarPanel({
|
||||||
|
data,
|
||||||
|
setData
|
||||||
|
}: SpacerSidebarPanelProps) {
|
||||||
|
const [, setErrors] = useState<ZodError | null>(null);
|
||||||
|
|
||||||
|
const updateData = (d: unknown) => {
|
||||||
|
const res = SpacerPropsSchema.safeParse(d);
|
||||||
|
if (res.success) {
|
||||||
|
setData(res.data);
|
||||||
|
setErrors(null);
|
||||||
|
} else {
|
||||||
|
setErrors(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseSidebarPanel title="Spacer block">
|
||||||
|
<SliderInput
|
||||||
|
label="Height"
|
||||||
|
iconLabel={<HeightOutlined sx={{ color: "text.secondary" }} />}
|
||||||
|
units="px"
|
||||||
|
step={4}
|
||||||
|
min={4}
|
||||||
|
max={128}
|
||||||
|
defaultValue={data.props?.height ?? SpacerPropsDefaults.height}
|
||||||
|
onChange={(height) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, height } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</BaseSidebarPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
import { TextProps, TextPropsSchema } from "@usewaypoint/block-text";
|
||||||
|
|
||||||
|
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||||
|
import BooleanInput from "./helpers/inputs/BooleanInput";
|
||||||
|
import TextInput from "./helpers/inputs/TextInput";
|
||||||
|
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||||
|
|
||||||
|
type TextSidebarPanelProps = {
|
||||||
|
data: TextProps;
|
||||||
|
setData: (v: TextProps) => void;
|
||||||
|
};
|
||||||
|
export default function TextSidebarPanel({
|
||||||
|
data,
|
||||||
|
setData
|
||||||
|
}: TextSidebarPanelProps) {
|
||||||
|
const [, setErrors] = useState<ZodError | null>(null);
|
||||||
|
|
||||||
|
const updateData = (d: unknown) => {
|
||||||
|
const res = TextPropsSchema.safeParse(d);
|
||||||
|
if (res.success) {
|
||||||
|
setData(res.data);
|
||||||
|
setErrors(null);
|
||||||
|
} else {
|
||||||
|
setErrors(res.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseSidebarPanel title="Text block">
|
||||||
|
<TextInput
|
||||||
|
label="Content"
|
||||||
|
rows={5}
|
||||||
|
defaultValue={data.props?.text ?? ""}
|
||||||
|
onChange={(text) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, text } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<BooleanInput
|
||||||
|
label="Markdown (GitHub flavored)"
|
||||||
|
defaultValue={data.props?.markdown ?? false}
|
||||||
|
onChange={(markdown) =>
|
||||||
|
updateData({ ...data, props: { ...data.props, markdown } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MultiStylePropertyPanel
|
||||||
|
names={[
|
||||||
|
"color",
|
||||||
|
"backgroundColor",
|
||||||
|
"fontFamily",
|
||||||
|
"fontSize",
|
||||||
|
"fontWeight",
|
||||||
|
"textAlign",
|
||||||
|
"padding"
|
||||||
|
]}
|
||||||
|
value={data.style}
|
||||||
|
onChange={(style) => updateData({ ...data, style })}
|
||||||
|
/>
|
||||||
|
</BaseSidebarPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Box, Stack, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
type SidebarPanelProps = {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
export default function BaseSidebarPanel({
|
||||||
|
title,
|
||||||
|
children
|
||||||
|
}: SidebarPanelProps) {
|
||||||
|
return (
|
||||||
|
<Box p={2}>
|
||||||
|
<Typography
|
||||||
|
variant="overline"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ display: "block", mb: 2 }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={5} mb={3}>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { FormControlLabel, Switch } from '@mui/material';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string;
|
||||||
|
defaultValue: boolean;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BooleanInput({ label, defaultValue, onChange }: Props) {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
label={label}
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={value}
|
||||||
|
onChange={(_, checked: boolean) => {
|
||||||
|
setValue(checked);
|
||||||
|
onChange(checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { AddOutlined, CloseOutlined } from "@mui/icons-material";
|
||||||
|
import { ButtonBase, InputLabel, Menu, Stack } from "@mui/material";
|
||||||
|
|
||||||
|
import Picker from "./Picker";
|
||||||
|
|
||||||
|
const BUTTON_SX = {
|
||||||
|
border: "1px solid",
|
||||||
|
borderColor: "cadet.400",
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: "4px",
|
||||||
|
bgcolor: "#FFFFFF"
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props =
|
||||||
|
| {
|
||||||
|
nullable: true;
|
||||||
|
label: string;
|
||||||
|
onChange: (value: string | null) => void;
|
||||||
|
defaultValue: string | null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
nullable: false;
|
||||||
|
label: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
defaultValue: string;
|
||||||
|
};
|
||||||
|
export default function ColorInput({
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
nullable
|
||||||
|
}: Props) {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
const handleClickOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderResetButton = () => {
|
||||||
|
if (!nullable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof value !== "string" || value.trim().length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ButtonBase
|
||||||
|
onClick={() => {
|
||||||
|
setValue(null);
|
||||||
|
onChange(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseOutlined fontSize="small" sx={{ color: "grey.600" }} />
|
||||||
|
</ButtonBase>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderOpenButton = () => {
|
||||||
|
if (value) {
|
||||||
|
return (
|
||||||
|
<ButtonBase
|
||||||
|
onClick={handleClickOpen}
|
||||||
|
sx={{ ...BUTTON_SX, bgcolor: value }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ButtonBase onClick={handleClickOpen} sx={{ ...BUTTON_SX }}>
|
||||||
|
<AddOutlined fontSize="small" />
|
||||||
|
</ButtonBase>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack alignItems="flex-start">
|
||||||
|
<InputLabel sx={{ mb: 0.5 }}>{label}</InputLabel>
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
{renderOpenButton()}
|
||||||
|
{renderResetButton()}
|
||||||
|
</Stack>
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={() => setAnchorEl(null)}
|
||||||
|
MenuListProps={{
|
||||||
|
sx: { height: "auto", padding: 0 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Picker
|
||||||
|
value={value || ""}
|
||||||
|
onChange={(v) => {
|
||||||
|
setValue(v);
|
||||||
|
onChange(v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||||
|
|
||||||
|
import { Box, Stack, SxProps } from "@mui/material";
|
||||||
|
|
||||||
|
import Swatch from "./Swatch";
|
||||||
|
|
||||||
|
const DEFAULT_PRESET_COLORS = [
|
||||||
|
"#E11D48",
|
||||||
|
"#DB2777",
|
||||||
|
"#C026D3",
|
||||||
|
"#9333EA",
|
||||||
|
"#7C3AED",
|
||||||
|
"#4F46E5",
|
||||||
|
"#2563EB",
|
||||||
|
"#0284C7",
|
||||||
|
"#0891B2",
|
||||||
|
"#0D9488",
|
||||||
|
"#059669",
|
||||||
|
"#16A34A",
|
||||||
|
"#65A30D",
|
||||||
|
"#CA8A04",
|
||||||
|
"#D97706",
|
||||||
|
"#EA580C",
|
||||||
|
"#DC2626",
|
||||||
|
"#FFFFFF",
|
||||||
|
"#FAFAFA",
|
||||||
|
"#F5F5F5",
|
||||||
|
"#E5E5E5",
|
||||||
|
"#D4D4D4",
|
||||||
|
"#A3A3A3",
|
||||||
|
"#737373",
|
||||||
|
"#525252",
|
||||||
|
"#404040",
|
||||||
|
"#262626",
|
||||||
|
"#171717",
|
||||||
|
"#0A0A0A",
|
||||||
|
"#000000"
|
||||||
|
];
|
||||||
|
|
||||||
|
const SX: SxProps = {
|
||||||
|
p: 1,
|
||||||
|
".react-colorful__pointer ": {
|
||||||
|
width: 16,
|
||||||
|
height: 16
|
||||||
|
},
|
||||||
|
".react-colorful__saturation": {
|
||||||
|
mb: 1,
|
||||||
|
borderRadius: "4px"
|
||||||
|
},
|
||||||
|
".react-colorful__last-control": {
|
||||||
|
borderRadius: "4px"
|
||||||
|
},
|
||||||
|
".react-colorful__hue-pointer": {
|
||||||
|
width: "4px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
height: 24,
|
||||||
|
cursor: "col-resize"
|
||||||
|
},
|
||||||
|
".react-colorful__saturation-pointer": {
|
||||||
|
cursor: "all-scroll"
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
padding: 1,
|
||||||
|
border: "1px solid",
|
||||||
|
borderColor: "grey.300",
|
||||||
|
borderRadius: "4px",
|
||||||
|
width: "100%"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
};
|
||||||
|
export default function Picker({ value, onChange }: Props) {
|
||||||
|
const [internalValue, setInternalValue] = useState(value);
|
||||||
|
const handleChange = (v: string) => {
|
||||||
|
setInternalValue(v);
|
||||||
|
if (/^#[0-9a-fA-F]{6}$/.test(v)) {
|
||||||
|
onChange(v);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={1} sx={SX}>
|
||||||
|
<HexColorPicker color={value} onChange={handleChange} />
|
||||||
|
<Swatch
|
||||||
|
paletteColors={DEFAULT_PRESET_COLORS}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<Box pt={1}>
|
||||||
|
<HexColorInput prefixed color={internalValue} onChange={handleChange} />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Box, Button, SxProps } from "@mui/material";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
paletteColors: string[];
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TILE_BUTTON: SxProps = {
|
||||||
|
width: 24,
|
||||||
|
height: 24
|
||||||
|
};
|
||||||
|
export default function Swatch({ paletteColors, value, onChange }: Props) {
|
||||||
|
const renderButton = (colorValue: string) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={colorValue}
|
||||||
|
onClick={() => onChange(colorValue)}
|
||||||
|
sx={{
|
||||||
|
...TILE_BUTTON,
|
||||||
|
backgroundColor: colorValue,
|
||||||
|
border: "1px solid",
|
||||||
|
borderColor: value === colorValue ? "black" : "grey.200",
|
||||||
|
minWidth: 24,
|
||||||
|
display: "inline-flex",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: colorValue,
|
||||||
|
borderColor: "grey.500"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gap: 1,
|
||||||
|
gridTemplateColumns: "1fr 1fr 1fr 1fr 1fr 1fr"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{paletteColors.map((c) => renderButton(c))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import BaseColorInput from "./BaseColorInput";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
defaultValue: string;
|
||||||
|
};
|
||||||
|
export default function ColorInput(props: Props) {
|
||||||
|
return <BaseColorInput {...props} nullable={false} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullableProps = {
|
||||||
|
label: string;
|
||||||
|
onChange: (value: null | string) => void;
|
||||||
|
defaultValue: null | string;
|
||||||
|
};
|
||||||
|
export function NullableColorInput(props: NullableProps) {
|
||||||
|
return <BaseColorInput {...props} nullable />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { Stack } from '@mui/material';
|
||||||
|
|
||||||
|
import TextDimensionInput from './TextDimensionInput';
|
||||||
|
|
||||||
|
export const DEFAULT_2_COLUMNS = [6] as [number];
|
||||||
|
export const DEFAULT_3_COLUMNS = [4, 8] as [number, number];
|
||||||
|
|
||||||
|
type TWidthValue = number | null | undefined;
|
||||||
|
type FixedWidths = [
|
||||||
|
//
|
||||||
|
number | null | undefined,
|
||||||
|
number | null | undefined,
|
||||||
|
number | null | undefined,
|
||||||
|
];
|
||||||
|
type ColumnsLayoutInputProps = {
|
||||||
|
defaultValue: FixedWidths | null | undefined;
|
||||||
|
onChange: (v: FixedWidths | null | undefined) => void;
|
||||||
|
};
|
||||||
|
export default function ColumnWidthsInput({ defaultValue, onChange }: ColumnsLayoutInputProps) {
|
||||||
|
const [currentValue, setCurrentValue] = useState<[TWidthValue, TWidthValue, TWidthValue]>(() => {
|
||||||
|
if (defaultValue) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return [null, null, null];
|
||||||
|
});
|
||||||
|
|
||||||
|
const setIndexValue = (index: 0 | 1 | 2, value: number | null | undefined) => {
|
||||||
|
const nValue: FixedWidths = [...currentValue];
|
||||||
|
nValue[index] = value;
|
||||||
|
setCurrentValue(nValue);
|
||||||
|
onChange(nValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnsCountValue = 3;
|
||||||
|
let column3 = null;
|
||||||
|
if (columnsCountValue === 3) {
|
||||||
|
column3 = (
|
||||||
|
<TextDimensionInput
|
||||||
|
label="Column 3"
|
||||||
|
defaultValue={currentValue?.[2]}
|
||||||
|
onChange={(v) => {
|
||||||
|
setIndexValue(2, v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
<TextDimensionInput
|
||||||
|
label="Column 1"
|
||||||
|
defaultValue={currentValue?.[0]}
|
||||||
|
onChange={(v) => {
|
||||||
|
setIndexValue(0, v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextDimensionInput
|
||||||
|
label="Column 2"
|
||||||
|
defaultValue={currentValue?.[1]}
|
||||||
|
onChange={(v) => {
|
||||||
|
setIndexValue(1, v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{column3}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { MenuItem, TextField } from '@mui/material';
|
||||||
|
|
||||||
|
import { FONT_FAMILIES } from '../../../../../../documents/blocks/helpers/fontFamily';
|
||||||
|
|
||||||
|
const OPTIONS = FONT_FAMILIES.map((option) => (
|
||||||
|
<MenuItem key={option.key} value={option.key} sx={{ fontFamily: option.value }}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
|
||||||
|
type NullableProps = {
|
||||||
|
label: string;
|
||||||
|
onChange: (value: null | string) => void;
|
||||||
|
defaultValue: null | string;
|
||||||
|
};
|
||||||
|
export function NullableFontFamily({ label, onChange, defaultValue }: NullableProps) {
|
||||||
|
const [value, setValue] = useState(defaultValue ?? 'inherit');
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
variant="standard"
|
||||||
|
label={label}
|
||||||
|
value={value}
|
||||||
|
onChange={(ev) => {
|
||||||
|
const v = ev.target.value;
|
||||||
|
setValue(v);
|
||||||
|
onChange(v === null ? null : v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="inherit">Match email settings</MenuItem>
|
||||||
|
{OPTIONS}
|
||||||
|
</TextField>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { TextFieldsOutlined } from '@mui/icons-material';
|
||||||
|
import { InputLabel, Stack } from '@mui/material';
|
||||||
|
|
||||||
|
import RawSliderInput from './raw/RawSliderInput';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string;
|
||||||
|
defaultValue: number;
|
||||||
|
onChange: (v: number) => void;
|
||||||
|
};
|
||||||
|
export default function FontSizeInput({ label, defaultValue, onChange }: Props) {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
const handleChange = (value: number) => {
|
||||||
|
setValue(value);
|
||||||
|
onChange(value);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Stack spacing={1} alignItems="flex-start">
|
||||||
|
<InputLabel shrink>{label}</InputLabel>
|
||||||
|
<RawSliderInput
|
||||||
|
iconLabel={<TextFieldsOutlined sx={{ fontSize: 16 }} />}
|
||||||
|
value={value}
|
||||||
|
setValue={handleChange}
|
||||||
|
units="px"
|
||||||
|
step={1}
|
||||||
|
min={10}
|
||||||
|
max={48}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { ToggleButton } from '@mui/material';
|
||||||
|
|
||||||
|
import RadioGroupInput from './RadioGroupInput';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string;
|
||||||
|
defaultValue: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
export default function FontWeightInput({ label, defaultValue, onChange }: Props) {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
return (
|
||||||
|
<RadioGroupInput
|
||||||
|
label={label}
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={(fontWeight) => {
|
||||||
|
setValue(fontWeight);
|
||||||
|
onChange(fontWeight);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="normal">Regular</ToggleButton>
|
||||||
|
<ToggleButton value="bold">Bold</ToggleButton>
|
||||||
|
</RadioGroupInput>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlignHorizontalLeftOutlined,
|
||||||
|
AlignHorizontalRightOutlined,
|
||||||
|
AlignVerticalBottomOutlined,
|
||||||
|
AlignVerticalTopOutlined,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import { InputLabel, Stack } from '@mui/material';
|
||||||
|
|
||||||
|
import RawSliderInput from './raw/RawSliderInput';
|
||||||
|
|
||||||
|
type TPaddingValue = {
|
||||||
|
top: number;
|
||||||
|
bottom: number;
|
||||||
|
right: number;
|
||||||
|
left: number;
|
||||||
|
};
|
||||||
|
type Props = {
|
||||||
|
label: string;
|
||||||
|
defaultValue: TPaddingValue | null;
|
||||||
|
onChange: (value: TPaddingValue) => void;
|
||||||
|
};
|
||||||
|
export default function PaddingInput({ label, defaultValue, onChange }: Props) {
|
||||||
|
const [value, setValue] = useState(() => {
|
||||||
|
if (defaultValue) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleChange(internalName: keyof TPaddingValue, nValue: number) {
|
||||||
|
const v = {
|
||||||
|
...value,
|
||||||
|
[internalName]: nValue,
|
||||||
|
};
|
||||||
|
setValue(v);
|
||||||
|
onChange(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={2} alignItems="flex-start" pb={1}>
|
||||||
|
<InputLabel shrink>{label}</InputLabel>
|
||||||
|
|
||||||
|
<RawSliderInput
|
||||||
|
iconLabel={<AlignVerticalTopOutlined sx={{ fontSize: 16 }} />}
|
||||||
|
value={value.top}
|
||||||
|
setValue={(num) => handleChange('top', num)}
|
||||||
|
units="px"
|
||||||
|
step={4}
|
||||||
|
min={0}
|
||||||
|
max={80}
|
||||||
|
marks
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RawSliderInput
|
||||||
|
iconLabel={<AlignVerticalBottomOutlined sx={{ fontSize: 16 }} />}
|
||||||
|
value={value.bottom}
|
||||||
|
setValue={(num) => handleChange('bottom', num)}
|
||||||
|
units="px"
|
||||||
|
step={4}
|
||||||
|
min={0}
|
||||||
|
max={80}
|
||||||
|
marks
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RawSliderInput
|
||||||
|
iconLabel={<AlignHorizontalLeftOutlined sx={{ fontSize: 16 }} />}
|
||||||
|
value={value.left}
|
||||||
|
setValue={(num) => handleChange('left', num)}
|
||||||
|
units="px"
|
||||||
|
step={4}
|
||||||
|
min={0}
|
||||||
|
max={80}
|
||||||
|
marks
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RawSliderInput
|
||||||
|
iconLabel={<AlignHorizontalRightOutlined sx={{ fontSize: 16 }} />}
|
||||||
|
value={value.right}
|
||||||
|
setValue={(num) => handleChange('right', num)}
|
||||||
|
units="px"
|
||||||
|
step={4}
|
||||||
|
min={0}
|
||||||
|
max={80}
|
||||||
|
marks
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { InputLabel, Stack, ToggleButtonGroup } from '@mui/material';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string | JSX.Element;
|
||||||
|
children: JSX.Element | JSX.Element[];
|
||||||
|
defaultValue: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
};
|
||||||
|
export default function RadioGroupInput({ label, children, defaultValue, onChange }: Props) {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
return (
|
||||||
|
<Stack alignItems="flex-start">
|
||||||
|
<InputLabel shrink>{label}</InputLabel>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
exclusive
|
||||||
|
fullWidth
|
||||||
|
value={value}
|
||||||
|
size="small"
|
||||||
|
onChange={(_, v: unknown) => {
|
||||||
|
if (typeof v !== 'string') {
|
||||||
|
throw new Error('RadioGroupInput can only receive string values');
|
||||||
|
}
|
||||||
|
setValue(v);
|
||||||
|
onChange(v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { InputLabel, Stack } from '@mui/material';
|
||||||
|
|
||||||
|
import RawSliderInput from './raw/RawSliderInput';
|
||||||
|
|
||||||
|
type SliderInputProps = {
|
||||||
|
label: string;
|
||||||
|
iconLabel: JSX.Element;
|
||||||
|
|
||||||
|
step?: number;
|
||||||
|
marks?: boolean;
|
||||||
|
units: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
|
||||||
|
defaultValue: number;
|
||||||
|
onChange: (v: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SliderInput({ label, defaultValue, onChange, ...props }: SliderInputProps) {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
return (
|
||||||
|
<Stack spacing={1} alignItems="flex-start">
|
||||||
|
<InputLabel shrink>{label}</InputLabel>
|
||||||
|
<RawSliderInput
|
||||||
|
value={value}
|
||||||
|
setValue={(value: number) => {
|
||||||
|
setValue(value);
|
||||||
|
onChange(value);
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { FormatAlignCenterOutlined, FormatAlignLeftOutlined, FormatAlignRightOutlined } from '@mui/icons-material';
|
||||||
|
import { ToggleButton } from '@mui/material';
|
||||||
|
|
||||||
|
import RadioGroupInput from './RadioGroupInput';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string;
|
||||||
|
defaultValue: string | null;
|
||||||
|
onChange: (value: string | null) => void;
|
||||||
|
};
|
||||||
|
export default function TextAlignInput({ label, defaultValue, onChange }: Props) {
|
||||||
|
const [value, setValue] = useState(defaultValue ?? 'left');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioGroupInput
|
||||||
|
label={label}
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={(value) => {
|
||||||
|
setValue(value);
|
||||||
|
onChange(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="left">
|
||||||
|
<FormatAlignLeftOutlined fontSize="small" />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="center">
|
||||||
|
<FormatAlignCenterOutlined fontSize="small" />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="right">
|
||||||
|
<FormatAlignRightOutlined fontSize="small" />
|
||||||
|
</ToggleButton>
|
||||||
|
</RadioGroupInput>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { TextField, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
type TextDimensionInputProps = {
|
||||||
|
label: string;
|
||||||
|
defaultValue: number | null | undefined;
|
||||||
|
onChange: (v: number | null) => void;
|
||||||
|
};
|
||||||
|
export default function TextDimensionInput({ label, defaultValue, onChange }: TextDimensionInputProps) {
|
||||||
|
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||||
|
const value = parseInt(ev.target.value);
|
||||||
|
onChange(isNaN(value) ? null : value);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
onChange={handleChange}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
label={label}
|
||||||
|
variant="standard"
|
||||||
|
placeholder="auto"
|
||||||
|
size="small"
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
px
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { InputProps, TextField } from '@mui/material';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string;
|
||||||
|
rows?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
helperText?: string | JSX.Element;
|
||||||
|
InputProps?: InputProps;
|
||||||
|
defaultValue: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
};
|
||||||
|
export default function TextInput({ helperText, label, placeholder, rows, InputProps, defaultValue, onChange }: Props) {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
const isMultiline = typeof rows === 'number' && rows > 1;
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline={isMultiline}
|
||||||
|
minRows={rows}
|
||||||
|
variant={isMultiline ? 'outlined' : 'standard'}
|
||||||
|
label={label}
|
||||||
|
placeholder={placeholder}
|
||||||
|
helperText={helperText}
|
||||||
|
InputProps={InputProps}
|
||||||
|
value={value}
|
||||||
|
onChange={(ev) => {
|
||||||
|
const v = ev.target.value;
|
||||||
|
setValue(v);
|
||||||
|
onChange(v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Box, Slider, Stack, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
type SliderInputProps = {
|
||||||
|
iconLabel: JSX.Element;
|
||||||
|
|
||||||
|
step?: number;
|
||||||
|
marks?: boolean;
|
||||||
|
units: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
|
||||||
|
value: number;
|
||||||
|
setValue: (v: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RawSliderInput({
|
||||||
|
iconLabel,
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
units,
|
||||||
|
...props
|
||||||
|
}: SliderInputProps) {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={2}
|
||||||
|
justifyContent="space-between"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<Box sx={{ minWidth: 24, lineHeight: 1, flexShrink: 0 }}>{iconLabel}</Box>
|
||||||
|
<Slider
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
onChange={(_, value: unknown) => {
|
||||||
|
if (typeof value !== "number") {
|
||||||
|
throw new Error(
|
||||||
|
"RawSliderInput values can only receive numeric values"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setValue(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box sx={{ minWidth: 32, textAlign: "right", flexShrink: 0 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ lineHeight: 1 }}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
{units}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { TStyle } from "../../../../../../documents/blocks/helpers/TStyle";
|
||||||
|
|
||||||
|
import SingleStylePropertyPanel from "./SingleStylePropertyPanel";
|
||||||
|
|
||||||
|
type MultiStylePropertyPanelProps = {
|
||||||
|
names: (keyof TStyle)[];
|
||||||
|
value: TStyle | undefined | null;
|
||||||
|
onChange: (style: TStyle) => void;
|
||||||
|
};
|
||||||
|
export default function MultiStylePropertyPanel({
|
||||||
|
names,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
}: MultiStylePropertyPanelProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{names.map((name) => (
|
||||||
|
<SingleStylePropertyPanel
|
||||||
|
key={name}
|
||||||
|
name={name}
|
||||||
|
value={value || {}}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { RoundedCornerOutlined } from "@mui/icons-material";
|
||||||
|
|
||||||
|
import { TStyle } from "../../../../../../documents/blocks/helpers/TStyle";
|
||||||
|
import { NullableColorInput } from "../inputs/ColorInput";
|
||||||
|
import { NullableFontFamily } from "../inputs/FontFamily";
|
||||||
|
import FontSizeInput from "../inputs/FontSizeInput";
|
||||||
|
import FontWeightInput from "../inputs/FontWeightInput";
|
||||||
|
import PaddingInput from "../inputs/PaddingInput";
|
||||||
|
import SliderInput from "../inputs/SliderInput";
|
||||||
|
import TextAlignInput from "../inputs/TextAlignInput";
|
||||||
|
|
||||||
|
type StylePropertyPanelProps = {
|
||||||
|
name: keyof TStyle;
|
||||||
|
value: TStyle;
|
||||||
|
onChange: (style: TStyle) => void;
|
||||||
|
};
|
||||||
|
export default function SingleStylePropertyPanel({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
}: StylePropertyPanelProps) {
|
||||||
|
const defaultValue = value[name] ?? null;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const handleChange = (v: any) => {
|
||||||
|
onChange({ ...value, [name]: v });
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case "backgroundColor":
|
||||||
|
return (
|
||||||
|
<NullableColorInput
|
||||||
|
label="Background color"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "borderColor":
|
||||||
|
return (
|
||||||
|
<NullableColorInput
|
||||||
|
label="Border color"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "borderRadius":
|
||||||
|
return (
|
||||||
|
<SliderInput
|
||||||
|
iconLabel={<RoundedCornerOutlined />}
|
||||||
|
units="px"
|
||||||
|
step={4}
|
||||||
|
marks
|
||||||
|
min={0}
|
||||||
|
max={48}
|
||||||
|
label="Border radius"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "color":
|
||||||
|
return (
|
||||||
|
<NullableColorInput
|
||||||
|
label="Text color"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "fontFamily":
|
||||||
|
return (
|
||||||
|
<NullableFontFamily
|
||||||
|
label="Font family"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "fontSize":
|
||||||
|
return (
|
||||||
|
<FontSizeInput
|
||||||
|
label="Font size"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "fontWeight":
|
||||||
|
return (
|
||||||
|
<FontWeightInput
|
||||||
|
label="Font weight"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "textAlign":
|
||||||
|
return (
|
||||||
|
<TextAlignInput
|
||||||
|
label="Alignment"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "padding":
|
||||||
|
return (
|
||||||
|
<PaddingInput
|
||||||
|
label="Padding"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { setDocument, useDocument } from "../../documents/editor/EditorContext";
|
||||||
|
|
||||||
|
import EmailLayoutSidebarPanel from "./ConfigurationPanel/input-panels/EmailLayoutSidebarPanel";
|
||||||
|
|
||||||
|
export default function StylesPanel() {
|
||||||
|
const block = useDocument().root;
|
||||||
|
if (!block) {
|
||||||
|
return <p>Block not found</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, type } = block;
|
||||||
|
if (type !== "EmailLayout") {
|
||||||
|
throw new Error('Expected "root" element to be of type EmailLayout');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmailLayoutSidebarPanel
|
||||||
|
key="root"
|
||||||
|
data={data}
|
||||||
|
setData={(data) => setDocument({ root: { type, data } })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { AppRegistrationOutlined, LastPageOutlined } from "@mui/icons-material";
|
||||||
|
import { IconButton } from "@mui/material";
|
||||||
|
|
||||||
|
import {
|
||||||
|
toggleInspectorDrawerOpen,
|
||||||
|
useInspectorDrawerOpen
|
||||||
|
} from "../../documents/editor/EditorContext";
|
||||||
|
|
||||||
|
export default function ToggleInspectorPanelButton() {
|
||||||
|
const inspectorDrawerOpen = useInspectorDrawerOpen();
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
toggleInspectorDrawerOpen();
|
||||||
|
};
|
||||||
|
if (inspectorDrawerOpen) {
|
||||||
|
return (
|
||||||
|
<IconButton onClick={handleClick}>
|
||||||
|
<LastPageOutlined fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<IconButton onClick={handleClick}>
|
||||||
|
<AppRegistrationOutlined fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Box, Drawer, Tab, Tabs } from "@mui/material";
|
||||||
|
|
||||||
|
import {
|
||||||
|
setSidebarTab,
|
||||||
|
useInspectorDrawerOpen,
|
||||||
|
useSelectedSidebarTab
|
||||||
|
} from "../../documents/editor/EditorContext";
|
||||||
|
|
||||||
|
import ConfigurationPanel from "./ConfigurationPanel";
|
||||||
|
import StylesPanel from "./StylesPanel";
|
||||||
|
|
||||||
|
export const INSPECTOR_DRAWER_WIDTH = 320;
|
||||||
|
|
||||||
|
export default function InspectorDrawer() {
|
||||||
|
const selectedSidebarTab = useSelectedSidebarTab();
|
||||||
|
const inspectorDrawerOpen = useInspectorDrawerOpen();
|
||||||
|
|
||||||
|
const renderCurrentSidebarPanel = () => {
|
||||||
|
switch (selectedSidebarTab) {
|
||||||
|
case "block-configuration":
|
||||||
|
return <ConfigurationPanel />;
|
||||||
|
case "styles":
|
||||||
|
return <StylesPanel />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
variant="persistent"
|
||||||
|
anchor="right"
|
||||||
|
open={inspectorDrawerOpen}
|
||||||
|
sx={{
|
||||||
|
width: inspectorDrawerOpen ? INSPECTOR_DRAWER_WIDTH : 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: INSPECTOR_DRAWER_WIDTH,
|
||||||
|
height: 49,
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: "divider"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box px={2}>
|
||||||
|
<Tabs
|
||||||
|
value={selectedSidebarTab}
|
||||||
|
onChange={(_, v) => setSidebarTab(v)}
|
||||||
|
>
|
||||||
|
<Tab value="styles" label="Styles" />
|
||||||
|
<Tab value="block-configuration" label="Inspect" />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: INSPECTOR_DRAWER_WIDTH,
|
||||||
|
height: "calc(100% - 49px)",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderCurrentSidebarPanel()}
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Button } from "@mui/material";
|
||||||
|
|
||||||
|
import { resetDocument } from "../../documents/editor/EditorContext";
|
||||||
|
import getConfiguration from "../../getConfiguration";
|
||||||
|
|
||||||
|
export default function SidebarButton({
|
||||||
|
href,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
href: string;
|
||||||
|
children: JSX.Element | string;
|
||||||
|
}) {
|
||||||
|
const handleClick = () => {
|
||||||
|
resetDocument(getConfiguration(href));
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Button size="small" href={href} onClick={handleClick}>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { FirstPageOutlined, MenuOutlined } from "@mui/icons-material";
|
||||||
|
import { IconButton } from "@mui/material";
|
||||||
|
|
||||||
|
import {
|
||||||
|
toggleSamplesDrawerOpen,
|
||||||
|
useSamplesDrawerOpen
|
||||||
|
} from "../../documents/editor/EditorContext";
|
||||||
|
|
||||||
|
function useIcon() {
|
||||||
|
const samplesDrawerOpen = useSamplesDrawerOpen();
|
||||||
|
if (samplesDrawerOpen) {
|
||||||
|
return <FirstPageOutlined fontSize="small" />;
|
||||||
|
}
|
||||||
|
return <MenuOutlined fontSize="small" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToggleSamplesPanelButton() {
|
||||||
|
const icon = useIcon();
|
||||||
|
return <IconButton onClick={toggleSamplesDrawerOpen}>{icon}</IconButton>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Divider, Drawer, Stack, Typography } from "@mui/material";
|
||||||
|
import { useSamplesDrawerOpen } from "../../documents/editor/EditorContext";
|
||||||
|
import SidebarButton from "./SidebarButton";
|
||||||
|
|
||||||
|
export const SAMPLES_DRAWER_WIDTH = 240;
|
||||||
|
|
||||||
|
export default function SamplesDrawer() {
|
||||||
|
const samplesDrawerOpen = useSamplesDrawerOpen();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
variant="persistent"
|
||||||
|
anchor="left"
|
||||||
|
open={samplesDrawerOpen}
|
||||||
|
sx={{ width: samplesDrawerOpen ? SAMPLES_DRAWER_WIDTH : 0 }}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
spacing={3}
|
||||||
|
py={1}
|
||||||
|
px={2}
|
||||||
|
width={SAMPLES_DRAWER_WIDTH}
|
||||||
|
justifyContent="space-between"
|
||||||
|
height="100%"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
spacing={2}
|
||||||
|
sx={{
|
||||||
|
"& .MuiButtonBase-root": {
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "flex-start"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" component="h1" sx={{ p: 0.75 }}>
|
||||||
|
EmailBuilder
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack alignItems="flex-start">
|
||||||
|
<SidebarButton href="#">Empty</SidebarButton>
|
||||||
|
<SidebarButton href="#sample/requestemail">
|
||||||
|
Request Email
|
||||||
|
</SidebarButton>
|
||||||
|
<SidebarButton href="#sample/completionemail">
|
||||||
|
Completion Email
|
||||||
|
</SidebarButton>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
|
import { FileDownloadOutlined } from "@mui/icons-material";
|
||||||
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
|
|
||||||
|
import { useDocument } from "../../../documents/editor/EditorContext";
|
||||||
|
// import { renderToStaticMarkup } from "@usewaypoint/email-builder";
|
||||||
|
import renderEmailHtml from "../helper/renderEmailHtml";
|
||||||
|
|
||||||
|
export default function DownloadJson() {
|
||||||
|
const doc = useDocument();
|
||||||
|
// const code = useMemo(
|
||||||
|
// () => renderToStaticMarkup(doc, { rootBlockId: "root" }),
|
||||||
|
// [doc]
|
||||||
|
// );
|
||||||
|
|
||||||
|
const code = useMemo(() => renderEmailHtml(doc, "root"), [doc]);
|
||||||
|
|
||||||
|
const href = useMemo(() => {
|
||||||
|
const blob = new Blob([code], { type: "text/html;charset=utf-8" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
return url;
|
||||||
|
}, [doc]);
|
||||||
|
return (
|
||||||
|
<Tooltip title="Download html of template in txt file">
|
||||||
|
<IconButton href={href} download="emailTemplate.txt">
|
||||||
|
<FileDownloadOutlined fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
|
// import { renderToStaticMarkup } from "@usewaypoint/email-builder";
|
||||||
|
|
||||||
|
import { useDocument } from "../../documents/editor/EditorContext";
|
||||||
|
|
||||||
|
import HighlightedCodePanel from "./helper/HighlightedCodePanel";
|
||||||
|
import renderEmailHtml from "./helper/renderEmailHtml";
|
||||||
|
|
||||||
|
export default function HtmlPanel() {
|
||||||
|
const document = useDocument();
|
||||||
|
// const code = useMemo(
|
||||||
|
// () => renderToStaticMarkup(document, { rootBlockId: "root" }),
|
||||||
|
// [document]
|
||||||
|
// );
|
||||||
|
const code = useMemo(() => renderEmailHtml(document, "root"), [document]);
|
||||||
|
return <HighlightedCodePanel type="html" value={code} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
|
import { useDocument } from "../../documents/editor/EditorContext";
|
||||||
|
|
||||||
|
import HighlightedCodePanel from "./helper/HighlightedCodePanel";
|
||||||
|
|
||||||
|
export default function JsonPanel() {
|
||||||
|
const document = useDocument();
|
||||||
|
const code = useMemo(() => JSON.stringify(document, null, " "), [document]);
|
||||||
|
return <HighlightedCodePanel type="json" value={code} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import React from "react";
|
||||||
|
// DataObjectOutlined,
|
||||||
|
import {
|
||||||
|
CodeOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
PreviewOutlined
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import { Tab, Tabs, Tooltip } from "@mui/material";
|
||||||
|
|
||||||
|
import {
|
||||||
|
setSelectedMainTab,
|
||||||
|
useSelectedMainTab
|
||||||
|
} from "../../documents/editor/EditorContext";
|
||||||
|
|
||||||
|
export default function MainTabsGroup() {
|
||||||
|
const selectedMainTab = useSelectedMainTab();
|
||||||
|
const handleChange = (_: unknown, v: unknown) => {
|
||||||
|
switch (v) {
|
||||||
|
case "json":
|
||||||
|
case "preview":
|
||||||
|
case "editor":
|
||||||
|
case "html":
|
||||||
|
setSelectedMainTab(v);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
setSelectedMainTab("editor");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs value={selectedMainTab} onChange={handleChange}>
|
||||||
|
<Tab
|
||||||
|
value="editor"
|
||||||
|
label={
|
||||||
|
<Tooltip title="Edit">
|
||||||
|
<EditOutlined fontSize="small" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value="preview"
|
||||||
|
label={
|
||||||
|
<Tooltip title="Preview">
|
||||||
|
<PreviewOutlined fontSize="small" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value="html"
|
||||||
|
label={
|
||||||
|
<Tooltip title="HTML output">
|
||||||
|
<CodeOutlined fontSize="small" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* <Tab
|
||||||
|
value="json"
|
||||||
|
label={
|
||||||
|
<Tooltip title="JSON output">
|
||||||
|
<DataObjectOutlined fontSize="small" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/> */}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { ContentCopyOutlined } from "@mui/icons-material";
|
||||||
|
import { IconButton, Snackbar, Tooltip } from "@mui/material";
|
||||||
|
|
||||||
|
import { useDocument } from "../../documents/editor/EditorContext";
|
||||||
|
// import { renderToStaticMarkup } from "@usewaypoint/email-builder";
|
||||||
|
import renderEmailHtml from "./helper/renderEmailHtml";
|
||||||
|
|
||||||
|
export default function ShareButton() {
|
||||||
|
const document = useDocument();
|
||||||
|
// const code = useMemo(
|
||||||
|
// () => renderToStaticMarkup(document, { rootBlockId: "root" }),
|
||||||
|
// [document]
|
||||||
|
// );
|
||||||
|
const code = useMemo(() => renderEmailHtml(document, "root"), [document]);
|
||||||
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const onClick = async () => {
|
||||||
|
await copyToClipboard(code);
|
||||||
|
setMessage("Your current template is copied");
|
||||||
|
setTimeout(() => {
|
||||||
|
setMessage(null);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function copyToClipboard(text: string): Promise<boolean> {
|
||||||
|
// 1) Try modern Clipboard API
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to legacy approach
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Fallback: execCommand('copy')
|
||||||
|
try {
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = text;
|
||||||
|
|
||||||
|
// Prevent scrolling to bottom on iOS
|
||||||
|
textarea.style.position = "fixed";
|
||||||
|
textarea.style.top = "0";
|
||||||
|
textarea.style.left = "0";
|
||||||
|
textarea.style.opacity = "0";
|
||||||
|
textarea.style.pointerEvents = "none";
|
||||||
|
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
|
const ok = document.execCommand("copy");
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
|
||||||
|
return ok;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
setMessage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* <IconButton onClick={onClick}>
|
||||||
|
<Tooltip title="Share current template">
|
||||||
|
<IosShareOutlined fontSize="small" />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton> */}
|
||||||
|
<IconButton onClick={onClick} sx={{ color: "text.primary" }}>
|
||||||
|
<Tooltip title="copy" placement="left-start">
|
||||||
|
<ContentCopyOutlined fontSize="small" />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
<Snackbar
|
||||||
|
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||||
|
open={message !== null}
|
||||||
|
onClose={onClose}
|
||||||
|
message={message}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { html, json } from "./highlighters";
|
||||||
|
|
||||||
|
type TextEditorPanelProps = {
|
||||||
|
type: "json" | "html" | "javascript";
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
export default function HighlightedCodePanel({
|
||||||
|
type,
|
||||||
|
value
|
||||||
|
}: TextEditorPanelProps) {
|
||||||
|
const [code, setCode] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (type) {
|
||||||
|
case "html":
|
||||||
|
html(value).then(setCode);
|
||||||
|
return;
|
||||||
|
case "json":
|
||||||
|
json(value).then(setCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [setCode, value, type]);
|
||||||
|
|
||||||
|
if (code === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre
|
||||||
|
style={{ margin: 0, padding: 16 }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: code }}
|
||||||
|
onClick={(ev) => {
|
||||||
|
const s = window.getSelection();
|
||||||
|
if (s === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s.selectAllChildren(ev.currentTarget);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import hljs from "highlight.js";
|
||||||
|
import jsonHighlighter from "highlight.js/lib/languages/json";
|
||||||
|
import xmlHighlighter from "highlight.js/lib/languages/xml";
|
||||||
|
import prettierPluginBabel from "prettier/plugins/babel";
|
||||||
|
import prettierPluginEstree from "prettier/plugins/estree";
|
||||||
|
import prettierPluginHtml from "prettier/plugins/html";
|
||||||
|
import { format } from "prettier/standalone";
|
||||||
|
|
||||||
|
hljs.registerLanguage("json", jsonHighlighter);
|
||||||
|
hljs.registerLanguage("html", xmlHighlighter);
|
||||||
|
|
||||||
|
export async function html(value: string): Promise<string> {
|
||||||
|
const prettyValue = await format(value, {
|
||||||
|
parser: "html",
|
||||||
|
plugins: [prettierPluginHtml]
|
||||||
|
});
|
||||||
|
return hljs.highlight(prettyValue, { language: "html" }).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function json(value: string): Promise<string> {
|
||||||
|
const prettyValue = await format(value, {
|
||||||
|
parser: "json",
|
||||||
|
printWidth: 0,
|
||||||
|
trailingComma: "all",
|
||||||
|
plugins: [prettierPluginBabel, prettierPluginEstree]
|
||||||
|
});
|
||||||
|
return hljs.highlight(prettyValue, { language: "javascript" }).value;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
renderToStaticMarkup
|
||||||
|
} from "@usewaypoint/email-builder";
|
||||||
|
import { TEditorConfiguration } from "../../../documents/editor/core";
|
||||||
|
|
||||||
|
function overrideCanvasSize(
|
||||||
|
html: string,
|
||||||
|
document: TEditorConfiguration,
|
||||||
|
rootBlockId: string
|
||||||
|
) {
|
||||||
|
const rootBlock = document[rootBlockId];
|
||||||
|
if (!rootBlock || rootBlock.type !== "EmailLayout") {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = rootBlock.data as { canvasWidth?: number | null };
|
||||||
|
|
||||||
|
const canvasWidth =
|
||||||
|
typeof data?.canvasWidth === "number" && data.canvasWidth > 0
|
||||||
|
? data.canvasWidth
|
||||||
|
: 600;
|
||||||
|
|
||||||
|
let updatedHtml = html.replace(
|
||||||
|
/max-width:\s*600px/gi,
|
||||||
|
`max-width:${canvasWidth}px`
|
||||||
|
);
|
||||||
|
|
||||||
|
return updatedHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function renderEmailHtml(
|
||||||
|
document: TEditorConfiguration,
|
||||||
|
rootBlockId = "root"
|
||||||
|
) {
|
||||||
|
const html = renderToStaticMarkup(document, { rootBlockId });
|
||||||
|
return overrideCanvasSize(html, document, rootBlockId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
|
import { MonitorOutlined, PhoneIphoneOutlined } from "@mui/icons-material";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Stack,
|
||||||
|
SxProps,
|
||||||
|
ToggleButton,
|
||||||
|
ToggleButtonGroup,
|
||||||
|
Tooltip
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Reader } from "@usewaypoint/email-builder";
|
||||||
|
|
||||||
|
import EditorBlock from "../../documents/editor/EditorBlock";
|
||||||
|
import {
|
||||||
|
setSelectedScreenSize,
|
||||||
|
useDocument,
|
||||||
|
useSelectedMainTab,
|
||||||
|
useSelectedScreenSize
|
||||||
|
} from "../../documents/editor/EditorContext";
|
||||||
|
import ToggleInspectorPanelButton from "../InspectorDrawer/ToggleInspectorPanelButton";
|
||||||
|
import ToggleSamplesPanelButton from "../SamplesDrawer/ToggleSamplesPanelButton";
|
||||||
|
|
||||||
|
import DownloadJson from "./DownloadJson";
|
||||||
|
import HtmlPanel from "./HtmlPanel";
|
||||||
|
import JsonPanel from "./JsonPanel";
|
||||||
|
import MainTabsGroup from "./MainTabsGroup";
|
||||||
|
import ShareButton from "./ShareButton";
|
||||||
|
import renderEmailHtml from "./helper/renderEmailHtml";
|
||||||
|
|
||||||
|
export default function TemplatePanel() {
|
||||||
|
const document = useDocument();
|
||||||
|
const selectedMainTab = useSelectedMainTab();
|
||||||
|
const selectedScreenSize = useSelectedScreenSize();
|
||||||
|
const previewHtml = useMemo(
|
||||||
|
() => renderEmailHtml(document, "root"),
|
||||||
|
[document]
|
||||||
|
);
|
||||||
|
|
||||||
|
let mainBoxSx: SxProps = { height: "100%" };
|
||||||
|
if (selectedScreenSize === "mobile") {
|
||||||
|
mainBoxSx = {
|
||||||
|
...mainBoxSx,
|
||||||
|
margin: "32px auto",
|
||||||
|
width: 370,
|
||||||
|
height: 800,
|
||||||
|
boxShadow:
|
||||||
|
"rgba(33, 36, 67, 0.04) 0px 10px 20px, rgba(33, 36, 67, 0.04) 0px 2px 6px, rgba(33, 36, 67, 0.04) 0px 0px 1px"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScreenSizeChange = (_: unknown, value: unknown) => {
|
||||||
|
switch (value) {
|
||||||
|
case "mobile":
|
||||||
|
case "desktop":
|
||||||
|
setSelectedScreenSize(value);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
setSelectedScreenSize("desktop");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMainPanel = () => {
|
||||||
|
switch (selectedMainTab) {
|
||||||
|
case "editor":
|
||||||
|
return (
|
||||||
|
<Box sx={mainBoxSx}>
|
||||||
|
<EditorBlock id="root" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
case "preview":
|
||||||
|
return (
|
||||||
|
<Box sx={mainBoxSx}>
|
||||||
|
<iframe
|
||||||
|
title="Email preview"
|
||||||
|
srcDoc={previewHtml}
|
||||||
|
sandbox="allow-popups allow-popups-to-escape-sandbox"
|
||||||
|
style={{ width: "100%", height: "100%", border: "none" }}
|
||||||
|
/>
|
||||||
|
{/* <Reader document={document} rootBlockId="root" /> */}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
case "html":
|
||||||
|
return <HtmlPanel />;
|
||||||
|
case "json":
|
||||||
|
return <JsonPanel />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
height: 49,
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
backgroundColor: "white",
|
||||||
|
position: "sticky",
|
||||||
|
top: 0,
|
||||||
|
zIndex: "appBar",
|
||||||
|
px: 1
|
||||||
|
}}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<ToggleSamplesPanelButton />
|
||||||
|
<Stack
|
||||||
|
px={2}
|
||||||
|
direction="row"
|
||||||
|
gap={2}
|
||||||
|
width="100%"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<MainTabsGroup />
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<DownloadJson />
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={selectedScreenSize}
|
||||||
|
exclusive
|
||||||
|
size="small"
|
||||||
|
onChange={handleScreenSizeChange}
|
||||||
|
>
|
||||||
|
<ToggleButton value="desktop">
|
||||||
|
<Tooltip title="Desktop view">
|
||||||
|
<MonitorOutlined fontSize="small" />
|
||||||
|
</Tooltip>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="mobile">
|
||||||
|
<Tooltip title="Mobile view">
|
||||||
|
<PhoneIphoneOutlined fontSize="small" />
|
||||||
|
</Tooltip>
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
<ShareButton />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<ToggleInspectorPanelButton />
|
||||||
|
</Stack>
|
||||||
|
<Box
|
||||||
|
sx={{ height: "calc(100vh - 49px)", overflow: "auto", minWidth: 370 }}
|
||||||
|
>
|
||||||
|
{renderMainPanel()}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
useState
|
||||||
|
} from "react";
|
||||||
|
import { Stack, useTheme } from "@mui/material";
|
||||||
|
import {
|
||||||
|
useInspectorDrawerOpen,
|
||||||
|
useSamplesDrawerOpen
|
||||||
|
} from "../documents/editor/EditorContext";
|
||||||
|
import InspectorDrawer, { INSPECTOR_DRAWER_WIDTH } from "./InspectorDrawer";
|
||||||
|
import SamplesDrawer, { SAMPLES_DRAWER_WIDTH } from "./SamplesDrawer";
|
||||||
|
import TemplatePanel from "./TemplatePanel";
|
||||||
|
|
||||||
|
function useDrawerTransition(
|
||||||
|
cssProperty: "margin-left" | "margin-right",
|
||||||
|
open: boolean
|
||||||
|
) {
|
||||||
|
const { transitions } = useTheme();
|
||||||
|
return transitions.create(cssProperty, {
|
||||||
|
easing: !open ? transitions.easing.sharp : transitions.easing.easeOut,
|
||||||
|
duration: !open
|
||||||
|
? transitions.duration.leavingScreen
|
||||||
|
: transitions.duration.enteringScreen
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function EmailBuilderApp() {
|
||||||
|
const inspectorDrawerOpen = useInspectorDrawerOpen();
|
||||||
|
const samplesDrawerOpen = useSamplesDrawerOpen();
|
||||||
|
|
||||||
|
const marginLeftTransition = useDrawerTransition(
|
||||||
|
"margin-left",
|
||||||
|
samplesDrawerOpen
|
||||||
|
);
|
||||||
|
const marginRightTransition = useDrawerTransition(
|
||||||
|
"margin-right",
|
||||||
|
inspectorDrawerOpen
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InspectorDrawer />
|
||||||
|
<SamplesDrawer />
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
marginRight: inspectorDrawerOpen ? `${INSPECTOR_DRAWER_WIDTH}px` : 0,
|
||||||
|
marginLeft: samplesDrawerOpen ? `${SAMPLES_DRAWER_WIDTH}px` : 0,
|
||||||
|
transition: [marginLeftTransition, marginRightTransition].join(", ")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TemplatePanel />
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { CssBaseline, ThemeProvider } from "@mui/material";
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
import theme from "./EmailBuildertheme";
|
||||||
|
|
||||||
|
const EmailBuilder = () => {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailBuilder;
|
||||||
@@ -0,0 +1,520 @@
|
|||||||
|
import { alpha, createTheme, darken, lighten } from "@mui/material/styles";
|
||||||
|
|
||||||
|
const BRAND_NAVY = "#212443";
|
||||||
|
const BRAND_BLUE = "#0079CC";
|
||||||
|
const BRAND_GREEN = "#1F8466";
|
||||||
|
const BRAND_RED = "#E81212";
|
||||||
|
const BRAND_YELLOW = "#F6DC9F";
|
||||||
|
const BRAND_PURPLE = "#6C0E7C";
|
||||||
|
const BRAND_BROWN = "#CC996C";
|
||||||
|
const STANDARD_FONT_FAMILY =
|
||||||
|
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"';
|
||||||
|
const MONOSPACE_FONT_FAMILY =
|
||||||
|
'ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace';
|
||||||
|
|
||||||
|
const BASE_THEME = createTheme({
|
||||||
|
palette: {
|
||||||
|
background: {
|
||||||
|
default: "#f2f5f7"
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: "#1F1F21",
|
||||||
|
secondary: "#4F4F4F"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: STANDARD_FONT_FAMILY
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const THEME = createTheme(BASE_THEME, {
|
||||||
|
palette: {
|
||||||
|
brand: {
|
||||||
|
navy: BRAND_NAVY,
|
||||||
|
blue: BRAND_BLUE,
|
||||||
|
red: BRAND_RED,
|
||||||
|
green: BRAND_GREEN,
|
||||||
|
yellow: BRAND_YELLOW,
|
||||||
|
purple: BRAND_PURPLE,
|
||||||
|
brown: BRAND_BROWN
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
main: BRAND_GREEN,
|
||||||
|
light: lighten(BRAND_GREEN, 0.15),
|
||||||
|
dark: darken(BRAND_GREEN, 0.15)
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
main: BRAND_RED,
|
||||||
|
light: lighten(BRAND_RED, 0.15),
|
||||||
|
dark: darken(BRAND_RED, 0.15)
|
||||||
|
},
|
||||||
|
cadet: {
|
||||||
|
100: "#F9FAFB",
|
||||||
|
200: "#F2F5F7",
|
||||||
|
300: "#DCE4EA",
|
||||||
|
400: "#A8BBCA",
|
||||||
|
500: "#6A8BA4"
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
100: lighten(BRAND_YELLOW, 0.8),
|
||||||
|
200: lighten(BRAND_YELLOW, 0.6),
|
||||||
|
300: lighten(BRAND_YELLOW, 0.4),
|
||||||
|
400: lighten(BRAND_YELLOW, 0.2),
|
||||||
|
500: BRAND_YELLOW
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
main: BRAND_BLUE
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
main: BRAND_BLUE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiCssBaseline: {
|
||||||
|
styleOverrides: `
|
||||||
|
address {
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
fieldset {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
font-family: ${MONOSPACE_FONT_FAMILY}
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
MuiAlert: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(14)
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
paddingTop: 0,
|
||||||
|
marginRight: 0
|
||||||
|
},
|
||||||
|
filledSuccess: {
|
||||||
|
backgroundColor: BRAND_GREEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiStepLabel: {
|
||||||
|
styleOverrides: {
|
||||||
|
label: {
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiDialog: {
|
||||||
|
defaultProps: {
|
||||||
|
fullWidth: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiDialogContent: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
paddingTop: BASE_THEME.spacing(1),
|
||||||
|
paddingBottom: BASE_THEME.spacing(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiDialogTitle: {
|
||||||
|
defaultProps: {
|
||||||
|
variant: "h4"
|
||||||
|
},
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
paddingTop: BASE_THEME.spacing(3),
|
||||||
|
paddingBottom: BASE_THEME.spacing(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiDialogActions: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderTop: "1px solid",
|
||||||
|
borderTopColor: BASE_THEME.palette.divider,
|
||||||
|
marginTop: BASE_THEME.spacing(2.5),
|
||||||
|
padding: `${BASE_THEME.spacing(1.5)} ${BASE_THEME.spacing(3)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiTableCell: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
...BASE_THEME.typography.body2,
|
||||||
|
borderColor: BASE_THEME.palette.grey[200]
|
||||||
|
},
|
||||||
|
head: {
|
||||||
|
...BASE_THEME.typography.overline,
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||||
|
letterSpacing: "0.075em",
|
||||||
|
color: BASE_THEME.palette.text.secondary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiTableRow: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
"&:last-child td": {
|
||||||
|
borderBottom: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiAvatar: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
textTransform: "uppercase",
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiChip: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
"&.MuiChip-filledError, &.MuiChip-filledSuccess": {
|
||||||
|
fill: BASE_THEME.palette.primary.contrastText
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sizeSmall: {
|
||||||
|
borderRadius: BASE_THEME.spacing(0.5),
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
iconSmall: {
|
||||||
|
fontSize: 14,
|
||||||
|
marginLeft: BASE_THEME.spacing(1)
|
||||||
|
},
|
||||||
|
colorSecondary: {
|
||||||
|
borderColor: BASE_THEME.palette.grey[400],
|
||||||
|
color: BASE_THEME.palette.text.secondary
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiDrawer: {
|
||||||
|
defaultProps: {
|
||||||
|
PaperProps: {
|
||||||
|
elevation: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiTooltip: {
|
||||||
|
styleOverrides: {
|
||||||
|
tooltip: {
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(12),
|
||||||
|
backgroundColor: alpha(BASE_THEME.palette.text.primary, 0.9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiSlider: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
height: 1
|
||||||
|
},
|
||||||
|
track: {
|
||||||
|
height: 1,
|
||||||
|
border: "none"
|
||||||
|
},
|
||||||
|
rail: {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: BASE_THEME.palette.grey[500]
|
||||||
|
},
|
||||||
|
mark: {
|
||||||
|
backgroundColor: BASE_THEME.palette.grey[500]
|
||||||
|
},
|
||||||
|
markActive: {
|
||||||
|
height: 0
|
||||||
|
},
|
||||||
|
thumb: {
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
cursor: "col-resize",
|
||||||
|
"&:hover, &.Mui-active, &.Mui-focusVisible": {
|
||||||
|
boxShadow: `0 0 0 4px ${alpha(BRAND_BLUE, 0.2)}`
|
||||||
|
},
|
||||||
|
"&:before": {
|
||||||
|
display: "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiPaper: {
|
||||||
|
defaultProps: {
|
||||||
|
elevation: 2,
|
||||||
|
square: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiButtonBase: {
|
||||||
|
defaultProps: {
|
||||||
|
disableTouchRipple: true,
|
||||||
|
focusRipple: true
|
||||||
|
},
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
"&.MuiButton-containedSecondary.Mui-disabled": {
|
||||||
|
backgroundColor: BASE_THEME.palette.grey[100]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiButtonGroup: {
|
||||||
|
defaultProps: {
|
||||||
|
disableElevation: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiIconButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
edgeStart: {
|
||||||
|
marginLeft: BASE_THEME.spacing(-1)
|
||||||
|
},
|
||||||
|
colorSecondary: {
|
||||||
|
color: BASE_THEME.palette.grey[500]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiButton: {
|
||||||
|
defaultProps: {
|
||||||
|
disableElevation: true
|
||||||
|
},
|
||||||
|
styleOverrides: {
|
||||||
|
textPrimary: {
|
||||||
|
color: BASE_THEME.palette.text.primary
|
||||||
|
},
|
||||||
|
textSecondary: {
|
||||||
|
color: BASE_THEME.palette.text.secondary
|
||||||
|
},
|
||||||
|
outlinedPrimary: {
|
||||||
|
borderColor: BASE_THEME.palette.grey[300],
|
||||||
|
color: BASE_THEME.palette.text.primary,
|
||||||
|
"&:hover, &:active, &:focus": {
|
||||||
|
borderColor: BASE_THEME.palette.grey[500],
|
||||||
|
color: BASE_THEME.palette.text.primary
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containedSecondary: {
|
||||||
|
backgroundColor: BASE_THEME.palette.common.white,
|
||||||
|
border: `1px solid ${BASE_THEME.palette.grey[300]}`,
|
||||||
|
color: BASE_THEME.palette.text.primary,
|
||||||
|
"&:hover, &:active, &:focus": {
|
||||||
|
backgroundColor: BASE_THEME.palette.common.white,
|
||||||
|
borderColor: BASE_THEME.palette.grey[500],
|
||||||
|
color: BASE_THEME.palette.text.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiToggleButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
paddingLeft: BASE_THEME.spacing(1.5),
|
||||||
|
paddingRight: BASE_THEME.spacing(1.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiInputBase: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
"&:not(.Mui-disabled, .Mui-error):before": {
|
||||||
|
borderBottom: `1px solid ${BASE_THEME.palette.grey[400]}`
|
||||||
|
},
|
||||||
|
"&:hover:not(.Mui-disabled, .Mui-error):before": {
|
||||||
|
borderBottom: `1px solid ${BASE_THEME.palette.grey[500]} !important`
|
||||||
|
},
|
||||||
|
"&:after": {
|
||||||
|
borderBottom: `1px solid ${BASE_THEME.palette.text.primary} !important`
|
||||||
|
},
|
||||||
|
"&.MuiOutlinedInput-root:not(.Mui-error)": {
|
||||||
|
"& fieldset": {
|
||||||
|
borderColor: BASE_THEME.palette.grey[300],
|
||||||
|
transition: "border-color 0.2s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"&.MuiOutlinedInput-root:not(.Mui-disabled, .Mui-error)": {
|
||||||
|
"&:hover fieldset": {
|
||||||
|
borderColor: BASE_THEME.palette.grey[400]
|
||||||
|
},
|
||||||
|
"&.Mui-focused fieldset": {
|
||||||
|
borderColor: BASE_THEME.palette.text.secondary,
|
||||||
|
borderWidth: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
WebkitTextFillColor: "inherit",
|
||||||
|
color: BASE_THEME.palette.text.secondary
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inputSizeSmall: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiOutlinedInput: {
|
||||||
|
styleOverrides: {
|
||||||
|
notchedOutline: {
|
||||||
|
"& legend": {
|
||||||
|
fontSize: "0.85em",
|
||||||
|
maxWidth: "100%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiInputAdornment: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
"& .MuiTypography-root": {
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||||
|
color: BASE_THEME.palette.text.secondary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiInputLabel: {
|
||||||
|
defaultProps: {
|
||||||
|
shrink: true
|
||||||
|
},
|
||||||
|
styleOverrides: {
|
||||||
|
shrink: {
|
||||||
|
transform: "scale(0.85)",
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||||
|
"&.Mui-focused": {
|
||||||
|
color: BASE_THEME.palette.text.primary
|
||||||
|
},
|
||||||
|
"&.MuiInputLabel-standard": {
|
||||||
|
transform: "translate(0, -4px) scale(0.85)",
|
||||||
|
color: "#4F4F4F"
|
||||||
|
},
|
||||||
|
"&.MuiInputLabel-outlined": {
|
||||||
|
transform: "translate(15px, -8px) scale(0.85)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiTabs: {
|
||||||
|
defaultProps: {
|
||||||
|
variant: "scrollable"
|
||||||
|
},
|
||||||
|
styleOverrides: {
|
||||||
|
indicator: {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: BASE_THEME.palette.text.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiTab: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
textTransform: "none",
|
||||||
|
minWidth: BASE_THEME.spacing(2),
|
||||||
|
paddingLeft: BASE_THEME.spacing(1.5),
|
||||||
|
paddingRight: BASE_THEME.spacing(1.5),
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||||
|
fontFamily: BASE_THEME.typography.fontFamily,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||||
|
transition: "color 0.2s",
|
||||||
|
"&.Mui-selected": {
|
||||||
|
color: BASE_THEME.palette.text.primary
|
||||||
|
},
|
||||||
|
"&:hover": {
|
||||||
|
color: BASE_THEME.palette.text.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiCard: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiCardHeader: {
|
||||||
|
styleOverrides: {
|
||||||
|
title: {
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(18),
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: BASE_THEME.typography.fontFamily,
|
||||||
|
h1: {
|
||||||
|
fontFamily: BASE_THEME.typography.fontFamily,
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(40),
|
||||||
|
lineHeight: 1.2,
|
||||||
|
letterSpacing: "-0.02em",
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontFamily: BASE_THEME.typography.fontFamily,
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(32),
|
||||||
|
lineHeight: 1.2,
|
||||||
|
letterSpacing: "-0.02em",
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontFamily: BASE_THEME.typography.fontFamily,
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(24),
|
||||||
|
lineHeight: 1.5,
|
||||||
|
letterSpacing: "-0.01em",
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontFamily: BASE_THEME.typography.fontFamily,
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(20),
|
||||||
|
lineHeight: 1.5,
|
||||||
|
letterSpacing: "-0.01em",
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
fontFamily: BASE_THEME.typography.fontFamily,
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(18),
|
||||||
|
lineHeight: 1.5,
|
||||||
|
letterSpacing: "-0.01em",
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||||
|
},
|
||||||
|
h6: {
|
||||||
|
fontFamily: BASE_THEME.typography.fontFamily,
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(16),
|
||||||
|
lineHeight: 1.5,
|
||||||
|
letterSpacing: "-0.005em",
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium
|
||||||
|
},
|
||||||
|
body1: {
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(14)
|
||||||
|
},
|
||||||
|
body2: {
|
||||||
|
fontSize: BASE_THEME.typography.pxToRem(12)
|
||||||
|
},
|
||||||
|
overline: {
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||||
|
letterSpacing: "0.05em"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
textTransform: "none",
|
||||||
|
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||||
|
lineHeight: 1.5
|
||||||
|
},
|
||||||
|
caption: {
|
||||||
|
letterSpacing: 0,
|
||||||
|
lineHeight: 1.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shadows: [
|
||||||
|
"none",
|
||||||
|
"0px 4px 15px rgba(33, 36, 67, 0.04), 0px 0px 2px rgba(33, 36, 67, 0.04), 0px 0px 1px rgba(33, 36, 67, 0.04)",
|
||||||
|
"0px 10px 20px rgba(33, 36, 67, 0.04), 0px 2px 6px rgba(33, 36, 67, 0.04), 0px 0px 1px rgba(33, 36, 67, 0.04)",
|
||||||
|
"0px 16px 24px rgba(33, 36, 67, 0.05), 0px 2px 6px rgba(33, 36, 67, 0.05), 0px 0px 1px rgba(33, 36, 67, 0.05)",
|
||||||
|
"0px 24px 32px rgba(33, 36, 67, 0.06), 0px 16px 24px rgba(33, 36, 67, 0.06), 0px 4px 8px rgba(33, 36, 67, 0.06)",
|
||||||
|
...Array(20).fill("none")
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export default THEME;
|
||||||