Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5f2c5c0a9 | ||
|
|
7e0c2517f2 | ||
|
|
a5456dadcf | ||
|
|
0158b0e176 | ||
|
|
5d385a0ea3 | ||
|
|
82584d6be9 | ||
|
|
cba774a336 | ||
|
|
c44351ea67 | ||
|
|
3a861f3e0f | ||
|
|
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 | ||
|
|
7204183542 | ||
|
|
5561673fdc | ||
|
|
0df44062d6 | ||
|
|
8983440763 | ||
|
|
5a28c0ed06 | ||
|
|
cbca22c308 | ||
|
|
d8a0d9137e | ||
|
|
b0e0b38e8f | ||
|
|
49c23fb52b | ||
|
|
da4b604710 | ||
|
|
82d518b3c9 | ||
|
|
dcbde2b661 | ||
|
|
27b0b426ad | ||
|
|
8788afaa8e | ||
|
|
428aa65b38 | ||
|
|
295d942427 | ||
|
|
1b2528ac1d | ||
|
|
c4e28e6e6e | ||
|
|
b19fb29b82 | ||
|
|
9b357acbdd | ||
|
|
3d223ba343 | ||
|
|
98754c776f | ||
|
|
1550e3a3d0 | ||
|
|
3500883205 | ||
|
|
d888897b62 | ||
|
|
ff4fa3a15f | ||
|
|
8f2433fbea | ||
|
|
b671dbac19 | ||
|
|
f104b0ca86 | ||
|
|
79cdbc9c8c | ||
|
|
e585b7c943 | ||
|
|
5336e2cb7c | ||
|
|
72401291f8 | ||
|
|
7b34314b9e | ||
|
|
b6f1cceeca | ||
|
|
c422cdac06 | ||
|
|
ab08d2615d | ||
|
|
2b0c7488d9 | ||
|
|
6927540741 | ||
|
|
1ceeee36ac | ||
|
|
c4779c14c1 | ||
|
|
e48869cb2f | ||
|
|
81d3969d1f | ||
|
|
5819103f57 | ||
|
|
31affb53f1 | ||
|
|
6653b7ae5a | ||
|
|
b50c1e70ee | ||
|
|
338b76029f | ||
|
|
365c2a217e | ||
|
|
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'>
|
||||||
@@ -73,12 +73,28 @@ Welcome to OpenSign, the premier open source docusign alternative - document e-s
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Installation
|
### Deploy
|
||||||
|
|
||||||
|
Note: The default MongoDB instance used in deployment is not persistant and will be cleared on every restart. To retain your data, configure and supply your own MongoDB connection URL.
|
||||||
|
|
||||||
|
#### DigitalOcean
|
||||||
|
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/OpenSignLabs/Deploy-OpenSign-to-Digital-Ocean/tree/main&refcode=30db1c901ab0)
|
||||||
|
|
||||||
|
#### Docker
|
||||||
The simplest way to install OpenSign on your own server is using official docker images by running the following command -
|
The simplest way to install OpenSign on your own server is using official docker images by running the following command -
|
||||||
|
|
||||||
|
**Command for linux/MacOS**
|
||||||
```
|
```
|
||||||
export HOST_URL=https://opensign.yourdomain.com && curl --remote-name-all https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && mv .env.local_dev .env.prod && docker compose up --force-recreate
|
export HOST_URL=https://opensign.yourdomain.com && curl --remote-name-all https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && mv .env.local_dev .env.prod && docker compose up --force-recreate
|
||||||
```
|
```
|
||||||
|
**Command for Windows (Powershell)**
|
||||||
|
```
|
||||||
|
$env:HOST_URL="https://opensign.yourdomain.com"; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml -OutFile docker-compose.yml; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile -OutFile Caddyfile; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev -OutFile .env.local_dev; Rename-Item -Path .env.local_dev -NewName .env.prod; docker compose up --force-recreate
|
||||||
|
```
|
||||||
|
**Command for Windows (CMD/Terminal)**
|
||||||
|
```
|
||||||
|
set HOST_URL=https://opensign.yourdomain.com && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && rename .env.local_dev .env.prod && docker compose up --force-recreate
|
||||||
|
```
|
||||||
Make sure that you have `Docker` and `git` installed before you run this command -
|
Make sure that you have `Docker` and `git` installed before you run this command -
|
||||||
|
|
||||||
Please refer to the [Installation Guide](https://docs.opensignlabs.com/docs/self-host/docker/run-locally/) for detailed instructions on how to install OpenSign on your system.
|
Please refer to the [Installation Guide](https://docs.opensignlabs.com/docs/self-host/docker/run-locally/) for detailed instructions on how to install OpenSign on your system.
|
||||||
@@ -149,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>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# Use an official Node runtime as the base image
|
|
||||||
FROM node:18
|
|
||||||
|
|
||||||
# Set the working directory inside the container
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
# Copy package.json and package-lock.json first to leverage Docker cache
|
|
||||||
COPY ./package*.json ./
|
|
||||||
|
|
||||||
# Install application dependencies
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Copy the current directory contents into the container
|
|
||||||
COPY ./ .
|
|
||||||
COPY ./.husky .
|
|
||||||
|
|
||||||
# Make port 3000 available to the world outside this container
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
# Define environment variables if needed
|
|
||||||
# ENV NODE_ENV production
|
|
||||||
|
|
||||||
# Run the application
|
|
||||||
ENTRYPOINT npm run start
|
|
||||||
|
|
||||||
@@ -13,6 +13,10 @@ RUN npm install
|
|||||||
# Copy the current directory contents into the container
|
# Copy the current directory contents into the container
|
||||||
COPY apps/OpenSign/ .
|
COPY apps/OpenSign/ .
|
||||||
COPY apps/OpenSign/.husky .
|
COPY apps/OpenSign/.husky .
|
||||||
|
COPY apps/OpenSign/entrypoint.sh .
|
||||||
|
|
||||||
|
# make the entrypoint.sh file executable
|
||||||
|
RUN chmod +x entrypoint.sh
|
||||||
|
|
||||||
# Define environment variables if needed
|
# Define environment variables if needed
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -20,8 +24,13 @@ ENV GENERATE_SOURCEMAP=false
|
|||||||
# build
|
# build
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
# Inject env.js loader into index.html
|
||||||
|
RUN sed -i '/<head>/a\<script src="/env.js"></script>' build/index.html
|
||||||
|
|
||||||
# Make port 3000 available to the world outside this container
|
# Make port 3000 available to the world outside this container
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# Tailwind Dark Mode Usage Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This guide shows how to use the new Tailwind utilities for better dark mode visibility in OpenSign.
|
||||||
|
|
||||||
|
## Button Styling
|
||||||
|
|
||||||
|
### VS Code-style Disabled Buttons
|
||||||
|
```jsx
|
||||||
|
// Option 1: Direct VS Code styling
|
||||||
|
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
|
||||||
|
Disabled Button
|
||||||
|
</button>
|
||||||
|
|
||||||
|
// Option 2: Themed disabled styling
|
||||||
|
<button className="op-btn btn-themed-disabled">
|
||||||
|
Themed Button
|
||||||
|
</button>
|
||||||
|
|
||||||
|
// Option 3: Conditional styling
|
||||||
|
<button
|
||||||
|
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
|
Dynamic Button
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Icon Styling
|
||||||
|
|
||||||
|
### Theme-aware Icons
|
||||||
|
```jsx
|
||||||
|
// Better visibility in dark mode
|
||||||
|
<i className="fa-light fa-folder icon-improved"></i>
|
||||||
|
|
||||||
|
// Muted but still visible
|
||||||
|
<i className="fa-light fa-plus icon-muted"></i>
|
||||||
|
|
||||||
|
// Disabled state
|
||||||
|
<i className="fa-light fa-trash icon-disabled"></i>
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Variable Approach
|
||||||
|
```jsx
|
||||||
|
// Using CSS variables
|
||||||
|
<i className="fa-light fa-search icon-themed"></i>
|
||||||
|
<i className="fa-light fa-settings icon-themed-muted"></i>
|
||||||
|
|
||||||
|
// Inline styles with CSS variables
|
||||||
|
<i
|
||||||
|
className="fa-light fa-plus"
|
||||||
|
style={{ color: 'var(--icon-color)' }}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Legacy JavaScript Function (Still Supported)
|
||||||
|
```jsx
|
||||||
|
// Existing approach - still works
|
||||||
|
<i
|
||||||
|
className="fa-light fa-plus"
|
||||||
|
style={{ color: getThemeIconColor() }}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Text Styling
|
||||||
|
|
||||||
|
### Improved Gray Text
|
||||||
|
```jsx
|
||||||
|
// These automatically improve in dark mode
|
||||||
|
<span className="text-gray-500">More visible in dark mode</span>
|
||||||
|
<span className="text-gray-400">Muted but readable</span>
|
||||||
|
<span className="text-gray-600">Clear text</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Examples
|
||||||
|
|
||||||
|
### Toolbar with Better Visibility
|
||||||
|
```jsx
|
||||||
|
const Toolbar = () => (
|
||||||
|
<div className="flex space-x-2 p-2">
|
||||||
|
<button className="p-2 hover:bg-gray-200 rounded">
|
||||||
|
<i className="fa-light fa-plus icon-improved"></i>
|
||||||
|
</button>
|
||||||
|
<button className="p-2 hover:bg-gray-200 rounded" disabled>
|
||||||
|
<i className="fa-light fa-trash icon-disabled"></i>
|
||||||
|
</button>
|
||||||
|
<button className="p-2 hover:bg-gray-200 rounded">
|
||||||
|
<i className="fa-light fa-edit icon-improved"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form with Disabled States
|
||||||
|
```jsx
|
||||||
|
const Form = ({ isSubmitting }) => (
|
||||||
|
<form>
|
||||||
|
<input className="op-input" />
|
||||||
|
<button
|
||||||
|
className={`op-btn op-btn-primary ${isSubmitting ? 'op-btn-vscode-disabled' : ''}`}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## React-Tour and Tooltip Dark Mode Support
|
||||||
|
|
||||||
|
### React-Tour Modals
|
||||||
|
The react-tour modals now automatically support dark mode with VS Code-inspired styling:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// These components automatically get dark mode styling
|
||||||
|
<Tour
|
||||||
|
onRequestClose={closeTour}
|
||||||
|
steps={tourConfig}
|
||||||
|
isOpen={isOpen}
|
||||||
|
rounded={5}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ReactTooltip Components
|
||||||
|
All ReactTooltip instances now support dark mode:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// Automatically styled for dark mode
|
||||||
|
<ReactTooltip id="my-tooltip" className="z-[999]">
|
||||||
|
<div className="max-w-[200px]">
|
||||||
|
<p>Tooltip content</p>
|
||||||
|
</div>
|
||||||
|
</ReactTooltip>
|
||||||
|
```
|
||||||
|
|
||||||
|
### HoverCard Balloon UI
|
||||||
|
The balloon tooltips in OpenSign Drive now properly support dark mode:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// These automatically get dark styling in dark mode
|
||||||
|
<HoverCard>
|
||||||
|
<HoverCardContent>
|
||||||
|
Document information
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dark Mode Features Added
|
||||||
|
|
||||||
|
### 1. **React-Tour Modal Styling**
|
||||||
|
- Background: `#1F2937` (VS Code modal background)
|
||||||
|
- Text: `#E5E7EB` (soft white for readability)
|
||||||
|
- Borders: `#374151` (subtle borders)
|
||||||
|
- Buttons: VS Code-style primary/secondary buttons
|
||||||
|
|
||||||
|
### 2. **ReactTooltip Styling**
|
||||||
|
- Background: `#1F2937` with proper contrast
|
||||||
|
- Border: `#374151` for definition
|
||||||
|
- Box shadow: Enhanced for dark backgrounds
|
||||||
|
- Text: `#E5E7EB` for optimal readability
|
||||||
|
|
||||||
|
### 3. **HoverCard Balloon UI**
|
||||||
|
- Background: `#1F2937` (matches VS Code)
|
||||||
|
- Text: `#E5E7EB` for readability
|
||||||
|
- Arrow: Automatically matches background color
|
||||||
|
- Enhanced shadows for dark backgrounds
|
||||||
|
|
||||||
|
### 4. **React-Datepicker Support**
|
||||||
|
- Calendar background: `#1F2937`
|
||||||
|
- Selected dates: VS Code blue (`#007ACC`)
|
||||||
|
- Hover states: Proper contrast ratios
|
||||||
|
- Navigation arrows: Themed appropriately
|
||||||
|
|
||||||
|
## CSS Classes Reference
|
||||||
|
|
||||||
|
| Class | Purpose | Dark Mode Color |
|
||||||
|
|-------|---------|----------------|
|
||||||
|
| `icon-improved` | Better icon visibility | `#CCCCCC` |
|
||||||
|
| `icon-muted` | Muted but visible icons | `#999999` |
|
||||||
|
| `icon-disabled` | Disabled icon state | `#858585` |
|
||||||
|
| `op-btn-vscode-disabled` | VS Code disabled button | Background: `#3C3C3C` |
|
||||||
|
| `btn-themed-disabled` | Themed disabled button | Uses CSS variables |
|
||||||
|
| `icon-themed` | Variable-based icon color | `var(--icon-color)` |
|
||||||
|
| `.reactour__helper` | `#1F2937` background |
|
||||||
|
| `.react-tooltip` | `#1F2937` background |
|
||||||
|
| `.HoverCardContent` | `#1F2937` background |
|
||||||
|
| `.react-datepicker` | `#1F2937` background |
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
### From JavaScript Function to Tailwind
|
||||||
|
```jsx
|
||||||
|
// Before
|
||||||
|
<i style={{ color: getThemeIconColor() }} className="fa-light fa-plus" />
|
||||||
|
|
||||||
|
// After
|
||||||
|
<i className="fa-light fa-plus icon-improved" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Hardcoded Colors to Theme-aware
|
||||||
|
```jsx
|
||||||
|
// Before
|
||||||
|
<i className="fa-light fa-plus text-gray-500" />
|
||||||
|
|
||||||
|
// After (automatic improvement)
|
||||||
|
<i className="fa-light fa-plus text-gray-500" />
|
||||||
|
// OR explicitly
|
||||||
|
<i className="fa-light fa-plus icon-improved" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
All existing tooltip and tour components will automatically inherit the new dark mode styling when the theme is set to `opensigndark`. No code changes required for existing implementations.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
ENV_FILE=./build/env.js
|
||||||
|
DOTENV_FILE=./.env.prod # ✅ use .env.prod
|
||||||
|
|
||||||
|
echo "Generating runtime env file at $ENV_FILE..."
|
||||||
|
|
||||||
|
echo "window.RUNTIME_ENV = {" > $ENV_FILE
|
||||||
|
|
||||||
|
# List of keys to include
|
||||||
|
RUNTIME_KEYS="REACT_APP_SERVERURL"
|
||||||
|
|
||||||
|
for key in $RUNTIME_KEYS; do
|
||||||
|
# First check docker env (-e), fallback to .env file
|
||||||
|
value=$(printenv "$key")
|
||||||
|
|
||||||
|
if [ -z "$value" ] && [ -f "$DOTENV_FILE" ]; then
|
||||||
|
# fallback: read from .env
|
||||||
|
value=$(grep "^$key=" "$DOTENV_FILE" | cut -d '=' -f2- | tr -d '\r\n' | sed 's/"/\\"/g')
|
||||||
|
else
|
||||||
|
value=$(echo "$value" | sed 's/"/\\"/g')
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " $key: \"$value\"," >> $ENV_FILE
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "};" >> $ENV_FILE
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* Tailwind Dark Mode Usage Examples for OpenSign
|
||||||
|
*
|
||||||
|
* This file demonstrates how to use the new Tailwind utilities
|
||||||
|
* for better dark mode visibility of buttons and icons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example 1: VS Code-style disabled buttons
|
||||||
|
const DisabledButtonExamples = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Option A: Using the VS Code disabled style */}
|
||||||
|
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
|
||||||
|
VS Code Style Disabled Button
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Option B: Using themed disabled style */}
|
||||||
|
<button className="op-btn btn-themed-disabled">
|
||||||
|
Themed Disabled Button
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Option C: Conditional styling */}
|
||||||
|
<button
|
||||||
|
className={`op-btn op-btn-primary ${isDisabled ? "op-btn-vscode-disabled" : ""}`}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
|
Conditional Button
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example 2: Icon visibility improvements
|
||||||
|
const IconExamples = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Theme-aware icons with better visibility */}
|
||||||
|
<i className="fa-light fa-folder icon-improved"></i>
|
||||||
|
<i className="fa-light fa-plus icon-muted"></i>
|
||||||
|
<i className="fa-light fa-trash icon-disabled"></i>
|
||||||
|
|
||||||
|
{/* Using CSS variables */}
|
||||||
|
<i className="fa-light fa-search icon-themed"></i>
|
||||||
|
<i className="fa-light fa-settings icon-themed-muted"></i>
|
||||||
|
|
||||||
|
{/* Gray text that automatically improves in dark mode */}
|
||||||
|
<span className="text-gray-500">
|
||||||
|
This text is now more visible in dark mode
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-400">Muted but still readable</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example 3: Using CSS variables in inline styles
|
||||||
|
const InlineStyleExamples = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Using CSS variables directly */}
|
||||||
|
<i className="fa-light fa-plus" style={{ color: "var(--icon-color)" }} />
|
||||||
|
|
||||||
|
{/* Using the existing JavaScript function */}
|
||||||
|
<i className="fa-light fa-minus" style={{ color: getThemeIconColor() }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example 4: Toolbar with improved icons
|
||||||
|
const ToolbarExample = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex space-x-2 p-2">
|
||||||
|
<button className="p-2 hover:bg-gray-200 rounded">
|
||||||
|
<i className="fa-light fa-plus icon-improved"></i>
|
||||||
|
</button>
|
||||||
|
<button className="p-2 hover:bg-gray-200 rounded" disabled>
|
||||||
|
<i className="fa-light fa-trash icon-disabled"></i>
|
||||||
|
</button>
|
||||||
|
<button className="p-2 hover:bg-gray-200 rounded">
|
||||||
|
<i className="fa-light fa-edit icon-improved"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
DisabledButtonExamples,
|
||||||
|
IconExamples,
|
||||||
|
InlineStyleExamples,
|
||||||
|
ToolbarExample
|
||||||
|
};
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Tailwind Dark Mode Usage Examples for OpenSign
|
||||||
|
*
|
||||||
|
* This file demonstrates how to use the new Tailwind utilities
|
||||||
|
* for better dark mode visibility of buttons and icons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example 1: VS Code-style disabled buttons
|
||||||
|
const DisabledButtonExamples = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Option A: Using the VS Code disabled style */}
|
||||||
|
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
|
||||||
|
VS Code Style Disabled Button
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Option B: Using themed disabled style */}
|
||||||
|
<button className="op-btn btn-themed-disabled">
|
||||||
|
Themed Disabled Button
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Option C: Conditional styling */}
|
||||||
|
<button
|
||||||
|
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
|
Conditional Button
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example 2: Icon visibility improvements
|
||||||
|
const IconExamples = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Theme-aware icons with better visibility */}
|
||||||
|
<i className="fa-light fa-folder icon-improved"></i>
|
||||||
|
<i className="fa-light fa-plus icon-muted"></i>
|
||||||
|
<i className="fa-light fa-trash icon-disabled"></i>
|
||||||
|
|
||||||
|
{/* Using CSS variables */}
|
||||||
|
<i className="fa-light fa-search icon-themed"></i>
|
||||||
|
<i className="fa-light fa-settings icon-themed-muted"></i>
|
||||||
|
|
||||||
|
{/* Gray text that automatically improves in dark mode */}
|
||||||
|
<span className="text-gray-500">This text is now more visible in dark mode</span>
|
||||||
|
<span className="text-gray-400">Muted but still readable</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example 3: Using CSS variables in inline styles
|
||||||
|
const InlineStyleExamples = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Using CSS variables directly */}
|
||||||
|
<i
|
||||||
|
className="fa-light fa-plus"
|
||||||
|
style={{ color: 'var(--icon-color)' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Using the existing JavaScript function */}
|
||||||
|
<i
|
||||||
|
className="fa-light fa-minus"
|
||||||
|
style={{ color: getThemeIconColor() }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example 4: Toolbar with improved icons
|
||||||
|
const ToolbarExample = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex space-x-2 p-2">
|
||||||
|
<button className="p-2 hover:bg-gray-200 rounded">
|
||||||
|
<i className="fa-light fa-plus icon-improved"></i>
|
||||||
|
</button>
|
||||||
|
<button className="p-2 hover:bg-gray-200 rounded" disabled>
|
||||||
|
<i className="fa-light fa-trash icon-disabled"></i>
|
||||||
|
</button>
|
||||||
|
<button className="p-2 hover:bg-gray-200 rounded">
|
||||||
|
<i className="fa-light fa-edit icon-improved"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
DisabledButtonExamples,
|
||||||
|
IconExamples,
|
||||||
|
InlineStyleExamples,
|
||||||
|
ToolbarExample
|
||||||
|
};
|
||||||
@@ -3,19 +3,20 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="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="%PUBLIC_URL%/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>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,74 +1,93 @@
|
|||||||
{
|
{
|
||||||
"name": "open_sign",
|
"name": "open_sign",
|
||||||
"version": "0.1.0",
|
"version": "2.37.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/auto-animate": "^0.8.2",
|
"@emotion/react": "^11.14.0",
|
||||||
"@lottiefiles/dotlottie-react": "^0.13.4",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@formkit/auto-animate": "^0.9.0",
|
||||||
|
"@imgly/background-removal": "^1.7.0",
|
||||||
|
"@lottiefiles/dotlottie-react": "^0.19.0",
|
||||||
|
"@mui/icons-material": "^5.15.10",
|
||||||
|
"@mui/material": "^5.15.10",
|
||||||
"@pdf-lib/fontkit": "^1.1.1",
|
"@pdf-lib/fontkit": "^1.1.1",
|
||||||
"@radix-ui/themes": "^3.1.6",
|
"@radix-ui/themes": "^3.3.0",
|
||||||
"@reduxjs/toolkit": "^2.7.0",
|
"@reactour/tour": "^3.8.0",
|
||||||
"axios": "^1.8.4",
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"css-minimizer-webpack-plugin": "^7.0.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.0.5",
|
"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",
|
||||||
"nth-check": "^2.1.1",
|
"parse": "^8.1.0",
|
||||||
"parse": "^5.3.0",
|
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
|
"pkijs": "^3.4.0",
|
||||||
|
"prettier": "^3.8.3",
|
||||||
"print-js": "^1.6.0",
|
"print-js": "^1.6.0",
|
||||||
"radix-ui": "^1.0.1",
|
"prismjs": "^1.30.0",
|
||||||
"react": "^18.2.0",
|
"process": "^0.11.10",
|
||||||
"react-bootstrap": "^2.10.9",
|
"quill-html-edit-button": "^3.0.0",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
|
"react": "^19.2.5",
|
||||||
|
"react-bootstrap": "^2.10.10",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-confetti": "^6.4.0",
|
"react-confetti": "^6.4.0",
|
||||||
"react-cookie": "^7.2.2",
|
"react-datepicker": "^9.1.0",
|
||||||
"react-datepicker": "^7.6.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.2.0",
|
|
||||||
"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.0",
|
"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.5.2",
|
"react-router": "^7.18.2",
|
||||||
"react-scripts": "^5.0.1",
|
|
||||||
"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.0.7",
|
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-timezone-select": "^3.3.3",
|
||||||
"react-timezone-select": "^3.2.8",
|
"react-to-print": "^3.3.0",
|
||||||
"react-tooltip": "^5.28.1",
|
"react-tooltip": "^5.30.1",
|
||||||
"react-web-share": "^2.0.2",
|
|
||||||
"reactour": "^1.19.4",
|
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-thunk": "^3.1.0",
|
|
||||||
"regex-parser": "^2.3.1",
|
"regex-parser": "^2.3.1",
|
||||||
"serve": "^14.2.4",
|
"web-vitals": "^5.2.0",
|
||||||
"styled-components": "^5.3.0",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"web-vitals": "^4.2.4",
|
"zod": "^3.22.4",
|
||||||
"ws": "^8.18.1",
|
"zustand": "^4.5.1"
|
||||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run version && react-scripts build",
|
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||||
"start-dev": "react-scripts start",
|
"start-dev": "vite",
|
||||||
"start": "serve -s build",
|
"dev": "vite",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"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 && react-scripts build",
|
"build-win": "npm run version-win && vite build",
|
||||||
"test": "react-scripts test",
|
"test": "vitest run",
|
||||||
"eject": "react-scripts eject",
|
"test:watch": "vitest",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"commit": "cz"
|
"commit": "cz"
|
||||||
},
|
},
|
||||||
@@ -96,33 +115,49 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.10",
|
"@babel/core": "^7.29.0",
|
||||||
"@babel/preset-env": "^7.26.9",
|
"@babel/preset-env": "^7.29.2",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.28.5",
|
||||||
"@babel/runtime-corejs2": "^7.27.0",
|
"@babel/runtime-corejs2": "^7.29.2",
|
||||||
"autoprefixer": "^10.4.21",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"babel-loader": "^10.0.0",
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"@vitest/ui": "^4.1.5",
|
||||||
|
"autoprefixer": "^10.5.0",
|
||||||
|
"babel-loader": "^9.2.1",
|
||||||
"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",
|
||||||
"dotenv-webpack": "^8.1.0",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"eslint": "^8.57.1",
|
|
||||||
"eslint-plugin-prettier": "^5.2.6",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"lint-staged": "^15.5.1",
|
"jsdom": "^29.0.2",
|
||||||
"mini-css-extract-plugin": "^2.9.2",
|
"lint-staged": "^16.4.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.23",
|
||||||
"prettier": "^3.5.3",
|
"pretty-quick": "^4.2.2",
|
||||||
"pretty-quick": "^4.1.1",
|
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.19",
|
||||||
"terser-webpack-plugin": "^5.3.14",
|
"tmp": "^0.2.7",
|
||||||
"webpack-cli": "^5.1.4"
|
"vite": ">=8.0.16",
|
||||||
|
"vite-plugin-svgr": "^5.2.0",
|
||||||
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
|
"vitest": "^4.1.5"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"nth-check": "$nth-check",
|
"brace-expansion": ">=2.1.2",
|
||||||
"ws": "$ws"
|
"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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
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}`);
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
import React, { useState, useEffect, lazy } from "react";
|
|
||||||
import { Routes, Route, BrowserRouter } from "react-router";
|
|
||||||
import { pdfjs } from "react-pdf";
|
|
||||||
import Login from "./pages/Login";
|
|
||||||
import Form from "./pages/Form";
|
|
||||||
import Report from "./pages/Report";
|
|
||||||
import Dashboard from "./pages/Dashboard";
|
|
||||||
import HomeLayout from "./layout/HomeLayout";
|
|
||||||
import PageNotFound from "./pages/PageNotFound";
|
|
||||||
import ValidateRoute from "./primitives/ValidateRoute";
|
|
||||||
import Validate from "./primitives/Validate";
|
|
||||||
import TemplatePlaceholder from "./pages/TemplatePlaceholder";
|
|
||||||
import SignYourSelf from "./pages/SignyourselfPdf";
|
|
||||||
import DraftDocument from "./components/pdf/DraftDocument";
|
|
||||||
import PlaceHolderSign from "./pages/PlaceHolderSign";
|
|
||||||
import PdfRequestFiles from "./pages/PdfRequestFiles";
|
|
||||||
import LazyPage from "./primitives/LazyPage";
|
|
||||||
import Loader from "./primitives/Loader";
|
|
||||||
import UserList from "./pages/UserList";
|
|
||||||
import { serverUrl_fn } from "./constant/appinfo";
|
|
||||||
import DocSuccessPage from "./pages/DocSuccessPage";
|
|
||||||
import ValidateSession from "./primitives/ValidateSession";
|
|
||||||
const DebugPdf = lazy(() => import("./pages/DebugPdf"));
|
|
||||||
const ForgetPassword = lazy(() => import("./pages/ForgetPassword"));
|
|
||||||
const GuestLogin = lazy(() => import("./pages/GuestLogin"));
|
|
||||||
const ChangePassword = lazy(() => import("./pages/ChangePassword"));
|
|
||||||
const UserProfile = lazy(() => import("./pages/UserProfile"));
|
|
||||||
const Opensigndrive = lazy(() => import("./pages/Opensigndrive"));
|
|
||||||
const ManageSign = lazy(() => import("./pages/Managesign"));
|
|
||||||
const AddAdmin = lazy(() => import("./pages/AddAdmin"));
|
|
||||||
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
|
|
||||||
const Preferences = lazy(() => import("./pages/Preferences"));
|
|
||||||
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
|
|
||||||
const AppLoader = () => {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center items-center h-[100vh]">
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
function App() {
|
|
||||||
const [isloading, setIsLoading] = useState(true);
|
|
||||||
useEffect(() => {
|
|
||||||
handleCredentials();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCredentials = () => {
|
|
||||||
const appId = process.env.REACT_APP_APPID
|
|
||||||
? process.env.REACT_APP_APPID
|
|
||||||
: "opensign";
|
|
||||||
const baseurl = serverUrl_fn();
|
|
||||||
try {
|
|
||||||
localStorage.setItem("baseUrl", `${baseurl}/`);
|
|
||||||
localStorage.setItem("parseAppId", appId);
|
|
||||||
setIsLoading(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("err ", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-base-200">
|
|
||||||
{isloading ? (
|
|
||||||
<AppLoader />
|
|
||||||
) : (
|
|
||||||
<BrowserRouter>
|
|
||||||
<Routes>
|
|
||||||
<Route element={<ValidateRoute />}>
|
|
||||||
<Route exact path="/" element={<Login />} />
|
|
||||||
<Route
|
|
||||||
path="/addadmin"
|
|
||||||
element={<LazyPage Page={AddAdmin} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/upgrade-2.1"
|
|
||||||
element={<LazyPage Page={UpdateExistUserAdmin} />}
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
<Route element={<Validate />}>
|
|
||||||
<Route
|
|
||||||
path="/load/template/:templateId"
|
|
||||||
element={<TemplatePlaceholder />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/load/placeholdersign/:docId"
|
|
||||||
element={<PlaceHolderSign />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/load/recipientSignPdf/:docId/:contactBookId"
|
|
||||||
element={<PdfRequestFiles />}
|
|
||||||
/>
|
|
||||||
</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
|
|
||||||
path="/login/:base64url"
|
|
||||||
element={<LazyPage Page={GuestLogin} />}
|
|
||||||
/>
|
|
||||||
<Route path="/debugpdf" element={<LazyPage Page={DebugPdf} />} />
|
|
||||||
<Route
|
|
||||||
path="/forgetpassword"
|
|
||||||
element={<LazyPage Page={ForgetPassword} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
element={
|
|
||||||
<ValidateSession>
|
|
||||||
<HomeLayout />
|
|
||||||
</ValidateSession>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Route
|
|
||||||
path="/changepassword"
|
|
||||||
element={<LazyPage Page={ChangePassword} />}
|
|
||||||
/>
|
|
||||||
<Route path="/form/:id" element={<Form />} />
|
|
||||||
<Route path="/report/:id" element={<Report />} />
|
|
||||||
<Route path="/dashboard/:id" element={<Dashboard />} />
|
|
||||||
<Route
|
|
||||||
path="/profile"
|
|
||||||
element={<LazyPage Page={UserProfile} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/drive"
|
|
||||||
element={<LazyPage Page={Opensigndrive} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/managesign"
|
|
||||||
element={<LazyPage Page={ManageSign} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/template/:templateId"
|
|
||||||
element={<TemplatePlaceholder />}
|
|
||||||
/>
|
|
||||||
{/* signyouself route with no rowlevel data using docId from url */}
|
|
||||||
<Route path="/signaturePdf/:docId" element={<SignYourSelf />} />
|
|
||||||
{/* draft document route to handle and navigate route page accordiing to document status */}
|
|
||||||
<Route path="/draftDocument" element={<DraftDocument />} />
|
|
||||||
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
|
|
||||||
<Route
|
|
||||||
path="/placeHolderSign/:docId"
|
|
||||||
element={<PlaceHolderSign />}
|
|
||||||
/>
|
|
||||||
{/* for user signature (need your sign route) with row level data */}
|
|
||||||
<Route path="/pdfRequestFiles" element={<PdfRequestFiles />} />
|
|
||||||
{/* for user signature (need your sign route) with no row level data */}
|
|
||||||
<Route
|
|
||||||
path="/pdfRequestFiles/:docId"
|
|
||||||
element={<PdfRequestFiles />}
|
|
||||||
/>
|
|
||||||
{/* recipient signature route with no rowlevel data using docId from url */}
|
|
||||||
<Route
|
|
||||||
path="/recipientSignPdf/:docId/:contactBookId"
|
|
||||||
element={<PdfRequestFiles />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/recipientSignPdf/:docId"
|
|
||||||
element={<PdfRequestFiles />}
|
|
||||||
/>
|
|
||||||
<Route path="/users" element={<UserList />} />
|
|
||||||
<Route
|
|
||||||
path="/preferences"
|
|
||||||
element={<LazyPage Page={Preferences} />}
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
<Route path="/success" element={<DocSuccessPage />} />
|
|
||||||
<Route path="*" element={<PageNotFound />} />
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { lazyWithRetry, hideUpgradeProgress } from "./utils";
|
||||||
|
import { Routes, Route, BrowserRouter } from "react-router";
|
||||||
|
import { pdfjs } from "react-pdf";
|
||||||
|
import Form from "./pages/Form";
|
||||||
|
import Report from "./pages/Report";
|
||||||
|
import Dashboard from "./pages/Dashboard";
|
||||||
|
import HomeLayout from "./layout/HomeLayout";
|
||||||
|
import PageNotFound from "./pages/PageNotFound";
|
||||||
|
import ValidateRoute from "./primitives/ValidateRoute";
|
||||||
|
import Validate from "./primitives/Validate";
|
||||||
|
import TemplatePlaceholder from "./pages/TemplatePlaceholder";
|
||||||
|
import SignYourSelf from "./pages/SignyourselfPdf";
|
||||||
|
import DraftDocument from "./components/pdf/DraftDocument";
|
||||||
|
import PlaceHolderSign from "./pages/PlaceHolderSign";
|
||||||
|
import PdfRequestFiles from "./pages/PdfRequestFiles";
|
||||||
|
import Lazy from "./primitives/LazyPage";
|
||||||
|
import Loader from "./primitives/Loader";
|
||||||
|
import UserList from "./pages/UserList";
|
||||||
|
import { serverUrl_fn } from "./constant/appinfo";
|
||||||
|
import DocSuccessPage from "./pages/DocSuccessPage";
|
||||||
|
import DragProvider from "./components/DragProivder";
|
||||||
|
import Title from "./components/Title";
|
||||||
|
const DebugPdf = lazyWithRetry(() => import("./pages/DebugPdf"));
|
||||||
|
const ForgetPassword = lazyWithRetry(() => import("./pages/ForgetPassword"));
|
||||||
|
const GuestLogin = lazyWithRetry(() => import("./pages/GuestLogin"));
|
||||||
|
const ChangePassword = lazyWithRetry(() => import("./pages/ChangePassword"));
|
||||||
|
const UserProfile = lazyWithRetry(() => import("./pages/UserProfile"));
|
||||||
|
const Opensigndrive = lazyWithRetry(() => import("./pages/Opensigndrive"));
|
||||||
|
const ManageSign = lazyWithRetry(() => import("./pages/Managesign"));
|
||||||
|
const AddAdmin = lazyWithRetry(() => import("./pages/AddAdmin"));
|
||||||
|
const UpdateExistUserAdmin = lazyWithRetry(
|
||||||
|
() => import("./pages/UpdateExistUserAdmin")
|
||||||
|
);
|
||||||
|
const Preferences = lazyWithRetry(() => import("./pages/Preferences"));
|
||||||
|
const Login = lazyWithRetry(() => import("./pages/Login"));
|
||||||
|
const VerifyDocument = lazyWithRetry(() => import("./pages/VerifyDocument"));
|
||||||
|
const EmailBuilder = lazyWithRetry(() => import("./pages/EmailBuilder"));
|
||||||
|
|
||||||
|
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
|
||||||
|
const AppLoader = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-[100vh]">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
function App() {
|
||||||
|
const [isloading, setIsLoading] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
// initialize creds
|
||||||
|
const id = process.env.REACT_APP_APPID ?? "opensign";
|
||||||
|
localStorage.setItem("parseAppId", id);
|
||||||
|
localStorage.setItem("baseUrl", `${serverUrl_fn()}/`);
|
||||||
|
hideUpgradeProgress();
|
||||||
|
localStorage.removeItem("showUpgradeProgress");
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-base-200">
|
||||||
|
{isloading ? (
|
||||||
|
<AppLoader />
|
||||||
|
) : (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Title />
|
||||||
|
<Routes>
|
||||||
|
<Route element={<ValidateRoute />}>
|
||||||
|
<Route exact path="/" element={<Lazy Page={Login} />} />
|
||||||
|
<Route path="/addadmin" element={<Lazy Page={AddAdmin} />} />
|
||||||
|
<Route
|
||||||
|
path="/upgrade-2.1"
|
||||||
|
element={<Lazy Page={UpdateExistUserAdmin} />}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
<Route element={<Validate />}>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/load/recipientSignPdf/:docId/:contactBookId"
|
||||||
|
element={<DragProvider Page={PdfRequestFiles} />}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
<Route
|
||||||
|
path="/login/:base64url"
|
||||||
|
element={<Lazy Page={GuestLogin} />}
|
||||||
|
/>
|
||||||
|
<Route path="/debugpdf" element={<Lazy Page={DebugPdf} />} />
|
||||||
|
<Route
|
||||||
|
path="/forgetpassword"
|
||||||
|
element={<Lazy Page={ForgetPassword} />}
|
||||||
|
/>
|
||||||
|
<Route element={<HomeLayout />}>
|
||||||
|
<Route path="/users" element={<UserList />} />
|
||||||
|
<Route
|
||||||
|
path="/changepassword"
|
||||||
|
element={<Lazy Page={ChangePassword} />}
|
||||||
|
/>
|
||||||
|
<Route path="/form/:id" element={<Form />} />
|
||||||
|
<Route path="/report/:id" element={<Report />} />
|
||||||
|
<Route path="/dashboard/:id" element={<Dashboard />} />
|
||||||
|
<Route path="/profile" element={<Lazy Page={UserProfile} />} />
|
||||||
|
<Route path="/drive" element={<Lazy Page={Opensigndrive} />} />
|
||||||
|
<Route path="/managesign" element={<Lazy Page={ManageSign} />} />
|
||||||
|
<Route
|
||||||
|
path="/template/:templateId"
|
||||||
|
element={<DragProvider Page={TemplatePlaceholder} />}
|
||||||
|
/>
|
||||||
|
{/* signyouself route with no rowlevel data using docId from url */}
|
||||||
|
<Route
|
||||||
|
path="/signaturePdf/:docId"
|
||||||
|
element={<DragProvider Page={SignYourSelf} />}
|
||||||
|
/>
|
||||||
|
{/* draft document route to handle and navigate route page according to document status */}
|
||||||
|
<Route
|
||||||
|
path="/draftDocument"
|
||||||
|
element={<DragProvider Page={DraftDocument} />}
|
||||||
|
/>
|
||||||
|
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
|
||||||
|
<Route
|
||||||
|
path="/placeHolderSign/:docId"
|
||||||
|
element={<DragProvider Page={PlaceHolderSign} />}
|
||||||
|
/>
|
||||||
|
{/* recipient signature route with no rowlevel data using docId from url */}
|
||||||
|
<Route
|
||||||
|
path="/recipientSignPdf/:docId/:contactBookId"
|
||||||
|
element={<DragProvider Page={PdfRequestFiles} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/recipientSignPdf/:docId"
|
||||||
|
element={<DragProvider Page={PdfRequestFiles} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/verify-document"
|
||||||
|
element={<Lazy Page={VerifyDocument} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/preferences"
|
||||||
|
element={<Lazy Page={Preferences} />}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
<Route path="/success" element={<DocSuccessPage />} />
|
||||||
|
<Route path="/emailbuilder" element={<EmailBuilder />} />
|
||||||
|
<Route path="*" element={<PageNotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
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,275 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Parse from "parse";
|
|
||||||
import Title from "./Title";
|
|
||||||
import Loader from "../primitives/Loader";
|
|
||||||
import { copytoData, usertimezone } from "../constant/Utils";
|
|
||||||
import { emailRegex } from "../constant/const";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
function generatePassword(length) {
|
|
||||||
const characters =
|
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
let result = "";
|
|
||||||
const charactersLength = characters.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddUser = (props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [formdata, setFormdata] = useState({
|
|
||||||
name: "",
|
|
||||||
phone: "",
|
|
||||||
email: "",
|
|
||||||
team: "",
|
|
||||||
password: "",
|
|
||||||
role: ""
|
|
||||||
});
|
|
||||||
const [isFormLoader, setIsFormLoader] = useState(false);
|
|
||||||
const [teamList, setTeamList] = useState([]);
|
|
||||||
const role = ["OrgAdmin", "Editor", "User"];
|
|
||||||
useEffect(() => {
|
|
||||||
getTeamList();
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getTeamList = async () => {
|
|
||||||
setFormdata((prev) => ({ ...prev, password: generatePassword(12) }));
|
|
||||||
const teamRes = await Parse.Cloud.run("getteams", { active: true });
|
|
||||||
if (teamRes.length > 0) {
|
|
||||||
const _teamRes = JSON.parse(JSON.stringify(teamRes));
|
|
||||||
setTeamList(_teamRes);
|
|
||||||
const allUserId =
|
|
||||||
_teamRes.find((x) => x.Name === "All Users")?.objectId || "";
|
|
||||||
setFormdata((prev) => ({ ...prev, team: allUserId }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const checkUserExist = async () => {
|
|
||||||
try {
|
|
||||||
const res = await Parse.Cloud.run("getUserDetails", {
|
|
||||||
email: formdata.email
|
|
||||||
});
|
|
||||||
if (res) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log("err", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Define a function to handle form submission
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!emailRegex.test(formdata.email)) {
|
|
||||||
alert("Please enter a valid email address.");
|
|
||||||
} else {
|
|
||||||
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
|
||||||
setIsFormLoader(true);
|
|
||||||
const res = await checkUserExist();
|
|
||||||
if (res) {
|
|
||||||
props.showAlert("danger", t("user-already-exist"));
|
|
||||||
setIsFormLoader(false);
|
|
||||||
} else {
|
|
||||||
if (localStorage.getItem("TenantId")) {
|
|
||||||
const timezone = usertimezone;
|
|
||||||
try {
|
|
||||||
const params = {
|
|
||||||
name: formdata.name,
|
|
||||||
email: formdata.email,
|
|
||||||
phone: formdata.phone,
|
|
||||||
password: formdata.password,
|
|
||||||
role: formdata.role,
|
|
||||||
team: formdata.team,
|
|
||||||
timezone: timezone,
|
|
||||||
tenantId: localStorage.getItem("TenantId"),
|
|
||||||
organization: {
|
|
||||||
objectId: localUser?.OrganizationId?.objectId,
|
|
||||||
company: localUser?.Company
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const res = await Parse.Cloud.run("adduser", params);
|
|
||||||
const parseData = JSON.parse(JSON.stringify(res));
|
|
||||||
console.log("parseData ", parseData);
|
|
||||||
if (props.closePopup) {
|
|
||||||
props.closePopup();
|
|
||||||
}
|
|
||||||
if (props.handleUserData) {
|
|
||||||
if (formdata?.team) {
|
|
||||||
const team = teamList.find((x) => x.objectId === formdata.team);
|
|
||||||
parseData.TeamIds = parseData.TeamIds.map((y) =>
|
|
||||||
y.objectId === team.objectId ? team : y
|
|
||||||
);
|
|
||||||
}
|
|
||||||
props.handleUserData(parseData);
|
|
||||||
}
|
|
||||||
setIsFormLoader(false);
|
|
||||||
setFormdata({
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
phone: "",
|
|
||||||
team: "",
|
|
||||||
role: ""
|
|
||||||
});
|
|
||||||
props.showAlert("success", t("user-created-successfully"));
|
|
||||||
} catch (err) {
|
|
||||||
console.log("err", err);
|
|
||||||
setIsFormLoader(false);
|
|
||||||
props.showAlert("danger", t("something-went-wrong-mssg"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
props.showAlert("danger", t("something-went-wrong-mssg"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define a function to handle the "add yourself" checkbox
|
|
||||||
const handleReset = () => {
|
|
||||||
setFormdata({ name: "", email: "", phone: "", team: "", role: "" });
|
|
||||||
if (props.closePopup) {
|
|
||||||
props.closePopup();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleChange = (event) => {
|
|
||||||
let { name, value } = event.target;
|
|
||||||
if (name === "email") {
|
|
||||||
value = value?.toLowerCase()?.replace(/\s/g, "");
|
|
||||||
}
|
|
||||||
setFormdata((prev) => ({ ...prev, [name]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const copytoclipboard = (text) => {
|
|
||||||
copytoData(text);
|
|
||||||
props.showAlert("success", t("copied"));
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
|
|
||||||
<Title title={t("add-user")} />
|
|
||||||
{isFormLoader && (
|
|
||||||
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="w-full mx-auto">
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="mb-3">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block text-xs text-gray-700 font-semibold"
|
|
||||||
>
|
|
||||||
{t("name")}
|
|
||||||
<span className="text-[red] text-[13px]"> *</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
value={formdata.name}
|
|
||||||
onChange={(e) => handleChange(e)}
|
|
||||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
|
||||||
onInput={(e) => e.target.setCustomValidity("")}
|
|
||||||
required
|
|
||||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mb-3">
|
|
||||||
<label
|
|
||||||
htmlFor="email"
|
|
||||||
className="block text-xs text-gray-700 font-semibold"
|
|
||||||
>
|
|
||||||
{t("email")}
|
|
||||||
<span className="text-[red] text-[13px]"> *</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
value={formdata.email}
|
|
||||||
onChange={(e) => handleChange(e)}
|
|
||||||
required
|
|
||||||
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 className="block text-xs text-gray-700 font-semibold">
|
|
||||||
{t("password")}
|
|
||||||
</label>
|
|
||||||
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
|
|
||||||
<div className="break-all">{formdata?.password}</div>
|
|
||||||
<i
|
|
||||||
onClick={() => copytoclipboard(formdata?.password)}
|
|
||||||
className="fa-light fa-copy rounded-full hover:bg-base-300 p-[8px] cursor-pointer "
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
|
|
||||||
{t("password-generateed")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-3">
|
|
||||||
<label
|
|
||||||
htmlFor="phone"
|
|
||||||
className="block text-xs text-gray-700 font-semibold"
|
|
||||||
>
|
|
||||||
{t("phone")}
|
|
||||||
{/* <span className="text-[red] text-[13px]"> *</span> */}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="phone"
|
|
||||||
placeholder={t("phone-optional")}
|
|
||||||
value={formdata.phone}
|
|
||||||
onChange={(e) => handleChange(e)}
|
|
||||||
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 text-gray-700 font-semibold"
|
|
||||||
>
|
|
||||||
{t("Role")}
|
|
||||||
<span className="text-[red] text-[13px]"> *</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={formdata.role}
|
|
||||||
onChange={(e) => handleChange(e)}
|
|
||||||
name="role"
|
|
||||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
|
|
||||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
|
||||||
onInput={(e) => e.target.setCustomValidity("")}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option defaultValue={""} value={""}>
|
|
||||||
{t("Select")}
|
|
||||||
</option>
|
|
||||||
{role.length > 0 &&
|
|
||||||
role.map((x) => (
|
|
||||||
<option key={x} value={x}>
|
|
||||||
{x}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center mt-3 gap-2 text-white">
|
|
||||||
<button type="submit" className="op-btn op-btn-primary">
|
|
||||||
{t("submit")}
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleReset()}
|
|
||||||
className="op-btn op-btn-secondary"
|
|
||||||
>
|
|
||||||
{t("cancel")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddUser;
|
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Parse from "parse";
|
||||||
|
import Loader from "../primitives/Loader";
|
||||||
|
import {
|
||||||
|
copytoData,
|
||||||
|
usertimezone
|
||||||
|
} from "../constant/Utils";
|
||||||
|
import {
|
||||||
|
emailRegex,
|
||||||
|
} from "../constant/const";
|
||||||
|
import {
|
||||||
|
useTranslation
|
||||||
|
} from "react-i18next";
|
||||||
|
import { withSessionValidation } from "../utils";
|
||||||
|
|
||||||
|
function generatePassword(length) {
|
||||||
|
const characters =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
let result = "";
|
||||||
|
const charactersLength = characters.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddUser = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [formdata, setFormdata] = useState({
|
||||||
|
name: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
team: "",
|
||||||
|
password: "",
|
||||||
|
role: ""
|
||||||
|
});
|
||||||
|
const [isFormLoader, setIsFormLoader] = useState(false);
|
||||||
|
const [teamList, setTeamList] = useState([]);
|
||||||
|
const role = ["OrgAdmin", "Editor", "User"];
|
||||||
|
useEffect(() => {
|
||||||
|
getTeamList();
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getTeamList = async () => {
|
||||||
|
setFormdata((prev) => ({ ...prev, password: generatePassword(12) }));
|
||||||
|
const teamRes = await Parse.Cloud.run("getteams", { active: true });
|
||||||
|
if (teamRes.length > 0) {
|
||||||
|
const _teamRes = JSON.parse(JSON.stringify(teamRes));
|
||||||
|
setTeamList(_teamRes);
|
||||||
|
const allUserId =
|
||||||
|
_teamRes.find((x) => x.Name === "All Users")?.objectId || "";
|
||||||
|
setFormdata((prev) => ({ ...prev, team: allUserId }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const checkUserExist = async () => {
|
||||||
|
try {
|
||||||
|
const res = await Parse.Cloud.run("getUserDetails", {
|
||||||
|
email: formdata.email
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("err", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Define a function to handle form submission
|
||||||
|
const handleSubmit = withSessionValidation(async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!emailRegex.test(formdata.email)) {
|
||||||
|
alert(t("valid-email-alert"));
|
||||||
|
} else {
|
||||||
|
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||||
|
setIsFormLoader(true);
|
||||||
|
const res = await checkUserExist();
|
||||||
|
if (res) {
|
||||||
|
props.showAlert("danger", t("user-already-exist"));
|
||||||
|
setIsFormLoader(false);
|
||||||
|
} else {
|
||||||
|
if (localStorage.getItem("TenantId")) {
|
||||||
|
const timezone = usertimezone;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
name: formdata.name,
|
||||||
|
email: formdata.email,
|
||||||
|
phone: formdata.phone,
|
||||||
|
password: formdata.password,
|
||||||
|
role: formdata.role,
|
||||||
|
team: formdata.team,
|
||||||
|
timezone: timezone,
|
||||||
|
tenantId: localStorage.getItem("TenantId"),
|
||||||
|
organization: {
|
||||||
|
objectId: localUser?.OrganizationId?.objectId,
|
||||||
|
company: localUser?.Company
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = await Parse.Cloud.run("adduser", params);
|
||||||
|
const parseData = JSON.parse(JSON.stringify(res));
|
||||||
|
if (props.closePopup) {
|
||||||
|
props.closePopup();
|
||||||
|
}
|
||||||
|
if (props.handleUserData) {
|
||||||
|
if (formdata?.team) {
|
||||||
|
const team = teamList.find((x) => x.objectId === formdata.team);
|
||||||
|
parseData.TeamIds = parseData.TeamIds.map((y) =>
|
||||||
|
y.objectId === team.objectId ? team : y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
props.handleUserData(parseData);
|
||||||
|
}
|
||||||
|
setIsFormLoader(false);
|
||||||
|
setFormdata({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
team: "",
|
||||||
|
role: ""
|
||||||
|
});
|
||||||
|
props.showAlert("success", t("user-created-successfully"));
|
||||||
|
} catch (err) {
|
||||||
|
console.log("err", err);
|
||||||
|
setIsFormLoader(false);
|
||||||
|
props.showAlert("danger", t("something-went-wrong-mssg"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
props.showAlert("danger", t("something-went-wrong-mssg"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define a function to handle the "add yourself" checkbox
|
||||||
|
const handleReset = () => {
|
||||||
|
setFormdata({ name: "", email: "", phone: "", team: "", role: "" });
|
||||||
|
if (props.closePopup) {
|
||||||
|
props.closePopup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleChange = (event) => {
|
||||||
|
let { name, value } = event.target;
|
||||||
|
if (name === "email") {
|
||||||
|
value = value?.toLowerCase()?.replace(/\s/g, "");
|
||||||
|
}
|
||||||
|
setFormdata((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const copytoclipboard = (text) => {
|
||||||
|
copytoData(text);
|
||||||
|
props.showAlert("success", t("copied"));
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
|
||||||
|
{isFormLoader && (
|
||||||
|
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="w-full mx-auto">
|
||||||
|
<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"
|
||||||
|
name="name"
|
||||||
|
value={formdata.name}
|
||||||
|
onChange={(e) => handleChange(e)}
|
||||||
|
onInvalid={(e) =>
|
||||||
|
e.target.setCustomValidity(t("input-required"))
|
||||||
|
}
|
||||||
|
onInput={(e) => e.target.setCustomValidity("")}
|
||||||
|
required
|
||||||
|
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||||
|
placeholder={t("enter-name")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-xs font-semibold"
|
||||||
|
>
|
||||||
|
{t("email")}
|
||||||
|
<span className="text-[red] text-[13px]"> *</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={formdata.email}
|
||||||
|
onChange={(e) => handleChange(e)}
|
||||||
|
required
|
||||||
|
onInvalid={(e) =>
|
||||||
|
e.target.setCustomValidity(t("input-required"))
|
||||||
|
}
|
||||||
|
onInput={(e) => e.target.setCustomValidity("")}
|
||||||
|
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||||
|
placeholder={t("enter-email")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="block text-xs font-semibold">
|
||||||
|
{t("password")}
|
||||||
|
</label>
|
||||||
|
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
|
||||||
|
<div className="break-all">{formdata?.password}</div>
|
||||||
|
<i
|
||||||
|
onClick={() => copytoclipboard(formdata?.password)}
|
||||||
|
className="fa-light fa-copy rounded-full hover:bg-base-300 p-[8px] cursor-pointer "
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
|
||||||
|
{t("password-generated")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<label
|
||||||
|
htmlFor="phone"
|
||||||
|
className="block text-xs font-semibold"
|
||||||
|
>
|
||||||
|
{t("phone")}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="phone"
|
||||||
|
placeholder={t("phone-optional")}
|
||||||
|
value={formdata.phone}
|
||||||
|
onChange={(e) => handleChange(e)}
|
||||||
|
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("Role")}
|
||||||
|
<span className="text-[red] text-[13px]"> *</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formdata.role}
|
||||||
|
onChange={(e) => handleChange(e)}
|
||||||
|
name="role"
|
||||||
|
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||||
|
onInvalid={(e) =>
|
||||||
|
e.target.setCustomValidity(t("input-required"))
|
||||||
|
}
|
||||||
|
onInput={(e) => e.target.setCustomValidity("")}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option defaultValue={""} value={""}>
|
||||||
|
{t("Select")}
|
||||||
|
</option>
|
||||||
|
{role.length > 0 &&
|
||||||
|
role.map((x) => (
|
||||||
|
<option key={x} value={x}>
|
||||||
|
{x}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center mt-3 gap-2 text-white">
|
||||||
|
<button type="submit" className="op-btn op-btn-primary">
|
||||||
|
{t("submit")}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleReset()}
|
||||||
|
className="op-btn op-btn-secondary"
|
||||||
|
>
|
||||||
|
{t("cancel")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddUser;
|
||||||
@@ -1,286 +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 atleast 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;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import dp from "../assets/images/dp.png";
|
import dp from "../assets/images/dp.png";
|
||||||
import FullScreenButton from "./FullScreenButton";
|
import FullScreenButton from "./FullScreenButton";
|
||||||
|
import ThemeToggle from "./ThemeToggle";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import Parse from "parse";
|
import Parse from "parse";
|
||||||
import { useWindowSize } from "../hook/useWindowSize";
|
import { useWindowSize } from "../hook/useWindowSize";
|
||||||
@@ -11,43 +12,63 @@ 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
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function initializeHead() {
|
useEffect(() => {
|
||||||
const applogo = await getAppLogo();
|
closeSidebar();
|
||||||
if (applogo?.logo) {
|
}, [width]);
|
||||||
setAppLogo(applogo?.logo);
|
|
||||||
} else {
|
|
||||||
const logo = localStorage.getItem("appLogo") || appInfo.applogo;
|
|
||||||
setAppLogo(logo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeDropdown = async () => {
|
const showSidebar = () => {
|
||||||
|
dispatch(toggleSidebar());
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
async function initializeHead() {
|
||||||
|
const applogo = await getAppLogo();
|
||||||
|
if (applogo?.logo) {
|
||||||
|
setAppLogo(applogo?.logo);
|
||||||
|
} else {
|
||||||
|
const logo = localStorage.getItem("appLogo") || appInfo.applogo;
|
||||||
|
setAppLogo(logo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleLogout = 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");
|
||||||
@@ -55,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);
|
||||||
@@ -64,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("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,9 +107,30 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateThemeStatus = () => {
|
||||||
|
const isDarkTheme =
|
||||||
|
document.documentElement.getAttribute("data-theme") === "opensigndark";
|
||||||
|
setIsDarkTheme(isDarkTheme);
|
||||||
|
};
|
||||||
|
updateThemeStatus();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
updateThemeStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["data-theme"]
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
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"
|
||||||
@@ -96,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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -108,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
|
||||||
@@ -146,21 +197,21 @@ 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-52 ${
|
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"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{!isConsole && (
|
{!isConsole && (
|
||||||
<>
|
<>
|
||||||
<li
|
<li
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openInNewTab("https://docs.opensignlabs.com")
|
openInNewTab("https://docs.opensignlabs.com")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<i className="fa-light fa-book"></i> {t("docs")}
|
<i className="fa-light fa-book"></i> {t("docs")}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
@@ -171,20 +222,41 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
|||||||
<i className="fa-light fa-user"></i> {t("profile")}
|
<i className="fa-light fa-user"></i> {t("profile")}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
navigate("/changepassword");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<i className="fa-light fa-lock"></i>{" "}
|
||||||
|
{t("change-password")}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
navigate("/changepassword");
|
navigate("/verify-document");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<i className="fa-light fa-lock"></i>{" "}
|
<i className="fa-light fa-check-square"></i>{" "}
|
||||||
{t("change-password")}
|
{t("verify-document")}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>
|
||||||
|
<i className="fa-light fa-moon"></i>
|
||||||
|
{t("dark-mode")}
|
||||||
|
<span className="text-[10px] font-semibold bg-base-300 text-base-content px-1 rounded-md">
|
||||||
|
BETA
|
||||||
|
</span>
|
||||||
|
<ThemeToggle />
|
||||||
</span>
|
</span>
|
||||||
</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")}
|
||||||
@@ -194,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>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const ThemeToggle = () => {
|
||||||
|
const [isDark, setIsDark] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedTheme = localStorage.getItem("theme");
|
||||||
|
if (storedTheme === "dark") {
|
||||||
|
setIsDark(true);
|
||||||
|
document.documentElement.setAttribute("data-theme", "opensigndark");
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute("data-theme", "opensigncss");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
const newTheme = !isDark;
|
||||||
|
setIsDark(newTheme);
|
||||||
|
if (newTheme) {
|
||||||
|
document.documentElement.setAttribute("data-theme", "opensigndark");
|
||||||
|
localStorage.setItem("theme", "dark");
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute("data-theme", "opensigncss");
|
||||||
|
localStorage.setItem("theme", "light");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
id="dark-mode-toggle"
|
||||||
|
type="checkbox"
|
||||||
|
className="op-toggle checked:[--tglbg:#3368ff] transition-all checked:bg-white"
|
||||||
|
checked={isDark}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeToggle;
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Helmet } from "react-helmet";
|
|
||||||
|
|
||||||
function Title({ title, drive }) {
|
|
||||||
const appName =
|
|
||||||
"OpenSign™";
|
|
||||||
return (
|
|
||||||
<Helmet>
|
|
||||||
<title>{drive ? title : `${title} - ${appName}`}</title>
|
|
||||||
<meta name="description" content={`${title} - ${appName}`} />
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
href={localStorage.getItem("fev_Icon")}
|
|
||||||
sizes="40x40"
|
|
||||||
/>
|
|
||||||
</Helmet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Title;
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { useLocation, matchPath } from "react-router";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useManifestUrl } from "../hook/useManifestUrl";
|
||||||
|
|
||||||
|
const TITLE_MAP = {
|
||||||
|
"/": "login",
|
||||||
|
// Homelayout
|
||||||
|
"/dashboard/35KBoSgoAK": "Dashboard",
|
||||||
|
"/form/sHAnZphf69": "Sign Yourself",
|
||||||
|
"/form/8mZzFxbG1z": "Request Signatures",
|
||||||
|
"/form/template": "New Template",
|
||||||
|
"/report/6TeaPr321t": "Templates",
|
||||||
|
"/report/4Hhwbp482K": "Need your sign",
|
||||||
|
"/report/1MwEuxLEkF": "In Progress",
|
||||||
|
"/report/kQUoW4hUXz": "Completed",
|
||||||
|
"/report/ByHuevtCFY": "Drafts",
|
||||||
|
"/report/UPr2Fm5WY3": "Declined",
|
||||||
|
"/report/zNqBHXHsYH": "Expired",
|
||||||
|
"/report/contacts": "Contactbook",
|
||||||
|
"/drive": "Drive",
|
||||||
|
"/managesign": "My Signature",
|
||||||
|
"/preferences": "Preferences",
|
||||||
|
"/users": "Users",
|
||||||
|
"/profile": "profile",
|
||||||
|
"/changepassword": "change-password",
|
||||||
|
"/verify-document": "verify-document",
|
||||||
|
|
||||||
|
"/signaturePdf/:docId": "Sign Yourself",
|
||||||
|
"/placeHolderSign/:docId": "Request Signatures",
|
||||||
|
"/template/:templateId": "New Template",
|
||||||
|
"/recipientSignPdf/:docId": "Request Signatures",
|
||||||
|
"/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
|
||||||
|
"/load/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
|
||||||
|
|
||||||
|
// alone
|
||||||
|
"/debugpdf": "Debug Pdf",
|
||||||
|
"/forgetpassword": "forgot-password",
|
||||||
|
"/success": "success",
|
||||||
|
"/addadmin": "add-admin",
|
||||||
|
"/upgrade-2.1": "add-admin",
|
||||||
|
"/draftDocument": "New Document",
|
||||||
|
"/login/:base64url": "Request Signatures",
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveTitle(pathname, override) {
|
||||||
|
if (override) return override;
|
||||||
|
for (let [pattern, label] of Object.entries(TITLE_MAP)) {
|
||||||
|
if (matchPath({ path: pattern, end: true }, pathname)) {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Title() {
|
||||||
|
const { pathname, state } = useLocation();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
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;
|
||||||
@@ -29,7 +29,7 @@ const DashboardButton = (props) => {
|
|||||||
: "cursor-default"
|
: "cursor-default"
|
||||||
} w-full shadow-md px-3 py-2 op-card bg-base-100`}
|
} w-full shadow-md px-3 py-2 op-card bg-base-100`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center text-base-content">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center">
|
<span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center">
|
||||||
<i
|
<i
|
||||||
@@ -39,7 +39,7 @@ const DashboardButton = (props) => {
|
|||||||
></i>
|
></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg ml-3 text-base-content">
|
<div className="text-lg ml-3">
|
||||||
{t(`sidebar.${props.Label}`)}
|
{t(`sidebar.${props.Label}`)}
|
||||||
{props.Label === "Sign yourself" && (
|
{props.Label === "Sign yourself" && (
|
||||||
<div className="text-gray-500 text-xs mt-1">
|
<div className="text-gray-500 text-xs mt-1">
|
||||||
@@ -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,158 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Parse from "parse";
|
|
||||||
import ReportTable from "../../primitives/GetReportDisplay";
|
|
||||||
import reportJson from "../../json/ReportJson";
|
|
||||||
import axios from "axios";
|
|
||||||
import Loader from "../../primitives/Loader";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
function DashboardReport(props) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
// console.log("props ", props)
|
|
||||||
const [List, setList] = useState([]);
|
|
||||||
const [isLoader, setIsLoader] = useState(true);
|
|
||||||
const [reportName, setReportName] = useState("");
|
|
||||||
const [actions, setActions] = useState([]);
|
|
||||||
const [heading, setHeading] = useState([]);
|
|
||||||
const [isNextRecord, setIsNextRecord] = useState(false);
|
|
||||||
const [isMoreDocs, setIsMoreDocs] = useState(true);
|
|
||||||
const abortController = new AbortController();
|
|
||||||
const docPerPage = 5;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setReportName("");
|
|
||||||
getReportData(props.Record.reportId);
|
|
||||||
|
|
||||||
// Function returned from useEffect is called on unmount
|
|
||||||
return () => {
|
|
||||||
setIsLoader(true);
|
|
||||||
setList([]);
|
|
||||||
setIsNextRecord(false);
|
|
||||||
// Here it'll abort the fetch
|
|
||||||
abortController.abort();
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [props.Record.reportId]);
|
|
||||||
|
|
||||||
// below useEffect call when isNextRecord state is true and fetch next record
|
|
||||||
useEffect(() => {
|
|
||||||
if (isNextRecord) {
|
|
||||||
getReportData(props.Record.reportId, List.length, 20);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [isNextRecord]);
|
|
||||||
|
|
||||||
const getReportData = async (id, skipUserRecord = 0, limit = 20) => {
|
|
||||||
setIsLoader(true);
|
|
||||||
const json = reportJson(id);
|
|
||||||
if (json) {
|
|
||||||
setActions(json.actions);
|
|
||||||
setReportName(json.reportName);
|
|
||||||
setHeading(json.heading);
|
|
||||||
const currentUser = Parse.User.current().id;
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
|
||||||
sessiontoken: localStorage.getItem("accesstoken")
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
|
||||||
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
|
||||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
|
||||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
|
||||||
const res = await axios.post(url, params, {
|
|
||||||
headers: headers,
|
|
||||||
signal: abortController.signal // is used to cancel fetch query
|
|
||||||
});
|
|
||||||
if (id === "5Go51Q7T8r") {
|
|
||||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
|
||||||
let arr = [];
|
|
||||||
for (const obj of listData) {
|
|
||||||
const isSigner = obj.Signers.some(
|
|
||||||
(item) => item.UserId.objectId === currentUser
|
|
||||||
);
|
|
||||||
if (isSigner) {
|
|
||||||
let isRecord;
|
|
||||||
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
|
||||||
isRecord = obj?.AuditTrail.some(
|
|
||||||
(item) =>
|
|
||||||
item?.UserPtr?.UserId?.objectId === currentUser &&
|
|
||||||
item.Activity === "Signed"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
isRecord = false;
|
|
||||||
}
|
|
||||||
if (isRecord === false) {
|
|
||||||
arr.push(obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (arr.length === docPerPage) {
|
|
||||||
setIsMoreDocs(true);
|
|
||||||
} else {
|
|
||||||
setIsMoreDocs(false);
|
|
||||||
}
|
|
||||||
setList((prevRecord) =>
|
|
||||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (res.data.result.length >= docPerPage) {
|
|
||||||
setIsMoreDocs(true);
|
|
||||||
} else {
|
|
||||||
setIsMoreDocs(false);
|
|
||||||
}
|
|
||||||
setIsNextRecord(false);
|
|
||||||
if (!res.data.result.error) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setIsLoader(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isLoader ? (
|
|
||||||
<div className="h-[250px] flex justify-center items-center">
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{reportName ? (
|
|
||||||
<ReportTable
|
|
||||||
ReportName={reportName}
|
|
||||||
List={List}
|
|
||||||
setList={setList}
|
|
||||||
actions={actions}
|
|
||||||
heading={heading}
|
|
||||||
setIsNextRecord={setIsNextRecord}
|
|
||||||
isMoreDocs={isMoreDocs}
|
|
||||||
docPerPage={docPerPage}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-xl text-black">{t("report-not-found")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DashboardReport;
|
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import Parse from "parse";
|
||||||
|
import DocumentsReport from "../../reports/document/DocumentsReport";
|
||||||
|
import reportJson from "../../json/ReportJson";
|
||||||
|
import axios from "axios";
|
||||||
|
import Loader from "../../primitives/Loader";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { withSessionValidation } from "../../utils";
|
||||||
|
function DashboardReport(props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [List, setList] = useState([]);
|
||||||
|
const [isLoader, setIsLoader] = useState(true);
|
||||||
|
const [reportName, setReportName] = useState("");
|
||||||
|
const [actions, setActions] = useState([]);
|
||||||
|
const [heading, setHeading] = useState([]);
|
||||||
|
const [isNextRecord, setIsNextRecord] = useState(false);
|
||||||
|
const [isMoreDocs, setIsMoreDocs] = useState(true);
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const docPerPage = 5;
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
|
||||||
|
const [isSearchResult, setIsSearchResult] = useState(false);
|
||||||
|
const debounceTimer = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setReportName("");
|
||||||
|
setSearchTerm("");
|
||||||
|
setMobileSearchOpen(false);
|
||||||
|
getReportData(props.Record.reportId, 0, 20, "");
|
||||||
|
|
||||||
|
// Function returned from useEffect is called on unmount
|
||||||
|
return () => {
|
||||||
|
setIsLoader(true);
|
||||||
|
setList([]);
|
||||||
|
setIsNextRecord(false);
|
||||||
|
// Here it'll abort the fetch
|
||||||
|
abortController.abort();
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [props.Record.reportId]);
|
||||||
|
|
||||||
|
// below useEffect call when isNextRecord state is true and fetch next record
|
||||||
|
useEffect(() => {
|
||||||
|
if (isNextRecord) {
|
||||||
|
getReportData(props.Record.reportId, List.length, 20, searchTerm);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [isNextRecord]);
|
||||||
|
|
||||||
|
const handleSearchChange = withSessionValidation(async (e) => {
|
||||||
|
const term = e.target.value.toLowerCase();
|
||||||
|
setSearchTerm(term);
|
||||||
|
if (debounceTimer.current) {
|
||||||
|
clearTimeout(debounceTimer.current);
|
||||||
|
}
|
||||||
|
debounceTimer.current = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||||
|
sessiontoken: localStorage.getItem("accesstoken")
|
||||||
|
};
|
||||||
|
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||||
|
const res = await axios.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
reportId: props.Record.reportId,
|
||||||
|
searchTerm: term,
|
||||||
|
skip: 0,
|
||||||
|
limit: docPerPage
|
||||||
|
},
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
const data = res.data?.result || [];
|
||||||
|
if (!data.error) {
|
||||||
|
setList(data);
|
||||||
|
setIsMoreDocs(data.length >= docPerPage);
|
||||||
|
setIsNextRecord(false);
|
||||||
|
setIsSearchResult(true);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Search error:", err);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
setIsSearchResult(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSearchPaste = (e) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
handleSearchChange({ target: { value: e.target.value } });
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (debounceTimer.current) {
|
||||||
|
clearTimeout(debounceTimer.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getReportData = withSessionValidation(
|
||||||
|
async (id, skipUserRecord = 0, limit = 20, term = searchTerm) => {
|
||||||
|
setIsLoader(true);
|
||||||
|
const json = reportJson(id);
|
||||||
|
if (json) {
|
||||||
|
setActions(json.actions);
|
||||||
|
setReportName(json.reportName);
|
||||||
|
setHeading(json.heading);
|
||||||
|
const currentUser = Parse.User.current().id;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||||
|
sessiontoken: localStorage.getItem("accesstoken")
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
||||||
|
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
||||||
|
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||||
|
if (term) {
|
||||||
|
params.searchTerm = term;
|
||||||
|
}
|
||||||
|
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||||
|
const res = await axios.post(url, params, {
|
||||||
|
headers: headers,
|
||||||
|
signal: abortController.signal // is used to cancel fetch query
|
||||||
|
});
|
||||||
|
if (id === "5Go51Q7T8r") {
|
||||||
|
const listData = res.data?.result.filter(
|
||||||
|
(x) => x.Signers.length > 0
|
||||||
|
);
|
||||||
|
let arr = [];
|
||||||
|
for (const obj of listData) {
|
||||||
|
const isSigner = obj.Signers?.some(
|
||||||
|
(item) => item.UserId.objectId === currentUser
|
||||||
|
);
|
||||||
|
if (isSigner) {
|
||||||
|
let isRecord;
|
||||||
|
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
||||||
|
isRecord = obj?.AuditTrail.some(
|
||||||
|
(item) =>
|
||||||
|
item?.UserPtr?.UserId?.objectId === currentUser &&
|
||||||
|
item.Activity === "Signed"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
isRecord = false;
|
||||||
|
}
|
||||||
|
if (isRecord === false) {
|
||||||
|
arr.push(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (arr.length === docPerPage) {
|
||||||
|
setIsMoreDocs(true);
|
||||||
|
} else {
|
||||||
|
setIsMoreDocs(false);
|
||||||
|
}
|
||||||
|
setList((prevRecord) =>
|
||||||
|
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (res.data.result.length >= docPerPage) {
|
||||||
|
setIsMoreDocs(true);
|
||||||
|
} else {
|
||||||
|
setIsMoreDocs(false);
|
||||||
|
}
|
||||||
|
setIsNextRecord(false);
|
||||||
|
if (!res.data.result.error) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsLoader(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoader ? (
|
||||||
|
<div className="h-[250px] flex justify-center items-center">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{reportName ? (
|
||||||
|
<DocumentsReport
|
||||||
|
ReportName={reportName}
|
||||||
|
List={List}
|
||||||
|
setList={setList}
|
||||||
|
actions={actions}
|
||||||
|
heading={heading}
|
||||||
|
setIsNextRecord={setIsNextRecord}
|
||||||
|
isMoreDocs={isMoreDocs}
|
||||||
|
docPerPage={docPerPage}
|
||||||
|
mobileSearchOpen={mobileSearchOpen}
|
||||||
|
setMobileSearchOpen={setMobileSearchOpen}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
handleSearchChange={handleSearchChange}
|
||||||
|
handleSearchPaste={handleSearchPaste}
|
||||||
|
isSearchResult={isSearchResult}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded-box">
|
||||||
|
<div className="text-center text-xl text-base-content">
|
||||||
|
{t("report-not-found")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardReport;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||