Compare commits

...
35 Commits
Author SHA1 Message Date
prafull-opensignlabsandGitHub 7204183542 Merge pull request #1804 from OpenSignLabs/updates-15754544632
fix: resolve issues
2025-06-19 15:03:08 +05:30
nxglabs 5561673fdc Merge pull request #866 from nxglabs/fix_main_issues 2025-06-19 09:31:17 +00:00
prafull-opensignlabsandGitHub 0df44062d6 Merge pull request #1803 from OpenSignLabs/updates-15736278136
fix: correct baseapi of docxtopdf and decryptpdf
2025-06-18 20:25:53 +05:30
nxglabs 8983440763 Merge pull request #864 from nxglabs/staging 2025-06-18 14:54:13 +00:00
prafull-opensignlabsandGitHub 5a28c0ed06 Merge pull request #1802 from OpenSignLabs/updates-15735030679
v2.25.0
2025-06-18 19:43:43 +05:30
nxglabs cbca22c308 Merge pull request #861 from nxglabs/staging
fix: handle password protected pdf
2025-06-18 14:03:05 +00:00
prafull-opensignlabsandGitHub b0e0b38e8f Merge pull request #1789 from OpenSignLabs/updates-15483575034
v2.24.1
2025-06-06 11:17:12 +05:30
prafull-opensignlabs 49c23fb52b Merge pull request #753 from OpenSignLabs/staging
v2.24.1
2025-06-06 05:33:37 +00:00
prafull-opensignlabsandGitHub da4b604710 Merge pull request #1785 from OpenSignLabs/updates-15425322075
handle crashing pattern for text input widgets
2025-06-04 00:17:30 +05:30
prafull-opensignlabs 82d518b3c9 Merge pull request #739 from OpenSignLabs/staging
disable signing request email when send_email flag is false
2025-06-03 18:45:49 +00:00
prafull-opensignlabsandGitHub dcbde2b661 Merge pull request #1784 from OpenSignLabs/updates-15419310132
v2.23.1
2025-06-03 19:28:35 +05:30
prafull-opensignlabs 27b0b426ad v2.23.1
Enable widget placement on click in desktop view
Disable signature types not working
Change icon of agreement sign checkbox
Update dependencies
2025-06-03 13:55:57 +00:00
prafull-opensignlabsandGitHub 8788afaa8e Merge pull request #1766 from OpenSignLabs/updates-15350472621
fix: add verify-document route
2025-05-30 21:19:35 +05:30
prafull-opensignlabs 428aa65b38 Merge pull request #710 from OpenSignLabs/staging
fix: add verify-document route
2025-05-30 15:45:27 +00:00
AmolandGitHub 295d942427 feat: add deploy to digitalocean button 2025-05-30 20:33:43 +05:30
prafull-opensignlabsandGitHub c4e28e6e6e Merge pull request #1764 from OpenSignLabs/updates-15349515719
v2.23.0
2025-05-30 20:26:41 +05:30
prafull-opensignlabs b19fb29b82 Merge pull request #708 from OpenSignLabs/staging
refactor: unnecessary code
2025-05-30 14:54:53 +00:00
prafull-opensignlabsandGitHub 3d223ba343 Merge pull request #1756 from OpenSignLabs/updates-15275255429
v2.22.0
2025-05-27 18:13:12 +05:30
prafull-opensignlabs 98754c776f Merge pull request #675 from OpenSignLabs/staging
v2.22.0
2025-05-27 12:28:40 +00:00
prafull-opensignlabsandGitHub 1550e3a3d0 Merge pull request #1748 from OpenSignLabs/updates-15156338467
v2.21.2
2025-05-21 13:09:14 +05:30
prafull-opensignlabs 3500883205 v2.21.2
fix: handle subscription error in publicsign
2025-05-21 07:37:15 +00:00
prafull-opensignlabsandGitHub d888897b62 Update README.md 2025-05-20 20:10:44 +05:30
prafull-opensignlabsandGitHub 8f2433fbea Merge pull request #1737 from OpenSignLabs/updates-15043583319
v2.21.1
2025-05-15 16:48:53 +05:30
prafull-opensignlabs b671dbac19 v2.21.1
fix: handle undefined placeholders condition
2025-05-15 11:17:30 +00:00
prafull-opensignlabsandGitHub 79cdbc9c8c Merge pull request #1735 from OpenSignLabs/updates-15024699698
fix: resolve error cannot read undefined props
2025-05-14 20:55:08 +05:30
prafull-opensignlabs e585b7c943 fix: resolve error cannot read undefined props 2025-05-14 15:24:27 +00:00
prafull-opensignlabsandGitHub 5336e2cb7c Merge pull request #1734 from OpenSignLabs/prafull-opensignlabs-patch-7
refactor: remove unnecessary file
2025-05-14 20:26:36 +05:30
prafull-opensignlabsandGitHub 72401291f8 Delete apps/OpenSign/vite.public-template.config.js 2025-05-14 20:24:54 +05:30
prafull-opensignlabsandGitHub 7b34314b9e Merge pull request #1733 from OpenSignLabs/updates-15023728155
fix: correct build commands for selfhosting
2025-05-14 20:13:14 +05:30
prafull-opensignlabs b6f1cceeca Merge pull request #612 from OpenSignLabs/staging
fix: correct build commands for selfhosting
2025-05-14 14:42:30 +00:00
prafull-opensignlabsandGitHub c422cdac06 Merge pull request #1732 from OpenSignLabs/updates-15023371589
fix: edit template option not visible in mobile view
2025-05-14 20:00:02 +05:30
prafull-opensignlabsandGitHub ab08d2615d Update package.json 2025-05-14 19:59:01 +05:30
prafull-opensignlabs 2b0c7488d9 Merge pull request #610 from OpenSignLabs/staging
fix: edit template option not visible in mobile view
2025-05-14 14:27:57 +00:00
prafull-opensignlabsandGitHub c4779c14c1 feat: add all three docker command 2025-05-14 19:11:18 +05:30
prafull-opensignlabsandGitHub e48869cb2f feat: add deploy to digitalocean button 2025-05-14 18:36:47 +05:30
112 changed files with 17702 additions and 25777 deletions
+17 -1
View File
@@ -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
[![Deploy on DigitalOcean](https://www.deploytodo.com/do-btn-blue.svg)](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 -
**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
```
**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 -
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.
-25
View File
@@ -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
+9
View File
@@ -13,6 +13,10 @@ RUN npm install
# Copy the current directory contents into the container
COPY apps/OpenSign/ .
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
ENV NODE_ENV=production
@@ -20,8 +24,13 @@ ENV GENERATE_SOURCEMAP=false
# 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
EXPOSE 3000
ENTRYPOINT ["./entrypoint.sh"]
# Run the application
CMD ["npm", "start"]
+213
View File
@@ -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.
+29
View File
@@ -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,92 @@
/**
* 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
};
+5264 -17621
View File
File diff suppressed because it is too large Load Diff
+34 -47
View File
@@ -1,15 +1,15 @@
{
"name": "open_sign",
"version": "0.1.0",
"version": "2.21.1",
"private": true,
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@lottiefiles/dotlottie-react": "^0.13.5",
"@imgly/background-removal": "^1.6.0",
"@lottiefiles/dotlottie-react": "^0.14.0",
"@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.1.6",
"@reduxjs/toolkit": "^2.7.0",
"@radix-ui/themes": "^3.2.1",
"@reduxjs/toolkit": "^2.8.2",
"axios": "^1.9.0",
"css-minimizer-webpack-plugin": "^7.0.2",
"date-fns-tz": "^3.2.0",
"file-saver": "^2.0.5",
"i18next": "^23.16.8",
@@ -18,60 +18,53 @@
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"nth-check": "^2.1.1",
"parse": "^6.1.1",
"pdf-lib": "^1.17.1",
"pkijs": "^3.0.8",
"print-js": "^1.6.0",
"radix-ui": "^1.0.1",
"react": "^18.2.0",
"react-bootstrap": "^2.10.9",
"prismjs": "^1.30.0",
"quill-html-edit-button": "^3.0.0",
"radix-ui": "^1.4.2",
"react": "^18.3.1",
"react-bootstrap": "^2.10.10",
"react-confetti": "^6.4.0",
"react-cookie": "^7.2.2",
"react-datepicker": "^7.6.0",
"react-datepicker": "^8.4.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dnd-multi-backend": "^9.0.0",
"react-dnd-touch-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
"react-i18next": "^15.5.0",
"react-i18next": "^15.5.2",
"react-konva": "^18.2.10",
"react-pdf": "^9.2.1",
"react-quill-new": "^3.4.6",
"react-redux": "^9.2.0",
"react-rnd": "^10.5.2",
"react-router": "^7.5.3",
"react-scripts": "^5.0.1",
"react-router": "^7.6.1",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.1",
"react-signature-canvas": "^1.0.7",
"react-syntax-highlighter": "^15.6.1",
"react-signature-canvas": "^1.1.0-alpha.2",
"react-timezone-select": "^3.2.8",
"react-tooltip": "^5.28.1",
"react-web-share": "^2.0.2",
"reactour": "^1.19.4",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"regex-parser": "^2.3.1",
"serve": "^14.2.4",
"styled-components": "^5.3.0",
"web-vitals": "^4.2.4",
"ws": "^8.18.2",
"styled-components": "^5.3.11",
"web-vitals": "^5.0.2",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
},
"scripts": {
"build-template-win": "vite build --config vite.public-template.config.js",
"build-template": "NODE_OPTIONS=\"--max-old-space-size=8192\" vite build --config vite.public-template.config.js",
"build": "npm run version && react-scripts build",
"build-template-watch": "NODE_OPTIONS=\"--max-old-space-size=8192\" vite build --config vite.public-template.config.js --watch",
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
"start-dev": "vite",
"dev": "vite",
"preview": "vite preview",
"start": "serve -s build",
"version": "curl -s https://api.github.com/repos/opensignlabs/opensign/releases/latest | grep '\"tag_name\":' | awk -F '\"' '{print $4}' > ./public/version.txt",
"version-win": "powershell -Command \"Invoke-RestMethod -Uri 'https://api.github.com/repos/opensignlabs/opensign/releases/latest' | Select-Object -ExpandProperty tag_name | Out-File -FilePath ./public/version.txt\"",
"build-win": "npm run version-win && react-scripts build",
"build-win": "npm run version-win && vite build",
"test": "vitest run",
"test:watch": "vitest",
"release": "standard-version",
@@ -101,16 +94,17 @@
}
},
"devDependencies": {
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.1",
"@babel/core": "^7.27.4",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@babel/runtime-corejs2": "^7.27.1",
"@babel/runtime-corejs2": "^7.27.4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^18.3.21",
"@vitejs/plugin-react": "^4.4.1",
"@vitejs/plugin-react-swc": "^3.9.0",
"@types/react": "^18.3.23",
"@vitejs/plugin-react": "^4.5.1",
"@vitejs/plugin-react-swc": "^3.10.1",
"@vitest/ui": "^3.2.0",
"autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0",
"commitizen": "^4.3.1",
@@ -118,27 +112,20 @@
"css-loader": "^7.1.2",
"daisyui": "^4.12.24",
"dotenv": "^16.5.0",
"dotenv-webpack": "^8.1.0",
"eslint": "^8.57.1",
"eslint-plugin-prettier": "^5.4.0",
"eslint": "^9.28.0",
"eslint-plugin-prettier": "^5.4.1",
"eslint-plugin-react": "^7.37.5",
"lint-staged": "^15.5.1",
"mini-css-extract-plugin": "^2.9.2",
"postcss": "^8.5.3",
"jsdom": "^26.1.0",
"lint-staged": "^16.1.0",
"postcss": "^8.5.4",
"prettier": "^3.5.3",
"pretty-quick": "^4.1.1",
"pretty-quick": "^4.2.2",
"rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.17",
"terser-webpack-plugin": "^5.3.14",
"vite": "^6.3.5",
"vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.3",
"webpack-cli": "^5.1.4"
},
"overrides": {
"nth-check": "$nth-check",
"ws": "$ws"
"vitest": "^3.2.0"
},
"engines": {
"node": "18 || 20 || 22"
@@ -1,9 +1,11 @@
{
"header-news": "Neue Funktion: Benutzer des Teams-Plans können jetzt ihre eigenen AWS S3-Buckets für die Dateispeicherung integrieren",
"header-news-btn": "Jetzt einrichten",
"sandbox-news": "Dies ist eine Sandbox-Umgebung. Bitte nicht für produktive Zwecke verwenden.",
"create-account": "Konto erstellen",
"login": "Anmelden",
"language": "Sprache",
"dark-mode": "Dunkelmodus",
"name": "Name",
"phone": "Telefon",
"phone-optional": "optional",
@@ -39,8 +41,10 @@
"save": "Speichern",
"cancel": "Abbrechen",
"upgrade-now": "Jetzt upgraden",
"contact-now": "Jetzt kontaktieren",
"upgrade-to": "Upgrade zu",
"plan": "Plan",
"subscription-renew-warning": "Ihr Abonnement läuft in {{remainingDays}} Tagen ab. Bitte verlängern Sie Ihr Abonnement.",
"subscribe-card-teamplan": "Entfesseln Sie die volle Kraft der Zusammenarbeit! Erstellen Sie unbegrenzt Organisationen, Teams und Hierarchien. Teilen Sie Vorlagen nahtlos zwischen Teams und weisen Sie benutzerdefinierte Benutzerrollen zu. Optimieren Sie Ihren Workflow noch heute!",
"subscribe-card-plan": "Entsperren Sie Premium-Funktionen ab nur {{premiumPrice}}/Monat. Genießen Sie eine verbesserte Leistung und zahlen Sie nur {{addonPrice}} pro zusätzlichem Credit nach den enthaltenen Premium-Credits.",
"user-name-limit-char": "Um einen Benutzernamen mit weniger als 8 Zeichen zu haben, abonnieren Sie bitte.",
@@ -142,6 +146,7 @@
"Quick send": "Schnell senden",
"Edit": "Bearbeiten",
"Share with team": "Mit Team teilen",
"Share with user": "Mit Kollegen teilen",
"Share": "Teilen",
"View": "Ansehen",
"option": "Option",
@@ -245,6 +250,7 @@
"API": "API",
"api-token": "API-Token",
"regenerate-token": "Live-Token neu generieren",
"remove-background": "Hintergrund entfernen",
"generate-token": "Live-Token generieren",
"view-docs": "Dokumentation ansehen",
"generate-token-alert": "Sind Sie sicher, dass Sie das Token neu generieren möchten? Das alte Token wird ablaufen.",
@@ -356,6 +362,7 @@
"date": "Datum",
"text": "Text",
"text input": "Texteingabe",
"cells": "Zellen",
"checkbox": "Checkbox",
"dropdown": "Dropdown",
"radio button": "Radiobutton",
@@ -410,10 +417,12 @@
"options": "Optionen",
"minimun-check": "Minimale Anzahl",
"maximum-check": "Maximale Anzahl",
"cell-count": "Zellzahl",
"default-value": "Standardwert",
"select": "Auswählen",
"read-only": "Nur lesen",
"read-only": "Ist schreibgeschützt",
"hide-labels": "Labels ausblenden",
"layout": "Layout",
"checkbox": "Checkbox",
"alert": "Warnung",
"zoom-in": "Vergrößern",
@@ -478,6 +487,7 @@
"document-alert": "Dokument-Warnung",
"owner-subscription-expired": "Das Abonnement des Besitzers ist abgelaufen.",
"subscription-expired": "Abonnement abgelaufen",
"owner-doesnt-have-paid-plan": "Der Inhaber hat keinen kostenpflichtigen Plan.",
"alert-message": "Warnmeldung",
"document-decline": "Dokument ablehnen",
"decline-alert-1": "Sind Sie sicher, dass Sie dieses Dokument ablehnen möchten?",
@@ -670,7 +680,7 @@
"public-template-mssg-1": "Um OpenSign in Ihr React- oder Next.js-Projekt zu integrieren, führen Sie einfach den folgenden Befehl aus:",
"public-template-mssg-2": "Stellen Sie sicher, dass npm oder yarn in Ihrem Projekt eingerichtet ist. Wenn Sie Yarn verwenden, können Sie npm install durch yarn add @opensign/react ersetzen.",
"public-template-mssg-3": "Benötigen Sie weitere Details oder Beispiele?",
"public-template-mssg-4": "Besuchen Sie die",
"public-template-mssg-4": "Besuchen Sie die ",
"public-template-mssg-5": " npm für die neuesten Updates, detaillierte Dokumentationen und Versionshistorie.",
"public-template-mssg-6": "Bevor Sie diesen Code-Schnipsel verwenden können, müssen Sie diese Vorlage öffentlich machen.",
"public-template-mssg-7": "Bevor Sie einen öffentlichen Link generieren können, müssen Sie diese Vorlage öffentlich machen.",
@@ -744,6 +754,7 @@
"delete-page": "Seite löschen",
"merge-pdf": "PDFs zusammenführen",
"add-pages": "Seiten hinzufügen",
"reorder-pages": "Seiten neu anordnen",
"delete-alert": "Einzelne Seite kann nicht gelöscht werden.",
"delete-alert-2": "Sind Sie sicher, dass Sie diese Seite löschen möchten?",
"delete-note": "Hinweis: Sobald Sie diese Seite löschen, kann dies nicht rückgängig gemacht werden.",
@@ -971,5 +982,56 @@
"kiosk-sign": "Kiosk-Unterschrift",
"dont-have-access-to-template": "Das template wurde gelöscht oder Sie haben keinen Zugriff. Bitte kontaktieren Sie den Absender.",
"kiosk-info": "Kiosk Modus ermöglicht es Ihnen, persönliche Unterschriften schnell und effizient zu erfassen. Ideal für Messen, Veranstaltungen oder Laufkundschaft, bei denen alle Unterzeichner physisch anwesend sind. ",
"learn-more": "Mehr erfahren"
"learn-more": "Mehr erfahren",
"finish-mssg": "Sind Sie sicher, dass Sie das Dokument abschließen möchten?",
"review": "Überprüfen",
"next-field": "Nächstes Feld",
"required-mssg": "{{leftRequiredWidget}} von {{totalWidget}} Feldern übrig",
"verify-document": "Dokument verifizieren",
"verify-document-signature": "Dokumentensignatur überprüfen",
"select-pdf-document": "PDF-Dokument auswählen",
"selected-file": "Ausgewählte Datei",
"verify-signature": "Signatur überprüfen",
"verification-status": "Überprüfungsstatus",
"verification-in-progress": "Überprüfung läuft...",
"verification-results-will-appear-here": "Überprüfungsergebnisse werden hier angezeigt",
"please-select-pdf": "Bitte wählen Sie eine gültige PDF-Datei aus",
"please-select-file-to-verify": "Bitte wählen Sie eine Datei zur Überprüfung aus",
"no-signature-found": "Keine Signatur im Dokument gefunden",
"error-verifying-pdf": "Fehler beim Überprüfen der PDF",
"signature-valid-basic": "Signatur ist gültig",
"signature-invalid-basic": "Signatur ist ungültig",
"all-signatures-verified-convincing": "Dokument überprüft: Alle Signaturen wurden erfolgreich validiert.",
"some-signatures-invalid-basic": "Einige Signaturen sind ungültig",
"no-signatures-processed": "Keine Signaturen verarbeitet",
"unnamed-signature-field": "Unbenanntes Signaturfeld",
"error-processing-signature": "Fehler beim Verarbeiten der Signatur",
"signer-info-not-available": "Signaturinformationen nicht verfügbar",
"cert-validity-not-checked": "Gültigkeit des Zertifikats nicht geprüft",
"valid": "Gültig",
"expired-or-not-yet-valid": "Abgelaufen oder noch nicht gültig",
"valid-from": "Gültig von",
"to": "bis",
"signer": "Unterzeichner",
"issuer": "Aussteller",
"not-available": "Nicht verfügbar",
"not-performed": "Nicht durchgeführt",
"missing-acrofield-dict": "Fehlendes Acrofield-Wörterbuch",
"signature-dictionary-not-found-or-invalid": "Signaturwörterbuch nicht gefunden oder ungültig",
"missing-or-invalid-byterange": "Fehlender oder ungültiger ByteRange",
"missing-or-invalid-contents": "Fehlender oder ungültiger Inhalt",
"missing-signature-contents": "Fehlender Signaturinhalt",
"invalid-signature-hex-format": "Ungültiges Signatur-Hex-Format",
"unsupported-signature-format-not-signeddata": "Nicht unterstütztes Signaturformat - nicht SignedData",
"signer-certificate-not-found": "Unterzeichnerzertifikat nicht gefunden",
"no-certificates-in-signature": "Keine Zertifikate in der Signatur",
"no-signer-info-in-pkcs7": "Keine Signaturinformationen in PKCS#7",
"could-not-parse-signer-info": "Signaturinformationen konnten nicht analysiert werden",
"not-calculated": "Nicht berechnet",
"not-found-in-signature": "Nicht in Signatur gefunden",
"readonly-error": "Das schreibgeschützte {{widgetName}}-Widget muss einen Standardwert haben oder kann optional gemacht werden.",
"choose-one":"Wählen Sie eine aus",
"search-templates": "Vorlagen durchsuchen…",
"search-documents": "Dokumente suchen…",
"search-contacts": "Kontakte durchsuchen…"
}
@@ -1,9 +1,11 @@
{
"header-news": "New feature: Protect your account with Two-Factor Authentication (2FA) and enjoy the future of login with Passkeys — no passwords needed.",
"header-news-btn": "Setup now",
"sandbox-news": "This is a sandbox environment. Please do not use it for production purposes.",
"create-account": "Create account",
"login": "Login",
"language": "Language",
"dark-mode": "Dark mode",
"name": "Name",
"phone": "Phone",
"phone-optional": "optional",
@@ -39,8 +41,10 @@
"save": "Save",
"cancel": "Cancel",
"upgrade-now": "Upgrade now",
"contact-now": "Contact now",
"upgrade-to": "Upgrade to",
"plan": "Plan",
"subscription-renew-warning": "Your subscription will expire in {{remainingDays}} days. Please renew your subscription.",
"subscribe-card-teamplan": "Unlock the full power of collaboration! Create unlimited organizations, teams, and hierarchies. Share templates seamlessly across teams and assign custom user roles. Elevate your workflow today!",
"subscribe-card-plan": "Unlock premium features starting at just {{premiumPrice}}/month. Enjoy enhanced performance and only {{addonPrice}} per additional credit after your included premium credits.",
"user-name-limit-char": "To have a username less than 8 character please subscribe",
@@ -142,6 +146,7 @@
"Quick send": "Quick send",
"Edit": "Edit",
"Share with team": "Share with team",
"Share with user": "Share with colleague",
"Share": "Share",
"View": "View",
"option": "Option",
@@ -195,7 +200,7 @@
},
"file-type": "pdf, png, jpg, jpeg",
"docx": "docx",
"file-selected": "file selected",
"file-selected": "file(s) selected",
"template-title": "Template title",
"document-title": "Document title",
"description": "Description",
@@ -245,6 +250,7 @@
"API": "API",
"api-token": "API token",
"regenerate-token": "Regenerate live token",
"remove-background": "Remove Background",
"generate-token": "Generate live token",
"view-docs": "View docs",
"generate-token-alert": "Are you sure you want to regenerate token it will expire old token?",
@@ -356,6 +362,7 @@
"date": "date",
"text": "text",
"text input": "text input",
"cells": "cells",
"checkbox": "checkbox",
"dropdown": "dropdown",
"radio button": "radio button",
@@ -410,10 +417,12 @@
"options": "Options",
"minimun-check": "Minimun check",
"maximum-check": "Maximum check",
"cell-count": "Cell count",
"default-value": "Default value",
"select": "Select",
"read-only": "Is read only",
"read-only": "read only",
"hide-labels": "Hide labels",
"layout": "Layout",
"checkbox": "Checkbox",
"alert": "Alert",
"zoom-in": "Zoom in",
@@ -478,6 +487,7 @@
"document-alert": "Document alert",
"owner-subscription-expired": "Owner's subscription has expired.",
"subscription-expired": "Subscription Expired",
"owner-doesnt-have-paid-plan": "Owner doesn't have paid plan.",
"alert-message": "Alert message",
"document-decline": "Document decline",
"decline-alert-1": "Are you sure want to decline this document ?",
@@ -520,7 +530,7 @@
"new-password": "New password",
"confirm-password": "Confirm password",
"file-alert-1": "The selected file size is too large. Please select a file less than",
"file-alert-2": "Please select file.",
"file-alert-2": "Please select file(s).",
"file-alert-3": "Please wait while the document is being uploaded.",
"enter-pdf-password": "Enter Pdf password",
"correct-password": "Please provide correct password",
@@ -670,7 +680,7 @@
"public-template-mssg-1": "To integrate OpenSign into your React or Next.js project, simply run the following command:",
"public-template-mssg-2": "Ensure you have npm or yarn set up in your project. If you're using Yarn, you can replace npm install with yarn add @opensign/react.",
"public-template-mssg-3": "Need more details or examples?",
"public-template-mssg-4": "Visit the",
"public-template-mssg-4": "Visit the ",
"public-template-mssg-5": " npm for the latest updates, detailed documentation, and version history.",
"public-template-mssg-6": "Before you can use this code snippet, you must make this template public.",
"public-template-mssg-7": "Before you can generate a public link you must make this template public.",
@@ -744,6 +754,7 @@
"delete-page": "Delete page",
"merge-pdf": "Merge pdf",
"add-pages": "Add pages",
"reorder-pages": "Reorder pages",
"delete-alert": "Can not delete single page",
"delete-alert-2": "Are you sure you want to delete this page?",
"delete-note": "Note: Once you delete this page, you cannot undo.",
@@ -879,7 +890,7 @@
"you-will-receive-email-shortly": "✅ That's it! You'll receive a confirmation email shortly.",
"please-provide-templateid": "Please provide templateid",
"this-template-is-not-public": "This template is not public",
"invalid-templateid": "Invaldi templateid",
"invalid-templateid": "Invalid templateid",
"contact-billing-at-opensign": "To add more seats, please contact OpenSign™ at <1>billing@opensignlabs.com</1> for assistance",
"title-length-alert": "Title must be at most 250 characters long.",
"note-length-alert": "Note must be at most 200 characters long.",
@@ -971,5 +982,56 @@
"kiosk-sign": "Kiosk Sign",
"dont-have-access-to-template": "The template has been deleted or you don't have access. Please contact the sender.",
"kiosk-info": "Kiosk Mode lets you collect in-person signatures quickly and efficiently. Ideal for trade shows, events, or walk-in scenarios where all signers are physically present. ",
"learn-more": "Learn more"
"learn-more": "Learn more",
"finish-mssg":" Are you sure you want to finish the document ?",
"review":"Review",
"next-field":"Next Field",
"required-mssg":"{{leftRequiredWidget}} of {{totalWidget}} fields left",
"verify-document": "Verify document",
"verify-document-signature": "Verify Document Signature",
"select-pdf-document": "Select PDF Document",
"selected-file": "Selected file",
"verify-signature": "Verify Signature",
"verification-status": "Verification Status",
"verification-in-progress": "Verification in progress...",
"verification-results-will-appear-here": "Verification results will appear here",
"please-select-pdf": "Please select a valid PDF file",
"please-select-file-to-verify": "Please select a file to verify",
"no-signature-found": "No signature found in the document",
"error-verifying-pdf": "Error verifying PDF",
"signature-valid-basic": "Signature is valid",
"signature-invalid-basic": "Signature is invalid",
"all-signatures-verified-convincing": "Document Verified: All signatures have been successfully validated.",
"some-signatures-invalid-basic": "Some signatures are invalid",
"no-signatures-processed": "No signatures were processed",
"unnamed-signature-field": "Unnamed Signature Field",
"error-processing-signature": "Error processing signature",
"signer-info-not-available": "Signer information not available",
"cert-validity-not-checked": "Certificate validity not checked",
"valid": "Valid",
"expired-or-not-yet-valid": "Expired or not yet valid",
"valid-from": "Valid from",
"to": "to",
"signer": "Signer",
"issuer": "Issuer",
"not-available": "Not available",
"not-performed": "Not performed",
"missing-acrofield-dict": "Missing acrofield dictionary",
"signature-dictionary-not-found-or-invalid": "Signature dictionary not found or invalid",
"missing-or-invalid-byterange": "Missing or invalid ByteRange",
"missing-or-invalid-contents": "Missing or invalid Contents",
"missing-signature-contents": "Missing signature contents",
"invalid-signature-hex-format": "Invalid signature hex format",
"unsupported-signature-format-not-signeddata": "Unsupported signature format - not SignedData",
"signer-certificate-not-found": "Signer certificate not found",
"no-certificates-in-signature": "No certificates in signature",
"no-signer-info-in-pkcs7": "No signer info in PKCS#7",
"could-not-parse-signer-info": "Could not parse signer info",
"not-calculated": "Not calculated",
"not-found-in-signature": "Not found in signature",
"readonly-error": "Read-only {{widgetName}} widget must have a default value or you can make it optional.",
"choose-one":"Choose One",
"search-templates": "Search templates…",
"search-documents": "Search documents…",
"search-contacts": "Search contacts…"
}
@@ -1,9 +1,11 @@
{
"header-news": "Nueva característica: los usuarios del plan Teams ahora pueden integrar sus propios depósitos de AWS S3 para el almacenamiento de archivos",
"header-news-btn": "Configurar ahora",
"sandbox-news": "Este es un entorno sandbox. Por favor, no lo utilice con fines de producción.",
"create-account": "Crear cuenta",
"login": "Iniciar sesión",
"language": "Idioma",
"dark-mode": "Modo oscuro",
"name": "Nombre",
"phone": "Teléfono",
"phone-optional": "opcional",
@@ -39,8 +41,10 @@
"save": "Guardar",
"cancel": "Cancelar",
"upgrade-now": "Mejorar ahora",
"contact-now": "Contactar ahora",
"upgrade-to": "Mejorar a",
"plan": "Plan",
"subscription-renew-warning": "Su suscripción vencerá en {{remainingDays}} días. Por favor, renueve su suscripción.",
"subscribe-card-teamplan": "¡Libera todo el poder de la colaboración! Crea organizaciones, equipos y jerarquías ilimitadas. Comparte plantillas sin problemas entre equipos y asigna funciones de usuario personalizadas. ¡Mejora tu flujo de trabajo hoy mismo!",
"subscribe-card-plan": "Desbloquea funciones premium desde solo {{premiumPrice}}/mes. Disfruta de un rendimiento mejorado y solo {{addonPrice}} por crédito adicional después de tus créditos premium incluidos.",
"user-name-limit-char": "Para tener un nombre de usuario menor a 8 caracteres por favor suscríbete",
@@ -142,6 +146,7 @@
"Quick send": "Envío rápido",
"Edit": "Editar",
"Share with team": "Compartir con el equipo",
"Share with user": "Compartir con un colega",
"Share": "Compartir",
"View": "Ver",
"option": "Opción",
@@ -246,6 +251,7 @@
"API": "API",
"api-token": "Token API",
"regenerate-token": "Regenerar token activo",
"remove-background": "Eliminar fondo",
"generate-token": "Generar token activo",
"view-docs": "Ver documentación",
"generate-token-alert": "¿En definitiva quieres regenerar el token? Esto expirará el token antiguo.",
@@ -357,6 +363,7 @@
"date": "fecha",
"text": "texto",
"text input": "entrada de texto",
"cells": "células",
"checkbox": "casilla",
"dropdown": "desplegable",
"radio button": "botón de radio",
@@ -411,10 +418,12 @@
"options": "Opciones",
"minimun-check": "Chequeo mínimo",
"maximum-check": "Chequeo máximo",
"cell-count": "recuento de células",
"default-value": "Valor por defecto",
"select": "Seleccionar",
"read-only": "Es de solo lectura",
"hide-labels": "Esconder etiquetas",
"layout": "Diseño",
"checkbox": "Casilla",
"alert": "Alerta",
"zoom-in": "Acercar",
@@ -477,6 +486,7 @@
"mail-not-delivered": "correo no entregado",
"document-alert": "Alerta de documento",
"owner-subscription-expired": "La suscripción del propietario ha expirado.",
"owner-doesnt-have-paid-plan": "El propietario no tiene un plan de pago.",
"subscription-expired": "Suscripción expirada",
"alert-message": "Mensaje de alerta",
"document-decline": "Rechazar documento",
@@ -670,7 +680,7 @@
"public-template-mssg-1": "Para integrar OpenSign a tu proyecto React o Next.js, simplemente ejecuta los siguientes comandos:",
"public-template-mssg-2": "Asegúrate de tener «npm» o «yarn» configurado en tu proyecto. Si estás usando «yarn», puedes reemplazar «npm install» con «yarn add @opensign/react».",
"public-template-mssg-3": "¿Necesitas más detalles o ejemplos?",
"public-template-mssg-4": "Visita la",
"public-template-mssg-4": "Visita la ",
"public-template-mssg-5": " «npm» para las últimas actualizaciones, documentación detallada e historial de versiones.",
"public-template-mssg-6": "Antes de que puedas usar este fragmento de código, debes convertir esta plantilla en pública.",
"public-template-mssg-7": "Antes de poder generar un enlace público, debes hacer que esta plantilla sea pública.",
@@ -744,6 +754,7 @@
"delete-page": "eliminar página",
"merge-pdf": "fusionar pdf",
"add-pages": "Agregar páginas",
"reorder-pages": "Reordenar páginas",
"delete-alert": "No se puede eliminar una sola página",
"delete-alert-2": "¿Está seguro de que desea eliminar esta página?",
"delete-note": "Nota: una vez que elimines esta página, no podrás deshacerla",
@@ -971,5 +982,56 @@
"kiosk-sign": "Firma en quiosco",
"dont-have-access-to-template": "El template ha sido eliminado o no tiene acceso. Por favor, contacte al remitente.",
"kiosk-info": "El Modo Kiosco le permite recopilar firmas en persona de forma rápida y eficiente. Ideal para ferias, eventos o situaciones con personas que firman en el lugar. ",
"learn-more": "Más información"
"learn-more": "Más información",
"finish-mssg": "¿Está seguro de que desea finalizar el documento?",
"review": "Revisar",
"next-field": "Siguiente campo",
"required-mssg": "{{leftRequiredWidget}} de {{totalWidget}} campos restantes",
"verify-document": "Verificar documento",
"verify-document-signature": "Verificar firma del documento",
"select-pdf-document": "Seleccionar documento PDF",
"selected-file": "Archivo seleccionado",
"verify-signature": "Verificar firma",
"verification-status": "Estado de verificación",
"verification-in-progress": "Verificación en curso...",
"verification-results-will-appear-here": "Los resultados de la verificación aparecerán aquí",
"please-select-pdf": "Por favor, seleccione un archivo PDF válido",
"please-select-file-to-verify": "Por favor, seleccione un archivo para verificar",
"no-signature-found": "No se encontró ninguna firma en el documento",
"error-verifying-pdf": "Error al verificar el PDF",
"signature-valid-basic": "La firma es válida",
"signature-invalid-basic": "La firma no es válida",
"all-signatures-verified-convincing": "Documento verificado: Todas las firmas han sido validadas exitosamente.",
"some-signatures-invalid-basic": "Algunas firmas no son válidas",
"no-signatures-processed": "No se procesaron firmas",
"unnamed-signature-field": "Campo de firma sin nombre",
"error-processing-signature": "Error al procesar la firma",
"signer-info-not-available": "Información del firmante no disponible",
"cert-validity-not-checked": "Validez del certificado no verificada",
"valid": "Válido",
"expired-or-not-yet-valid": "Caducado o aún no válido",
"valid-from": "Válido desde",
"to": "hasta",
"signer": "Firmante",
"issuer": "Emisor",
"not-available": "No disponible",
"not-performed": "No realizado",
"missing-acrofield-dict": "Falta el diccionario Acrofield",
"signature-dictionary-not-found-or-invalid": "Diccionario de firmas no encontrado o inválido",
"missing-or-invalid-byterange": "ByteRange faltante o inválido",
"missing-or-invalid-contents": "Contenido faltante o inválido",
"missing-signature-contents": "Falta el contenido de la firma",
"invalid-signature-hex-format": "Formato hexadecimal de firma inválido",
"unsupported-signature-format-not-signeddata": "Formato de firma no compatible - no SignedData",
"signer-certificate-not-found": "Certificado del firmante no encontrado",
"no-certificates-in-signature": "No hay certificados en la firma",
"no-signer-info-in-pkcs7": "No hay información del firmante en PKCS#7",
"could-not-parse-signer-info": "No se pudo analizar la información del firmante",
"not-calculated": "No calculado",
"not-found-in-signature": "No encontrado en la firma",
"readonly-error": "El widget de solo lectura {{widgetName}} debe tener un valor predeterminado o puede hacerlo opcional.",
"choose-one":"Elige uno",
"search-templates": "Buscar plantillas…",
"search-documents": "Buscar documentos…",
"search-contacts": "Buscar contactos…"
}
@@ -1,9 +1,11 @@
{
"header-news": "Nouvelle fonctionnalité : les utilisateurs du forfait Teams peuvent désormais intégrer leurs propres compartiments AWS S3 pour le stockage de fichiers",
"header-news-btn": "Configurer maintenant",
"sandbox-news": "Ceci est un environnement sandbox. Veuillez ne pas l'utiliser à des fins de production.",
"create-account": "Créer un compte",
"login": "Se Connecter",
"language": "Langue",
"dark-mode": "Mode sombre",
"name": "Nom et Prénom",
"phone": "Téléphone",
"phone-optional": "(facultatif)",
@@ -39,9 +41,11 @@
"save": "Sauvegarder",
"cancel": "Annuler",
"upgrade-now": "Mettre à jour maintenant",
"contact-now": "Contacter maintenant",
"upgrade-to": "Mettre à niveau vers",
"pro": "PRO",
"plan": "Offre",
"subscription-renew-warning": "Votre abonnement expirera dans {{remainingDays}} jours. Veuillez renouveler votre abonnement.",
"subscribe-card-teamplan": "Libérez toute la puissance de la collaboration ! Créez un nombre illimité d'organisations, d'équipes et de hiérarchies. Partagez des modèles de manière transparente entre les équipes et attribuez des rôles d'utilisateur personnalisés. Améliorez votre flux de travail dès aujourd'hui !",
"subscribe-card-plan": "Débloquez des fonctionnalités premium à partir de seulement {{premiumPrice}}/mois. Bénéficiez de performances améliorées et de seulement {{addonPrice}} par crédit supplémentaire après vos crédits premium inclus.",
"user-name-limit-char": "Pour avoir un nom d'utilisateur de moins de 8 caractères s'il vous plaît s'abonner",
@@ -163,6 +167,7 @@
"Quick send": "Envoi rapide",
"Edit": "Modifier",
"Share with team": "Partager avec l'équipe",
"Share with user": "Partager avec un collègue",
"Share": "Partager",
"View": "Voir",
"option": "Option",
@@ -245,6 +250,7 @@
"API": "API",
"api-token": "Jeton API",
"regenerate-token": "Régénérer en direct jeton",
"remove-background": "Supprimer l'arrière-plan",
"generate-token": "Générer en direct jeton",
"view-docs": "Afficher les documents",
"generate-token-alert": "Êtes-vous sûr de vouloir régénérer le jeton, votre ancien jeton sera supprimer?",
@@ -356,6 +362,7 @@
"date": "date",
"text": "texte",
"text input": "saisie de texte",
"cells": "cellules",
"checkbox": "case à cocher",
"dropdown": "dérouler",
"radio button": "bouton radio",
@@ -410,10 +417,12 @@
"options": "Possibilités",
"minimun-check": "Vérification minimale",
"maximum-check": "Contrôle maximum",
"cell-count": "numération cellulaire",
"default-value": "Valeur par défaut",
"select": "Sélectionner",
"read-only": "Est en lecture seule",
"hide-labels": "Masquer les étiquettes",
"layout": "Disposition",
"checkbox": "Case à cocher",
"alert": "Alerte",
"zoom-in": "Agrandir",
@@ -477,6 +486,7 @@
"mail-not-delivered": "Courrier non distribué",
"document-alert": "Alerte document",
"owner-subscription-expired": "L'abonnement du propriétaire a expiré.",
"owner-doesnt-have-paid-plan": "Le propriétaire n'a pas de plan payant.",
"subscription-expired": "Abonnement expiré",
"alert-message": "Message d'alerte",
"document-decline": "Document-refusé",
@@ -670,7 +680,7 @@
"public-template-mssg-1": "Pour intégrer OpenSign dans votre projet React ou Next.js, exécutez simplement la commande suivante :",
"public-template-mssg-2": "Assurez-vous que npm ou Yarn est configuré dans votre projet. Si vous utilisez Yarn, vous pouvez remplacer npm install par Yarn Add @opensign/react.",
"public-template-mssg-3": "Besoin de plus de détails ou d'exemples ?",
"public-template-mssg-4": "Visitez le",
"public-template-mssg-4": "Visitez le ",
"public-template-mssg-5": "npm pour les dernières mises à jour, une documentation détaillée et l'historique des versions.",
"public-template-mssg-6": "Avant de pouvoir utiliser cet extrait de code, vous devez rendre ce modèle public.",
"public-template-mssg-7": "Avant de pouvoir générer un lien public, vous devez rendre ce modèle public.",
@@ -744,6 +754,7 @@
"delete-page": "supprimer la page",
"merge-pdf": "Fusionner le pdf",
"add-pages": "Ajouter des pages",
"reorder-pages": "Réorganiser les pages",
"delete-alert": "Impossible de supprimer une seule page",
"delete-alert-2": "Etes-vous sûr de vouloir supprimer cette page ?",
"delete-note": "Remarque : une fois cette page supprimée, vous ne pouvez plus l'annuler",
@@ -971,5 +982,56 @@
"kiosk-sign": "Signature sur kiosque",
"dont-have-access-to-template": "Le template a été supprimé ou vous n'y avez pas accès. Veuillez contacter l'expéditeur.",
"kiosk-info": "Le Mode Kiosque vous permet de recueillir des signatures en personne rapidement et efficacement. Idéal pour les salons, événements ou situations où tous les signataires sont physiquement présents. ",
"learn-more": "En savoir plus"
"learn-more": "En savoir plus",
"finish-mssg": "Êtes-vous sûr de vouloir terminer le document ?",
"review": "Revoir",
"next-field": "Champ suivant",
"required-mssg":"{{leftRequiredWidget}} champs sur {{totalWidget}} restants",
"verify-document": "Vérifier le document",
"verify-document-signature": "Vérifier la signature du document",
"select-pdf-document": "Sélectionner le document PDF",
"selected-file": "Fichier sélectionné",
"verify-signature": "Vérifier la signature",
"verification-status": "État de la vérification",
"verification-in-progress": "Vérification en cours...",
"verification-results-will-appear-here": "Les résultats de la vérification apparaîtront ici",
"please-select-pdf": "Veuillez sélectionner un fichier PDF valide",
"please-select-file-to-verify": "Veuillez sélectionner un fichier à vérifier",
"no-signature-found": "Aucune signature trouvée dans le document",
"error-verifying-pdf": "Erreur lors de la vérification du PDF",
"signature-valid-basic": "La signature est valide",
"signature-invalid-basic": "La signature est invalide",
"all-signatures-verified-convincing": "Document vérifié : Toutes les signatures ont été validées avec succès.",
"some-signatures-invalid-basic": "Certaines signatures sont invalides",
"no-signatures-processed": "Aucune signature traitée",
"unnamed-signature-field": "Champ de signature sans nom",
"error-processing-signature": "Erreur lors du traitement de la signature",
"signer-info-not-available": "Informations sur le signataire non disponibles",
"cert-validity-not-checked": "Validité du certificat non vérifiée",
"valid": "Valide",
"expired-or-not-yet-valid": "Expiré ou pas encore valide",
"valid-from": "Valide du",
"to": "au",
"signer": "Signataire",
"issuer": "Émetteur",
"not-available": "Non disponible",
"not-performed": "Non effectué",
"missing-acrofield-dict": "Dictionnaire Acrofield manquant",
"signature-dictionary-not-found-or-invalid": "Dictionnaire de signatures introuvable ou invalide",
"missing-or-invalid-byterange": "ByteRange manquant ou invalide",
"missing-or-invalid-contents": "Contenu manquant ou invalide",
"missing-signature-contents": "Contenu de la signature manquant",
"invalid-signature-hex-format": "Format hexadécimal de signature invalide",
"unsupported-signature-format-not-signeddata": "Format de signature non pris en charge - pas SignedData",
"signer-certificate-not-found": "Certificat du signataire introuvable",
"no-certificates-in-signature": "Aucun certificat dans la signature",
"no-signer-info-in-pkcs7": "Aucune information sur le signataire dans PKCS#7",
"could-not-parse-signer-info": "Impossible d'analyser les informations sur le signataire",
"not-calculated": "Non calculé",
"not-found-in-signature": "Introuvable dans la signature",
"readonly-error": "Le widget en lecture seule {{widgetName}} doit avoir une valeur par défaut ou vous pouvez le rendre facultatif.",
"choose-one":"Choisissez-en un",
"search-templates": "Rechercher des modèles…",
"search-documents": "Rechercher des documents…",
"search-contacts": "Rechercher des contacts…"
}
File diff suppressed because it is too large Load Diff
@@ -1,9 +1,11 @@
{
"header-news": "Nuova funzionalità: Gli utenti del piano Teams possono ora integrare i propri bucket AWS S3 per l'archiviazione dei file",
"header-news-btn": "Configura Ora",
"sandbox-news": "Questo è un ambiente sandbox. Si prega di non utilizzarlo per scopi di produzione.",
"create-account": "Crea Account",
"login": "Accedi",
"language": "Lingua",
"dark-mode": "Modalità scura",
"name": "Nome",
"phone": "Telefono",
"phone-optional": "facoltativo",
@@ -39,8 +41,10 @@
"save": "Salva",
"cancel": "Annulla",
"upgrade-now": "Aggiorna ora",
"contact-now": "Contatta ora",
"upgrade-to": "Aggiorna a",
"plan": "Piano",
"subscription-renew-warning": "Il tuo abbonamento scadrà tra {{remainingDays}} giorni. Ti preghiamo di rinnovarlo.",
"subscribe-card-teamplan": "Sblocca tutto il potenziale della collaborazione! Crea organizzazioni, team e gerarchie illimitati. Condividi modelli senza problemi tra i team e assegna ruoli personalizzati agli utenti. Migliora il tuo flusso di lavoro oggi stesso!",
"subscribe-card-plan": "Sblocca le funzionalità premium a partire da soli {{premiumPrice}}/mese. Approfitta di prestazioni migliorate e paga solo {{addonPrice}} per ogni credito aggiuntivo dopo quelli inclusi.",
"user-name-limit-char": "Per un nome utente con meno di 8 caratteri, abbonati",
@@ -142,6 +146,7 @@
"Quick send": "Invio rapido",
"Edit": "Modifica",
"Share with team": "Condividi con il team",
"Share with user": "Condividi con un collega",
"Share": "Condividi",
"View": "Visualizza",
"option": "Opzione",
@@ -245,6 +250,7 @@
"API": "API",
"api-token": "Token API",
"regenerate-token": "Rigenera token live",
"remove-background": "Rimuovi sfondo",
"generate-token": "Genera token live",
"view-docs": "Visualizza documenti",
"generate-token-alert": "Sei sicuro di voler rigenerare il token? Questo invaliderà il vecchio token.",
@@ -356,6 +362,7 @@
"date": "data",
"text": "testo",
"text input": "campo di testo",
"cells": "cellule",
"checkbox": "casella di controllo",
"dropdown": "menu a tendina",
"radio button": "pulsante di opzione",
@@ -410,10 +417,12 @@
"options": "Opzioni",
"minimun-check": "Controllo minimo",
"maximum-check": "Controllo massimo",
"cell-count": "conteggio delle cellule",
"default-value": "Valore predefinita",
"select": "Seleziona",
"read-only": "È solo lettura",
"read-only": di sola lettura",
"hide-labels": "Nascondi etichette",
"layout": "Layout",
"checkbox": "Casella di controllo",
"alert": "Avviso",
"zoom-in": "Ingrandisci",
@@ -477,6 +486,7 @@
"mail-not-delivered": "Mail non consegnata",
"document-alert": "Avviso Documento",
"owner-subscription-expired": "L'abbonamento del proprietario è scaduto.",
"owner-doesnt-have-paid-plan": "Il proprietario non ha un piano a pagamento.",
"subscription-expired": "Abbonamento Scaduto",
"alert-message": "Messaggio di avviso",
"document-decline": "Documento rifiutato",
@@ -670,7 +680,7 @@
"public-template-mssg-1": "Per integrare OpenSign nel tuo progetto React o Next.js, esegui semplicemente il seguente comando:",
"public-template-mssg-2": "Assicurati di avere npm o yarn configurato nel tuo progetto. Se usi Yarn, puoi sostituire npm install con yarn add @opensign/react.",
"public-template-mssg-3": "Hai bisogno di maggiori dettagli o esempi?",
"public-template-mssg-4": "Visita il",
"public-template-mssg-4": "Visita il ",
"public-template-mssg-5": " npm per gli aggiornamenti più recenti, documentazione dettagliata e cronologia delle versioni.",
"public-template-mssg-6": "Prima di poter utilizzare questo frammento di codice, devi rendere questo modello pubblico.",
"public-template-mssg-7": "Prima di poter generare un link pubblico, devi rendere questo modello pubblico.",
@@ -744,6 +754,7 @@
"delete-page": "Elimina pagina",
"merge-pdf": "Unisci PDF",
"add-pages": "Aggiungi pagine",
"reorder-pages": "Riordina pagine",
"delete-alert": "Non è possibile eliminare una singola pagina",
"delete-alert-2": "Sei sicuro di voler eliminare questa pagina?",
"delete-note": "Nota: Una volta eliminata questa pagina, non potrai annullare l'operazione.",
@@ -971,5 +982,56 @@
"kiosk-sign": "Firma su chiosco",
"dont-have-access-to-template": "Il template è stato eliminato o non hai accesso. Si prega di contattare il mittente.",
"kiosk-info": "La Modalità Kiosk consente di raccogliere firme in presenza in modo rapido ed efficiente. Ideale per fiere, eventi o situazioni con firmatari fisicamente presenti. ",
"learn-more": "Scopri di più"
"learn-more": "Scopri di più",
"finish-mssg": "Sei sicuro di voler completare il documento?",
"review": "Rivedere",
"next-field": "Campo successivo",
"required-mssg":"{{leftRequiredWidget}} di {{totalWidget}} campi rimanenti",
"verify-document": "Verifica documento",
"verify-document-signature": "Verifica firma documento",
"select-pdf-document": "Seleziona documento PDF",
"selected-file": "File selezionato",
"verify-signature": "Verifica firma",
"verification-status": "Stato verifica",
"verification-in-progress": "Verifica in corso...",
"verification-results-will-appear-here": "I risultati della verifica appariranno qui",
"please-select-pdf": "Seleziona un file PDF valido",
"please-select-file-to-verify": "Seleziona un file da verificare",
"no-signature-found": "Nessuna firma trovata nel documento",
"error-verifying-pdf": "Errore durante la verifica del PDF",
"signature-valid-basic": "La firma è valida",
"signature-invalid-basic": "La firma non è valida",
"all-signatures-verified-convincing": "Documento Verificato: Tutte le firme sono state validate con successo.",
"some-signatures-invalid-basic": "Alcune firme non sono valide",
"no-signatures-processed": "Nessuna firma elaborata",
"unnamed-signature-field": "Campo firma senza nome",
"error-processing-signature": "Errore durante l'elaborazione della firma",
"signer-info-not-available": "Informazioni firmatario non disponibili",
"cert-validity-not-checked": "Validità certificato non verificata",
"valid": "Valido",
"expired-or-not-yet-valid": "Scaduto o non ancora valido",
"valid-from": "Valido dal",
"to": "al",
"signer": "Firmatario",
"issuer": "Emittente",
"not-available": "Non disponibile",
"not-performed": "Non eseguito",
"missing-acrofield-dict": "Dizionario Acrofield mancante",
"signature-dictionary-not-found-or-invalid": "Dizionario firme non trovato o non valido",
"missing-or-invalid-byterange": "ByteRange mancante o non valido",
"missing-or-invalid-contents": "Contenuto mancante o non valido",
"missing-signature-contents": "Contenuto firma mancante",
"invalid-signature-hex-format": "Formato esadecimale firma non valido",
"unsupported-signature-format-not-signeddata": "Formato firma non supportato - non SignedData",
"signer-certificate-not-found": "Certificato firmatario non trovato",
"no-certificates-in-signature": "Nessun certificato nella firma",
"no-signer-info-in-pkcs7": "Nessuna informazione firmatario in PKCS#7",
"could-not-parse-signer-info": "Impossibile analizzare le informazioni del firmatario",
"not-calculated": "Non calcolato",
"not-found-in-signature": "Non trovato nella firma",
"readonly-error": "Il widget {{widgetName}} di sola lettura deve avere un valore predefinito oppure può essere reso facoltativo.",
"choose-one":"Scegline uno",
"search-templates": "Cerca modelli…",
"search-documents": "Cerca documenti…",
"search-contacts": "Cerca contatti…"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.
+16 -12
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, lazy } from "react";
import { useState, useEffect, lazy } from "react";
import { Routes, Route, BrowserRouter } from "react-router";
import { pdfjs } from "react-pdf";
import Form from "./pages/Form";
@@ -30,7 +30,7 @@ const AddAdmin = lazy(() => import("./pages/AddAdmin"));
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
const Preferences = lazy(() => import("./pages/Preferences"));
const Login = lazy(() => import("./pages/Login"));
const VerifyDocument = lazy(() => import("./pages/VerifyDocument"));
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
const AppLoader = () => {
return (
@@ -106,10 +106,10 @@ function App() {
element={<LazyPage Page={GuestLogin} />}
/>
<Route path="/debugpdf" element={<LazyPage Page={DebugPdf} />} />
<Route
path="/forgetpassword"
element={<LazyPage Page={ForgetPassword} />}
/>
<Route
path="/forgetpassword"
element={<LazyPage Page={ForgetPassword} />}
/>
<Route
element={
<ValidateSession>
@@ -117,10 +117,10 @@ function App() {
</ValidateSession>
}
>
<Route
path="/changepassword"
element={<LazyPage Page={ChangePassword} />}
/>
<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 />} />
@@ -142,7 +142,7 @@ function App() {
/>
{/* 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 */}
{/* draft document route to handle and navigate route page according to document status */}
<Route path="/draftDocument" element={<DraftDocument />} />
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
<Route
@@ -165,7 +165,11 @@ function App() {
path="/recipientSignPdf/:docId"
element={<PdfRequestFiles />}
/>
<Route path="/users" element={<UserList />} />
<Route path="/users" element={<UserList />} />
<Route
path="/verify-document"
element={<LazyPage Page={VerifyDocument} />}
/>
<Route
path="/preferences"
element={<LazyPage Page={Preferences} />}
+1 -1
View File
@@ -20,7 +20,7 @@ const BulkSendUi = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//function to check atleast one signature field exist
//function to check at least one signature field exist
const signatureExist = async () => {
setIsDisableBulkSend(false);
const getPlaceholder = props?.Placeholders;
+55 -5
View File
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import dp from "../assets/images/dp.png";
import FullScreenButton from "./FullScreenButton";
import ThemeToggle from "./ThemeToggle";
import { useNavigate } from "react-router";
import Parse from "parse";
import { useWindowSize } from "../hook/useWindowSize";
@@ -85,6 +86,34 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
};
}, [isOpen]);
useEffect(() => {
const updateLogoForTheme = () => {
const isDarkMode =
document.documentElement.getAttribute("data-theme") === "opensigndark";
const logo = isDarkMode
? "/static/js/assets/images/logo-dark.png" // Path to the dark mode logo
: appInfo.applogo; // Use current logo for light mode
if (applogo !== logo) {
setAppLogo(logo);
}
};
// Set the logo immediately based on the current theme
updateLogoForTheme();
const observer = new MutationObserver(() => {
updateLogoForTheme();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"]
});
return () => observer.disconnect();
}, [applogo]);
return (
<div>
<div className="op-navbar bg-base-100 shadow">
@@ -147,7 +176,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
</div>
<ul
tabIndex={0}
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-52 ${
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
isOpen ? "" : "hidden"
}`}
>
@@ -172,15 +201,36 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
<i className="fa-light fa-user"></i> {t("profile")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/changepassword");
}}
>
<span>
<i className="fa-light fa-lock"></i>{" "}
{t("change-password")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/changepassword");
navigate("/verify-document");
}}
>
<span>
<i className="fa-light fa-lock"></i>{" "}
{t("change-password")}
<i className="fa-light fa-check-square"></i>{" "}
{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>
</li>
</>
@@ -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
View File
@@ -1,4 +1,3 @@
import React from "react";
import { Helmet } from "react-helmet";
function Title({ title, drive }) {
@@ -29,7 +29,7 @@ const DashboardButton = (props) => {
: "cursor-default"
} 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">
<span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center">
<i
@@ -39,7 +39,7 @@ const DashboardButton = (props) => {
></i>
</span>
</div>
<div className="text-lg ml-3 text-base-content">
<div className="text-lg ml-3">
{t(`sidebar.${props.Label}`)}
{props.Label === "Sign yourself" && (
<div className="text-gray-500 text-xs mt-1">
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef } from "react";
import Parse from "parse";
import ReportTable from "../../primitives/GetReportDisplay";
import reportJson from "../../json/ReportJson";
@@ -17,10 +17,16 @@ function DashboardReport(props) {
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("");
getReportData(props.Record.reportId);
setSearchTerm("");
setMobileSearchOpen(false);
getReportData(props.Record.reportId, 0, 20, "");
// Function returned from useEffect is called on unmount
return () => {
@@ -36,12 +42,69 @@ function DashboardReport(props) {
// below useEffect call when isNextRecord state is true and fetch next record
useEffect(() => {
if (isNextRecord) {
getReportData(props.Record.reportId, List.length, 20);
getReportData(props.Record.reportId, List.length, 20, searchTerm);
}
// eslint-disable-next-line
}, [isNextRecord]);
const getReportData = async (id, skipUserRecord = 0, limit = 20) => {
const handleSearchChange = 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 = async (
id,
skipUserRecord = 0,
limit = 20,
term = searchTerm
) => {
setIsLoader(true);
const json = reportJson(id);
if (json) {
@@ -59,6 +122,9 @@ function DashboardReport(props) {
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,
@@ -141,11 +207,17 @@ function DashboardReport(props) {
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">
<div className="text-center">
<p className="text-xl text-black">{t("report-not-found")}</p>
<p className="text-xl text-base-content">{t("report-not-found")}</p>
</div>
</div>
)}
@@ -1,10 +1,10 @@
import React, { useState, useEffect, useRef } from "react";
import "../../styles/opensigndrive.css";
import axios from "axios";
import * as ContextMenu from "@radix-ui/react-context-menu";
import { ContextMenu } from "radix-ui";
import { useNavigate } from "react-router";
import Table from "react-bootstrap/Table";
import * as HoverCard from "@radix-ui/react-hover-card";
import { HoverCard } from "radix-ui";
import ModalUi from "../../primitives/ModalUi";
import FolderModal from "../shared/fields/FolderModal";
import { useTranslation } from "react-i18next";
@@ -10,7 +10,7 @@ const AddRoleModal = (props) => {
isOpen={props.isModalRole}
handleClose={props.handleCloseRoleModal}
>
<div className="h-full py-[10px] px-[20px]">
<div className="text-base-content h-full py-[10px] px-[20px]">
<form className="flex flex-col" onSubmit={props.handleAddRole}>
<input
value={props.roleName}
@@ -12,20 +12,33 @@ function AgreementSign(props) {
<div className="op-modal op-modal-open absolute z-[448]">
<div className="w-[95%] md:w-[60%] lg:w-[40%] op-modal-box overflow-y-auto hide-scrollbar text-sm p-4">
<div className="flex flex-row items-center">
<input
data-tut="IsAgree"
className="mr-3 op-checkbox op-checkbox-m"
type="checkbox"
value={isChecked}
onChange={(e) => {
setIsChecked(e.target.checked);
if (e.target.checked) {
props.setIsAgreeTour(false);
}
props.showFirstWidget();
}}
/>
<div className="text-[11px] md:text-base">
<label className="inline-flex justify-center items-center cursor-pointer mb-0">
{/* 1) This div becomes the “fake” checkbox */}
<div
data-tut="IsAgree"
className={`w-6 h-6 border-2 mr-3 rounded-full flex text-center items-center justify-center ${isChecked ? "op-border-primary" : "border-red-500"}`}
>
{isChecked ? (
<span className="op-text-primary text-sm font-bold"></span>
) : (
<span className="text-red-500 text-sm font-bold">X</span>
)}
</div>
{/* 2) Visually hide the native checkbox but keep it in the DOM */}
<input
className="sr-only"
type="checkbox"
checked={isChecked}
onChange={(e) => {
setIsChecked(e.target.checked);
if (e.target.checked) {
props.setIsAgreeTour(false);
}
props.showFirstWidget();
}}
/>
</label>
<div className="text-[11px] md:text-base text-base-content">
<span>{t("agree-p1")}</span>
<span
className="font-bold text-blue-600 cursor-pointer"
@@ -54,7 +67,7 @@ function AgreementSign(props) {
{t("agrre-button")}
</button>
</div>
<div className="mt-2">
<div className="mt-2 text-base-content">
<span className="text-[11px]">{t("agreement-note")}</span>
</div>
</div>
@@ -12,12 +12,9 @@ function BorderResize(props) {
return (
<div
style={{
width: getHeight() || "14px",
height: getHeight() || "14px"
}}
style={{ width: getHeight() || "14px", height: getHeight() || "14px" }}
className={`${props.right ? `-right-[12px]` : "-right-[2px]"} ${
props.top ? `-bottom-[12px]` : "-bottom-[2px] "
props.top ? `-bottom-[12px]` : "-bottom-[2px]"
} absolute inline-block hover:cursor-sw-resize border-r-[3px] border-b-[3px] border-[#188ae2]`}
></div>
);
@@ -0,0 +1,104 @@
import React, { useState, useEffect } from "react";
import ModalUi from "../../primitives/ModalUi";
import { fontsizeArr, fontColorArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
export default function CellsSettingModal({
isOpen,
handleClose,
defaultData,
handleSave
}) {
const { t } = useTranslation();
const [name, setName] = useState("");
const [cellCount, setCellCount] = useState(5);
const [fontSize, setFontSize] = useState(12);
const [fontColor, setFontColor] = useState("black");
useEffect(() => {
if (defaultData) {
setName(defaultData.options?.name || "Cells");
setCellCount(defaultData.options?.cellCount || 5);
setFontSize(defaultData.options?.fontSize || 12);
setFontColor(defaultData.options?.fontColor || "black");
}
}, [defaultData]);
const onSubmit = (e) => {
e.preventDefault();
handleSave &&
handleSave({
name,
cellCount: parseInt(cellCount, 10),
fontSize,
fontColor
});
};
return (
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("widget-info")}>
<form onSubmit={onSubmit} className="p-[20px] text-base-content flex flex-col gap-3">
<div>
<label htmlFor="name" className="text-[13px]">
{t("name")} <span className="text-[red]">*</span>
</label>
<input
required
name="name"
className="op-input op-input-bordered op-input-sm w-full text-xs focus:outline-none hover:border-base-content"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label htmlFor="cellCount" className="text-[13px]">
{t("cell-count")}
</label>
<input
type="number"
min="1"
name="cellCount"
className="op-input op-input-bordered op-input-sm w-full text-xs focus:outline-none hover:border-base-content"
value={cellCount}
onChange={(e) => setCellCount(e.target.value)}
/>
</div>
<div className="flex flex-col md:flex-row md:items-center gap-3">
<div className="flex items-center gap-2">
<span className="whitespace-nowrap">{t("font-size")}: </span>
<select
className="ml-[7px] w-[60%] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={fontSize}
onChange={(e) => setFontSize(parseInt(e.target.value))}
>
{fontsizeArr.map((size, ind) => (
<option className="text-[13px]" value={size} key={ind}>
{size}
</option>
))}
</select>
</div>
<div className="flex items-center">
<span>{t("color")}: </span>
<select
className="ml-[33px] md:ml-4 w-[65%] md:w-full op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={fontColor}
onChange={(e) => setFontColor(e.target.value)}
>
{fontColorArr.map((color, ind) => (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
))}
</select>
<span className="w-5 h-[19px] ml-1" style={{ background: fontColor }} />
</div>
</div>
<div className="h-[1px] w-full bg-[#b7b3b3] my-[16px]"></div>
<button type="submit" className="op-btn op-btn-primary">
{t("save")}
</button>
</form>
</ModalUi>
);
}
@@ -0,0 +1,143 @@
import { useState, useEffect, useRef } from "react";
const Cell = ({
isEnabled,
count,
h,
value,
editable,
onChange,
onKeyDown,
onBlur,
inputRef,
index,
fontSize,
fontColor,
hint
}) => (
<div
className={`${isEnabled ? "bg-white border-gray-400" : "select-none-cls pointer-events-none border-gray-500"} flex items-center justify-center border-[1px]`}
style={{ flex: `0 0 ${100 / count}%`, height: h }}
>
<input
disabled={!isEnabled}
maxLength={1}
value={value}
readOnly={!editable}
ref={inputRef}
onChange={editable ? (e) => onChange && onChange(e, index) : undefined}
onKeyDown={editable ? (e) => onKeyDown && onKeyDown(e, index) : undefined}
// trigger validation when leaving a cell
onBlur={editable ? (e) => onBlur && onBlur(e, index) : undefined}
className={`${isEnabled ? "placeholder-gray-300" : "placeholder-gray-500"} w-full text-center focus:outline-none bg-transparent`}
placeholder={hint}
style={{ fontFamily: "Arial, sans-serif", fontSize, color: fontColor }}
/>
</div>
);
export default function CellsWidget({
isEnabled,
count = 8,
height = 40,
value = "",
editable = false,
onChange,
onKeyDown,
onBlur,
onCellCountChange,
inputRefs,
resizable = false,
fontSize = "12px",
fontColor = "black",
hint = ""
}) {
const [cellCount, setCellCount] = useState(count);
// keep internal state in sync with prop updates
useEffect(() => setCellCount(count), [count]);
const startX = useRef(0);
const startCount = useRef(cellCount);
const capture = (downEv, onMove, onUp = () => {}) => {
const id = downEv.pointerId;
const move = (ev) => id === ev.pointerId && onMove(ev);
const up = (ev) => {
if (id !== ev.pointerId) return;
onUp(ev);
downEv.target.releasePointerCapture(id);
window.removeEventListener("pointermove", move);
window.removeEventListener("pointerup", up);
};
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", up);
downEv.target.setPointerCapture(id);
};
const onTopHandlePointerDown = (ev) => {
// Prevent triggering the widget drag logic
ev.stopPropagation();
ev.preventDefault();
startX.current = ev.clientX;
startCount.current = cellCount;
capture(ev, (moveEv) => {
const dx = moveEv.clientX - startX.current;
const delta = Math.floor(-dx / 5);
let newCount = Math.max(1, startCount.current + delta);
if (newCount !== cellCount) {
setCellCount(newCount);
onCellCountChange?.(newCount);
}
});
};
const cells = Array.from({ length: cellCount }).map((_, i) => value[i] || "");
const hints = Array.from({ length: cellCount }).map((_, i) => hint[i] || "");
return (
<div
className="relative flex w-full h-full overflow-visible"
style={{ height }}
>
{resizable && (
<div
className="cell-size-handle absolute left-1/2 -translate-x-1/2 -bottom-4 rotate-180 cursor-ew-resize touch-none"
onPointerDown={onTopHandlePointerDown}
>
<svg
className="w-4 h-4 text-blue-600"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="m9.69 18.933.003.001C9.89 19.02 10 19 10 19s.11.02.308-.066l.002-.001.006-.003.018-.008a5.741 5.741 0 0 0 .281-.14c.186-.096.446-.24.757-.433.62-.384 1.445-.966 2.274-1.765C15.302 14.988 17 12.493 17 9A7 7 0 1 0 3 9c0 3.492 1.698 5.988 3.355 7.584a13.731 13.731 0 0 0 2.273 1.765 11.842 11.842 0 0 0 .976.544l.062.029.018.008.006.003ZM10 11.25a2.25 2.25 0 1 0 0-4.5 2.25 2.25 0 0 0 0 4.5Z"
clipRule="evenodd"
/>
</svg>
</div>
)}
{cells.map((val, i) => (
<Cell
key={i}
isEnabled={isEnabled}
count={cellCount}
h={height}
value={val}
editable={editable}
onChange={onChange}
onKeyDown={onKeyDown}
onBlur={onBlur}
inputRef={inputRefs ? (el) => (inputRefs.current[i] = el) : undefined}
index={i}
fontSize={fontSize}
fontColor={fontColor}
hint={hints[i]}
/>
))}
</div>
);
}
@@ -1,7 +1,10 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
function DefaultSignature(props) {
const { t } = useTranslation();
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
const myInitial = useSelector((state) => state.widget.myInitial)
const tabName = ["my-signature", "my-initials"];
const [activeTab, setActiveTab] = useState(0);
const confirmToaddDefaultSign = (type) => {
@@ -69,15 +72,15 @@ function DefaultSignature(props) {
<img
alt="signature"
className="w-full h-full object-contain"
src={props?.defaultSignImg}
src={defaultSignImg}
/>
) : (
activeTab === 1 &&
(props?.myInitial ? (
(myInitial ? (
<img
alt="signature"
className="w-full h-full object-contain"
src={props?.myInitial}
src={myInitial}
/>
) : (
<div className="flex justify-center items-center h-full">
@@ -97,7 +100,7 @@ function DefaultSignature(props) {
disabled={
activeTab === 0 && !props?.isDefault
? true
: activeTab === 1 && !props.myInitial
: activeTab === 1 && !myInitial
? true
: false
}
@@ -7,34 +7,36 @@ import { fontColorArr, fontsizeArr } from "../../constant/Utils";
function DropdownWidgetOption(props) {
const { t } = useTranslation();
const [dropdownOptionList, setDropdownOptionList] = useState([
"option-1",
"option-2"
"Option-1",
"Option-2"
]);
const [minCount, setMinCount] = useState(0);
const [maxCount, setMaxCount] = useState(0);
const [dropdownName, setDropdownName] = useState(props.type);
const [dropdownName, setDropdownName] = useState();
const [isReadOnly, setIsReadOnly] = useState(false);
const [isHideLabel, setIsHideLabel] = useState(false);
const [status, setStatus] = useState("required");
const [defaultValue, setDefaultValue] = useState("");
const statusArr = ["required", "optional"];
const [defaultCheckbox, setDefaultCheckbox] = useState([]);
const [layout, setLayout] = useState("vertical");
const statusArr = ["required", "optional"];
const layoutArr = ["vertical", "horizontal"];
const resetState = () => {
setDropdownOptionList(["option-1", "option-2"]);
setDropdownName(props.type);
setDropdownOptionList(["Option-1", "Option-2"]);
setDropdownName(props.currWidgetsDetails?.options?.name || props.type);
setIsReadOnly(false);
setIsHideLabel(false);
setMinCount(0);
setMaxCount(0);
setDefaultCheckbox([]);
setDefaultValue("");
setLayout("vertical");
};
useEffect(() => {
if (
props.currWidgetsDetails?.options?.name &&
props.currWidgetsDetails?.options?.values
props.currWidgetsDetails?.options?.values?.length > 0
) {
setDropdownName(props.currWidgetsDetails?.options?.name);
setDropdownOptionList(props.currWidgetsDetails?.options?.values);
@@ -49,6 +51,7 @@ function DropdownWidgetOption(props) {
setStatus(props.currWidgetsDetails?.options?.status || "required");
setDefaultValue(props.currWidgetsDetails?.options?.defaultValue || "");
setDefaultCheckbox(props.currWidgetsDetails?.options?.defaultValue || []);
setLayout(props.currWidgetsDetails?.options?.layout || "vertical");
} else {
setStatus("required");
resetState();
@@ -104,6 +107,30 @@ function DropdownWidgetOption(props) {
? defaultCheckbox
: defaultValue;
const isDropdownOrRadio =
props?.type === "dropdown" || props?.type === radioButtonWidget;
const readOnlyWithoutValue =
isReadOnly && !defaultValue && status !== "optional";
const isCheckbox = props?.type === "checkbox";
const WidgetLayout = ["checkbox", radioButtonWidget].includes(props.type)
? layout
: null;
// If its a dropdown and its read-only without a value (nor marked optional), stop here.
if (isDropdownOrRadio && readOnlyWithoutValue) {
alert(t("readonly-error", { widgetName: props?.type }));
return;
} else if (
isCheckbox &&
isReadOnly &&
minCount > 0 &&
defaultCheckbox?.length === 0
) {
alert(t("readonly-error", { widgetName: props?.type }));
return;
}
// Otherwise (either not a dropdown, or a valid dropdown), do the save + reset exactly once.
props.handleSaveWidgetsOptions(
dropdownName,
dropdownOptionList,
@@ -114,18 +141,10 @@ function DropdownWidgetOption(props) {
null,
status,
defaultData,
isHideLabel
isHideLabel,
WidgetLayout
);
// props.setShowDropdown(false);
setDropdownOptionList(["option-1", "option-2"]);
setDropdownName(props.type);
// props.setCurrWidgetsDetails({});
setIsReadOnly(false);
setIsHideLabel(false);
setMinCount(0);
setMaxCount(0);
setDefaultCheckbox([]);
setDefaultValue("");
resetState();
};
@@ -137,7 +156,6 @@ function DropdownWidgetOption(props) {
return false;
}
};
return (
<ModalUi isOpen={props.showDropdown} title={props.title} showClose={false}>
<div className="h-full p-[15px] text-base-content">
@@ -148,25 +166,25 @@ function DropdownWidgetOption(props) {
}}
>
<div>
<label className="text-[13px] font-semibold">
<label htmlFor="title" className="text-[13px] font-semibold">
{t("name")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
id="title"
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
defaultValue={dropdownName}
value={dropdownName}
onChange={(e) => setDropdownName(e.target.value)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
required
/>
<label className="text-[13px] font-semibold mt-[5px]">
{t("options")}
</label>
<div className="flex flex-col">
{dropdownOptionList.map((option, index) => (
{dropdownOptionList?.map((option, index) => (
<div
key={index}
className="flex flex-row mb-[5px] items-center"
@@ -240,28 +258,26 @@ function DropdownWidgetOption(props) {
</select>
</>
)}
{props.type !== "checkbox" && props.type !== radioButtonWidget && (
<>
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
{statusArr.map((data, ind) => {
return (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="op-radio op-radio-xs my-1"
type="radio"
name="status"
onChange={() => setStatus(data.toLowerCase())}
checked={status.toLowerCase() === data.toLowerCase()}
/>
<div className="text-[13px] font-500">{data}</div>
</div>
);
})}
</div>
</>
{props.type !== "checkbox" && (
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
{statusArr.map((data, ind) => (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="op-radio op-radio-xs my-1"
type="radio"
name="status"
onChange={() => setStatus(data.toLowerCase())}
checked={status.toLowerCase() === data.toLowerCase()}
/>
<div className="text-[13px] font-500 capitalize">
{data}
</div>
</div>
))}
</div>
)}
<div className="flex items-center mt-3 mb-3">
<span>{t("font-size")} :</span>
@@ -282,8 +298,8 @@ function DropdownWidgetOption(props) {
);
})}
</select>
<div className="flex flex-row gap-1 items-center ml-4 ">
<span>{t("color")} : </span>
<div className="flex flex-row gap-1 items-center ml-4">
<span className="capitalize">{t("color")} : </span>
<select
value={
props.fontColor ||
@@ -325,7 +341,10 @@ function DropdownWidgetOption(props) {
className="op-checkbox op-checkbox-sm"
onChange={(e) => setIsReadOnly(e.target.checked)}
/>
<label className="ml-1 mb-0" htmlFor="isreadonly">
<label
className="ml-2 mb-0 capitalize"
htmlFor="isreadonly"
>
{t("read-only")}
</label>
</div>
@@ -340,15 +359,45 @@ function DropdownWidgetOption(props) {
onChange={(e) => setIsHideLabel(e.target.checked)}
/>
<label className="ml-1 mb-0" htmlFor="ishidelabel">
<label
className="ml-2 mb-0 capitalize"
htmlFor="ishidelabel"
>
{t("hide-labels")}
</label>
</div>
)}
</div>
)}
{["checkbox", radioButtonWidget].includes(props.type) && (
<>
<div className="text-[13px] font-semibold mt-[5px] capitalize">
{t("layout")}
</div>
<div
className={`${props.type === "checkbox" ? "mb-[10px]" : ""} flex flex-row gap-[10px] mt-[0.5rem]`}
>
{layoutArr.map((data, ind) => (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="op-radio op-radio-xs my-1"
type="radio"
name="layout"
checked={layout.toLowerCase() === data.toLowerCase()}
onChange={() => setLayout(data.toLowerCase())}
/>
<label className="text-[13px] font-500 mb-0 capitalize">
{data}
</label>
</div>
))}
</div>
</>
)}
</div>
<div
className={`${
props.type === "checkbox" && props.isShowAdvanceFeature
@@ -1,4 +1,4 @@
import React, {
import {
useState,
useRef,
} from "react";
@@ -1,5 +1,10 @@
import React from "react";
import { Quill } from "react-quill-new";
// Use a third-party plugin so editors can toggle raw HTML view.
import htmlEditButton from "quill-html-edit-button";
// Register the module which adds a "<>" button for HTML editing.
Quill.register("modules/htmlEditButton", htmlEditButton);
// Custom Undo button icon component for Quill editor. You can import it directly
// from 'quill/assets/icons/undo.svg' but I found that a number of loaders do not
@@ -56,7 +61,9 @@ export const module1 = {
container: "#toolbar1",
handlers: { undo: undoChange, redo: redoChange }
},
history: { delay: 500, maxStack: 100, userOnly: true }
history: { delay: 500, maxStack: 100, userOnly: true },
// Enable the "<>" button registered above
htmlEditButton: {}
};
// Modules object for setting up the Quill editor
@@ -65,7 +72,9 @@ export const module2 = {
container: "#toolbar2",
handlers: { undo: undoChange, redo: redoChange }
},
history: { delay: 500, maxStack: 100, userOnly: true }
history: { delay: 500, maxStack: 100, userOnly: true },
// Enable the "<>" button registered above
htmlEditButton: {}
};
// Formats objects for setting up the Quill editor
@@ -186,7 +186,7 @@ function EmailComponent({
<input
type="email"
value={emailValue}
className="p-[10px] pb-[20px] rounded-md w-full text-[15px] outline-none bg-transparent border-[1px] op-border-primary"
className="p-[10px] pb-[20px] text-base-content rounded-md w-full text-[15px] outline-none bg-transparent border-[1px] op-border-primary"
onChange={handleEmailValue}
onKeyDown={handleEnterPress}
placeholder={t("enter-email-plaholder")}
@@ -204,19 +204,6 @@ function EmailComponent({
{t("email-error-1")}
</p>
)}
{/* <button
className={`${
emailValue ? "cursor-pointer" : "cursor-default"
} op-btn op-btn-primary op-btn-sm m-2 shadow-md`}
onClick={() => emailValue && handleEnterPress("add")}
>
<i className="fa-light fa-plus" aria-hidden="true"></i>
</button>
<div className="bg-[#e3e2e1] mt-[10px] p-[5px] rounded">
<span className="font-bold">{t("report-heading.Note")}: </span>
<span className="text-[15px]">{t("email-error-2")}</span>
</div>
<hr className="w-full my-[15px] bg-base-content" /> */}
<div className="mt-2">
<button
type="button"
@@ -227,7 +214,7 @@ function EmailComponent({
</button>
<button
type="button"
className="op-btn op-btn-ghost ml-2"
className="op-btn op-btn-ghost text-base-content ml-2"
onClick={() => handleClose()}
>
{t("close")}
@@ -0,0 +1,94 @@
import React, { useEffect, useState, useRef } from "react";
import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next";
export default function PageReorderModal({
isOpen,
handleClose,
totalPages = 0,
onSave
}) {
const { t } = useTranslation();
const [order, setOrder] = useState([]);
// Keeps track of the page order relative to the original PDF
const orderRef = useRef([]);
// Captures the order when the modal opens
const initialOrderRef = useRef([]);
// Initialize orderRef when total pages change (e.g. after upload)
useEffect(() => {
if (orderRef.current.length !== totalPages) {
orderRef.current = Array.from({ length: totalPages }, (_, i) => i + 1);
}
}, [totalPages]);
// When modal opens, display the last saved order and store it as initial
useEffect(() => {
if (isOpen) {
setOrder(orderRef.current);
initialOrderRef.current = [...orderRef.current];
}
}, [isOpen]);
const move = (index, dir) => {
const swapIndex = index + dir;
if (swapIndex < 0 || swapIndex >= order.length) return;
const newOrder = [...order];
[newOrder[index], newOrder[swapIndex]] = [newOrder[swapIndex], newOrder[index]];
setOrder(newOrder);
};
const handleSave = () => {
const saveOrder = order.map((num) =>
initialOrderRef.current.indexOf(num) + 1
);
// Persist the new display order for next time
orderRef.current = [...order];
onSave && onSave(saveOrder);
};
const isUnchanged =
order.length === initialOrderRef.current.length &&
order.every((n, i) => n === initialOrderRef.current[i]);
return (
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("reorder-pages")}>
<div className="p-[20px] flex flex-col gap-2 text-base-content">
{order.map((num, i) => (
<div key={num} className="flex items-center justify-between">
<span>
{t("page")} {num}
</span>
<div className="flex gap-1">
<button
className="op-btn op-btn-xs op-btn-ghost"
disabled={i === 0}
onClick={() => move(i, -1)}
>
<i className="fa-light fa-arrow-up"></i>
</button>
<button
className="op-btn op-btn-xs op-btn-ghost"
disabled={i === order.length - 1}
onClick={() => move(i, 1)}
>
<i className="fa-light fa-arrow-down"></i>
</button>
</div>
</div>
))}
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button onClick={handleSave} type="button" className="op-btn op-btn-primary" disabled={isUnchanged}>
{t("save")}
</button>
<button
onClick={handleClose}
type="button"
className="op-btn op-btn-ghost ml-1"
>
{t("close")}
</button>
</div>
</ModalUi>
);
}
+90 -3
View File
@@ -2,16 +2,21 @@ import React, { useRef, useState } from "react";
import PrevNext from "./PrevNext";
import {
base64ToArrayBuffer,
decryptPdf,
deletePdfPage,
flattenPdf,
getFileAsArrayBuffer,
handleDownloadCertificate,
handleDownloadPdf,
handleRemoveWidgets,
handleToPrint
handleToPrint,
reorderPdfPages
} from "../../constant/Utils";
import "../../styles/signature.css";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { DropdownMenu } from "radix-ui";
import ModalUi from "../../primitives/ModalUi";
import Loader from "../../primitives/Loader";
import PageReorderModal from "./PageReorderModal";
import { useTranslation } from "react-i18next";
import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const";
@@ -24,6 +29,7 @@ function Header(props) {
const isMobile = window.innerWidth < 767;
const [isDownloading, setIsDownloading] = useState("");
const [isDeletePage, setIsDeletePage] = useState(false);
const [isReorderModal, setIsReorderModal] = useState(false);
const mergePdfInputRef = useRef(null);
const enabledBackBtn = props?.disabledBackBtn === true ? false : true;
//function for show decline alert
@@ -80,7 +86,42 @@ function Header(props) {
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert("Incorrect password or decryption failed.");
}
} else {
alert("Please provided Password.");
}
} else {
console.log("Err ", err);
alert("error while uploading pdf.");
}
}
} else {
alert("error while uploading pdf.");
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -106,6 +147,21 @@ function Header(props) {
console.error("Error merging PDF:", error);
}
};
const handleReorderSave = async (order) => {
try {
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
if (pdfupdatedData) {
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
props.setPdfBase64Url(pdfupdatedData.base64);
props.setAllPages(pdfupdatedData.totalPages);
props.setPageNumber(1);
}
} catch (e) {
console.log("error in reorder pdf pages", e);
}
setIsReorderModal(false);
};
return (
<div className="flex py-[5px]">
{isMobile && props?.isShowHeader ? (
@@ -276,6 +332,20 @@ function Header(props) {
className="bg-white shadow-md rounded-md px-3 py-2"
sideOffset={5}
>
{props?.setIsEditTemplate && (
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => props?.setIsEditTemplate(true)}
>
<div className="flex flex-row">
<i
className="fa-light fa-gear mr-[3px]"
aria-hidden="true"
></i>
<span className="font-[500]">{t("Edit")}</span>
</div>
</DropdownMenu.Item>
)}
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() =>
@@ -320,6 +390,17 @@ function Header(props) {
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => setIsReorderModal(true)}
>
<div className="flex flex-row">
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[30px] mr-[3px]"></i>
<span className="font-[500]">
{t("reorder-pages")}
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
@@ -682,6 +763,12 @@ function Header(props) {
</button>
</div>
</ModalUi>
<PageReorderModal
isOpen={isReorderModal}
handleClose={() => setIsReorderModal(false)}
totalPages={props.allPages}
onSave={handleReorderSave}
/>
</div>
);
}
+72 -3
View File
@@ -1,18 +1,24 @@
import React, { useRef, useState } from "react";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
base64ToArrayBuffer,
decryptPdf,
deletePdfPage,
handleRemoveWidgets
flattenPdf,
getFileAsArrayBuffer,
handleRemoveWidgets,
reorderPdfPages
} from "../../constant/Utils";
import ModalUi from "../../primitives/ModalUi";
import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const";
import PageReorderModal from "./PageReorderModal";
function PdfZoom(props) {
const { t } = useTranslation();
const mergePdfInputRef = useRef(null);
const [isDeletePage, setIsDeletePage] = useState(false);
const [isReorderModal, setIsReorderModal] = useState(false);
const handleDetelePage = async () => {
props.setIsUploadPdf && props.setIsUploadPdf(true);
try {
@@ -67,7 +73,42 @@ function PdfZoom(props) {
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert("Incorrect password or decryption failed.");
}
} else {
alert("Please provided Password.");
}
} else {
console.log("Err ", err);
alert("error while uploading pdf.");
}
}
} else {
alert("error while uploading pdf.");
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -94,6 +135,21 @@ function PdfZoom(props) {
}
};
const handleReorderSave = async (order) => {
try {
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
if (pdfupdatedData) {
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
props.setPdfBase64Url(pdfupdatedData.base64);
props.setAllPages(pdfupdatedData.totalPages);
props.setPageNumber(1);
}
} catch (e) {
console.log("error in reorder pdf pages", e);
}
setIsReorderModal(false);
};
return (
<>
<span className="hidden md:flex flex-col gap-1 text-center md:w-[5%] mt-[42px]">
@@ -120,6 +176,13 @@ function PdfZoom(props) {
>
<i className="fa-light fa-trash text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => setIsReorderModal(true)}
title={t("reorder-pages")}
>
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[25px]"></i>
</span>
</>
)}
<span
@@ -185,6 +248,12 @@ function PdfZoom(props) {
</button>
</div>
</ModalUi>
<PageReorderModal
isOpen={isReorderModal}
handleClose={() => setIsReorderModal(false)}
totalPages={props.allPages}
onSave={handleReorderSave}
/>
</>
);
}
+231 -296
View File
@@ -1,6 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { useState, useEffect } from "react";
import BorderResize from "./BorderResize";
import PlaceholderBorder from "./PlaceholderBorder";
import { Rnd } from "react-rnd";
import {
changeDateToMomentFormat,
@@ -14,6 +13,7 @@ import {
onChangeInput,
radioButtonWidget,
textInputWidget,
cellsWidget,
textWidget
} from "../../constant/Utils";
import PlaceholderType from "./PlaceholderType";
@@ -21,6 +21,9 @@ import moment from "moment";
import "../../styles/opensigndrive.css";
import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { setIsShowModal } from "../../redux/reducers/widgetSlice";
import { themeColor } from "../../constant/const";
const selectFormat = (data) => {
switch (data) {
@@ -72,31 +75,25 @@ const getDefaultDate = (dateStr, format) => {
};
function Placeholder(props) {
//'isTouchDevice' is used to detect whether a device has a touchscreen or is mouse-based
const isTouchDevice = navigator.maxTouchPoints > 0;
const { t } = useTranslation();
const [placeholderBorder, setPlaceholderBorder] = useState({ w: 0, h: 0 });
const [isDraggingEnabled, setDraggingEnabled] = useState(true);
const dispatch = useDispatch();
const widgetData =
props.pos?.options?.defaultValue || props.pos?.options?.response;
const [isDateModal, setIsDateModal] = useState(false);
const [containerScale, setContainerScale] = useState();
const holdTimeout = useRef(null);
const startTime = useRef(null); // Track when the user starts holdings
const [isDisableDragging, setIsDisableDragging] = useState(true);
const [selectDate, setSelectDate] = useState({});
const [dateFormat, setDateFormat] = useState([]);
const [clickonWidget, setClickonWidget] = useState({});
const [startDate, setStartDate] = useState(
props?.pos?.options?.response
? getDefaultDate(
props?.pos?.options?.response,
props.pos?.options?.validation?.format
)
: new Date()
const [isDateReadOnly, setIsDateReadOnly] = useState(
props?.pos?.options?.isReadOnly || false
);
const [getCheckboxRenderWidth, setGetCheckboxRenderWidth] = useState({
width: null,
height: null
});
const startDate = props?.pos?.options?.response
? getDefaultDate(
props?.pos?.options?.response,
props.pos?.options?.validation?.format
)
: new Date();
useEffect(() => {
const getPdfPageWidth = props.pdfOriginalWH.find(
(data) => data.pageNumber === props.pageNumber
@@ -117,33 +114,16 @@ function Placeholder(props) {
];
useEffect(() => {
const updateWidth = () => {
const rndElement = document.getElementById(props.pos.key);
if (rndElement) {
const { width, height } = rndElement.getBoundingClientRect();
setGetCheckboxRenderWidth({ width: width, height: height });
}
};
if (props?.pos?.type === "date") {
const isDateChange = true;
const dateObj = {
date: startDate,
format: getDefaultFormat(props.pos?.options?.validation?.format)
};
handleSaveDate(dateObj, isDateChange); //function to save date and format in local array
}
}, [widgetData]);
// Delay to ensure rendering is complete
const timer = setTimeout(updateWidth, 0);
return () => clearTimeout(timer);
}, [props.pos]);
useEffect(() => {
const onOutsideClick = () => {
if (!isDraggingEnabled) {
setDraggingEnabled(true);
}
};
document.addEventListener("click", onOutsideClick);
return () => {
// Cleanup the event listener when the component unmounts
document.removeEventListener("click", onOutsideClick);
};
}, [isDraggingEnabled]);
//function change format array list with selected date and format
const changeDateFormat = () => {
const updateDate = [];
@@ -175,30 +155,18 @@ function Placeholder(props) {
//`handleWidgetIdandPopup` is used to set current widget id and open relative popup
const handleWidgetIdandPopup = async () => {
if (props.setSelectWidgetId) {
props.setSelectWidgetId(props.pos.key);
}
const widgetTypeExist = [
textInputWidget,
"checkbox",
"name",
"company",
"job title",
"date",
"email",
textWidget
].includes(props.pos.type);
if (widgetTypeExist) {
setDraggingEnabled(false);
}
if (props.isOpenSignPad && !props.isDragging) {
//'props.isOpenSignPad' variable is used to check flow to open signature pad for finish document
if (
props.isOpenSignPad &&
!props.isDragging &&
!props.pos.options?.isReadOnly &&
props.pos.type !== textWidget
) {
if (props?.ispublicTemplate) {
props.handleUserDetails();
} else {
if (props?.isNeedSign) {
//funcion is used to heightlight widgets on top if two widgets on overlap
//funcion is used to highlight widgets on top when click any widget if two widgets on overlap
const getCurrentSignerPos = props.xyPosition.find(
(x) => x.Id === props.uniqueId
);
@@ -212,23 +180,14 @@ function Placeholder(props) {
);
props.setXyPosition(updatesignerPos);
}
if (
["signature", "stamp", "image", "initials"].includes(props.pos.type)
) {
props.setIsSignPad(true);
props.setSignKey(props.pos.key);
props.setIsStamp(props.pos.isStamp);
}
if (props.pos.type === "initials") {
props.setIsInitial(true);
}
dispatch(setIsShowModal({ [props.pos.key]: true }));
}
} else if (
props.isPlaceholder &&
!props.isDragging &&
props.pos.type !== textWidget
) {
if (props.pos.key === props.selectWidgetId) {
if (props.pos.key === props?.currWidgetsDetails?.key) {
props.handleLinkUser(props.data.Id);
props.setUniqueId(props.data.Id);
const checkIndex = props.xyPosition.findIndex(
@@ -236,31 +195,17 @@ function Placeholder(props) {
);
props.setIsSelectId(checkIndex || 0);
}
} else if (!props.pos.type) {
if (
!props.pos.type &&
props.isNeedSign &&
props.data.signerObjId === props.signerObjId
) {
props.setIsSignPad(true);
props.setSignKey(props.pos.key);
props.setIsStamp(props.pos.isStamp);
} else if (
(props.isNeedSign && props.pos.type === "signature") ||
props.pos.type === "stamp"
) {
props.setIsSignPad(true);
props.setSignKey(props.pos.key);
props.setIsStamp(props.pos.isStamp);
} else if (props.isNeedSign && props.pos.type === "dropdown") {
props.setSignKey(props.pos.key);
}
//handle prefill 'text widget' click then save previos in tem variable after save or close button again assign current selected userId
} else if (props.pos.type === textWidget) {
dispatch(setIsShowModal({ [props.pos.key]: true }));
props.setTempSignerId(props?.uniqueId);
props.setUniqueId(props?.data?.Id);
}
};
const widgetClickHandler = () => {
//The else condition opens the signature pad if it's a request signature flow and the user clicking is identified as a signer.
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails(props.pos);
//condition to check in request signing flow user click on agree or not
if (props?.data?.signerObjId === props?.signerObjId && !props.isDragging) {
if (!props.isAgree && !props.isSelfSign) {
props.setIsAgreeTour && props.setIsAgreeTour(true);
@@ -272,7 +217,10 @@ function Placeholder(props) {
handleWidgetIdandPopup();
}
};
const handleOnClickPlaceholder = () => {
//'props.isDragging' variable is used to checking if user take any widget and try to drag then in that case
//onclick event call. so to prevent onclick and do not open unecessary modal open.
//condition only for request signing flow and self signing flow then apply one click copy sign url of previous drawn signature
if (props.isApplyAll) {
props.setRequestSignTour && props.setRequestSignTour(true);
@@ -342,12 +290,9 @@ function Placeholder(props) {
} else {
widgetClickHandler();
}
// }
// }
} else {
//The else condition is used to handle the case when the user clicks on a widget and open signature pad to draw sign
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails(props.pos);
props.setWidgetType(props.pos.type);
handleWidgetIdandPopup();
}
};
@@ -360,6 +305,13 @@ function Placeholder(props) {
} else if (props.pos.type === "checkbox") {
props?.setIsCheckbox(true);
}
// cells widget settings in sign yourself flow
else if (
props.pos.type === cellsWidget &&
(props.isSignYourself || props.isSelfSign)
) {
props.handleCellSettingModal && props.handleCellSettingModal();
}
//condition to handle setting icon for signyour-self flow for all type text widgets
else if (
[
@@ -391,8 +343,6 @@ function Placeholder(props) {
props.setTempSignerId(props.uniqueId);
props.setUniqueId(props?.data?.Id);
}
props.setSignKey(props.pos.key);
props.setWidgetType(props.pos.type);
props.setCurrWidgetsDetails(props.pos);
};
//function to set required state value onclick on widget's copy icon
@@ -425,7 +375,7 @@ function Placeholder(props) {
)
) {
props.setIsPageCopy(true);
props.setSignKey(props.pos.key);
props.setCurrWidgetsDetails(props.pos);
} else {
//function to create new widget next to just widget
handleCopyNextToWidget(
@@ -439,18 +389,6 @@ function Placeholder(props) {
}
};
//function to save date and format after seleted new date in response field and after finish document it should be emebed new selected date instead of current date
useEffect(() => {
if (props.pos.type === "date") {
const isDateChange = true;
const dateObj = {
date: startDate,
format: getDefaultFormat(props.pos?.options?.validation?.format)
};
handleSaveDate(dateObj, isDateChange); //function to save date and format in local array
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startDate]);
//function to save date and format on local array onchange date and onclick format
const handleSaveDate = (data, isDateChange) => {
let updateDate = data.date;
@@ -476,14 +414,55 @@ function Placeholder(props) {
props.data && props.data.Id,
false,
data?.format,
null,
null,
props.fontSize || props.pos?.options?.fontSize || 12,
props.fontColor || props.pos?.options?.fontColor || "black"
props.fontColor || props.pos?.options?.fontColor || "black",
isDateReadOnly || false
);
setSelectDate({ date: date, format: data?.format });
};
const setCellCount = (key, newCount) => {
props.setXyPosition((prev) => {
const isSignerList = prev.some((d) => d.signerPtr);
if (isSignerList) {
const signerId = props.data?.Id || props.uniqueId;
const filterSignerPos = prev.filter((d) => d.Id === signerId);
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const updatedPlaceHolder = getPlaceHolder.map((ph) => {
if (ph.pageNumber !== props.pageNumber) return ph;
const newPos = ph.pos.map((p) =>
p.key === key
? { ...p, options: { ...p.options, cellCount: newCount } }
: p
);
return { ...ph, pos: newPos };
});
return prev.map((obj) =>
obj.Id === signerId
? { ...obj, placeHolder: updatedPlaceHolder }
: obj
);
}
} else {
const updatePos = prev[props.index].pos.map((p) =>
p.key === key
? { ...p, options: { ...p.options, cellCount: newCount } }
: p
);
return prev.map((obj, ind) =>
ind === props.index ? { ...obj, pos: updatePos } : obj
);
}
return prev;
});
};
const PlaceholderIcon = () => {
const isSettingForCells =
props?.isAlllowModify && !props?.assignedWidgetId.includes(props.pos.key)
? []
: [cellsWidget];
// 1- If props.isShowBorder is true, display border's icon for all widgets. OR
// 2- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show border's icon:
// 1- When isAlllowModify is true, show border's icon.
@@ -507,10 +486,15 @@ function Placeholder(props) {
"name",
"company",
"job title",
"email"
"email",
...isSettingForCells
].includes(props.pos.type) &&
(props.isSignYourself || props.isSelfSign) ? (
<i
onPointerDown={(e) => {
e.stopPropagation();
handleOnClickSettingIcon();
}}
onClick={(e) => {
e.stopPropagation();
handleOnClickSettingIcon();
@@ -520,7 +504,14 @@ function Placeholder(props) {
handleOnClickSettingIcon();
}}
className="fa-light fa-gear icon"
style={{ color: "#188ae2", right: "29px", top: "-19px" }}
style={{
color: "#188ae2",
right: "29px",
top: "-19px",
cursor: "pointer",
zIndex: 99,
pointerEvents: "auto"
}}
></i>
) : (
/* condition to add setting icon for placeholder & template flow for all widgets except signature and date */
@@ -530,6 +521,10 @@ function Placeholder(props) {
!props.isSignYourself &&
!props.isSelfSign)) && (
<i
onPointerDown={(e) => {
e.stopPropagation();
handleOnClickSettingIcon();
}}
onClick={(e) => {
e.stopPropagation();
handleOnClickSettingIcon();
@@ -542,7 +537,10 @@ function Placeholder(props) {
style={{
color: "#188ae2",
right: props?.pos?.type === textWidget ? "32px" : "51px",
top: "-19px"
top: "-19px",
cursor: "pointer",
zIndex: 99,
pointerEvents: "auto"
}}
></i>
)
@@ -580,13 +578,14 @@ function Placeholder(props) {
{/* setting icon only for date widgets */}
{props.pos.type === "date" && selectDate && (
<i
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
props.setCurrWidgetsDetails(props.pos);
setIsDateModal(!isDateModal);
e.stopPropagation();
setClickonWidget(props.pos);
if (props.data) {
props.setSignKey(props.pos.key);
props.setCurrWidgetsDetails(props.pos);
props.setUniqueId(props.data.Id);
const checkIndex = props.xyPosition.findIndex(
(data) => data.Id === props.data.Id
@@ -599,7 +598,7 @@ function Placeholder(props) {
e.stopPropagation();
setIsDateModal(!isDateModal);
if (props.data) {
props.setSignKey(props.pos.key);
props.setCurrWidgetsDetails(props.pos);
props.setUniqueId(props.data.Id);
const checkIndex = props.xyPosition.findIndex(
(data) => data.Id === props.data.Id
@@ -612,7 +611,9 @@ function Placeholder(props) {
top: "-18px",
right: props.isPlaceholder ? "50px" : "30px",
color: "#188ae2",
fontSize: "14px"
fontSize: "14px",
cursor: "pointer",
pointerEvents: "auto"
}}
className="fa-light fa-gear icon"
></i>
@@ -636,7 +637,6 @@ function Placeholder(props) {
//condition for signyour-self flow
else {
props.handleDeleteSign(props.pos.key);
props.setIsStamp(false);
}
}}
//for mobile and tablet touch event
@@ -649,7 +649,6 @@ function Placeholder(props) {
//condition for signyour-self flow
else {
props.handleDeleteSign(props.pos.key);
props.setIsStamp(false);
}
}}
style={{ color: "#188ae2", right: "-8px", top: "-18px" }}
@@ -741,46 +740,10 @@ function Placeholder(props) {
return "not-allowed";
}
} else {
return "all-scroll";
}
};
const handleDragging = () => {
//condition for request signing flow
if (props.isNeedSign) {
//enable dragging functionality only if isAlllowModify true on tab that widget and hold 1sec
if (
props.isAlllowModify &&
props?.assignedWidgetId.includes(props.pos.key) &&
props.data?.signerObjId === props.signerObjId
) {
//if 'isTouchDevice' then handle dragging functionality conditionaly
if (isTouchDevice) {
return isDisableDragging;
} else {
//no need to handle dragging functionality it auto enable and working or mouse click devices
return false;
}
} else if (
//condition when 'isAlllowModify' is true and user add new widgets then handle dragging functionality like signyourself flow
props.isAlllowModify &&
!props?.assignedWidgetId.includes(props.pos.key) &&
props.data?.signerObjId === props.signerObjId
) {
return !isDraggingEnabled;
} else {
//if 'isAlllowModify' is false then disbale dragging functionality
return true;
}
} //dragging enable in placeholder,template and not text widget flow
else if (props.isPlaceholder && ![textWidget].includes(props.pos.type)) {
return false;
} //dragging depend on 'isDraggingEnabled' variable in self sign and signyourself flow
else if (isTouchDevice) {
return !isDraggingEnabled;
} else {
return false;
return "move";
}
};
//function to handle widget background color
const handleBackground = () => {
if (props.data) {
@@ -797,57 +760,38 @@ function Placeholder(props) {
return "rgba(203, 233, 237, 0.69)";
}
};
const handleTouchEnd = () => {
if (!props.isNeedSign || props.isAlllowModify) {
const holdDuration = Date.now() - startTime.current; // Calculate hold time
clearTimeout(holdTimeout.current); // Cancel timeout if touch ended early
if (holdDuration < 1000) {
try {
navigator.vibrate([]); // Cancel any ongoing vibration
} catch (e) {
console.log("error in navigator.vibrate", e);
}
}
}
if (!props.isNeedSign || props.isAlllowModify) handleOnClickPlaceholder();
};
const handleTouchStart = () => {
clearTimeout(holdTimeout.current); // Ensure no previous timeouts are running
startTime.current = Date.now(); // Store touch start time
holdTimeout.current = setTimeout(() => {
//handlle vibration and tab any widget and hold for 1 sec then show border outside widget and then user can able to drag
if (isDisableDragging) {
if (
props.isNeedSign &&
props.isAlllowModify &&
props?.assignedWidgetId.includes(props.pos.key)
) {
try {
navigator.vibrate(200); // Vibrate for 200ms
} catch (e) {
console.log("error in navigator.vibrate", e);
}
setIsDisableDragging(false);
props.setSelectWidgetId(props.pos.key);
} else if (!props.isNeedSign) {
setIsDisableDragging(false);
}
}
}, 1000); // Hold for 1 second before vibrating
};
const fontSize = calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
const handleDragging = () => {
//condition for request signing flow
if (props.isNeedSign) {
//enable dragging functionality only if isAlllowModify true on tab that widget and hold 1sec
if (
props.isAlllowModify &&
!props?.assignedWidgetId.includes(props.pos.key) &&
props.data?.signerObjId === props.signerObjId
) {
return false;
} else {
//if 'isAlllowModify' is false then disbale dragging functionality
return true;
}
} else {
return false;
}
};
return (
<>
{/* Check if a text widget (prefill type) exists. Once the user enters a value and clicks outside or the widget becomes non-selectable, it should appear as plain text (just like embedded text in a document). When the user clicks on the text again, it should become editable. */}
{props.pos?.options?.response &&
props.pos.key !== props.selectWidgetId &&
props.pos.key !== props?.currWidgetsDetails?.key &&
props.pos.type === textWidget ? (
<span
onClick={() => {
props.setSelectWidgetId && props.setSelectWidgetId(props.pos.key);
props.setCurrWidgetsDetails &&
props.setCurrWidgetsDetails(props.pos);
}}
style={{
fontFamily: "Arial, sans-serif",
@@ -866,20 +810,16 @@ function Placeholder(props) {
id={props.pos.key}
data-tut={props.pos.key === props.unSignedWidgetId ? "IsSigned" : ""}
key={props.pos.key}
cancel=".cell-size-handle, .icon"
lockAspectRatio={
!props.isFreeResize &&
![
textWidget,
"email",
"name",
"company",
"job title",
textInputWidget
].includes(props.pos.type) &&
(props.pos.Width
? props.pos.Width / props.pos.Height
: defaultWidthHeight(props.pos.type).width /
defaultWidthHeight(props.pos.type).height)
props?.isAlllowModify &&
!props?.assignedWidgetId.includes(props.pos.key)
? false
: !props.isFreeResize &&
(props.pos.Width
? props.pos.Width / props.pos.Height
: defaultWidthHeight(props.pos.type).width /
defaultWidthHeight(props.pos.type).height)
}
enableResizing={{
top: false,
@@ -896,7 +836,7 @@ function Placeholder(props) {
: false
: props.pos.type !== radioButtonWidget &&
props.pos.type !== "checkbox" &&
props.pos.key === props.selectWidgetId &&
props.pos.key === props?.currWidgetsDetails?.key &&
true,
bottomLeft: false,
topLeft: false
@@ -909,7 +849,7 @@ function Placeholder(props) {
cursor: getCursor(),
zIndex:
props.pos.type === "date"
? props.pos.key === props.selectWidgetId
? props.pos.key === props?.currWidgetsDetails?.key
? 99 + 1
: 99
: props?.pos?.zIndex
@@ -920,7 +860,6 @@ function Placeholder(props) {
background: handleBackground()
}}
onDrag={() => {
setDraggingEnabled(true);
props.handleTabDrag && props.handleTabDrag(props.pos.key);
}}
size={{
@@ -935,10 +874,14 @@ function Placeholder(props) {
? "auto"
: props.posHeight(props.pos, props.isSignYourself)
}}
minHeight={calculateFont(props.pos.options?.fontSize, true)}
minHeight={
props.pos.type === cellsWidget
? calculateFont(props.pos.options?.fontSize, true)
: props.pos.type !== "checkbox" &&
calculateFont(props.pos.options?.fontSize, true)
}
maxHeight="auto"
onResizeStart={() => {
setDraggingEnabled(true);
props.setIsResize && props.setIsResize(true);
}}
onResizeStop={(e, direction, ref) => {
@@ -958,9 +901,7 @@ function Placeholder(props) {
props.isResize
);
}}
disableDragging={handleDragging()}
onDragStop={(event, dragElement) => {
setIsDisableDragging(true);
props.handleStop &&
props.handleStop(
event,
@@ -973,17 +914,9 @@ function Placeholder(props) {
x: xPos(props.pos, props.isSignYourself),
y: yPos(props.pos, props.isSignYourself)
}}
onResize={(e, direction, ref) => {
setPlaceholderBorder({
w: ref.offsetWidth / (props.scale * containerScale),
h: ref.offsetHeight / (props.scale * containerScale)
});
}}
// onClick={() =>
// !props.isResize && !isMobile && handleOnClickPlaceholder()
// }
disableDragging={handleDragging()}
>
{props.pos.key === props.selectWidgetId &&
{props.pos.key === props?.currWidgetsDetails?.key &&
((props.isShowBorder &&
![radioButtonWidget, "checkbox"].includes(props.pos.type)) ||
(props?.isAlllowModify &&
@@ -1010,59 +943,45 @@ function Placeholder(props) {
)
) : (
![radioButtonWidget, "checkbox"].includes(props.pos.type) &&
props.pos.key === props.selectWidgetId && <BorderResize />
props.pos.key === props?.currWidgetsDetails?.key && <BorderResize />
)}
{/* 1- Show a border if props.pos.key === props.selectWidgetId, indicating the current user's selected widget.
2- If props.isShowBorder is true, display borders for all widgets.
3- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show borders:
1- When isAlllowModify is true, show borders.
2- Do not display border for widgets already assigned (props.assignedWidgetId.includes(props.pos.key) is true).
*/}
{props.pos.key === props.selectWidgetId &&
{/* 1- Show a border if props.pos.key === props?.currWidgetsDetails?.key, indicating the current user's selected widget.
2- If props.isShowBorder is true, display border for all widgets.
3- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show border:
3.1- When isAlllowModify is true, show border.
3.2- Do not display border for widgets already assigned (props.assignedWidgetId.includes(props.pos.key) is true).
*/}
{props.pos.key === props?.currWidgetsDetails?.key &&
(props.isShowBorder ||
!isDisableDragging ||
(props?.isAlllowModify &&
!props?.assignedWidgetId.includes(props.pos.key))) && (
<PlaceholderBorder
setDraggingEnabled={setDraggingEnabled}
pos={props.pos}
isPlaceholder={props.isPlaceholder}
getCheckboxRenderWidth={getCheckboxRenderWidth}
scale={props.scale}
containerScale={containerScale}
placeholderBorder={placeholderBorder}
/>
<div
style={{ borderColor: themeColor }}
className="w-[calc(100%+21px)] h-[calc(100%+21px)] cursor-move absolute inline-block border-[1px] border-dashed"
></div>
)}
<div
className="flex items-stretch justify-center"
style={{
left: xPos(props.pos, props.isSignYourself),
top: yPos(props.pos, props.isSignYourself),
width:
props.pos.type === radioButtonWidget ||
props.pos.type === "checkbox"
? "auto"
: props.posWidth(props.pos, props.isSignYourself),
height:
props.pos.type === radioButtonWidget ||
props.pos.type === "checkbox"
? "auto"
: props.posHeight(props.pos, props.isSignYourself),
width: "100%",
height: "100%",
zIndex: "10"
}}
onTouchEnd={() => handleTouchEnd()}
onTouchEnd={() => handleOnClickPlaceholder()}
onClick={() => handleOnClickPlaceholder()}
onTouchStart={() => handleTouchStart()}
>
{props.pos.key === props.selectWidgetId && <PlaceholderIcon />}
{props.pos.key === props?.currWidgetsDetails?.key && (
<PlaceholderIcon />
)}
<PlaceholderType
pos={props.pos}
xyPosition={props.xyPosition}
index={props.index}
setXyPosition={props.setXyPosition}
data={props.data}
setSignKey={props.setSignKey}
isShowDropdown={props?.isShowDropdown}
isPlaceholder={props.isPlaceholder}
isSignYourself={props.isSignYourself}
@@ -1073,24 +992,23 @@ function Placeholder(props) {
isNeedSign={props.isNeedSign}
setSelectDate={setSelectDate}
selectDate={selectDate}
setValidateAlert={props.setValidateAlert}
setStartDate={setStartDate}
startDate={startDate}
handleSaveDate={handleSaveDate}
xPos={props.xPos}
calculateFont={calculateFont}
setCellCount={setCellCount}
/>
</div>
</Rnd>
)}
<ModalUi isOpen={isDateModal} title={t("widget-info")} showClose={false}>
<div className="h-[100%] p-[20px]">
<div className="flex flex-row items-center">
<span>{t("format")} : </span>
<div className="flex">
<div className="text-base-content h-[100%] p-[20px]">
<div className="flex flex-col md:flex-row md:items-center gap-y-3">
<div className="flex flex-row items-center gap-x-1">
<span className="capitalize">{t("format")} :</span>
<select
className="ml-[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
defaultValue={""}
onChange={(e) => {
const selectedIndex = e.target.value;
@@ -1114,23 +1032,23 @@ function Placeholder(props) {
{selectDate.format}
</span>
</div>
<div className="flex items-center mt-4 md:mt-2">
<span>{t("font-size")} :</span>
<select
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={props.fontSize || clickonWidget.options?.fontSize || 12}
onChange={(e) => props.setFontSize(parseInt(e.target.value))}
>
{fontsizeArr.map((size, ind) => {
return (
<div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2 mt-3">
<div className="flex flex-row items-center">
<span className="capitalize">{t("font-size")} :</span>
<select
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={props.fontSize || clickonWidget.options?.fontSize || 12}
onChange={(e) => props.setFontSize(parseInt(e.target.value))}
>
{fontsizeArr.map((size, ind) => (
<option className="text-[13px]" value={size} key={ind}>
{size}
</option>
);
})}
</select>
<div className="flex flex-row gap-1 items-center ml-2 md:ml-4 ">
<span>{t("color")}: </span>
))}
</select>
</div>
<div className="flex flex-row gap-1 items-center">
<span className="capitalize">{t("color")} :</span>
<select
value={
props.fontColor || clickonWidget.options?.fontColor || "black"
@@ -1138,13 +1056,11 @@ function Placeholder(props) {
onChange={(e) => props.setFontColor(e.target.value)}
className="ml-[4px] md:ml[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
>
{fontColorArr.map((color, ind) => {
return (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
);
})}
{fontColorArr.map((color, ind) => (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
))}
</select>
<span
style={{
@@ -1155,7 +1071,26 @@ function Placeholder(props) {
></span>
</div>
</div>
{props?.isPlaceholder && (
<div className="flex items-center mt-3">
<input
id="isReadOnly"
name="isReadOnly"
type="checkbox"
checked={
isDateReadOnly || props.pos.options?.isReadOnly || false
}
className="op-checkbox op-checkbox-xs"
onChange={() => setIsDateReadOnly(!isDateReadOnly)}
/>
<label
className="ml-1.5 mb-0 capitalize text-[13px]"
htmlFor="isreadonly"
>
{t("read-only")}
</label>
</div>
)}
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button
type="button"
@@ -1,60 +0,0 @@
import React from "react";
import { themeColor } from "../../constant/const";
import {
defaultWidthHeight,
isMobile,
radioButtonWidget,
resizeBorderExtraWidth,
textWidget
} from "../../constant/Utils";
function PlaceholderBorder(props) {
const getResizeBorderExtraWidth = resizeBorderExtraWidth();
const defaultWidth = defaultWidthHeight(props.pos.type).width;
const defaultHeight = defaultWidthHeight(props.pos.type).height;
const width = () => {
const getWidth =
props.placeholderBorder.w || props.pos.Width || defaultWidth;
return (
getWidth * props.scale * props.containerScale + getResizeBorderExtraWidth
);
};
const height = () => {
const getHeight =
props.placeholderBorder.h || props.pos.Height || defaultHeight;
return (
getHeight * props.scale * props.containerScale + getResizeBorderExtraWidth
);
};
const handleMinWidth = () => {
if (props.pos.type === "checkbox" || props.pos.type === radioButtonWidget) {
return props.getCheckboxRenderWidth.width + getResizeBorderExtraWidth;
} else {
return width();
}
};
const handleMinHeight = () => {
if (props.pos.type === "checkbox" || props.pos.type === radioButtonWidget) {
return props.getCheckboxRenderWidth.height + getResizeBorderExtraWidth;
} else {
return height();
}
};
return (
<div
onMouseEnter={() => !isMobile && props?.setDraggingEnabled(false)}
onTouchEnd={() =>
props.pos.type === textWidget && props?.setDraggingEnabled(false)
}
className="absolute inline-block w-[14px] h-[14px] border-[0.2px] overflow-hidden border-dashed"
style={{
borderColor: themeColor,
minWidth: handleMinWidth() || 0,
minHeight: handleMinHeight() || 0
}}
></div>
);
}
export default PlaceholderBorder;
@@ -2,22 +2,22 @@ import React, { useEffect, useState, forwardRef, useRef } from "react";
import {
getMonth,
getYear,
onChangeHeightOfTextArea,
onChangeInput,
radioButtonWidget,
range,
textInputWidget,
cellsWidget,
textWidget,
widgetDataValue
months,
years,
selectCheckbox,
checkRegularExpress
} from "../../constant/Utils";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import "../../styles/signature.css";
import RegexParser from "regex-parser";
import { emailRegex } from "../../constant/const";
import { useTranslation } from "react-i18next";
import CellsWidget from "./CellsWidget";
const textWidgetCls =
"w-full h-full md:min-w-full md:min-h-full z-[999] text-[12px] rounded-[2px] border-[1px] border-[#007bff] overflow-hidden resize-none outline-none text-base-content item-center whitespace-pre-wrap bg-white";
"w-full h-full md:min-w-full md:min-h-full z-[999] text-[12px] rounded-[2px] border-[1px] border-[#007bff] overflow-hidden resize-none outline-none text-base-content item-center whitespace-pre-wrap";
const selectWidgetCls =
"w-full h-full absolute left-0 top-0 border-[1px] border-[#007bff] rounded-[2px] focus:outline-none text-base-content";
const widgetCls =
@@ -25,32 +25,23 @@ const widgetCls =
function PlaceholderType(props) {
const { t } = useTranslation();
const type = props?.pos?.type;
const iswidgetEnable =
props.isSignYourself ||
((props.isSelfSign || props.isNeedSign) &&
props.data?.signerObjId === props.signerObjId);
const isReadOnly =
props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId;
// prefer the latest response value over any default value
const widgetData =
props.pos?.options?.defaultValue || props.pos?.options?.response;
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
const widgetTypeTranslation = t(`widgets-name.${props?.pos?.type}`);
const [selectOption, setSelectOption] = useState("");
const [validatePlaceholder, setValidatePlaceholder] = useState("");
const inputRef = useRef(null);
const [textValue, setTextValue] = useState();
const [widgetValue, setwidgetValue] = useState();
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
const [hint, setHint] = useState("");
const years = range(1950, getYear(new Date()) + 16, 1);
const fontSize = props.calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
];
const textWidgetStyle = {
fontSize: fontSize,
color: fontColor,
@@ -61,89 +52,27 @@ function PlaceholderType(props) {
display: "flex",
height: "100%"
};
const validateExpression = (regexValidation) => {
if (textValue && regexValidation) {
let regexObject = regexValidation;
if (props.pos?.options?.validation?.type === "regex") {
regexObject = RegexParser(regexValidation);
}
// new RegExp(regexValidation);
let isValidate = regexObject.test(textValue);
if (!isValidate) {
props?.setValidateAlert(true);
inputRef.current.focus();
}
}
};
const handleInputBlur = () => {
const validateType = props.pos?.options?.validation?.type;
let regexValidation;
if (validateType && validateType !== "text") {
switch (validateType) {
case "email":
regexValidation = emailRegex;
validateExpression(regexValidation);
break;
case "number":
regexValidation = /^[0-9\s]*$/;
validateExpression(regexValidation);
break;
default:
regexValidation = props.pos?.options?.validation?.pattern || "";
validateExpression(regexValidation);
}
}
};
const handleTextValid = (e) => {
const textInput = e.target.value;
setTextValue(textInput);
};
function checkRegularExpress(validateType) {
switch (validateType) {
case "email":
setValidatePlaceholder("demo@gmail.com");
break;
case "number":
setValidatePlaceholder("12345");
break;
case "text":
setValidatePlaceholder("please enter text");
break;
default:
setValidatePlaceholder("please enter value");
}
}
useEffect(() => {
if (type && type === "checkbox" && props.isNeedSign) {
const isDefaultValue = props.pos.options?.defaultValue;
if (isDefaultValue) {
setSelectedCheckbox(isDefaultValue);
if (type !== "date") {
if (type && type === "checkbox") {
setSelectedCheckbox(
props?.pos?.options?.response ||
props?.pos?.options?.defaultValue ||
[]
);
} else {
// keep displayed value in sync with the stored response
setwidgetValue(widgetData);
}
if (props.pos?.options?.hint) {
setHint(props.pos?.options.hint);
} else if (props.pos?.options?.validation?.type) {
checkRegularExpress(props.pos?.options?.validation?.type, setHint);
}
} else if (props.pos?.options?.hint) {
setValidatePlaceholder(props.pos?.options.hint);
} else if (props.pos?.options?.validation?.type) {
checkRegularExpress(props.pos?.options?.validation?.type);
}
setTextValue(
props.pos?.options?.response
? props.pos?.options?.response
: props.pos?.options?.defaultValue
? props.pos?.options?.defaultValue
: ""
);
setSelectOption(
props.pos?.options?.response
? props.pos?.options?.response
: props.pos?.options?.defaultValue
? props.pos?.options?.defaultValue
: ""
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [props.pos]);
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{
@@ -151,7 +80,8 @@ function PlaceholderType(props) {
color: fontColor,
fontFamily: "Arial, sans-serif"
}}
className={`${selectWidgetCls} overflow-hidden`}
className={`${isReadOnly ? `select-none` : ``} ${selectWidgetCls} overflow-hidden`}
disabled={isReadOnly}
onClick={onClick}
ref={ref}
>
@@ -160,53 +90,10 @@ function PlaceholderType(props) {
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
useEffect(() => {
if (
["name", "email", "job title", "company", textInputWidget].includes(
type
) &&
props.isNeedSign &&
props.data?.signerObjId === props.signerObjId
) {
if (widgetData) {
setTextValue(widgetData);
}
}
if (props.pos?.options?.hint) {
setHint(props.pos?.options.hint);
} else {
setHint(props.pos?.type);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [type, widgetData]);
//function for show checked checkbox
const selectCheckbox = (ind) => {
const res = props.pos.options?.response;
const defaultCheck = props.pos.options?.defaultValue;
if (res && res?.length > 0) {
const isSelectIndex = res.indexOf(ind);
if (isSelectIndex > -1) {
return true;
} else {
return false;
}
// }
} else if (defaultCheck) {
const isSelectIndex = defaultCheck.indexOf(ind);
if (isSelectIndex > -1) {
return true;
} else {
return false;
}
} else {
return false;
}
};
const handleRadioCheck = (data) => {
const defaultData = props.pos.options?.defaultValue;
if (textValue === data) {
if (widgetValue === data) {
return true;
} else if (defaultData === data) {
return true;
@@ -215,90 +102,6 @@ function PlaceholderType(props) {
}
};
//function for set checked and unchecked value of checkbox
const handleCheckboxValue = (isChecked, ind) => {
let updateSelectedCheckbox = [],
checkedList;
let isDefaultValue, isDefaultEmpty;
if (type === "checkbox") {
updateSelectedCheckbox = selectedCheckbox ? selectedCheckbox : [];
if (isChecked) {
updateSelectedCheckbox.push(ind);
setSelectedCheckbox(updateSelectedCheckbox);
} else {
checkedList = selectedCheckbox.filter((data) => data !== ind);
setSelectedCheckbox(checkedList);
}
if (props.isNeedSign) {
isDefaultValue = props.pos.options?.defaultValue;
}
if (isDefaultValue && isDefaultValue.length > 0) {
isDefaultEmpty = true;
}
onChangeInput(
checkedList ? checkedList : updateSelectedCheckbox,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data.Id,
false,
null,
isDefaultEmpty
);
}
};
//function to handle select radio widget and set value seletced by user
const handleCheckRadio = (isChecked, data) => {
let isDefaultValue,
isDefaultEmpty,
isRadio = true;
if (props.isNeedSign) {
isDefaultValue = props.pos.options?.defaultValue;
}
if (isDefaultValue) {
isDefaultEmpty = true;
}
if (isChecked) {
setTextValue(data);
} else {
setTextValue("");
}
onChangeInput(
data,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data.Id,
false,
null,
isDefaultEmpty,
isRadio
);
};
//function to set onchange date
const handleOnDateChange = (date) => {
props.setStartDate(date);
};
//handle height on enter press in text area
const handleEnterPress = (e) => {
const height = 18;
if (e.key === "Enter") {
//function to save height of text area
onChangeHeightOfTextArea(
height,
props.pos.type,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id
);
}
};
switch (type) {
case "signature":
return props.pos.SignUrl ? (
@@ -319,9 +122,7 @@ function PlaceholderType(props) {
}}
className="font-medium"
>
{props.isNeedSign
? props.pos?.options?.hint || widgetTypeTranslation
: widgetTypeTranslation}
{hint || widgetTypeTranslation}
</div>
)}
</div>
@@ -345,182 +146,106 @@ function PlaceholderType(props) {
}}
className="font-medium"
>
{props.isNeedSign
? props.pos?.options?.hint || widgetTypeTranslation
: widgetTypeTranslation}
{hint || widgetTypeTranslation}
</div>
)}
</div>
);
case "checkbox":
const checkBoxLayout = props.pos.options?.layout || "vertical";
const isMultipleCheckbox =
props.pos.options?.values?.length > 0 ? true : false;
const checkBoxWrapperClass = `flex items-start ${
checkBoxLayout === "horizontal"
? `flex-row flex-wrap ${isMultipleCheckbox ? "gap-x-2" : ""}`
: `flex-col ${isMultipleCheckbox ? "gap-y-[5px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
return (
<div style={{ zIndex: props.isSignYourself && "99" }}>
{props.pos.options?.values?.map((data, ind) => {
return (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
<div
className={checkBoxWrapperClass}
style={{ zIndex: props.isSignYourself && "99" }}
>
{props.pos.options?.values?.map((data, ind) => (
<div key={ind} className="select-none-cls pointer-events-none">
<label
htmlFor={`checkbox-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className={`mb-0 flex items-center gap-1`}
>
<input
id={`checkbox-${props.pos.key + ind}`}
style={{ width: fontSize, height: fontSize }}
className={`${
ind === 0 ? "mt-0" : "mt-[5px]"
} flex justify-center op-checkbox rounded-[1px] `}
onBlur={handleInputBlur}
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
className="op-checkbox rounded-[1px]"
disabled={props.isNeedSign && isReadOnly}
type="checkbox"
checked={selectCheckbox(ind)}
onChange={(e) => {
if (e.target.checked) {
if (!props.isPlaceholder) {
const maxRequired =
props.pos.options?.validation?.maxRequiredCount;
const maxCountInt =
maxRequired && parseInt(maxRequired);
if (maxCountInt > 0) {
if (
selectedCheckbox &&
selectedCheckbox?.length <= maxCountInt - 1
) {
handleCheckboxValue(e.target.checked, ind);
}
} else {
handleCheckboxValue(e.target.checked, ind);
}
}
} else {
handleCheckboxValue(e.target.checked, ind);
}
}}
readOnly
checked={!!selectCheckbox(ind, selectedCheckbox)}
/>
{!props.pos.options?.isHideLabel && (
<label
htmlFor={`checkbox-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0 text-center"
>
{data}
</label>
<span className="leading-none">{data}</span>
)}
</div>
);
})}
</label>
</div>
))}
</div>
);
case textInputWidget:
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
return props.isSignYourself || iswidgetEnable ? (
<textarea
ref={inputRef}
placeholder={validatePlaceholder || t("widgets-name.text")}
placeholder={hint || t("widgets-name.text")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onBlur={handleInputBlur}
onChange={(e) => {
setTextValue(e.target.value);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={`${
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
? "select-none"
: "" + textWidgetCls
}`}
value={widgetValue}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: props.data?.blockColor
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
readOnly
disabled={props.isNeedSign && isReadOnly}
cols="50"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{textValue || widgetTypeTranslation}</span>
<span>{hint || widgetTypeTranslation}</span>
</div>
);
case cellsWidget: {
const count = props.pos.options?.cellCount || 5;
const cells = (widgetValue || "").split("");
const height = "100%";
const fontSize = props.calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
const handleCellResize = (newCount) => {
if (props.setCellCount) props.setCellCount(props.pos.key, newCount);
};
const isEditable =
props.isPlaceholder || props.isSignYourself || props.isSelfSign;
return (
<CellsWidget
isEnabled={iswidgetEnable}
count={count}
height={height}
value={cells.join("")}
editable={isEditable}
resizable={isEditable}
fontSize={fontSize}
fontColor={fontColor}
hint={hint}
onCellCountChange={handleCellResize}
/>
);
}
case "dropdown":
return props.data?.signerObjId === props.signerObjId ? (
<select
style={{ fontSize: fontSize, color: fontColor }}
className={`${
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
? " disabled:bg-inherit select-none "
: "" + `${selectWidgetCls} text-[12px] bg-inherit`
}`}
id="myDropdown"
value={selectOption}
onChange={(e) => {
setSelectOption(e.target.value);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
>
{/* Default/Title option */}
<option
style={{ fontSize: fontSize, color: fontColor }}
value=""
disabled
hidden
>
{props?.pos?.options?.name}
</option>
{props.pos?.options?.values?.map((data, ind) => {
return (
<option
style={{ fontSize: fontSize, color: fontColor }}
key={ind}
value={data}
>
{data}
</option>
);
})}
</select>
) : (
return (
<div
style={textWidgetStyle}
className="select-none-cls flex justify-between items-center"
>
{props.pos?.options?.name
? props.pos.options.name
: widgetTypeTranslation}
{widgetData || t("choose-one")}
<i className="fa-light fa-circle-chevron-down mr-1 "></i>
</div>
);
@@ -543,111 +268,82 @@ function PlaceholderType(props) {
}}
className="font-medium text-center"
>
{props.isNeedSign
? props.pos?.options?.hint || widgetTypeTranslation
: widgetTypeTranslation}
{hint || widgetTypeTranslation}
</div>
)}
</div>
);
case "name":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
return iswidgetEnable ? (
<textarea
readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onChange={(e) => {
const isDefault = false;
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
isDefault
);
value={widgetValue}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
className={textWidgetCls}
style={{ fontSize: fontSize, color: fontColor }}
cols="50"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div className="flex h-full select-none-cls" style={textWidgetStyle}>
<span>{widgetTypeTranslation}</span>
<span> {props.pos?.options?.hint || widgetTypeTranslation}</span>
</div>
);
case "company":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
return iswidgetEnable ? (
<textarea
readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onChange={(e) => {
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
value={widgetValue}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
className={textWidgetCls}
style={{ fontSize: fontSize, color: fontColor }}
cols="50"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTranslation}</span>
<span>{hint || widgetTypeTranslation}</span>
</div>
);
case "job title":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
return iswidgetEnable ? (
<textarea
readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onChange={(e) => {
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
value={widgetValue}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
className={textWidgetCls}
style={{ fontSize: fontSize, color: fontColor }}
cols="50"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTranslation}</span>
<span>{hint || widgetTypeTranslation}</span>
</div>
);
case "date":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
return iswidgetEnable ? (
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start ml-2 ">
@@ -677,15 +373,10 @@ function PlaceholderType(props) {
</select>
</div>
)}
disabled={
props.isPlaceholder ||
(props.isNeedSign && props.data?.signerObjId !== props.signerObjId)
}
onBlur={handleInputBlur}
disabled={true}
closeOnScroll={true}
className={`${selectWidgetCls} outline-[#007bff]`}
selected={props?.startDate}
onChange={(date) => handleOnDateChange(date)}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
dateFormat={
@@ -729,123 +420,83 @@ function PlaceholderType(props) {
}}
className="font-medium text-center"
>
{props.isNeedSign
? props.pos?.options?.hint || widgetTypeTranslation
: widgetTypeTranslation}
{hint || widgetTypeTranslation}
</div>
)}
</div>
);
case "email":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
return iswidgetEnable ? (
<textarea
readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation}
rows={1}
onKeyDown={(e) => {
// Prevent new line on Enter key press
if (e.key === "Enter") {
e.preventDefault();
}
}}
value={textValue}
onBlur={handleInputBlur}
onChange={(e) => {
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={textWidgetCls}
value={widgetValue}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
fontFamily: "Arial, sans-serif"
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
cols="1"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTranslation}</span>
<span>{hint || widgetTypeTranslation}</span>
</div>
);
case radioButtonWidget:
const radioLayout = props.pos.options?.layout || "vertical";
const isOnlyOneBtn = props.pos.options?.values?.length > 0 ? true : false;
const radioWrapperClass = `flex items-start ${
radioLayout === "horizontal"
? `flex-row flex-wrap ${isOnlyOneBtn ? "gap-x-2" : ""}`
: `flex-col ${isOnlyOneBtn ? "gap-y-[5px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
return (
<div>
{props.pos.options?.values.map((data, ind) => {
return (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
<div className={radioWrapperClass}>
{props.pos.options?.values.map((data, ind) => (
<div key={ind} className="select-none-cls pointer-events-none">
<label
htmlFor={`radio-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0 flex items-center gap-1"
>
<input
readOnly
id={`radio-${props.pos.key + ind}`}
style={{
width: fontSize,
height: fontSize,
marginTop: ind > 0 ? "10px" : "0px"
}}
className={`flex justify-center op-radio`}
style={{ width: fontSize, height: fontSize, lineHeight: 2 }}
className={`op-radio rounded-full border-black appearance-none bg-white inline-block align-middle relative ${
handleRadioCheck(data) ? "checked-radio" : ""
}`}
type="radio"
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
disabled={props.isNeedSign && isReadOnly}
checked={handleRadioCheck(data)}
onChange={(e) => {
if (!props.isPlaceholder) {
handleCheckRadio(e.target.checked, data);
}
}}
/>
{!props.pos.options?.isHideLabel && (
<label
htmlFor={`radio-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0"
>
{data}
</label>
<span className="leading-none">{data}</span>
)}
</div>
);
})}
</label>
</div>
))}
</div>
);
case textWidget:
return (
<textarea
readOnly
placeholder={t("widgets-name.text")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onBlur={handleInputBlur}
onChange={(e) => {
setTextValue(e.target.value);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
value={widgetValue}
className={textWidgetCls}
style={{
fontFamily: "Arial, sans-serif",
fontSize: fontSize,
color: fontColor
color: fontColor,
background: "white"
}}
cols="50"
/>
@@ -872,9 +523,7 @@ function PlaceholderType(props) {
}}
className="font-medium"
>
{props.isNeedSign
? props.pos?.options?.hint || widgetTypeTranslation
: widgetTypeTranslation}
{hint || widgetTypeTranslation}
</div>
)}
</div>
@@ -31,7 +31,7 @@ const RecipientList = (props) => {
e.preventDefault();
};
//handle draggable element drop and also used in mobile view on up and down key to chnage sequence of recipient's list
//handle draggable element drop and also used in mobile view on up and down key to change sequence of recipient's list
const handleChangeSequence = (e, ind, isUp, isDown, obj) => {
e.preventDefault();
let draggedItemId;
@@ -3,7 +3,12 @@ import { useTranslation } from "react-i18next";
import { Document, Page } from "react-pdf";
import { useSelector } from "react-redux";
import { PDFDocument } from "pdf-lib";
import { base64ToArrayBuffer } from "../../constant/Utils";
import {
base64ToArrayBuffer,
decryptPdf,
flattenPdf,
getFileAsArrayBuffer
} from "../../constant/Utils";
import { maxFileSize } from "../../constant/const";
function RenderAllPdfPage(props) {
@@ -87,7 +92,42 @@ function RenderAllPdfPage(props) {
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert("Incorrect password or decryption failed.");
}
} else {
alert("Please provided Password.");
}
} else {
console.log("Err ", err);
alert("error while uploading pdf.");
}
}
} else {
alert("error while uploading pdf.");
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
+225 -462
View File
@@ -79,7 +79,7 @@ function RenderPdf(props) {
}
};
//function for render placeholder block over pdf document
// function for render placeholder block over pdf document (all signing flow)
const checkSignedSigners = (data) => {
let checkSign = [];
//condition to handle quick send flow and using normal request sign flow
@@ -91,79 +91,72 @@ function RenderPdf(props) {
: [];
return (
checkSign.length === 0 &&
data?.placeHolder?.map((placeData, key) => {
return (
<React.Fragment key={key}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => {
return (
pos && (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setSignKey={props.setSignKey}
setIsSignPad={props.setIsSignPad}
setIsStamp={props.setIsStamp}
handleSignYourselfImageResize={handleImageResize}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
isShowBorder={props.isSelfSign}
isAlllowModify={props.isAlllowModify}
signerObjId={props.signerObjectId}
isShowDropdown={true}
isNeedSign={props.pdfRequest}
isSelfSign={true}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
pdfDetails={props.pdfDetails}
setIsInitial={props.setIsInitial}
setValidateAlert={props.setValidateAlert}
unSignedWidgetId={props.unSignedWidgetId}
setSelectWidgetId={props.setSelectWidgetId}
selectWidgetId={props.selectWidgetId}
setCurrWidgetsDetails={props.setCurrWidgetsDetails}
uniqueId={props.uniqueId}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
ispublicTemplate={props.ispublicTemplate}
handleUserDetails={props.handleUserDetails}
isResize={props.isResize}
setIsAgreeTour={props.setIsAgreeTour}
isAgree={props.isAgree}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
setWidgetType={props.setWidgetType}
setUniqueId={props.setUniqueId}
setIsSelectId={props.setIsSelectId}
handleDeleteSign={props.handleDeleteSign}
setIsPageCopy={props.setIsPageCopy}
handleTextSettingModal={props.handleTextSettingModal}
setIsCheckbox={props.setIsCheckbox}
isFreeResize={false}
isOpenSignPad={true}
assignedWidgetId={props.assignedWidgetId}
isApplyAll={true}
setFontSize={props.setFontSize}
fontSize={props.fontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
setRequestSignTour={props.setRequestSignTour}
calculateFontsize={calculateFontsize}
/>
</React.Fragment>
)
);
})}
</React.Fragment>
);
})
data?.placeHolder?.map((placeData, key) => (
<React.Fragment key={key}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map(
(pos, ind) =>
pos && (
<React.Fragment key={ind}>
<Placeholder
pos={pos}
handleSignYourselfImageResize={handleImageResize}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
isShowBorder={props.isSelfSign}
isAlllowModify={props.isAlllowModify}
signerObjId={props.signerObjectId}
isShowDropdown={true}
isNeedSign={props.pdfRequest}
isSelfSign={true}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
pdfDetails={props.pdfDetails}
unSignedWidgetId={props.unSignedWidgetId}
setCurrWidgetsDetails={props.setCurrWidgetsDetails}
uniqueId={props.uniqueId}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
ispublicTemplate={props.ispublicTemplate}
handleUserDetails={props.handleUserDetails}
isResize={props.isResize}
setIsAgreeTour={props.setIsAgreeTour}
isAgree={props.isAgree}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
setUniqueId={props.setUniqueId}
setIsSelectId={props.setIsSelectId}
handleDeleteSign={props.handleDeleteSign}
setIsPageCopy={props.setIsPageCopy}
handleTextSettingModal={props.handleTextSettingModal}
handleCellSettingModal={props.handleCellSettingModal}
setIsCheckbox={props.setIsCheckbox}
isFreeResize={props.isSelfSign ? true : false}
isOpenSignPad={true}
assignedWidgetId={props.assignedWidgetId}
isApplyAll={true}
setCellCount={props.setCellCount}
setFontSize={props.setFontSize}
fontSize={props.fontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
setRequestSignTour={props.setRequestSignTour}
calculateFontsize={calculateFontsize}
currWidgetsDetails={props?.currWidgetsDetails}
setTempSignerId={props.setTempSignerId}
/>
</React.Fragment>
)
)}
</React.Fragment>
))
);
};
@@ -178,162 +171,147 @@ function RenderPdf(props) {
}
};
const pdfDataBase64 = `data:application/pdf;base64,${props.pdfBase64Url}`;
//calculate render height of pdf in mobile view
// calculate render height of pdf in mobile view
const handlePageLoadSuccess = (page) => {
const containerWidth = props.divRef.current.offsetWidth; // Get container width
const viewport = page.getViewport({ scale: 1 });
const scale = containerWidth / viewport.width; // Scale to fit container width
const scaleHeight = viewport.height * scale;
setScaledHeight(scaleHeight);
if (isMobile) {
const containerWidth = props.divRef.current.offsetWidth; // Get container width
const viewport = page.getViewport({ scale: 1 });
const scale = containerWidth / viewport.width; // Scale to fit container width
const scaleHeight = viewport.height * scale;
setScaledHeight(scaleHeight);
}
};
return (
<>
{props.successEmail && (
<Alert type={"success"}>{t("success-email-alert")}</Alert>
)}
{isMobile ? (
<RSC
<RSC
style={{
position: "relative",
boxShadow: "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px",
height: isMobile
? isGuestSigner
? window.innerHeight - 49 // 49 is height of header
: scaledHeight
: `${window.innerHeight}px`,
zIndex: 0
}}
noScrollY={isMobile ? props.scale === 1 : false}
noScrollX={props.scale === 1}
>
<div
data-tut={isMobile ? "reactourForth" : undefined}
className={
isMobile
? `${isGuestSigner ? "30px" : ""} border-[0.1px] border-[#ebe8e8] overflow-x-auto`
: ""
}
style={{
position: "relative",
boxShadow: "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px",
//49 is height of header
height: isGuestSigner ? window.innerHeight - 49 : scaledHeight,
zIndex: 0
width:
props.containerWH?.width && props.containerWH?.width * props.scale
}}
noScrollY={props.scale === 1 ? true : false}
noScrollX={props.scale === 1 ? true : false}
ref={props.drop}
id="container"
>
<div
data-tut="reactourForth"
className={`${
isGuestSigner ? "30px" : ""
} border-[0.1px] border-[#ebe8e8] overflow-x-auto`}
style={{
width:
props.containerWH?.width &&
props.containerWH?.width * props.scale
}}
ref={props.drop}
id="container"
>
{props.containerWH?.width &&
props.pdfOriginalWH.length > 0 &&
(props.pdfRequest || props.isSelfSign
? props.signerPos?.map((data, key) => {
return (
{props.pdfLoad !== false &&
props.containerWH?.width &&
props.pdfOriginalWH.length > 0 && (
<>
{props.pdfRequest || props.isSelfSign
? // request sign, guest sign,
props.signerPos?.map((data, key) => (
<React.Fragment key={key}>
{checkSignedSigners(data)}
</React.Fragment>
);
})
: props.placeholder // placeholder mobile
? props.signerPos?.map((data, ind) => {
return (
))
: props.placeholder // placeholdersign document, draft document, create template, draft template
? props.signerPos?.map((data, ind) => (
<React.Fragment key={ind}>
{data?.placeHolder &&
data?.placeHolder.map((placeData, index) => {
return (
<React.Fragment key={index}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => {
return (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={
props.handleDeleteSign
}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleImageResize
}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
setShowDropdown={
props.setShowDropdown
}
isShowBorder={true}
isPlaceholder={true}
setUniqueId={props.setUniqueId}
handleLinkUser={
props.handleLinkUser
}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
setIsValidate={props.setIsValidate}
setWidgetType={props.setWidgetType}
setIsRadio={props.setIsRadio}
setIsCheckbox={props.setIsCheckbox}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
setSelectWidgetId={
props.setSelectWidgetId
}
selectWidgetId={
props.selectWidgetId
}
handleNameModal={
props.handleNameModal
}
setTempSignerId={
props.setTempSignerId
}
uniqueId={props.uniqueId}
handleTextSettingModal={
props.handleTextSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
setIsSelectId={props.setIsSelectId}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
unSignedWidgetId={
props.unSignedWidgetId
}
isFreeResize={true}
calculateFontsize={
calculateFontsize
}
/>
</React.Fragment>
);
})}
</React.Fragment>
);
})}
data?.placeHolder.map((placeData, index) => (
<React.Fragment key={index}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
handleDeleteSign={
props.handleDeleteSign
}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleImageResize
}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
setShowDropdown={props.setShowDropdown}
isShowBorder={true}
isPlaceholder={true}
setUniqueId={props.setUniqueId}
handleLinkUser={props.handleLinkUser}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
setIsValidate={props.setIsValidate}
setIsRadio={props.setIsRadio}
setIsCheckbox={props.setIsCheckbox}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
handleNameModal={props.handleNameModal}
setTempSignerId={props.setTempSignerId}
uniqueId={props.uniqueId}
handleTextSettingModal={
props.handleTextSettingModal
}
handleCellSettingModal={
props.handleCellSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
setIsSelectId={props.setIsSelectId}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
setCellCount={props.setCellCount}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
unSignedWidgetId={
props.unSignedWidgetId
}
isFreeResize={true}
calculateFontsize={calculateFontsize}
currWidgetsDetails={
props?.currWidgetsDetails
}
/>
</React.Fragment>
))}
</React.Fragment>
))}
</React.Fragment>
);
})
: !props.pdfDetails?.[0]?.IsCompleted &&
props.xyPosition?.map((data, ind) => {
return (
))
: !props.pdfDetails?.[0]?.IsCompleted && // signyourself flow
props.xyPosition?.map((data, ind) => (
<React.Fragment key={ind}>
{data.pageNumber === props.pageNumber &&
data.pos.map((pos, id) => {
return (
data.pos.map(
(pos, id) =>
pos && (
<Placeholder
key={id}
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={props.handleDeleteSign}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
@@ -343,25 +321,22 @@ function RenderPdf(props) {
xyPosition={props.xyPosition}
setXyPosition={props.setXyPosition}
containerWH={props.containerWH}
setIsSignPad={props.setIsSignPad}
isShowBorder={true}
isSignYourself={true}
posWidth={posWidth}
posHeight={posHeight}
pdfDetails={props.pdfDetails[0]}
isDragging={props.isDragging}
setIsInitial={props.setIsInitial}
setWidgetType={props.setWidgetType}
setSelectWidgetId={props.setSelectWidgetId}
selectWidgetId={props.selectWidgetId}
setIsCheckbox={props.setIsCheckbox}
setValidateAlert={props.setValidateAlert}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
handleTextSettingModal={
props.handleTextSettingModal
}
handleCellSettingModal={
props.handleCellSettingModal
}
scale={props.scale}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
@@ -371,263 +346,51 @@ function RenderPdf(props) {
setFontColor={props.setFontColor}
isResize={props.isResize}
setIsResize={props.setIsResize}
isFreeResize={false}
isFreeResize={true}
isOpenSignPad={true}
calculateFontsize={calculateFontsize}
currWidgetsDetails={
props?.currWidgetsDetails
}
/>
)
);
})}
)}
</React.Fragment>
);
}))}
{/* Mobile */}
<Document
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
onLoadError={() => props.setPdfLoad(false)}
loading={t("loading-doc")}
onLoadSuccess={props.pageDetails}
onClick={() =>
props.setSelectWidgetId && props.setSelectWidgetId("")
}
file={pdfDataBase64}
>
<Page
onLoadSuccess={handlePageLoadSuccess}
scale={props.scale || 1}
key={props.index}
pageNumber={props.pageNumber}
width={props.containerWH.width}
renderAnnotationLayer={false}
renderTextLayer={false}
onGetAnnotationsError={(error) => {
console.log("annotation error", error);
}}
className="select-none touch-callout-none"
/>
</Document>
</div>
</RSC>
) : (
<RSC
style={{
position: "relative",
boxShadow: "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px",
height: window.innerHeight + "px",
zIndex: 0
}}
noScrollY={false}
noScrollX={props.scale === 1 ? true : false}
>
<div
style={{
width:
props.containerWH?.width &&
props.containerWH?.width * props.scale
))}
</>
)}
<Document
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
onLoadError={(e) => {
console.log("PDF load error", e);
props.setPdfLoad(false);
}}
ref={props.drop}
id="container"
loading={t("loading-doc")}
onLoadSuccess={(pdf) => {
props.setPdfLoad(true);
props.pageDetails(pdf);
}}
onClick={() =>
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails({})
}
file={pdfDataBase64}
>
{props.pdfLoad &&
props.containerWH?.width &&
props.pdfOriginalWH.length > 0 &&
(props.pdfRequest || props.isSelfSign //pdf request sign flow
? props.signerPos?.map((data, key) => {
return (
<React.Fragment key={key}>
{checkSignedSigners(data)}
</React.Fragment>
);
})
: props.placeholder //placeholder and template flow
? props.signerPos.map((data, ind) => {
return (
<React.Fragment key={ind}>
{data?.placeHolder &&
data?.placeHolder.map((placeData, index) => {
return (
<React.Fragment key={index}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => {
return (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={
props.handleDeleteSign
}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleImageResize
}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
setShowDropdown={
props.setShowDropdown
}
isShowBorder={true}
isPlaceholder={true}
setUniqueId={props.setUniqueId}
handleLinkUser={
props.handleLinkUser
}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
setIsValidate={props.setIsValidate}
setWidgetType={props.setWidgetType}
setIsRadio={props.setIsRadio}
setIsCheckbox={props.setIsCheckbox}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
setSelectWidgetId={
props.setSelectWidgetId
}
selectWidgetId={
props.selectWidgetId
}
handleNameModal={
props.handleNameModal
}
setTempSignerId={
props.setTempSignerId
}
uniqueId={props.uniqueId}
handleTextSettingModal={
props.handleTextSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
setIsSelectId={props.setIsSelectId}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
unSignedWidgetId={
props.unSignedWidgetId
}
isFreeResize={true}
calculateFontsize={
calculateFontsize
}
/>
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</React.Fragment>
);
})
: !props.pdfDetails?.[0]?.IsCompleted &&
props.xyPosition?.map((data, ind) => {
// signyourself flow
return (
<React.Fragment key={ind}>
{data.pageNumber === props.pageNumber &&
data.pos.map((pos) => {
return (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={props.handleDeleteSign}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={(event, dragElement) =>
props.handleStop(
event,
dragElement,
pos.type
)
}
handleSignYourselfImageResize={
handleSignYourselfImageResize
}
index={props.index}
xyPosition={props.xyPosition}
setXyPosition={props.setXyPosition}
setIsSignPad={props.setIsSignPad}
isShowBorder={true}
isSignYourself={true}
posWidth={posWidth}
posHeight={posHeight}
pdfDetails={props.pdfDetails[0]}
isDragging={props.isDragging}
setIsInitial={props.setIsInitial}
setWidgetType={props.setWidgetType}
setSelectWidgetId={props.setSelectWidgetId}
selectWidgetId={props.selectWidgetId}
setIsCheckbox={props.setIsCheckbox}
setValidateAlert={props.setValidateAlert}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
handleTextSettingModal={
props.handleTextSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
setIsResize={props.setIsResize}
isFreeResize={false}
isOpenSignPad={true}
calculateFontsize={calculateFontsize}
/>
</React.Fragment>
);
})}
</React.Fragment>
);
}))}
{/* large device */}
{/* this component for render pdf document is in middle of the component */}
<Document
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
onLoadError={() => props.setPdfLoad(false)}
loading={t("loading-doc")}
onLoadSuccess={props.pageDetails}
onClick={() =>
props.setSelectWidgetId && props.setSelectWidgetId("")
}
file={pdfDataBase64}
>
<Page
key={props.index}
width={props.containerWH.width}
scale={props.scale || 1}
className={"-z-[1]"} // when user zoom-in in tablet widgets move backward that's why pass -z-[1]
pageNumber={props.pageNumber}
renderAnnotationLayer={false}
renderTextLayer={false}
onGetAnnotationsError={(error) => {
console.log("annotation error", error);
}}
/>
</Document>
</div>
</RSC>
)}
<Page
key={props.index}
onLoadSuccess={handlePageLoadSuccess}
width={props.containerWH.width}
scale={props.scale || 1}
className={isMobile ? "select-none touch-callout-none" : "-z-[1]"}
pageNumber={props.pageNumber}
renderAnnotationLayer={false}
renderTextLayer={false}
onGetAnnotationsError={(error) => {
console.log("annotation error", error);
}}
/>
</Document>
</div>
</RSC>
</>
);
}
@@ -9,7 +9,8 @@ function SelectLanguage(props) {
{ value: "es", text: "Española" }, //spanish
{ value: "fr", text: "Français" }, //french
{ value: "it", text: "Italiano" }, //italian
{ value: "de", text: "Deutsch" } //german
{ value: "de", text: "Deutsch" }, //german
{ value: "hi", text: "हिन्दी" } //hindi
];
const defaultLanguage = i18next.language || "en";
const [lang, setLang] = useState(defaultLanguage);
@@ -23,14 +24,14 @@ function SelectLanguage(props) {
<div
className={`${
!props.isProfile && " mt-[9px] pb-2 md:pb-0 "
} flex justify-center items-center `}
} flex justify-center items-center text-base-content`}
>
<select
value={lang}
onChange={handleChangeLang}
className={`${
!props.isProfile ? " md:w-[15%] w-[50%]" : "w-[180px]"
} op-select op-select-bordered bg-white op-select-sm `}
} op-select op-select-bordered op-select-sm `}
>
<option disabled>select</option>
{languages.map((item) => {
@@ -1,854 +0,0 @@
import React, { useRef, useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import SignatureCanvas from "react-signature-canvas";
import Parse from "parse";
import {
generateTitleFromFilename,
getBase64FromUrl,
getSecureUrl
} from "../../constant/Utils";
import sanitizeFileName from "../../primitives/sanitizeFileName";
import { SaveFileSize } from "../../constant/saveFileSize";
import Loader from "../../primitives/Loader";
function SignPad(props) {
const { t } = useTranslation();
const [penColor, setPenColor] = useState("blue");
const allColor = ["blue", "red", "black"];
const canvasRef = useRef(null);
const [isDefaultSign, setIsDefaultSign] = useState(false);
const [isTab, setIsTab] = useState("");
const [isSignImg, setIsSignImg] = useState("");
const [textWidth, setTextWidth] = useState(0);
const [textHeight, setTextHeight] = useState(0);
const [signatureType, setSignatureType] = useState("");
const [isSignTypes, setIsSignTypes] = useState(true);
const [typedSignature, setTypedSignature] = useState("");
const fontOptions = [
{ value: "Fasthand" },
{ value: "Dancing Script" },
{ value: "Cedarville Cursive" },
{ value: "Delicious Handrawn" }
// Add more font options as needed
];
const [fontSelect, setFontSelect] = useState(fontOptions[0].value);
const [isSavedSign, setIsSavedSign] = useState(false);
const [isLoader, setIsLoader] = useState(false);
const accesstoken = localStorage.getItem("accesstoken") || "";
const senderUser = localStorage.getItem(
`Parse/${localStorage.getItem("parseAppId")}/currentUser`
);
const jsonSender = senderUser && JSON.parse(senderUser);
const currentUserName = jsonSender && jsonSender?.name;
useEffect(() => {
handleTab();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.signatureTypes]);
function handleTab() {
const signtypes = props?.signatureTypes || [];
const defaultIndex = signtypes?.findIndex(
(x) =>
x.name === "default" &&
x.enabled === true &&
props.defaultSign &&
(props?.currWidgetsDetails?.type || props.widgetType) !== "image" &&
(props?.currWidgetsDetails?.type || props.widgetType) !== "stamp"
);
const getIndex =
defaultIndex !== -1 // Check if the default index exists
? defaultIndex // If found, use it
: signtypes?.findIndex((x) => x.enabled === true);
if (getIndex !== -1) {
setIsSignTypes(true);
const tab = props?.signatureTypes[getIndex].name;
if (tab === "draw") {
setIsTab("draw");
setSignatureType("draw");
} else if (tab === "upload") {
props?.setIsImageSelect(true);
setIsTab("uploadImage");
} else if (tab === "typed") {
setIsTab("type");
} else if (tab === "default") {
if (
(props?.isInitial && props?.myInitial) ||
(!props?.isInitial && props?.defaultSign)
) {
setIsDefaultSign(true);
setIsTab("mysignature");
} else {
setIsTab("draw");
}
} else {
setIsTab(true);
}
} else {
setIsSignTypes(false);
}
}
function isTabEnabled(tabName) {
const isEnabled = props?.signatureTypes.find(
(x) => x.name === tabName
)?.enabled;
return isEnabled;
}
//function for clear signature image
const handleClear = () => {
if (isTab === "draw") {
if (canvasRef.current) {
canvasRef.current.clear();
} else if (props?.isStamp) {
props?.setImage("");
}
setIsSignImg("");
} else if (isTab === "uploadImage") {
props?.setImage("");
}
};
//function for set signature url
const handleSignatureChange = (data) => {
props?.setSignature(data);
setIsSignImg(data);
};
function base64StringtoFile(base64String, filename) {
let arr = base64String.split(","),
// type of uploaded image
mime = arr[0].match(/:(.*?);/)[1],
// decode base64
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const ext = mime.split("/").pop();
const name = `${filename}.${ext}`;
return new File([u8arr], name, { type: mime });
}
const uploadFile = async (file) => {
try {
const parseFile = new Parse.File(file.name, file);
const response = await parseFile.save();
if (response?.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
const tenantId = localStorage.getItem("TenantId");
SaveFileSize(file.size, fileRes.url, tenantId);
return fileRes?.url;
} else {
alert(`${t("something-went-wrong-mssg")}`);
return false;
}
} else {
alert(`${t("something-went-wrong-mssg")}`);
return false;
}
} catch (err) {
console.log("sign upload err", err);
alert(`${err.message}`);
return false;
}
};
// `handlesavesign` is used to save signaute, initials, stamp as a default
const handleSaveSign = async () => {
if (isSignImg || props?.image?.src) {
setIsLoader(true);
try {
const User = Parse?.User?.current();
const sanitizename = generateTitleFromFilename(User?.get("name"));
const replaceSpace = sanitizeFileName(sanitizename);
let file;
if (isSignImg) {
file = base64StringtoFile(isSignImg, `${replaceSpace}__sign`);
} else {
file = base64StringtoFile(props?.image?.src, `${replaceSpace}__sign`);
}
const imageUrl = await uploadFile(file);
const userId = {
__type: "Pointer",
className: "_User",
objectId: User?.id
};
if (imageUrl) {
// below code is used to save or update default signaute, initials, stamp
try {
const signCls = new Parse.Object("contracts_Signature");
if (props?.saveSignCheckbox?.signId) {
signCls.id = props.saveSignCheckbox.signId;
}
if (
props.currWidgetsDetails?.type === "initials" ||
props?.widgetType === "initials"
) {
signCls.set("Initials", imageUrl);
} else if (
props.currWidgetsDetails?.type === "signature" ||
props?.widgetType === "signature"
) {
signCls.set("ImageURL", imageUrl);
}
signCls.set("UserId", userId);
const signRes = await signCls.save();
if (signRes) {
props.saveSignCheckbox.signId;
props.setSaveSignCheckbox((prev) => ({
...prev,
signId: signRes?.id
}));
const _signRes = JSON.parse(JSON.stringify(signRes));
if (
props.currWidgetsDetails?.type === "signature" ||
props?.widgetType === "signature"
) {
const defaultSign = await getBase64FromUrl(
_signRes?.ImageURL,
true
);
props.setDefaultSignImg(defaultSign);
} else if (
props.currWidgetsDetails?.type === "initials" ||
props?.widgetType === "initials"
) {
const defaultInitials = await getBase64FromUrl(
_signRes?.Initials,
true
);
props.setMyInitial(defaultInitials);
}
alert(t("saved-successfully"));
}
return signRes;
} catch (err) {
console.log(err);
alert(`${err.message}`);
} finally {
setIsLoader(false);
}
}
} catch (err) {
console.log("Err while saving signature", err);
}
}
};
const handleSaveBtn = async () => {
if (accesstoken && isSavedSign) {
await handleSaveSign();
resetToDefault();
} else {
resetToDefault();
}
};
const resetToDefault = () => {
props?.setCurrWidgetsDetails({});
if (!props?.image) {
if (isTab === "mysignature") {
setIsSignImg("");
if (props?.isInitial) {
props?.onSaveSign(signatureType, "initials");
} else {
props?.onSaveSign(null, "default");
}
} else {
if (isTab === "type") {
setIsSignImg("");
props?.onSaveSign(
null,
false,
!props?.isInitial && textWidth > 150 ? 150 : textWidth,
!props?.isInitial && textHeight > 35 ? 35 : textHeight,
typedSignature
);
} else {
setIsSignImg("");
canvasRef.current.clear();
props?.onSaveSign(signatureType);
}
}
setPenColor("blue");
} else {
setIsSignImg("");
props?.onSaveImage(signatureType);
}
props?.setIsSignPad(false);
props?.setIsInitial && props?.setIsInitial(false);
props?.setIsImageSelect(false);
setIsDefaultSign(false);
props?.setImage();
handleTab();
props?.setIsStamp(false);
};
//save button component
const SaveBtn = () => {
return (
<div>
{(isTab === "draw" || isTab === "uploadImage") && (
<button
type="button"
className="op-btn op-btn-ghost mr-1 mt-[2px]"
onClick={() => handleClear()}
>
{t("clear")}
</button>
)}
<button
onClick={() => handleSaveBtn()}
type="button"
className={`${
isSignImg ||
props?.image ||
isDefaultSign ||
textWidth ||
props.isAutoSign
? ""
: "pointer-events-none"
} op-btn op-btn-primary shadow-lg`}
disabled={
(isTab === "draw" && isSignImg) ||
(isTab === "image" && props?.image) ||
(isTab === "mysignature" && isDefaultSign) ||
(isTab === "type" && typedSignature) ||
props.isAutoSign
? false
: props?.image
? false
: true
}
>
{t("save")}
</button>
</div>
);
};
const autoSignAll = () => {
return (
<label className="cursor-pointer flex items-center mb-[6px] text-center text-[11px] md:text-base">
<input
className="mr-2 md:mr-3 op-checkbox op-checkbox-xs md:op-checkbox-sm"
type="checkbox"
value={props.isAutoSign}
onChange={(e) => {
props.setIsAutoSign(e.target.checked);
}}
/>
{t("auto-sign-mssg")}
</label>
);
};
//useEffect for set already draw or save signature url/text url of signature text type and draw type for initial type and signature type widgets
useEffect(() => {
if (props?.currWidgetsDetails && canvasRef.current && props.isSignPad) {
const isWidgetType = props?.currWidgetsDetails?.type;
const signatureType = props?.currWidgetsDetails?.signatureType;
const url = props?.currWidgetsDetails?.SignUrl;
//checking widget type and draw type signature url
if (props?.isInitial) {
if (isWidgetType === "initials" && signatureType === "draw" && url) {
canvasRef.current.fromDataURL(url);
}
} else if (
isWidgetType === "signature" &&
signatureType === "draw" &&
url
) {
canvasRef.current.fromDataURL(url);
}
const trimmedName = currentUserName && currentUserName?.trim();
const firstCharacter = trimmedName?.charAt(0);
const userName = props?.isInitial ? firstCharacter : currentUserName;
const signatureValue = props?.currWidgetsDetails?.typeSignature;
setTypedSignature(signatureValue || userName || "");
setFontSelect("Fasthand");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.isSignPad]);
useEffect(() => {
const loadFont = async () => {
try {
await document.fonts.load(`20px ${fontSelect}`);
const selectFontSTyle = fontOptions.find(
(font) => font.value === fontSelect
);
setFontSelect(selectFontSTyle?.value || fontOptions[0].value);
} catch (error) {
console.error("Error loading font:", error);
}
};
loadFont();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fontSelect]);
useEffect(() => {
// Load the default signature after the component mounts
if (canvasRef.current) {
canvasRef.current.fromDataURL(isSignImg);
}
if (isTab === "type") {
const trimmedName = typedSignature
? typedSignature?.trim()
: currentUserName?.trim();
const firstCharacter = trimmedName?.charAt(0);
const userName = props?.isInitial ? firstCharacter : typedSignature;
const signatureValue = props?.currWidgetsDetails?.typeSignature;
setTypedSignature(signatureValue || userName || "");
convertToImg(fontSelect, userName);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isTab]);
//function for convert input text value in image
const convertToImg = async (fontStyle, text, color) => {
//get text content to convert in image
const textContent = text;
const fontfamily = fontStyle
? fontStyle
: fontSelect
? fontSelect
: "Fasthand";
const fontSizeValue = "40px";
//creating span for getting text content width
const span = document.createElement("span");
span.textContent = textContent;
span.style.font = `${fontSizeValue} ${fontfamily}`; // here put your text size and font family
span.style.color = color ? color : penColor;
span.style.display = "hidden";
document.body.appendChild(span); // Replace 'container' with the ID of the container element
//create canvas to render text in canvas and convert in image
const canvasElement = document.createElement("canvas");
// Draw the text content on the canvas
const ctx = canvasElement.getContext("2d");
const pixelRatio = window.devicePixelRatio || 1;
const addExtraWidth = props?.isInitial ? 10 : 50;
const width = span.offsetWidth + addExtraWidth;
const height = span.offsetHeight;
setTextWidth(width);
setTextHeight(height);
const font = span.style["font"];
// Set the canvas dimensions to match the span
canvasElement.width = width * pixelRatio;
canvasElement.height = height * pixelRatio;
// You can customize text styles if needed
ctx.font = font;
ctx.fillStyle = color ? color : penColor; // Set the text color
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.scale(pixelRatio, pixelRatio);
// Draw the content of the span onto the canvas
ctx.fillText(span.textContent, width / 2, height / 2); // Adjust the x,y-coordinate as needed
//remove span tag
document.body.removeChild(span);
// Convert the canvas to image data
const dataUrl = canvasElement.toDataURL("image/png");
props?.setSignature(dataUrl);
};
const PenColorComponent = (props) => {
return (
<div className="flex flex-row items-center m-[5px] gap-2">
{allColor.map((data, key) => {
return (
<i
key={key}
onClick={() => {
props?.convertToImg &&
props?.convertToImg(fontSelect, typedSignature, data);
setPenColor(allColor[key]);
}}
className={`border-b-[2px] ${key === 0 && penColor === "blue" ? "border-blue-600" : key === 1 && penColor === "red" ? "border-red-500" : key === 2 && penColor === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
></i>
);
})}
</div>
);
};
// `handleCancelBtn` function trigger when user click on cross button
const handleCancelBtn = () => {
setPenColor("blue");
props?.setIsSignPad(false);
props?.setIsInitial && props?.setIsInitial(false);
props?.setIsImageSelect(false);
setIsDefaultSign(false);
props?.setImage();
handleTab();
props?.setIsStamp(false);
};
const savesigncheckbox = (
<label className="cursor-pointer flex items-center mb-0 text-center text-[11px] md:text-base">
<input
className="mr-2 md:mr-3 op-checkbox op-checkbox-xs md:op-checkbox-sm"
type="checkbox"
checked={isSavedSign}
onChange={(e) => setIsSavedSign(e.target.checked)}
/>
Save {props?.currWidgetsDetails?.type || props?.widgetType}
</label>
);
return (
<div>
{props?.isSignPad && (
<div className="op-modal op-modal-open">
<div className="op-modal-box px-[13px] pt-2 pb-0">
{isLoader && (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
<Loader />
</div>
)}
{isSignTypes ? (
<>
<div className="flex justify-between text-base-content items-center">
<div className="text-[1.2rem]">
<div className="flex flex-row justify-between mt-[3px]">
<div className="flex flex-row justify-between gap-[5px] md:gap-[8px] text-[11px] md:text-base">
{props?.isStamp ? (
<span className="text-base-content font-bold text-lg">
{props?.widgetType === "image" ||
props?.currWidgetsDetails?.type === "image"
? t("upload-image")
: t("upload-stamp-image")}
</span>
) : (
<>
{!props?.isInitial &&
props?.defaultSign &&
isTabEnabled("default") ? (
<div>
<span
onClick={() => {
setIsDefaultSign(true);
props?.setIsImageSelect(true);
setIsTab("mysignature");
setSignatureType("");
props?.setImage();
}}
className={`${
isTab === "mysignature"
? "op-link-primary"
: "no-underline"
} op-link underline-offset-8 ml-[2px]`}
>
{t("my-signature")}
</span>
</div>
) : (
props?.isInitial &&
props?.myInitial &&
isTabEnabled("default") && (
<div>
<span
onClick={() => {
setIsDefaultSign(true);
props?.setIsImageSelect(true);
setIsTab("mysignature");
setSignatureType("");
props?.setImage();
}}
className={`${
isTab === "mysignature"
? "op-link-primary"
: "no-underline"
} op-link underline-offset-8 ml-[2px]`}
>
{t("my-initials")}
</span>
</div>
)
)}
{isTabEnabled("draw") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
props?.setIsImageSelect(false);
setIsTab("draw");
props?.setImage();
if (isSignImg) {
props?.setSignature(isSignImg);
}
}}
className={`${
isTab === "draw"
? "op-link-primary"
: "no-underline"
} op-link underline-offset-8 ml-[2px]`}
>
{t("draw")}
</span>
</div>
)}
{isTabEnabled("upload") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
props?.setIsImageSelect(true);
setIsTab("uploadImage");
setSignatureType("");
}}
className={`${
isTab === "uploadImage"
? "op-link-primary"
: "no-underline"
} op-link underline-offset-8 ml-[2px]`}
>
{t("upload-image")}
</span>
</div>
)}
{isTabEnabled("typed") && (
<div>
<span
onClick={() => {
setIsDefaultSign(false);
props?.setIsImageSelect(false);
setIsTab("type");
setSignatureType("");
props?.setImage();
}}
className={`${
isTab === "type"
? "op-link-primary"
: "no-underline"
} op-link underline-offset-8 ml-[2px]`}
>
{t("type")}
</span>
</div>
)}
</>
)}
</div>
</div>
</div>
<div
className="text-[1.5rem] cursor-pointer"
onClick={handleCancelBtn}
>
&times;
</div>
</div>
<div className="p-[20px] h-full">
{isDefaultSign ? (
<>
{!props?.isInitial &&
props?.defaultSign &&
isTabEnabled("default") && (
<>
<div className="flex justify-center">
<div
className={`${props?.isInitial ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-[#007bff] flex flex-col justify-center items-center mb-[6px] cursor-pointer`}
>
<img
alt="stamp img"
className="w-full h-full object-contain bg-white"
draggable="false"
src={
props?.isInitial
? props?.myInitial
: props?.defaultSign
}
/>
</div>
</div>
{props.setIsAutoSign && autoSignAll()}
<div className="flex justify-end">
<SaveBtn />
</div>
</>
)}
{props?.isInitial &&
props?.myInitial &&
isTabEnabled("default") && (
<>
<div className="flex justify-center">
<div
className={`${props?.isInitial ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-[#007bff] flex flex-col justify-center items-center mb-[6px] cursor-pointer`}
>
<img
alt="stamp img"
className="w-full h-full object-contain bg-white"
draggable="false"
src={
props?.isInitial
? props?.myInitial
: props?.defaultSign
}
/>
</div>
</div>
{props.setIsAutoSign && autoSignAll()}
<div className="flex justify-end">
<SaveBtn />
</div>
</>
)}
</>
) : props?.isImageSelect || props?.isStamp ? (
!props?.image ? (
<div className="flex justify-center">
<div
className={`${props?.isInitial ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-[#007bff] flex flex-col justify-center items-center mb-[6px] cursor-pointer`}
onClick={() => props?.imageRef.current.click()}
>
<input
type="file"
onChange={props?.onImageChange}
className="filetype"
accept="image/png,image/jpeg"
ref={props?.imageRef}
hidden
/>
<i className="fa-light fa-cloud-upload-alt uploadImgLogo"></i>
<div className="text-[10px]">{t("upload")}</div>
</div>
</div>
) : (
<>
<div className="flex justify-center">
<div
className={`${props?.isInitial ? "intialSignatureCanvas" : "signatureCanvas"} bg-white border-[1.3px] border-[#007bff] mb-[6px] overflow-hidden`}
>
<img
alt="print img"
ref={props?.imageRef}
src={props?.image.src}
draggable="false"
className="object-contain h-full w-full"
/>
</div>
</div>
{props.setIsAutoSign && autoSignAll()}
<div className="flex justify-end">
<SaveBtn />
</div>
</>
)
) : isTab === "type" ? (
<div>
<div className="flex justify-between items-center">
<span className="mr-[5px] text-[12px]">
{props?.isInitial
? t("initial-teb")
: t("signature-tab")}
:
</span>
<input
maxLength={props?.isInitial ? 3 : 30}
style={{ fontFamily: fontSelect, color: penColor }}
type="text"
className="ml-1 op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-[20px]"
placeholder={
props?.isInitial
? t("initial-type")
: t("signature-type")
}
value={typedSignature}
onChange={(e) => {
setTypedSignature(e.target.value);
convertToImg(fontSelect, e.target.value);
}}
/>
</div>
<div className="border-[1px] border-[#d6d3d3] mt-[10px] ml-[5px]">
{fontOptions.map((font, ind) => {
return (
<div
key={ind}
style={{
cursor: "pointer",
fontFamily: font.value,
backgroundColor:
fontSelect === font.value &&
"rgb(206 225 247)"
}}
onClick={() => {
setFontSelect(font.value);
convertToImg(font.value, typedSignature);
}}
>
<div
className="py-[5px] px-[10px] text-[20px]"
style={{ color: penColor }}
>
{typedSignature
? typedSignature
: "Your signature"}
</div>
</div>
);
})}
</div>
<div className="flex flex-col justify-between mt-[10px]">
{props.setIsAutoSign && autoSignAll()}
<div className="flex flex-row justify-between mt-[10px]">
<PenColorComponent convertToImg={convertToImg} />
<SaveBtn />
</div>
</div>
</div>
) : (
<>
<div className="flex justify-center">
<SignatureCanvas
ref={canvasRef}
penColor={penColor}
canvasProps={{
className: `${props?.isInitial ? "intialSignatureCanvas" : "signatureCanvas"} border-[1.3px] border-[#007bff]`
}}
onEnd={() =>
handleSignatureChange(
canvasRef.current?.toDataURL()
)
}
dotSize={1}
/>
</div>
<div className="flex flex-col justify-between mt-[10px]">
{props.setIsAutoSign && autoSignAll()}
{accesstoken &&
props?.saveSignCheckbox?.isVisible &&
savesigncheckbox}
<div className="flex flex-row justify-between mt-[10px]">
<PenColorComponent />
<SaveBtn />
</div>
</div>
</>
)}
</div>
</>
) : (
<div>
<div className="relative flex flex-row items-center justify-between">
<div className="text-base-content font-bold text-lg">
Signature
</div>
<div
className="text-[1.5rem] cursor-pointer"
onClick={handleCancelBtn}
>
&times;
</div>
</div>
<div className="mx-3 mb-6 mt-3">
<p>{t("at-least-one-signature-type")}</p>
</div>
</div>
)}
</div>
</div>
)}
</div>
);
}
export default SignPad;
@@ -13,7 +13,7 @@ function TextFontSetting(props) {
title={t("text-field")}
handleClose={() => props.setIsTextSetting(false)}
>
<div className="h-full p-[20px]">
<div className="h-full p-[20px] text-base-content">
<div className="flex flex-col md:flex-row md:items-center gap-3">
{/* Font Size Selector */}
<div className="flex items-center gap-2">
@@ -8,6 +8,7 @@ import {
isMobile,
radioButtonWidget,
textInputWidget,
cellsWidget,
textWidget,
widgets
} from "../../constant/Utils";
@@ -37,6 +38,10 @@ function WidgetComponent(props) {
type: "BOX",
item: { type: "BOX", id: 7, text: textInputWidget }
});
const [, cells] = useDrag({
type: "BOX",
item: { type: "BOX", id: 17, text: cellsWidget }
});
const [, initials] = useDrag({
type: "BOX",
item: { type: "BOX", id: 8, text: "initials" }
@@ -89,6 +94,7 @@ function WidgetComponent(props) {
date,
text,
textInput,
cells,
checkbox,
dropdown,
radioButton,
@@ -132,7 +138,9 @@ function WidgetComponent(props) {
);
const filterWidgets = widget.filter(
(data) =>
!["dropdown", radioButtonWidget, textInputWidget].includes(data.type)
!["dropdown", radioButtonWidget, textInputWidget].includes(
data.type
)
);
const textWidgetData = widget.filter((data) => data.type !== textWidget);
const updateWidgets = props.isSignYourself
@@ -241,6 +249,7 @@ function WidgetComponent(props) {
handleDivClick={props.handleDivClick}
handleMouseLeave={props.handleMouseLeave}
signRef={signRef}
addPositionOfSignature={props.addPositionOfSignature}
/>
</div>
</div>
@@ -5,6 +5,7 @@ import RegexParser from "regex-parser";
import {
signatureTypes,
textInputWidget,
cellsWidget,
textWidget
} from "../../constant/Utils";
import { fontColorArr, fontsizeArr } from "../../constant/Utils";
@@ -19,33 +20,47 @@ const WidgetNameModal = (props) => {
status: "required",
hint: "",
textvalidate: "",
isReadOnly: false
isReadOnly: false,
cellCount: 5
});
const [isValid, setIsValid] = useState(true);
const statusArr = ["Required", "Optional"];
const [signatureType, setSignatureType] = useState([]);
const handleHint = () => {
const type = props.defaultdata?.type;
if (type === "signature") {
return "Draw signature";
} else if (type === "stamp" || type === "image") {
return `Upload ${type}`;
} else if (type === "initials") {
return "Draw initial";
} else if (type === textInputWidget) {
return "Enter text";
} else {
return `Enter ${type}`;
}
};
useEffect(() => {
if (props.defaultdata) {
setFormdata({
name: props.defaultdata?.options?.name || "",
defaultValue: props.defaultdata?.options?.defaultValue || "",
status: props.defaultdata?.options?.status || "required",
hint:
props.defaultdata?.options?.hint ||
(props.defaultdata?.type === textInputWidget
? "Enter text"
: `Enter ${props.defaultdata?.options?.name}`),
hint: props.defaultdata?.options?.hint || handleHint(),
textvalidate:
props.defaultdata?.options?.validation?.type === "regex"
? props.defaultdata?.options?.validation?.pattern
: props.defaultdata?.options?.validation?.type || "",
isReadOnly: props.defaultdata?.options?.isReadOnly || false
isReadOnly: props.defaultdata?.options?.isReadOnly || false,
cellCount: props.defaultdata?.options?.cellCount || 5
});
} else {
setFormdata({
...formdata,
name: props.defaultdata?.options?.name || ""
name: props.defaultdata?.options?.name || "",
cellCount: props.defaultdata?.options?.cellCount || 5
});
}
@@ -72,6 +87,20 @@ const WidgetNameModal = (props) => {
props.handleData(data, props.defaultdata?.type);
}
} else {
const isTextInput = [textInputWidget, cellsWidget].includes(
props.defaultdata?.type
);
const { isReadOnly, defaultValue, status } = formdata;
// If its a textinput widget, enforce that read-only fields have
// either a defaultValue or an "optional" status.
if (isTextInput) {
const readOnlyWithoutValue =
isReadOnly && !defaultValue && status !== "optional";
if (readOnlyWithoutValue) {
alert(t("readonly-error", { widgetName: props.defaultdata?.type }));
return;
}
}
props.handleData(formdata);
}
setFormdata({
@@ -80,7 +109,8 @@ const WidgetNameModal = (props) => {
defaultValue: "",
status: "required",
hint: "",
textvalidate: ""
textvalidate: "",
cellCount: 5
});
setSignatureType(signTypes);
}
@@ -93,6 +123,23 @@ const WidgetNameModal = (props) => {
}
};
const handleChangeValidateInput = (e) => {
if (e) {
if (e.target.value === "ssn") {
setFormdata({
...formdata,
[e.target.name]: e.target.value,
hint: "xxx-xx-xxxx",
cellCount: 11
});
} else {
setFormdata({ ...formdata, [e.target.name]: e.target.value });
}
} else {
setFormdata({ ...formdata, textvalidate: "" });
}
};
const handledefaultChange = (e) => {
if (formdata.textvalidate) {
const regexObject = RegexParser(handleValidation(formdata.textvalidate));
@@ -101,7 +148,11 @@ const WidgetNameModal = (props) => {
} else {
setIsValid(true);
}
setFormdata({ ...formdata, [e.target.name]: e.target.value });
const val =
props.defaultdata?.type === cellsWidget
? e.target.value.slice(0, formdata.cellCount)
: e.target.value;
setFormdata({ ...formdata, [e.target.name]: val });
};
function handleValidation(type) {
@@ -111,7 +162,10 @@ const WidgetNameModal = (props) => {
case "number":
return "/^\\d+$/";
case "text":
return "/^[a-zA-Zs]+$/";
//allow space in text regex
return "/^[a-zA-Z ]+$/";
case "ssn":
return "/^(?!000|666|9\\d{2})\\d{3}-(?!00)\\d{2}-(?!0000)\\d{4}$/";
default:
return type;
}
@@ -139,7 +193,7 @@ const WidgetNameModal = (props) => {
<form
onSubmit={handleSubmit}
className={`${
props.defaultdata?.type === textInputWidget
[textInputWidget, cellsWidget].includes(props.defaultdata?.type)
? "pt-0"
: ["signature", "initials"].includes(props.defaultdata?.type)
? "pt-2"
@@ -163,7 +217,21 @@ const WidgetNameModal = (props) => {
/>
</div>
)}
{props.defaultdata?.type === textInputWidget && (
{props.defaultdata?.type === cellsWidget && (
<div className="mb-[0.75rem] text-[13px]">
<label htmlFor="cellCount">{t("cell-count")}</label>
<input
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
type="number"
min="1"
name="cellCount"
value={formdata.cellCount}
onChange={(e) => handleChange(e)}
required
/>
</div>
)}
{[textInputWidget, cellsWidget].includes(props.defaultdata?.type) && (
<>
<div className="mb-[0.75rem]">
<label htmlFor="name" className="text-[13px]">
@@ -175,6 +243,11 @@ const WidgetNameModal = (props) => {
value={formdata.defaultValue}
onChange={(e) => handledefaultChange(e)}
autoComplete="off"
maxLength={
props.defaultdata?.type === cellsWidget
? formdata.cellCount
: undefined
}
onBlur={() => {
if (isValid === false) {
setFormdata({ ...formdata, defaultValue: "" });
@@ -226,7 +299,9 @@ const WidgetNameModal = (props) => {
);
})}
</div>
{[textInputWidget].includes(props.defaultdata?.type) && (
{[textInputWidget, cellsWidget].includes(
props.defaultdata?.type
) && (
<div className="flex items-center">
<input
id="isReadOnly"
@@ -241,7 +316,10 @@ const WidgetNameModal = (props) => {
}))
}
/>
<label className="ml-1 mb-0" htmlFor="isreadonly">
<label
className="ml-1.5 mb-0 capitalize text-[13px]"
htmlFor="isreadonly"
>
{t("read-only")}
</label>
</div>
@@ -293,6 +371,7 @@ const WidgetNameModal = (props) => {
{[
textInputWidget,
textWidget,
cellsWidget,
"name",
"company",
"job title",
File diff suppressed because it is too large Load Diff
@@ -195,7 +195,7 @@ const SelectFolder = ({ required, onSuccess, folderCls, isReset }) => {
? selectFolder.Name
: t("OpenSign-drive", { appName: drivename })}
</p>
<div className="text-black text-sm">
<div className="text-sm">
<i
className="fa-light fa-pencil cursor-pointer"
title={t("select-folder")}
@@ -2,9 +2,19 @@ import React, { useEffect, useState } from "react";
import AsyncSelect from "react-select/async";
import { useTranslation } from "react-i18next";
import axios from "axios";
import { handleUnlinkSigner } from "../../../constant/Utils";
const SelectSigners = (props) => {
const { t } = useTranslation();
const {
signerPos,
setSignerPos,
signersData,
setSignersData,
uniqueId,
isRemove,
handleAddUser
} = props;
const [userList, setUserList] = useState([]);
const [selected, setSelected] = useState();
const [userData, setUserData] = useState({});
@@ -30,7 +40,7 @@ const SelectSigners = (props) => {
//checking if user select no signer option from dropdown
if (item) {
//checking selected signer is already assign to the document or not
const alreadyAssign = props.signersData.some(
const alreadyAssign = signersData.some(
(item2) => item2.objectId === item.value
);
if (alreadyAssign) {
@@ -49,7 +59,7 @@ const SelectSigners = (props) => {
};
const handleAdd = () => {
if (userData && userData.objectId) {
props.details(userData);
handleAddUser(userData);
if (props.closePopup) {
props.closePopup();
}
@@ -60,7 +70,13 @@ const SelectSigners = (props) => {
};
//function to use remove signer from assigned widgets in create template flow
const handleRemove = () => {
props.handleUnlinkSigner();
handleUnlinkSigner(
signerPos,
setSignerPos,
signersData,
setSignersData,
uniqueId
);
if (props.closePopup) {
props.closePopup();
}
@@ -81,7 +97,7 @@ const SelectSigners = (props) => {
const contactRes = axiosRes?.data?.result || [];
if (contactRes) {
const res = JSON.parse(JSON.stringify(contactRes));
//compareArrays is a function where compare between two array (total signersList and dcument signers list)
//compareArrays is a function where compare between two array (total signersList and document signers list)
//and filter signers from total signer's list which already present in document's signers list
// const compareArrays = (res, signerObj) => {
// return res.filter(
@@ -161,7 +177,7 @@ const SelectSigners = (props) => {
<button className="op-btn op-btn-primary" onClick={() => handleAdd()}>
{t("submit")}
</button>
{props.isExistSigner && props.handleUnlinkSigner && (
{props.isExistSigner && isRemove && (
<button
className="op-btn op-btn-accent op-btn-outline"
onClick={() => handleRemove()}
@@ -122,7 +122,7 @@ const SignersInput = (props) => {
);
if (contactRes) {
const res = JSON.parse(JSON.stringify(contactRes));
//compareArrays is a function where compare between two array (total signersList and dcument signers list)
//compareArrays is a function where compare between two array (total signersList and document signers list)
//and filter signers from total signer's list which already present in document's signers list
const compareArrays = (res, signerObj) => {
return res.filter(
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -1,9 +1,12 @@
import logo from "../assets/images/logo.png";
import { getEnv } from "./Utils";
export function serverUrl_fn() {
let baseUrl = process.env.REACT_APP_SERVERURL
? process.env.REACT_APP_SERVERURL
: window.location.origin + "/api/app";
const env = getEnv();
const serverurl = env?.REACT_APP_SERVERURL
? env.REACT_APP_SERVERURL // env.REACT_APP_SERVERURL is used for prod
: process.env.REACT_APP_SERVERURL; // process.env.REACT_APP_SERVERURL is used for dev (locally)
let baseUrl = serverurl ? serverurl : window.location.origin + "/api/app";
return baseUrl;
}
export const appInfo = {
+5
View File
@@ -3,6 +3,11 @@ export const templateCls = "contracts_Template";
export const documentCls = "contracts_Document";
export const themeColor = "#47a3ad";
export const iconColor = "#686968";
// Dynamic icon color function for better dark mode visibility
export const getThemeIconColor = () => {
const theme = document.documentElement.getAttribute("data-theme");
return theme === "opensigndark" ? "#CCCCCC" : "#686968";
};
export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
export const maxFileSize = 10; // 10MB
export const maxTitleLength = 250; // 250 characters
+38
View File
@@ -0,0 +1,38 @@
import { useState, useEffect } from 'react';
type Size = { width: number; height: number; };
/**
* useSize
* @param ref React ref object pointing to a DOM element
* @returns current size of that element: { width, height }
*/
export function useElSize(ref: React.RefObject<HTMLElement>): Size {
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
useEffect(() => {
const el = ref.current;
if (!el) return;
// Initialize size
setSize({
width: el.offsetWidth,
height: el.offsetHeight,
});
// Watch for resize
const observer = new ResizeObserver(entries => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
setSize({ width, height });
}
});
observer.observe(el);
return () => {
observer.disconnect();
};
}, [ref]);
return size;
}
+1 -2
View File
@@ -1,6 +1,6 @@
import { useEffect } from "react";
/**`useScript` hook is generated scripte for google sign in button */
/**`useScript` hook is generated script for google sign in button */
export const useScript = (url, onload) => {
useEffect(() => {
const script = document.createElement("script");
@@ -15,4 +15,3 @@ export const useScript = (url, onload) => {
};
}, [url, onload]);
};
+1 -1
View File
@@ -25,7 +25,7 @@ i18n
interpolation: {
escapeValue: false // Not needed for react as it escapes by default
},
whitelist: ["en", "es", "fr", "it", "de"] // List of allowed languages
whitelist: ["en", "es", "fr", "it", "de", "hi"] // List of allowed languages
});
export default i18n;
+163
View File
@@ -17,6 +17,9 @@ body {
scrollbar-width: none;
/* Firefox */
}
.react-datepicker-popper {
z-index: 9999 !important;
}
@media screen and (max-width: 766px) {
.reactour__close {
@@ -95,4 +98,164 @@ body {
*::-webkit-scrollbar-thumb {
background-color: gray;
border-radius: 10px;
}
/* Note: Dark mode styling is now handled via Tailwind utilities in tailwind.config.js */
/* You can use classes like: icon-improved, icon-muted, icon-disabled, op-btn-vscode-disabled */
/* React-tour and ReactTooltip dark mode styling */
[data-theme="opensigndark"] {
/* React-tour modal styling */
.reactour__helper {
background-color: #1F2937 !important;
color: #E5E7EB !important;
border: 1px solid #374151 !important;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5) !important;
}
.reactour__close {
color: #E5E7EB !important;
background-color: #374151 !important;
border: 1px solid #4B5563 !important;
}
.reactour__close:hover {
background-color: #4B5563 !important;
color: #FFFFFF !important;
}
/* React-tour navigation buttons */
.reactour__controls {
background-color: #1F2937 !important;
border-top: 1px solid #374151 !important;
}
.reactour__controls button {
background-color: #374151 !important;
color: #E5E7EB !important;
border: 1px solid #4B5563 !important;
}
.reactour__controls button:hover {
background-color: #4B5563 !important;
color: #FFFFFF !important;
}
.reactour__controls button:disabled {
background-color: #3C3C3C !important;
color: #858585 !important;
border-color: #565656 !important;
cursor: not-allowed !important;
}
/* ReactTooltip styling */
.react-tooltip {
background-color: #1F2937 !important;
color: #E5E7EB !important;
border: 1px solid #374151 !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
}
.react-tooltip.type-dark {
background-color: #1F2937 !important;
color: #E5E7EB !important;
}
.react-tooltip.place-top:after,
.react-tooltip.place-bottom:after,
.react-tooltip.place-left:after,
.react-tooltip.place-right:after {
border-color: #1F2937 !important;
}
/* Tour content improvements */
.reactour__badge {
background-color: #007ACC !important;
color: #FFFFFF !important;
}
.reactour__helper h1,
.reactour__helper h2,
.reactour__helper h3,
.reactour__helper h4,
.reactour__helper h5,
.reactour__helper h6 {
color: #FFFFFF !important;
}
.reactour__helper p {
color: #E5E7EB !important;
}
/* VS Code-style tour buttons */
.reactour__controls .op-btn {
background-color: #007ACC !important;
color: #FFFFFF !important;
border: 1px solid #007ACC !important;
}
.reactour__controls .op-btn:hover {
background-color: #0086D1 !important;
border-color: #0086D1 !important;
}
.reactour__controls .op-btn-secondary {
background-color: #374151 !important;
color: #E5E7EB !important;
border: 1px solid #4B5563 !important;
}
.reactour__controls .op-btn-secondary:hover {
background-color: #4B5563 !important;
color: #FFFFFF !important;
}
/* React-datepicker dark mode styling */
.react-datepicker {
background-color: #1F2937 !important;
border: 1px solid #374151 !important;
color: #E5E7EB !important;
}
.react-datepicker__header {
background-color: #374151 !important;
border-bottom: 1px solid #4B5563 !important;
color: #E5E7EB !important;
}
.react-datepicker__current-month,
.react-datepicker__day-name {
color: #E5E7EB !important;
}
.react-datepicker__day {
color: #E5E7EB !important;
}
.react-datepicker__day:hover {
background-color: #4B5563 !important;
color: #FFFFFF !important;
}
.react-datepicker__day--selected {
background-color: #007ACC !important;
color: #FFFFFF !important;
}
.react-datepicker__day--keyboard-selected {
background-color: #374151 !important;
color: #E5E7EB !important;
}
.react-datepicker__day--outside-month {
color: #6B7280 !important;
}
.react-datepicker__navigation {
color: #E5E7EB !important;
}
.react-datepicker__navigation:hover {
background-color: #4B5563 !important;
}
}
+14 -10
View File
@@ -1,10 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import "./styles/dark-theme-improvements.css";
import App from "./App";
import { Provider } from "react-redux";
import { store } from "./redux/store";
import { CookiesProvider } from "react-cookie";
import { HTML5Backend } from "react-dnd-html5-backend";
import { TouchBackend } from "react-dnd-touch-backend";
import {
@@ -19,11 +19,17 @@ import "./polyfills";
import { serverUrl_fn } from "./constant/appinfo";
import "./i18n";
const appId = import.meta.env.VITE_APPID || process.env.REACT_APP_APPID || "opensign";
const appId =
import.meta.env.VITE_APPID || process.env.REACT_APP_APPID || "opensign";
const serverUrl = serverUrl_fn();
Parse.initialize(appId);
Parse.serverURL = serverUrl;
const savedTheme = localStorage.getItem("theme");
if (savedTheme === "dark") {
document.documentElement.setAttribute("data-theme", "opensigndark");
}
const HTML5toTouch = {
backends: [
{
@@ -56,12 +62,10 @@ const generatePreview = (props) => {
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<CookiesProvider defaultSetOptions={{ path: "/" }}>
<Provider store={store}>
<DndProvider options={HTML5toTouch}>
<Preview>{generatePreview}</Preview>
<App />
</DndProvider>
</Provider>
</CookiesProvider>
<Provider store={store}>
<DndProvider options={HTML5toTouch}>
<Preview>{generatePreview}</Preview>
<App />
</DndProvider>
</Provider>
);
+10 -2
View File
@@ -186,6 +186,14 @@ export default function reportJson(id) {
btnIcon: "fa-light fa-envelope",
redirectUrl: "",
action: "saveastemplate"
},
{
btnId: "8440",
btnLabel: "Fix & resend",
hoverLabel: "Fix & resend",
btnIcon: "fa-light fa-paper-plane",
redirectUrl: "",
action: "recreatedocument"
}
]
}
@@ -533,9 +541,9 @@ export default function reportJson(id) {
btnId: "1873",
btnLabel: "Share with team",
hoverLabel: "Share with team",
btnIcon: "fa-light fa-share-nodes",
btnIcon: "fa-light fa-user-group",
redirectUrl: "",
action: "sharewith"
action: "sharewithteam"
});
return newItem;
}
+12 -9
View File
@@ -1,3 +1,13 @@
const userssetting = [
{
icon: "fa-light fa-users fa-fw",
title: "Users",
target: "_self",
pageType: "",
description: "",
objectId: "users"
}
];
export const subSetting = [
{
icon: "fa-light fa-sliders",
@@ -7,14 +17,7 @@ export const subSetting = [
description: "",
objectId: "preferences"
},
{
icon: "fa-light fa-users fa-fw",
title: "Users",
target: "_self",
pageType: "",
description: "",
objectId: "users"
}
...userssetting
];
const sidebarList = [
@@ -65,7 +68,7 @@ const sidebarList = [
pageType: "report",
description: "",
objectId: "6TeaPr321t"
},
}
]
},
{
+7 -45
View File
@@ -3,13 +3,12 @@ import Header from "../components/Header";
import Footer from "../components/Footer";
import Sidebar from "../components/sidebar/Sidebar";
import { useWindowSize } from "../hook/useWindowSize";
import Tour from "reactour";
import Tour from "../primitives/Tour";
import axios from "axios";
import { useDispatch, useSelector } from "react-redux";
import Parse from "parse";
import ModalUi from "../primitives/ModalUi";
import { useNavigate, useLocation, Outlet } from "react-router";
import { useCookies } from "react-cookie";
import Loader from "../primitives/Loader";
import { showHeader } from "../redux/reducers/showHeader";
import { useTranslation } from "react-i18next";
@@ -30,7 +29,6 @@ const HomeLayout = () => {
const [isTour, setIsTour] = useState(false);
const [tourStatusArr, setTourStatusArr] = useState([]);
const [tourConfigs, setTourConfigs] = useState([]);
const [, setCookie] = useCookies(["accesstoken", "main_Domain"]);
const tenantId = localStorage.getItem("TenantId");
@@ -64,38 +62,11 @@ const HomeLayout = () => {
setIsUserValid(false);
}
})();
saveCookies();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tenantId]);
//function to use save data in cookies storage
const saveCookies = () => {
const main_Domain = window.location.origin;
const domainName = window.location.hostname;
// Find the index of the first dot in the string
const indexOfFirstDot = domainName.indexOf(".");
// Remove the first dot and get the substring starting from the next character
const updateDomain = domainName.substring(indexOfFirstDot); //.opensignlabs.com
const serverUrl = localStorage.getItem("baseUrl");
const parseAppId = localStorage.getItem("parseAppId");
setCookie("accesstoken", localStorage.getItem("accesstoken"), {
secure: true,
domain: updateDomain
});
setCookie("main_Domain", main_Domain, {
secure: true,
domain: updateDomain
});
setCookie("server_url", serverUrl, {
secure: true,
domain: updateDomain
});
setCookie("parse_app_id", parseAppId, {
secure: true,
domain: updateDomain
});
};
const showSidebar = () => {
setIsOpen((value) => !value);
dispatch(showHeader(!isOpen));
@@ -120,12 +91,7 @@ const HomeLayout = () => {
// const resArr = arr;
const resArr = arr.map((obj, index) => {
if (arr.length - 1 === index) {
return {
...obj
// actions: () => {
// setIsCloseBtn(true);
// },
};
return { ...obj };
} else {
return {
...obj,
@@ -137,27 +103,23 @@ const HomeLayout = () => {
});
setTourConfigs([
{
selector: '[data-tut="reactourFirst"]',
selector: '[data-tut="nonpresentmask"]',
content: t("tour-mssg.home-layout-1"),
position: "top"
// style: { backgroundColor: "#abd4d2" },
position: "center",
},
{
selector: '[data-tut="tourbutton"]',
content: t("tour-mssg.home-layout-2"),
position: "top"
// style: { backgroundColor: "#abd4d2" },
},
...resArr,
{
selector: '[data-tut="reactourLast"]',
selector: '[data-tut="nonpresentmask"]',
content: t("tour-mssg.home-layout-3", { appName }),
position: "top"
// style: { backgroundColor: "#abd4d2" },
position: "center",
}
]);
checkTourStatus();
// console.log("resArr ", resArr);
}
};
const closeTour = async () => {
+204 -234
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { formJson } from "../json/FormJson";
import Parse from "parse";
@@ -12,9 +12,12 @@ import {
flattenPdf,
generatePdfName,
generateTitleFromFilename,
getFileName,
getSecureUrl,
toDataUrl
toDataUrl,
decryptPdf,
base64ToFile,
getFileAsArrayBuffer,
removeTrailingSegment
} from "../constant/Utils";
import { PDFDocument } from "pdf-lib";
import axios from "axios";
@@ -68,6 +71,7 @@ const Forms = (props) => {
AllowModifications: false
});
const [fileupload, setFileUpload] = useState("");
const [selectedFiles, setSelectedFiles] = useState([]);
const [fileload, setfileload] = useState(false);
const [percentage, setpercentage] = useState(0);
const [isReset, setIsReset] = useState(false);
@@ -114,15 +118,6 @@ const Forms = (props) => {
}));
};
function getFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e.target.error);
reader.readAsArrayBuffer(file);
});
}
// `removeFile` is used to reset progress, percentage and remove file if exists
const removeFile = (e) => {
setfileload(false);
@@ -134,219 +129,212 @@ const Forms = (props) => {
const handleFileInput = async (e) => {
setpercentage(0);
try {
let files = e.target.files;
setFormData((prev) => ({ ...prev, file: e.target.files[0] }));
if (typeof files[0] !== "undefined") {
const mb = Math.round(files[0].size / Math.pow(1024, 2));
if (mb > maxFileSize) {
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
setFileUpload("");
removeFile(e);
return;
} else {
if (files?.[0]?.type === "application/pdf") {
const size = files?.[0]?.size;
const name = generatePdfName(16);
const pdfName = `${name?.split(".")[0]}.pdf`;
setfileload(true);
try {
const res = await getFileAsArrayBuffer(files[0]);
const flatPdf = await flattenPdf(res);
const parseFile = new Parse.File(
pdfName,
[...flatPdf],
"application/pdf"
);
const files = Array.from(e.target.files);
const filesNameArr = files.map((f) => f.name);
setSelectedFiles(filesNameArr);
if (!files.length) {
alert(t("file-alert-2"));
return;
}
// setFormData((prev) => ({ ...prev, file: files[0] }));
const totalMb = Math.round(
files.reduce((sum, f) => sum + f.size, 0) / Math.pow(1024, 2)
);
if (totalMb > maxFileSize) {
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
setFileUpload("");
setSelectedFiles([]);
removeFile(e);
return;
}
try {
const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round(
(loaded * 100) / total
);
setpercentage(percentCompleted);
}
}
});
// The response object will contain information about the uploaded file
// You can access the URL of the uploaded file using response.url()
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
setfileload(false);
const tenantId = localStorage.getItem("TenantId");
const title = generateTitleFromFilename(files?.[0]?.name);
setFormData((obj) => ({ ...obj, Name: title }));
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
removeFile(e);
}
} else {
removeFile(e);
}
} catch (error) {
removeFile(e);
console.error("Error uploading file:", error);
}
const pdfBuffers = [];
for (const file of files) {
setFormData((prev) => ({ ...prev, file: file }));
if (file.type === "application/pdf") {
try {
const buffer = await getFileAsArrayBuffer(file);
const flat = await flattenPdf(buffer);
pdfBuffers.push(flat);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
setIsDecrypting(true);
const pdfFile = await decryptPdf(file, "");
setIsDecrypting(false);
setfileload(true);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
// Upload the file to Parse Server
pdfBuffers.push(flatPdf);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
setIsDecrypting(true);
const size = files?.[0].size;
const name = generatePdfName(16);
const url = "https://ai.nxglabs.in/decryptpdf"; //
let formData = new FormData();
formData.append("file", files[0]);
formData.append("password", "");
const config = {
headers: { "content-type": "multipart/form-data" },
responseType: "blob"
};
const response = await axios.post(url, formData, config);
const pdfBlob = new Blob([response.data], {
type: "application/pdf"
});
const pdfFile = new File([pdfBlob], name, {
type: "application/pdf"
});
setIsDecrypting(false);
setfileload(true);
removeFile(e);
if (err?.response?.status === 401) {
// setIsPassword(true);
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
setIsDecrypting(false);
setfileload(true);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
// Upload the file to Parse Server
const parseFile = new Parse.File(
name,
[...flatPdf],
"application/pdf"
pdfBuffers.push(flatPdf);
} catch (err) {
console.error(
"Incorrect password or decryption failed",
err
);
setSelectedFiles(
filesNameArr.filter((f) => f !== file.name)
);
await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round(
(loaded * 100) / total
);
setpercentage(percentCompleted);
}
}
});
// Retrieve the URL of the uploaded file
if (parseFile.url()) {
const fileRes = await getSecureUrl(parseFile.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
removeFile();
const title = generateTitleFromFilename(
files?.[0]?.name
);
setFormData((obj) => ({ ...obj, Name: title }));
const tenantId = localStorage.getItem("TenantId");
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
removeFile(e);
}
} else {
removeFile(e);
}
} catch (err) {
removeFile();
if (err?.response?.status === 401) {
setIsPassword(true);
} else {
console.log("Error uploading file: ", err?.response);
setIsDecrypting(false);
e.target.value = "";
}
}
} else {
console.log("err ", err);
setFileUpload("");
removeFile(e);
}
}
} else {
const isImage = files?.[0]?.type.includes("image/");
if (isImage) {
const image = await toDataUrl(files[0]);
const pdfDoc = await PDFDocument.create();
let embedImg;
if (files?.[0]?.type === "image/png") {
embedImg = await pdfDoc.embedPng(image);
} else {
embedImg = await pdfDoc.embedJpg(image);
}
// Get image dimensions
const imageWidth = embedImg.width;
const imageHeight = embedImg.height;
const page = pdfDoc.addPage([imageWidth, imageHeight]);
page.drawImage(embedImg, {
x: 0,
y: 0,
width: imageWidth,
height: imageHeight
});
const size = files?.[0]?.size;
const name = generatePdfName(16);
const getFile = await pdfDoc.save({
useObjectStreams: false
});
setfileload(true);
const pdfName = `${name?.split(".")[0]}.pdf`;
const parseFile = new Parse.File(
pdfName,
[...getFile],
"application/pdf"
);
try {
const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round(
(loaded * 100) / total
);
setpercentage(percentCompleted);
}
}
});
// The response object will contain information about the uploaded file
// You can access the URL of the uploaded file using response.url()
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
setFileUpload(fileRes.url);
setfileload(false);
const tenantId = localStorage.getItem("TenantId");
const title = generateTitleFromFilename(files?.[0]?.name);
setFormData((obj) => ({ ...obj, Name: title }));
SaveFileSize(size, fileRes.url, tenantId);
return fileRes.url;
} else {
removeFile(e);
alert(`Incorrect password for file: ${file.name}`);
}
} else {
console.error("password not provided");
setSelectedFiles(
filesNameArr.filter((f) => f !== file.name)
);
setIsDecrypting(false);
setfileload(false);
removeFile(e);
}
} catch (error) {
} else {
console.log("Error uploading file: ", err?.response);
setIsDecrypting(false);
e.target.value = "";
removeFile(e);
console.error("Error uploading file:", error);
}
}
} else {
console.log("err ", err);
removeFile(e);
}
}
} else if (file.type.includes("image/")) {
const image = await toDataUrl(file);
const pdfDoc = await PDFDocument.create();
const embed =
file.type === "image/png"
? await pdfDoc.embedPng(image)
: await pdfDoc.embedJpg(image);
const page = pdfDoc.addPage([embed.width, embed.height]);
page.drawImage(embed, {
x: 0,
y: 0,
width: embed.width,
height: embed.height
});
const bytes = await pdfDoc.save({ useObjectStreams: false });
pdfBuffers.push(bytes);
} else if (
file.type ===
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
file.name.toLowerCase().endsWith(".docx")
) {
try {
const baseApi = localStorage.getItem("baseUrl") || "";
const url = removeTrailingSegment(baseApi) + "/docxtopdf";
let fd = new FormData();
fd.append("file", file);
setfileload(true);
setpercentage(0);
const config = {
headers: {
"content-type": "multipart/form-data",
sessiontoken: Parse.User.current().getSessionToken()
},
signal: abortController.signal,
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setpercentage(percentCompleted);
}
}
};
const res = await axios.post(url, fd, config);
if (res.data?.url) {
const pdfRes = await axios.get(res.data.url, {
responseType: "arraybuffer"
});
pdfBuffers.push(pdfRes.data);
}
setfileload(false);
} catch (err) {
setfileload(false);
removeFile(e);
console.log("err in docx to pdf ", err);
const error = isOpenSignDomain
? `${t("docx-error")} ${t("docx-error-contact")}`
: t("docx-error");
alert(error);
return;
}
}
} else {
}
if (!pdfBuffers.length) {
alert(t("file-alert-2"));
return false;
setSelectedFiles([]);
return;
}
setfileload(true);
const merged = await PDFDocument.create();
for (const bytes of pdfBuffers) {
const doc = await PDFDocument.load(bytes, { ignoreEncryption: true });
const pages = await merged.copyPages(doc, doc.getPageIndices());
pages.forEach((p) => merged.addPage(p));
}
const pdfBytes = await merged.save({ useObjectStreams: false });
const name = generatePdfName(16);
const pdfName = `${name}.pdf`;
let uploadedUrl = "";
const parseFile = new Parse.File(
pdfName,
[...pdfBytes],
"application/pdf"
);
const response = await parseFile.save({
progress: (progressValue, loaded, total, { type }) => {
if (type === "upload" && progressValue !== null) {
const percentCompleted = Math.round((loaded * 100) / total);
setpercentage(percentCompleted);
}
}
});
if (response.url()) {
const fileRes = await getSecureUrl(response.url());
if (fileRes.url) {
uploadedUrl = fileRes.url;
}
}
if (uploadedUrl) {
const tenantId = localStorage.getItem("TenantId");
SaveFileSize(pdfBytes.byteLength, uploadedUrl, tenantId);
setFileUpload(uploadedUrl);
setfileload(false);
const title = generateTitleFromFilename(filesNameArr?.[0]);
setFormData((obj) => ({ ...obj, Name: title }));
removeFile(e);
} else {
setfileload(false);
removeFile(e);
setSelectedFiles([]);
}
} catch (error) {
alert(error.message);
return false;
setSelectedFiles([]);
}
};
// `isValidURL` is used to check valid webhook url
@@ -446,16 +434,13 @@ const Forms = (props) => {
className: "contracts_Users",
objectId: ExtCls[0].objectId
});
if (extUserData?.TenantId?.ActiveFileAdapter) {
object.set("FileAdapterId", extUserData?.TenantId?.ActiveFileAdapter);
}
const res = await object.save();
if (res) {
setSigners([]);
setBcc([]);
setFolder({ ObjectId: "", Name: "" });
const notifySign =
extUserData?.NotifyOnSignatures
extUserData?.NotifyOnSignatures !== undefined
? extUserData?.NotifyOnSignatures
: true;
setFormData({
@@ -478,6 +463,7 @@ const Forms = (props) => {
AllowModifications: false
});
setFileUpload("");
setSelectedFiles([]);
setpercentage(0);
navigate(`/${props?.redirectRoute}/${res.id}`);
}
@@ -534,7 +520,7 @@ const Forms = (props) => {
setBcc([]);
setFolder({ ObjectId: "", Name: "" });
const notifySign =
extUserData?.NotifyOnSignatures
extUserData?.NotifyOnSignatures !== undefined
? extUserData?.NotifyOnSignatures
: true;
let obj = {
@@ -559,6 +545,7 @@ const Forms = (props) => {
setFormData(obj);
removeFile();
setFileUpload("");
setSelectedFiles([]);
setTimeout(() => setIsReset(false), 50);
};
const handleCancel = () => {
@@ -571,24 +558,7 @@ const Forms = (props) => {
try {
const size = formData?.file?.size;
const name = generatePdfName(16);
const url = "https://ai.nxglabs.in/decryptpdf"; //
let Data = new FormData();
Data.append("file", formData?.file);
Data.append("password", formData.password);
const config = {
headers: {
"content-type": "multipart/form-data"
// sessiontoken: Parse.User.current().getSessionToken()
},
responseType: "blob"
};
const response = await axios.post(url, Data, config);
const pdfBlob = new Blob([response.data], {
type: "application/pdf"
});
const pdfFile = new File([pdfBlob], name, {
type: "application/pdf"
});
const pdfFile = await decryptPdf(formData?.file, formData?.password);
setIsDecrypting(false);
const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res);
@@ -749,19 +719,20 @@ const Forms = (props) => {
)}
<div className="text-xs">
<label className="block">
{`${`${t("report-heading.File")} (${t("file-type")}`}${
")"
}`}
{`${`${t("report-heading.File")} (${t("file-type")}`}${", docx)"}`}
<span className="text-red-500 text-[13px]">*</span>
</label>
{fileupload.length > 0 ? (
<div className="flex gap-1 justify-center items-center">
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm w-full h-full text-[13px]">
<div className="break-all cursor-default">
{t("file-selected")}: {getFileName(fileupload)}
{t("file-selected")}: {selectedFiles.join(", ")}
</div>
<div
onClick={() => setFileUpload("")}
onClick={() => {
setFileUpload("");
setSelectedFiles([]);
}}
className="cursor-pointer px-[10px] text-[20px] font-bold text-red-500"
>
<i className="fa-light fa-xmark"></i>
@@ -772,12 +743,11 @@ const Forms = (props) => {
<div className="flex gap-1 justify-center items-center">
<input
type="file"
multiple
className="op-file-input op-file-input-bordered op-file-input-sm focus:outline-none hover:border-base-content w-full text-xs"
onChange={(e) => handleFileInput(e)}
ref={inputFileRef}
accept={
"application/pdf,image/png,image/jpeg"
}
accept="application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/png,image/jpeg"
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
+24 -23
View File
@@ -68,6 +68,7 @@ function Login() {
setState({ ...state, loading: false, alertType: type, alertMsg: msg });
setTimeout(() => setState({ ...state, alertMsg: "" }), 2000);
};
const checkUserExt = async () => {
const app = await getAppLogo();
if (app?.error === "invalid_json") {
@@ -82,11 +83,11 @@ function Login() {
} else {
setImage(appInfo?.applogo || undefined);
}
dispatch(fetchAppInfo());
if (localStorage.getItem("accesstoken")) {
setState({ ...state, loading: true });
GetLoginData();
}
dispatch(fetchAppInfo());
};
const handleChange = (event) => {
let { name, value } = event.target;
@@ -96,20 +97,15 @@ function Login() {
setState({ ...state, [name]: value });
};
const handleSubmit = async (event) => {
localStorage.removeItem("accesstoken");
event.preventDefault();
const handleLogin = async (
) => {
const email = state?.email
const password = state?.password
if (!emailRegex.test(state.email)) {
alert("Please enter a valid email address.");
return;
}
const { email, password } = state;
if (!email || !password) {
return;
}
localStorage.removeItem("accesstoken");
try {
setState({ ...state, loading: true });
localStorage.setItem("appLogo", appInfo.applogo);
@@ -132,6 +128,14 @@ function Login() {
showToast("danger", "Invalid username/password or region");
}
};
const handleLoginBtn = async (event) => {
event.preventDefault();
if (!emailRegex.test(state.email)) {
alert("Please enter a valid email address.");
return;
}
await handleLogin();
};
const setThirdpartyLoader = (value) => {
setState({ ...state, thirdpartyLoader: value });
@@ -275,7 +279,6 @@ function Login() {
const userInformation = JSON.parse(
localStorage.getItem("UserInformation")
);
// console.log("payload ", payload);
if (payload && payload.sessionToken) {
const params = {
userDetails: {
@@ -289,7 +292,6 @@ function Login() {
}
};
const userSignUp = await Parse.Cloud.run("usersignup", params);
// console.log("userSignUp ", userSignUp);
if (userSignUp && userSignUp.sessionToken) {
const LocalUserDetails = {
name: userInformation.name,
@@ -430,7 +432,7 @@ function Login() {
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-2">
<div>
<form onSubmit={handleSubmit} aria-label="Login Form">
<form onSubmit={handleLoginBtn} aria-label="Login Form">
<h1 className="text-[30px] mt-6">{t("welcome")}</h1>
<fieldset>
<legend className="text-[12px] text-[#878787]">
@@ -488,15 +490,14 @@ function Login() {
)}
</span>
</div>
<div className="relative mt-1">
<NavLink
to="/forgetpassword"
className="text-[13px] op-link op-link-primary underline-offset-1 focus:outline-none ml-1"
>
{t("forgot-password")}
</NavLink>
</div>
<div className="relative mt-1">
<NavLink
to="/forgetpassword"
className="text-[13px] op-link op-link-primary underline-offset-1 focus:outline-none ml-1"
>
{t("forgot-password")}
</NavLink>
</div>
</div>
</fieldset>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-center text-xs font-bold mt-2">
+57 -56
View File
@@ -324,63 +324,65 @@ const ManageSign = () => {
hidden
/>
<div className="relative">
<div>
{image ? (
<div className="signatureCanvas relative border-[2px] border-[#888] rounded-box overflow-hidden">
<img
alt="signature"
src={image}
className="w-full h-full object-contain"
/>
</div>
) : (
<SignatureCanvas
ref={canvasRef}
penColor={penColor}
canvasProps={{
width: "456px",
height: "180px",
className:
"signatureCanvas border-[2px] border-[#888] rounded-box"
}}
// backgroundColor="rgb(255, 255, 255)"
onEnd={() =>
handleSignatureChange(canvasRef.current.toDataURL())
}
dotSize={1}
{isLoader && (
<div className="absolute bg-black bg-opacity-30 z-50 w-full h-full flex justify-center items-center">
<Loader />
</div>
)}
{image ? (
<div className="signatureCanvas relative border-[2px] border-[#888] rounded-box overflow-hidden">
<img
alt="signature"
src={image}
className="w-full h-full object-contain"
/>
)}
<div className="penContainerDefault flex flex-row justify-between">
<div>
{!image && (
<div className="flex flex-row gap-1.5 m-[5px]">
{allColor.map((data, key) => {
return (
<i
key={key}
onClick={() => setPenColor(allColor[key])}
className={`border-b-[2px] ${key === 0 && penColor === "blue" ? "border-blue-600" : key === 1 && penColor === "red" ? "border-red-500" : key === 2 && penColor === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
></i>
);
})}
</div>
)}
</div>
) : (
<SignatureCanvas
ref={canvasRef}
penColor={penColor}
canvasProps={{
width: "456px",
height: "180px",
className:
"signatureCanvas border-[2px] border-[#888] rounded-box"
}}
onEnd={() =>
handleSignatureChange(canvasRef.current.toDataURL())
}
dotSize={1}
/>
)}
<div className="penContainerDefault flex flex-row justify-between">
<div>
{!image && (
<div className="flex flex-row gap-1.5 m-[5px]">
{allColor.map((data, key) => {
return (
<i
key={key}
onClick={() => setPenColor(allColor[key])}
className={`border-b-[2px] ${key === 0 && penColor === "blue" ? "border-blue-600" : key === 1 && penColor === "red" ? "border-red-500" : key === 2 && penColor === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
></i>
);
})}
</div>
)}
</div>
<div className="flex flex-row gap-2 text-sm md:text-base mr-1">
<div
type="button"
className="op-link"
onClick={() => handleUploadBtn()}
>
{t("upload")}
</div>
<div className="flex flex-row gap-2 text-sm md:text-base mr-1">
<div
type="button"
className="op-link"
onClick={() => handleUploadBtn()}
>
{t("upload")}
</div>
<div
type="button"
className="op-link"
onClick={() => handleClear()}
>
{t("clear")}
</div>
<div
type="button"
className="op-link"
onClick={() => handleClear()}
>
{t("clear")}
</div>
</div>
</div>
@@ -420,7 +422,6 @@ const ManageSign = () => {
canvasProps={{
className: "intialSignature rounded-box"
}}
// backgroundColor="rgb(255, 255, 255)"
onEnd={() => handleInitialsChange()}
dotSize={1}
/>
+18 -10
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useState, useRef } from "react";
import "../styles/opensigndrive.css";
import {
iconColor,
getThemeIconColor,
} from "../constant/const";
import {
getDrive
@@ -11,7 +11,7 @@ import Title from "../components/Title";
import Parse from "parse";
import ModalUi from "../primitives/ModalUi";
import TourContentWithBtn from "../primitives/TourContentWithBtn";
import Tour from "reactour";
import Tour from "../primitives/Tour";
import axios from "axios";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
@@ -539,6 +539,12 @@ function Opensigndrive() {
}
}, 300);
};
const handleSearchPaste = (e) => {
setTimeout(() => {
handleSearchChange({ target: { value: e.target.value } });
}, 0);
};
// Cleanup on unmount
useEffect(() => {
return () => {
@@ -657,7 +663,8 @@ function Opensigndrive() {
type="search"
value={searchTerm}
onChange={handleSearchChange}
placeholder="Search documents"
placeholder={t("search-documents")}
onPaste={handleSearchPaste}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-64 text-xs"
/>
</div>
@@ -668,7 +675,7 @@ function Opensigndrive() {
onClick={() => setMobileSearchOpen((open) => !open)}
>
<i
style={{ color: `${iconColor}` }}
style={{ color: `${getThemeIconColor()}` }}
className="fa-solid fa-magnifying-glass"
></i>
</button>
@@ -681,7 +688,7 @@ function Opensigndrive() {
<i
className="fa-light fa-plus-square text-[24px]"
aria-hidden="true"
style={{ color: `${iconColor}` }}
style={{ color: `${getThemeIconColor()}` }}
></i>
</div>
<div
@@ -730,9 +737,9 @@ function Opensigndrive() {
<i
className="fa-light fa-sort-amount-asc mr-[5px] text-[19px]"
aria-hidden="true"
style={{ color: `${iconColor}` }}
style={{ color: `${getThemeIconColor()}` }}
></i>
<span style={{ fontSize: "15px", color: `${iconColor}` }}>
<span style={{ fontSize: "15px", color: `${getThemeIconColor()}` }}>
{selectedSort}
</span>
</div>
@@ -793,7 +800,7 @@ function Opensigndrive() {
>
<i
className={`${isList ? "fa-light fa-th-large" : "fa-light fa-list"} text-[20px]`}
style={{ color: `${iconColor}` }}
style={{ color: `${getThemeIconColor()}` }}
aria-hidden="true"
></i>
</div>
@@ -809,7 +816,7 @@ function Opensigndrive() {
<i
className="fa-light fa-ellipsis-vertical fa-lg"
aria-hidden="true"
style={{ color: `${iconColor}` }}
style={{ color: `${getThemeIconColor()}` }}
></i>
</div>
<div
@@ -854,7 +861,8 @@ function Opensigndrive() {
type="search"
value={searchTerm}
onChange={handleSearchChange}
placeholder="Search documents"
placeholder={t("search-documents")}
onPaste={handleSearchPaste}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
+141 -444
View File
@@ -1,25 +1,28 @@
import React, { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect } from "react";
import { PDFDocument } from "pdf-lib";
import "../styles/signature.css";
import Parse from "parse";
import axios from "axios";
import { DndProvider, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import SignPad from "../components/pdf/SignPad";
import { useDispatch, useSelector } from "react-redux";
import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import Tour from "reactour";
import Tour from "../primitives/Tour";
import Confetti from "react-confetti";
import moment from "moment";
import {
setSaveSignCheckbox,
setMyInitial,
setDefaultSignImg,
resetWidgetState
} from "../redux/reducers/widgetSlice.js";
import {
contractDocument,
multiSignEmbed,
embedDocId,
pdfNewWidthFun,
signPdfFun,
onSaveSign,
onSaveImage,
addDefaultSignatureImg,
radioButtonWidget,
replaceMailVaribles,
convertPdfArrayBuffer,
contractUsers,
@@ -40,16 +43,16 @@ import {
defaultWidthHeight,
addWidgetOptions,
textWidget,
compressedFileSize,
mailTemplate,
updateDateWidgetsRes,
widgetDataValue
widgetDataValue,
getOriginalWH,
handleCheckResponse,
} from "../constant/Utils";
import Header from "../components/pdf/PdfHeader";
import RenderPdf from "../components/pdf/RenderPdf";
import Title from "../components/Title";
import DefaultSignature from "../components/pdf/DefaultSignature";
import { useSelector } from "react-redux";
import SignerListComponent from "../components/pdf/SignerListComponent";
import PdfZoom from "../components/pdf/PdfZoom";
import { useTranslation } from "react-i18next";
@@ -65,29 +68,28 @@ import AgreementSign from "../components/pdf/AgreementSign";
import WidgetComponent from "../components/pdf/WidgetComponent";
import PlaceholderCopy from "../components/pdf/PlaceholderCopy";
import TextFontSetting from "../components/pdf/TextFontSetting";
import WidgetsValueModal from "../components/pdf/WidgetsValueModal.jsx";
function PdfRequestFiles(
) {
const { t } = useTranslation();
const dispatch = useDispatch();
const isShowModal = useSelector((state) => state.widget.isShowModal);
const saveSignCheckbox = useSelector(
(state) => state.widget.saveSignCheckbox
);
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
const myInitial = useSelector((state) => state.widget.myInitial);
const appName =
"OpenSign™";
const [pdfDetails, setPdfDetails] = useState([]);
const [signedSigners, setSignedSigners] = useState([]);
const [unsignedSigners, setUnSignedSigners] = useState([]);
const [isSignPad, setIsSignPad] = useState(false);
const [pdfUrl, setPdfUrl] = useState();
const [allPages, setAllPages] = useState(null);
const numPages = 1;
const [pageNumber, setPageNumber] = useState(1);
const [image, setImage] = useState(null);
const [isImageSelect, setIsImageSelect] = useState(false);
const [signature, setSignature] = useState();
const [isStamp, setIsStamp] = useState(false);
const [signKey, setSignKey] = useState();
const [imgWH, setImgWH] = useState({});
const imageRef = useRef(null);
const [handleError, setHandleError] = useState();
const [selectWidgetId, setSelectWidgetId] = useState("");
const [isCelebration, setIsCelebration] = useState(false);
const [requestSignTour, setRequestSignTour] = useState(true);
const [tourStatus, setTourStatus] = useState([]);
@@ -95,7 +97,6 @@ function PdfRequestFiles(
isLoad: true,
message: t("loading-mssg")
});
const [defaultSignImg, setDefaultSignImg] = useState();
const [isDocId, setIsDocId] = useState(false);
const [pdfNewWidth, setPdfNewWidth] = useState();
const [pdfOriginalWH, setPdfOriginalWH] = useState([]);
@@ -111,6 +112,9 @@ function PdfRequestFiles(
const [signerUserId, setSignerUserId] = useState();
const [isDontShow, setIsDontShow] = useState(false);
const [isDownloading, setIsDownloading] = useState("");
// tempSignerId is used to temporarily store the currently selected signer's unique ID, When editing a text widget, it automatically attaches a prefill user, and since prefill users are not shown in the signer list, the selected signer from before editing would be lost. To handle this, we store the currently selected signer's unique ID in tempSignerId before entering the text widget edit mode. Once the text widget settings are completed,
// we restore the original selected signer by setting tempSignerId back to uniqueId.This ensures that the correct signer remains selected and visible in the UI even after interacting with a prefill-only widget like the text widget.
const [tempSignerId, setTempSignerId] = useState("");
const [defaultSignAlert, setDefaultSignAlert] = useState({
isShow: false,
alertMessage: ""
@@ -119,14 +123,11 @@ function PdfRequestFiles(
isCertificate: false,
isModal: false
});
const [myInitial, setMyInitial] = useState("");
const [isInitial, setIsInitial] = useState(false);
const [pdfLoad, setPdfLoad] = useState(false);
const [isSigned, setIsSigned] = useState(false);
const [isExpired, setIsExpired] = useState(false);
const [alreadySign, setAlreadySign] = useState(false);
const [containerWH, setContainerWH] = useState({});
const [validateAlert, setValidateAlert] = useState(false);
const [widgetsTour, setWidgetsTour] = useState(false);
const [minRequiredCount, setminRequiredCount] = useState();
const [sendInOrder, setSendInOrder] = useState(false);
@@ -148,20 +149,14 @@ function PdfRequestFiles(
const [isredirectCanceled, setIsredirectCanceled] = useState(true);
const [isDragging, setIsDragging] = useState(false);
const [dragKey, setDragKey] = useState();
const [isAutoSign, setIsAutoSign] = useState(false);
const [signBtnPosition, setSignBtnPosition] = useState([]);
const [xySignature, setXYSignature] = useState({});
const [zIndex, setZIndex] = useState(1);
const [fontSize, setFontSize] = useState();
const [fontColor, setFontColor] = useState();
const [widgetType, setWidgetType] = useState("");
const [isTextSetting, setIsTextSetting] = useState(false);
const [isPageCopy, setIsPageCopy] = useState(false);
const [assignedWidgetId, setAssignedWidgetId] = useState([]);
const [saveSignCheckbox, setSaveSignCheckbox] = useState({
isVisible: false,
signId: ""
});
const [showSignPagenumber, setShowSignPagenumber] = useState([]);
const [owner, setOwner] = useState({});
const [, drop] = useDrop({
@@ -195,6 +190,7 @@ function PdfRequestFiles(
}
let getDocumentId = getDocId || documentId;
useEffect(() => {
dispatch(resetWidgetState([]));
if (getDocumentId) {
setDocumentId(getDocumentId);
getDocumentDetails(getDocumentId);
@@ -264,7 +260,6 @@ function PdfRequestFiles(
docId,
isNextUser,
isSuccessPage = false,
publiccontactId
) => {
try {
let isUpdatedSubscribe;
@@ -274,12 +269,23 @@ function PdfRequestFiles(
const jsonSender = JSON.parse(senderUser);
const contactId = jsonSender?.objectId
? ""
: contactBookId || publiccontactId || signerObjectId || "";
: contactBookId ||
signerObjectId ||
"";
const tenantSignTypes = await fetchTenantDetails(contactId);
// `currUserId` will be contactId or extUserId
let currUserId;
//getting document details
const documentData = await contractDocument(docId);
// Filter out 'prefill' roles from the Placeholder array
const filteredPlaceholder = documentData[0].Placeholders.filter(
(data) => data.Role !== "prefill"
);
// Reassign the updated Placeholder back to the documentData array
documentData[0] = {
...documentData[0],
Placeholders: filteredPlaceholder
};
if (documentData && documentData.length > 0) {
const userSignatureType =
documentData[0]?.ExtUserPtr?.SignatureType || signatureTypes;
@@ -322,7 +328,9 @@ function PdfRequestFiles(
currUserId = getCurrentSigner?.objectId
? getCurrentSigner.objectId
: contactBookId || publiccontactId || signerObjectId || ""; //signerObjectId is contactBookId refer from public template flow
: contactBookId ||
signerObjectId ||
""; //signerObjectId is contactBookId refer from public template flow
if (currUserId) {
setSignerObjectId(currUserId);
}
@@ -477,21 +485,28 @@ function PdfRequestFiles(
} else {
setRequestSignTour(false);
}
setSaveSignCheckbox((prev) => ({ ...prev, isVisible: true }));
dispatch(
setSaveSignCheckbox({
...saveSignCheckbox,
isVisible: true
})
);
//function to get default signatur of current user from `contracts_Signature` class
const defaultSignRes = await getDefaultSignature(
jsonSender?.objectId
);
if (defaultSignRes?.status === "success") {
setSaveSignCheckbox((prev) => ({
...prev,
isVisible: true,
signId: defaultSignRes?.res?.id
}));
dispatch(
setSaveSignCheckbox({
...saveSignCheckbox,
isVisible: true,
signId: defaultSignRes?.res?.id
})
);
const sign = defaultSignRes?.res?.defaultSignature || "";
const initials = defaultSignRes?.res?.defaultInitial || "";
setDefaultSignImg(sign);
setMyInitial(initials);
dispatch(setDefaultSignImg(sign));
dispatch(setMyInitial(initials));
}
} else if (res?.length === 0) {
const res = await contactBook(currUserId);
@@ -581,9 +596,12 @@ function PdfRequestFiles(
}
};
//function for embed signature or image url in pdf
async function embedWidgetsData(publiccontactId, publicDocId) {
let contactId = publiccontactId || signerObjectId;
let docId = publicDocId || documentId;
async function embedWidgetsData(
) {
let contactId =
signerObjectId;
let docId =
documentId;
const addExtraDays = pdfDetails[0]?.TimeToCompleteDays
? pdfDetails[0].TimeToCompleteDays
: 15;
@@ -615,6 +633,7 @@ function PdfRequestFiles(
} catch (err) {
console.log("err in get email verification ", err);
setHandleError(t("something-went-wrong-mssg"));
setIsUiLoading(false);
}
}
//check if isEmailVerified then go on next step
@@ -624,153 +643,13 @@ function PdfRequestFiles(
(data) => data.signerObjId === signerObjectId
);
if (checkUser && checkUser.length > 0) {
let checkboxExist,
requiredRadio,
showAlert = false,
widgetKey,
radioExist,
requiredCheckbox,
TourPageNumber; // `pageNumber` is used to check on which page user did not fill widget's data then change current pageNumber and show tour message on that page
for (let i = 0; i < checkUser[0].placeHolder.length; i++) {
for (let j = 0; j < checkUser[0].placeHolder[i].pos.length; j++) {
//get current page
const updatePage = checkUser[0].placeHolder[i]?.pageNumber;
//checking checbox type widget
checkboxExist =
checkUser[0].placeHolder[i].pos[j].type === "checkbox";
//checking radio button type widget
radioExist =
checkUser[0].placeHolder[i].pos[j].type === radioButtonWidget;
//condition to check checkbox widget exist or not
if (checkboxExist) {
//get all required type checkbox
requiredCheckbox = checkUser[0].placeHolder[i].pos.filter(
(position) =>
!position.options?.isReadOnly &&
position.type === "checkbox"
);
//if required type checkbox data exit then check user checked all checkbox or some checkbox remain to check
//also validate to minimum and maximum required checkbox
if (requiredCheckbox && requiredCheckbox.length > 0) {
for (let i = 0; i < requiredCheckbox.length; i++) {
//get minimum required count if exit
const minCount =
requiredCheckbox[i].options?.validation?.minRequiredCount;
const parseMin = minCount && parseInt(minCount);
//get maximum required count if exit
const maxCount =
requiredCheckbox[i].options?.validation?.maxRequiredCount;
const parseMax = maxCount && parseInt(maxCount);
//in `response` variable is used to get how many checkbox checked by user
const response =
requiredCheckbox[i].options?.response?.length;
//in `defaultValue` variable is used to get how many checkbox checked by default
const defaultValue =
requiredCheckbox[i].options?.defaultValue?.length;
//condition to check parseMin and parseMax greater than 0 then consider it as a required check box
if (
parseMin > 0 &&
parseMax > 0 &&
!response &&
!defaultValue &&
!showAlert
) {
showAlert = true;
widgetKey = requiredCheckbox[i].key;
TourPageNumber = updatePage;
setminRequiredCount(parseMin);
}
//else condition to validate minimum required checkbox
else if (
parseMin > 0 &&
(parseMin > response || !response)
) {
if (!showAlert) {
showAlert = true;
widgetKey = requiredCheckbox[i].key;
TourPageNumber = updatePage;
setminRequiredCount(parseMin);
}
}
}
}
}
//condition to check radio widget exist or not
else if (radioExist) {
//get all required type radio button
requiredRadio = checkUser[0].placeHolder[i].pos.filter(
(position) =>
!position.options?.isReadOnly &&
position.type === radioButtonWidget
);
//if required type radio data exit then check user checked all radio button or some radio remain to check
if (requiredRadio && requiredRadio?.length > 0) {
let checkSigned;
for (let i = 0; i < requiredRadio?.length; i++) {
checkSigned = requiredRadio[i]?.options?.response;
if (!checkSigned) {
let checkDefaultSigned =
requiredRadio[i]?.options?.defaultValue;
if (!checkDefaultSigned && !showAlert) {
showAlert = true;
widgetKey = requiredRadio[i].key;
TourPageNumber = updatePage;
setminRequiredCount(null);
}
}
}
}
}
//else condition to check all type widget data fill or not except checkbox and radio button
else {
//get all required type widgets except checkbox and radio
const requiredWidgets = checkUser[0].placeHolder[i].pos.filter(
(position) =>
position.options?.status === "required" &&
position.type !== radioButtonWidget &&
position.type !== "checkbox"
);
if (requiredWidgets && requiredWidgets?.length > 0) {
let checkSigned;
for (let i = 0; i < requiredWidgets?.length; i++) {
checkSigned = requiredWidgets[i]?.options?.response;
if (!checkSigned) {
const checkSignUrl = requiredWidgets[i]?.pos?.SignUrl;
if (!checkSignUrl) {
let checkDefaultSigned =
requiredWidgets[i]?.options?.defaultValue;
if (!checkDefaultSigned && !showAlert) {
showAlert = true;
widgetKey = requiredWidgets[i].key;
TourPageNumber = updatePage;
setminRequiredCount(null);
}
}
}
}
}
}
}
//when showAlert is true then break the loop and show alert to fill required data in widgets
if (showAlert) {
break;
}
}
if (checkboxExist && requiredCheckbox && showAlert) {
setUnSignedWidgetId(widgetKey);
setPageNumber(TourPageNumber);
setWidgetsTour(true);
} else if (radioExist && showAlert) {
setUnSignedWidgetId(widgetKey);
setPageNumber(TourPageNumber);
setWidgetsTour(true);
} else if (showAlert) {
setUnSignedWidgetId(widgetKey);
setPageNumber(TourPageNumber);
const status = handleCheckResponse(checkUser,setminRequiredCount)
if (status?.showAlert) {
setUnSignedWidgetId(status?.widgetKey);
setPageNumber(status?.tourPageNumber);
setWidgetsTour(true);
setIsUiLoading(false);
} else {
setIsUiLoading(true);
// `widgets` is Used to return widgets details with page number of current user
const widgets = checkUser?.[0]?.placeHolder;
let pdfArrBuffer;
@@ -808,17 +687,18 @@ function PdfRequestFiles(
//embed document's object id to all pages in pdf document
if (!HeaderDocId) {
if (!isDocId) {
await embedDocId(pdfDoc, docId, allPages);
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
await embedDocId(pdfOriginalWH, pdfDoc, docId);
}
}
//embed multi signature in pdf
//embed all widgets in document
const pdfBytes = await multiSignEmbed(
pdfOriginalWH,
widgets,
pdfDoc,
isSignYourSelfFlow,
scale
);
// console.log("pdfte", pdfBytes);
//get ExistUserPtr object id of user class to get tenantDetails
if (!pdfBytes?.error) {
const objectId = pdfDetails?.[0]?.ExtUserPtr?.UserId?.objectId;
@@ -844,17 +724,23 @@ function PdfRequestFiles(
isSuccessRoute,
contactId
);
const index = pdfDetails?.[0]?.Signers.findIndex(
(x) => x.objectId === signerObjectId
);
const index =
updatedDoc.updatedPdfDetails?.[0]?.Signers.findIndex(
(x) => x.objectId === contactId
);
const newIndex = index + 1;
const usermail = {
Email: pdfDetails?.[0]?.Placeholders[newIndex]?.email || ""
Email:
updatedDoc.updatedPdfDetails?.[0]?.Placeholders[newIndex]
?.email || ""
};
const user = usermail?.Email
? usermail
: pdfDetails?.[0]?.Signers[newIndex];
if (sendmail !== "false" && sendInOrder) {
: updatedDoc.updatedPdfDetails?.[0]?.Signers[newIndex];
if (
sendmail !== "false" &&
sendInOrder
) {
const requestBody =
updatedDoc.updatedPdfDetails?.[0]?.RequestBody;
const requestSubject =
@@ -908,7 +794,7 @@ function PdfRequestFiles(
const htmlReqBody =
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>" +
replacedRequestBody +
"</body> </html>";
"</body></html>";
const variables = {
document_title: documentName,
@@ -921,7 +807,7 @@ function PdfRequestFiles(
receiver_phone: user?.Phone || "",
expiry_date: localExpireDate,
company_name: orgName,
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>`
signing_url: signPdf
};
replaceVar = replaceMailVaribles(
requestSubject,
@@ -936,7 +822,7 @@ function PdfRequestFiles(
title: documentName,
organization: orgName,
localExpireDate: localExpireDate,
sigingUrl: signPdf
signingUrl: signPdf
};
let params = {
replyto: senderEmail || "",
@@ -1004,12 +890,12 @@ function PdfRequestFiles(
}
}
}
setIsSignPad(false);
} else {
setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
});
setIsUiLoading(false);
}
} catch (err) {
console.log("err in embedsign", err);
@@ -1021,33 +907,11 @@ function PdfRequestFiles(
}
}
}
const handleSignPdf = async () => {
if (props.templateId) {
const params = {
...contact,
templateid: pdfDetails[0]?.objectId,
role: pdfDetails[0]?.PublicRole[0]
};
const linkRes = await axios.post(
`${localStorage.getItem(
"baseUrl"
)}/functions/publicuserlinkcontacttodoc`,
params,
{
headers: {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId")
}
}
);
const contactId = linkRes.data.result?.contactId;
const docId = linkRes.data?.result?.docId;
await embedWidgetsData(contactId, docId);
} else {
setIsUiLoading(true);
await embedWidgetsData();
}
};
//function for save x and y position and show signature tab on that position
const handleTabDrag = (key) => {
setDragKey(key);
@@ -1068,15 +932,15 @@ function PdfRequestFiles(
let filterSignerPos = [];
if (signerObjId) {
//get current signerObjId placeholder details
filterSignerPos = updateSignPos.filter(
filterSignerPos = updateSignPos?.filter(
(data) => data.Id === signerObjId
);
}
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPlaceHolder = filterSignerPos[0]?.placeHolder;
//get position of current pagenumber
const getPageNumer = getPlaceHolder.filter(
const getPageNumer = getPlaceHolder?.filter(
(data) => data.pageNumber === pageNumber
);
if (getPageNumer.length > 0) {
@@ -1120,9 +984,9 @@ function PdfRequestFiles(
setIsTextSetting(value);
};
const handleSaveFontSize = () => {
const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId);
const filterSignerPos = signerPos?.filter((data) => data.Id === uniqueId);
if (filterSignerPos) {
const placehoder = filterSignerPos[0].placeHolder;
const placehoder = filterSignerPos[0]?.placeHolder;
const getPageNumer = placehoder.filter(
(data) => data.pageNumber === pageNumber
);
@@ -1130,7 +994,7 @@ function PdfRequestFiles(
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const updateSignPos = getPosData.map((position) => {
if (position.key === signKey) {
if (position.key === currWidgetsDetails?.key) {
return {
...position,
options: {
@@ -1161,7 +1025,6 @@ function PdfRequestFiles(
return obj;
});
setSignerPos(newUpdateSigner);
setFontSize();
setFontColor();
handleTextSettingModal(false);
@@ -1185,14 +1048,7 @@ function PdfRequestFiles(
];
//function for get pdf page details
const pageDetails = async (pdf) => {
let pdfWHObj = [];
const totalPages = pdf.numPages; // Get the total number of pages
for (let index = 0; index < totalPages; index++) {
const getPage = await pdf.getPage(index + 1);
const scale = 1;
const { width, height } = getPage.getViewport({ scale });
pdfWHObj.push({ pageNumber: index + 1, width, height });
}
const pdfWHObj = await getOriginalWH(pdf);
setPdfOriginalWH(pdfWHObj);
setPdfLoad(true);
};
@@ -1200,127 +1056,6 @@ function PdfRequestFiles(
function changePage(offset) {
setPageNumber((prevPageNumber) => prevPageNumber + offset);
}
//function for image upload or update
const onImageChange = (event) => {
if (event.target.files && event.target.files[0]) {
const file = event.target.files[0];
compressedFileSize(file, setImgWH, setImage);
}
};
//function for upload stamp image
const saveImage = () => {
const widgetsType = currWidgetsDetails?.type;
//get current signers placeholder position data
const currentSigner = signerPos.filter(
(data) => data.signerObjId === signerObjectId
);
//get current pagenumber placeholder index
const getIndex = currentSigner[0].placeHolder.findIndex((object) => {
return object.pageNumber === pageNumber;
});
//get current signer placeholder position data
const placeholderPosition = currentSigner[0].placeHolder;
//`isApplyAll` is used when user edit stamp then updated signature apply all existing drawn signatures
const isApplyAll = true;
//function of save image and get updated position with image url
const getUpdatePosition = onSaveImage(
placeholderPosition,
getIndex,
signKey,
imgWH,
image,
isAutoSign,
widgetsType,
isApplyAll
);
//replace updated placeholder position with old data
placeholderPosition.splice(
0,
placeholderPosition.length,
...getUpdatePosition
);
//get current signers placeholder position data index number in array
const indexofSigner = signerPos.findIndex((object) => {
return object.signerObjId === signerObjectId;
});
//update current signers data with new placeholder position array data
setSignerPos((prevState) => {
const newState = [...prevState]; // Create a copy of the state
newState.splice(indexofSigner, 1, ...currentSigner); // Modify the copy
return newState; // Update the state with the modified copy
});
setIsAutoSign(false);
};
//function for save button to save signature or image url
const saveSign = (type, isDefaultSign, width, height, typedSignature) => {
const widgetsType = currWidgetsDetails?.type;
const isTypeText = width && height ? true : false;
const signatureImg = isDefaultSign
? isDefaultSign === "initials"
? myInitial
: defaultSignImg
: signature;
let imgWH = { width: width ? width : "", height: height ? height : "" };
setIsSignPad(false);
setIsImageSelect(false);
setImage();
//get current signers placeholder position data
const currentSigner = signerPos.filter(
(data) => data.signerObjId === signerObjectId
);
//get current pagenumber placeholder index
const getIndex = currentSigner[0].placeHolder.findIndex((object) => {
return object.pageNumber === pageNumber;
});
//set default signature image width and height
if (isDefaultSign) {
const img = new Image();
img.src = defaultSignImg;
if (img.complete) {
imgWH = { width: img.width, height: img.height };
}
}
//get current signer placeholder position data
const placeholderPosition = currentSigner[0].placeHolder;
//`isApplyAll` is used when user edit signature/initial then updated signature apply all existing drawn signatures
const isApplyAll = true;
//function of save signature image and get updated position with signature image url
const getUpdatePosition = onSaveSign(
type,
placeholderPosition,
getIndex,
signKey,
signatureImg,
imgWH,
isDefaultSign,
isTypeText,
typedSignature,
isAutoSign,
widgetsType,
isApplyAll
);
const updateSignerData = currentSigner.map((obj) => {
if (obj.signerObjId === signerObjectId) {
return { ...obj, placeHolder: getUpdatePosition };
}
return obj;
});
const index = signerPos.findIndex(
(data) => data.signerObjId === signerObjectId
);
setSignerPos((prevState) => {
const newState = [...prevState];
newState.splice(index, 1, ...updateSignerData);
return newState;
});
setIsAutoSign(false);
};
//function for set decline true on press decline button
const declineDoc = async (reason) => {
const senderUser = localStorage.getItem(
@@ -1367,7 +1102,7 @@ function PdfRequestFiles(
const addDefaultSignature = () => {
const type = defaultSignAlert?.type;
//get current signers placeholder position data
const currentSignerPosition = signerPos.filter(
const currentSignerPosition = signerPos?.filter(
(data) => data.signerObjId === signerObjectId
);
const defaultSign = type === "signature" ? defaultSignImg : myInitial;
@@ -1392,7 +1127,8 @@ function PdfRequestFiles(
setRequestSignTour(true);
if (isDontShow) {
const isEnableOTP = pdfDetails?.[0]?.IsEnableOTP || false;
if (!isEnableOTP) {
const sessionToken = localStorage.getItem("accesstoken");
if (!isEnableOTP && !sessionToken) {
try {
await axios.post(
`${localStorage.getItem("baseUrl")}functions/updatecontacttour`,
@@ -1692,7 +1428,8 @@ function PdfRequestFiles(
pageNumber,
containerWH
);
let dropData = [];
let dropData = [],
dropObj;
let placeHolder;
const dragTypeValue = item?.text ? item.text : monitor.type;
const widgetWidth =
@@ -1704,11 +1441,15 @@ function PdfRequestFiles(
const widgetValue = widgetDataValue(dragTypeValue, parseUser);
//adding and updating drop position in array when user drop signature button in div
if (item === "onclick") {
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
const divWidth = divRef.current.getBoundingClientRect().width;
const divHeight = divRef.current.getBoundingClientRect().height;
// `getBoundingClientRect()` is used to get accurate measurement height of the div
const dropObj = {
// Compute the pixelspace center within the PDF viewport:
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
const xPosition_Final = centerX_Pixels / (containerScale * scale);
dropObj = {
//onclick put placeholder center on pdf
xPosition: widgetWidth / 4 + containerWH.width / 2,
xPosition: xPosition_Final,
yPosition: widgetHeight + divHeight / 2,
isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
@@ -1734,7 +1475,7 @@ function PdfRequestFiles(
const y = offset.y - containerRect.top;
const getXPosition = signBtnPosition[0] ? x - signBtnPosition[0].xPos : x;
const getYPosition = signBtnPosition[0] ? y - signBtnPosition[0].yPos : y;
const dropObj = {
dropObj = {
xPosition: getXPosition / (containerScale * scale),
yPosition: getYPosition / (containerScale * scale),
isStamp:
@@ -1750,14 +1491,13 @@ function PdfRequestFiles(
dropData.push(dropObj);
placeHolder = { pageNumber: pageNumber, pos: dropData };
}
setSelectWidgetId(key);
if (uniqueId) {
let filterSignerPos, currentPagePosition;
filterSignerPos = signerPos.find((data) => data.Id === uniqueId);
filterSignerPos = signerPos?.find((data) => data.Id === uniqueId);
const getPlaceHolder = filterSignerPos?.placeHolder;
if (getPlaceHolder) {
//checking exist placeholder on same page
currentPagePosition = getPlaceHolder.find(
currentPagePosition = getPlaceHolder?.find(
(data) => data.pageNumber === pageNumber
);
}
@@ -1785,12 +1525,6 @@ function PdfRequestFiles(
);
setSignerPos(updatesignerPos);
}
// if (dragTypeValue === "dropdown") {
// setShowDropdown(true);
// } else if (dragTypeValue === "checkbox") {
// setIsCheckbox(true);
// } else
if (
[textWidget, "name", "company", "job title", "email"].includes(
dragTypeValue
@@ -1799,16 +1533,14 @@ function PdfRequestFiles(
setFontSize(12);
setFontColor("black");
}
setWidgetType(dragTypeValue);
setSignKey(key);
setCurrWidgetsDetails({});
setCurrWidgetsDetails(dropObj);
}
};
//function for delete signature block
const handleDeleteSign = (key, Id) => {
const updateData = [];
const filterSignerPos = signerPos.filter((data) => data.Id === Id);
const filterSignerPos = signerPos?.filter((data) => data.Id === Id);
if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPageNumer = getPlaceHolder.filter(
@@ -1836,7 +1568,7 @@ function PdfRequestFiles(
});
setSignerPos(newUpdateSigner);
} else {
const getRemainPage = filterSignerPos[0].placeHolder.filter(
const getRemainPage = filterSignerPos[0]?.placeHolder?.filter(
(data) => data.pageNumber !== pageNumber
);
//condition to check placeholder length is greater than 1 do not need to remove whole placeholder
@@ -1849,11 +1581,11 @@ function PdfRequestFiles(
return obj;
});
let signerupdate = [];
signerupdate = signerPos.filter((data) => data.Id !== Id);
signerupdate = signerPos?.filter((data) => data.Id !== Id);
signerupdate.push(newUpdatePos[0]);
setSignerPos(signerupdate);
} else {
const updatedData = signerPos.map((item) => {
const updatedData = signerPos?.map((item) => {
if (item.Id === Id) {
// Create a copy of the item object and delete the placeHolder field
const updatedItem = { ...item };
@@ -1871,7 +1603,7 @@ function PdfRequestFiles(
//function to get first widget and page number to assign currect signer and tour message
const showFirstWidget = () => {
if (!requestSignTour) {
const getCurrentUserPlaceholder = signerPos.find(
const getCurrentUserPlaceholder = signerPos?.find(
(x) => x.Id === uniqueId
);
const placeholder = getCurrentUserPlaceholder.placeHolder;
@@ -1963,6 +1695,7 @@ function PdfRequestFiles(
{!requestSignTour &&
isAgree &&
signerObjectId &&
!alreadySign &&
requestSignTourFunction()}
<Tour
showNumber={false}
@@ -2074,10 +1807,12 @@ function PdfRequestFiles(
setXyPosition={setSignerPos}
allPages={allPages}
pageNumber={pageNumber}
signKey={signKey}
signKey={currWidgetsDetails?.key}
Id={uniqueId}
widgetType={widgetType}
widgetType={currWidgetsDetails?.type}
setUniqueId={setUniqueId}
tempSignerId={tempSignerId}
setTempSignerId={setTempSignerId}
/>
<div className=" w-full md:w-[95%] ">
{/* this modal is used show this document is already sign */}
@@ -2214,37 +1949,6 @@ function PdfRequestFiles(
)}
</div>
</ModalUi>
{/* this component is used for signature pad modal */}
{currentSigner && isSignPad && (
<SignPad
saveSignCheckbox={saveSignCheckbox}
setSaveSignCheckbox={setSaveSignCheckbox}
signatureTypes={signatureType}
isSignPad={isSignPad}
isStamp={isStamp}
setIsImageSelect={setIsImageSelect}
setIsSignPad={setIsSignPad}
setImage={setImage}
isImageSelect={isImageSelect}
imageRef={imageRef}
onImageChange={onImageChange}
setSignature={setSignature}
image={image}
onSaveImage={saveImage}
onSaveSign={saveSign}
defaultSign={defaultSignImg}
myInitial={myInitial}
setDefaultSignImg={setDefaultSignImg}
setMyInitial={setMyInitial}
isInitial={isInitial}
setIsInitial={setIsInitial}
setIsStamp={setIsStamp}
currWidgetsDetails={currWidgetsDetails}
setCurrWidgetsDetails={setCurrWidgetsDetails}
setIsAutoSign={setIsAutoSign}
isAutoSign={isAutoSign}
/>
)}
{/* pdf header which contain funish back button */}
<Header
isPdfRequestFiles={
@@ -2286,9 +1990,6 @@ function PdfRequestFiles(
pageNumber={pageNumber}
pdfOriginalWH={pdfOriginalWH}
pdfNewWidth={pdfNewWidth}
setIsSignPad={setIsSignPad}
setIsStamp={setIsStamp}
setSignKey={setSignKey}
pdfDetails={pdfDetails}
signerPos={signerPos}
successEmail={false}
@@ -2302,11 +2003,7 @@ function PdfRequestFiles(
pdfLoad={pdfLoad}
setSignerPos={setSignerPos}
containerWH={containerWH}
setIsInitial={setIsInitial}
setValidateAlert={setValidateAlert}
unSignedWidgetId={unSignedWidgetId}
setSelectWidgetId={setSelectWidgetId}
selectWidgetId={selectWidgetId}
setCurrWidgetsDetails={setCurrWidgetsDetails}
divRef={divRef}
setIsResize={setIsResize}
@@ -2324,9 +2021,10 @@ function PdfRequestFiles(
setUniqueId={setUniqueId}
handleDeleteSign={handleDeleteSign}
handleTextSettingModal={handleTextSettingModal}
setWidgetType={setWidgetType}
assignedWidgetId={assignedWidgetId}
setRequestSignTour={setRequestSignTour}
currWidgetsDetails={currWidgetsDetails}
setTempSignerId={setTempSignerId}
/>
)}
</div>
@@ -2389,8 +2087,6 @@ function PdfRequestFiles(
!alreadySign &&
currentSigner && (
<DefaultSignature
defaultSignImg={defaultSignImg}
myInitial={myInitial}
userObjectId={signerObjectId}
setIsLoading={setIsLoading}
xyPosition={signerPos}
@@ -2424,23 +2120,24 @@ function PdfRequestFiles(
</div>
</div>
)}
<ModalUi
isOpen={validateAlert}
title={t("validation-alert")}
handleClose={() => setValidateAlert(false)}
>
<div className="h-[100%] p-[20px]">
<p>{t("validation-alert-1")}</p>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button
onClick={() => setValidateAlert(false)}
type="button"
className="op-btn op-btn-ghost"
>
{t("close")}
</button>
</div>
</ModalUi>
{currentSigner && isShowModal[currWidgetsDetails?.key] && (
<WidgetsValueModal
key={currWidgetsDetails?.key}
xyPosition={signerPos}
pageNumber={pageNumber}
setXyPosition={setSignerPos}
uniqueId={uniqueId}
setPageNumber={setPageNumber}
finishDocument={handleSignPdf}
setCurrWidgetsDetails={setCurrWidgetsDetails}
currWidgetsDetails={currWidgetsDetails}
index={pageNumber}
setUniqueId={setUniqueId}
tempSignerId={tempSignerId}
signatureTypes={signatureType}
allowCellResize={pdfDetails[0]?.AllowModifications ?? false}
/>
)}
<DownloadPdfZip
setIsDownloadModal={setIsDownloadModal}
isDownloadModal={isDownloadModal}
+94 -51
View File
@@ -11,11 +11,11 @@ import { HTML5Backend } from "react-dnd-html5-backend";
import { useDrop } from "react-dnd";
import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import WidgetComponent from "../components/pdf/WidgetComponent";
import Tour from "reactour";
import Tour from "../primitives/Tour";
import { useLocation, useParams } from "react-router";
import SignerListPlace from "../components/pdf/SignerListPlace";
import Header from "../components/pdf/PdfHeader";
import { RWebShare } from "react-web-share";
import ShareButton from "../primitives/ShareButton";
import {
replaceMailVaribles,
pdfNewWidthFun,
@@ -27,6 +27,7 @@ import {
multiSignEmbed,
addWidgetOptions,
textInputWidget,
cellsWidget,
textWidget,
radioButtonWidget,
color,
@@ -44,7 +45,8 @@ import {
handleSignatureType,
getBase64FromUrl,
generatePdfName,
mailTemplate
mailTemplate,
getOriginalWH
} from "../constant/Utils";
import RenderPdf from "../components/pdf/RenderPdf";
import { useNavigate } from "react-router";
@@ -68,10 +70,12 @@ import LottieWithLoader from "../primitives/DotLottieReact";
import Alert from "../primitives/Alert";
import AsyncSelect from "react-select/async";
import AddContact from "../primitives/AddContact";
import WidgetsValueModal from "../components/pdf/WidgetsValueModal.jsx";
function PlaceHolderSign() {
const { t } = useTranslation();
const copyUrlRef = useRef(null);
const isShowModal = useSelector((state) => state.widget.isShowModal);
const appName =
"OpenSign™";
const editorRef = useRef();
@@ -121,7 +125,8 @@ function PlaceHolderSign() {
const [selectedEmail, setSelectedEmail] = useState(false);
const [isResize, setIsResize] = useState(false);
const [zIndex, setZIndex] = useState(1);
const [signKey, setSignKey] = useState();
// tempSignerId is used to temporarily store the currently selected signer's unique ID, When editing a text widget, it automatically attaches a prefill user, and since prefill users are not shown in the signer list, the selected signer from before editing would be lost. To handle this, we store the currently selected signer's unique ID in tempSignerId before entering the text widget edit mode. Once the text widget settings are completed,
// we restore the original selected signer by setting tempSignerId back to uniqueId.This ensures that the correct signer remains selected and visible in the UI even after interacting with a prefill-only widget like the text widget.
const [tempSignerId, setTempSignerId] = useState("");
const [blockColor, setBlockColor] = useState("");
const [isTextSetting, setIsTextSetting] = useState(false);
@@ -134,14 +139,11 @@ function PlaceHolderSign() {
const [isDontShow, setIsDontShow] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [showDropdown, setShowDropdown] = useState(false);
const [widgetType, setWidgetType] = useState("");
const [isUiLoading, setIsUiLoading] = useState(false);
const [isRadio, setIsRadio] = useState(false);
const [currWidgetsDetails, setCurrWidgetsDetails] = useState({});
const [selectWidgetId, setSelectWidgetId] = useState("");
const [isCheckbox, setIsCheckbox] = useState(false);
const [isNameModal, setIsNameModal] = useState(false);
const [widgetName, setWidgetName] = useState(false);
const [mailStatus, setMailStatus] = useState("");
const [isCurrUser, setIsCurrUser] = useState(false);
const [pdfArrayBuffer, setPdfArrayBuffer] = useState("");
@@ -192,7 +194,7 @@ function PlaceHolderSign() {
);
if (user) {
try {
const defaultRequestBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}}&nbsp;has requested you to review and sign&nbsp;{{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p>{{signing_url}}</p><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
const defaultRequestBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}}&nbsp;has requested you to review and sign&nbsp;{{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p><a href='{{signing_url}}' rel='noopener noreferrer' target='_blank'>Sign here</a></p><br><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
const defaultSubject = `{{sender_name}} has requested you to sign {{document_title}}`;
setDefaultBody(defaultRequestBody);
setDefaultSubject(defaultSubject);
@@ -513,7 +515,8 @@ function PlaceHolderSign() {
pageNumber,
containerWH
);
let dropData = [];
let dropData = [],
dropObj;
let placeHolder;
const dragTypeValue = item?.text ? item.text : monitor.type;
const widgetWidth =
@@ -522,11 +525,15 @@ function PlaceHolderSign() {
defaultWidthHeight(dragTypeValue).height * containerScale;
//adding and updating drop position in array when user drop signature button in div
if (item === "onclick") {
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
const divWidth = divRef.current.getBoundingClientRect().width;
const divHeight = divRef.current.getBoundingClientRect().height;
// `getBoundingClientRect()` is used to get accurate measurement height of the div
const dropObj = {
// Compute the pixelspace center within the PDF viewport:
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
const xPosition_Final = centerX_Pixels / (containerScale * scale);
dropObj = {
//onclick put placeholder center on pdf
xPosition: widgetWidth / 4 + containerWH.width / 2,
xPosition: xPosition_Final,
yPosition: widgetHeight + divHeight / 2,
isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
@@ -556,7 +563,7 @@ function PlaceHolderSign() {
const getYPosition = signBtnPosition[0]
? y - signBtnPosition[0].yPos
: y;
const dropObj = {
dropObj = {
xPosition: getXPosition / (containerScale * scale),
yPosition: getYPosition / (containerScale * scale),
isStamp:
@@ -572,7 +579,6 @@ function PlaceHolderSign() {
dropData.push(dropObj);
placeHolder = { pageNumber: pageNumber, pos: dropData };
}
setSelectWidgetId(key);
if (signer) {
let filterSignerPos, currentPagePosition;
if (dragTypeValue === textWidget) {
@@ -664,24 +670,14 @@ function PlaceHolderSign() {
} else if (dragTypeValue === radioButtonWidget) {
setIsRadio(true);
}
setWidgetType(dragTypeValue);
setSignKey(key);
setCurrWidgetsDetails({});
setWidgetName(dragTypeValue);
setCurrWidgetsDetails(dropObj);
}
}
};
//function for get pdf page details
const pageDetails = async (pdf) => {
let pdfWHObj = [];
const totalPages = pdf?.numPages;
for (let index = 0; index < totalPages; index++) {
const getPage = await pdf.getPage(index + 1);
const scale = 1;
const { width, height } = getPage.getViewport({ scale });
pdfWHObj.push({ pageNumber: index + 1, width, height });
}
const pdfWHObj = await getOriginalWH(pdf);
setPdfOriginalWH(pdfWHObj);
setPdfLoad(true);
};
@@ -865,7 +861,9 @@ function PlaceHolderSign() {
});
const isSignYourSelfFlow = false;
try {
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
const pdfBase64 = await multiSignEmbed(
pdfOriginalWH,
placeholder,
pdfDoc,
isSignYourSelfFlow,
@@ -966,7 +964,7 @@ function PlaceHolderSign() {
const IsSignerNotExist = filterPrefill?.filter((x) => !x.signerObjId);
if (IsSignerNotExist && IsSignerNotExist?.length > 0) {
setSignerExistModal(true);
setSelectWidgetId(IsSignerNotExist[0]?.placeHolder?.[0]?.pos?.[0]?.key);
setCurrWidgetsDetails(IsSignerNotExist[0]?.placeHolder?.[0]?.pos);
} else {
saveDocumentDetails();
}
@@ -1144,9 +1142,13 @@ function PlaceHolderSign() {
<i className="fa-light fa-copy" />
<span className=" hidden md:block ml-1 ">{t("copy-link")}</span>
</button>
<RWebShare data={{ url: data.url, title: t("sign-url") }}>
<ShareButton
title={t("sign-url")}
text={t("sign-url")}
url={data.url}
>
<i className="fa-light fa-share-from-square op-link op-link-secondary no-underline"></i>
</RWebShare>
</ShareButton>
</div>
</div>
);
@@ -1219,7 +1221,7 @@ function PlaceHolderSign() {
receiver_phone: signerMail[i]?.Phone || "",
expiry_date: localExpireDate,
company_name: orgName,
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>`
signing_url: signPdf
};
replaceVar = replaceMailVaribles(
requestSubject,
@@ -1248,7 +1250,7 @@ function PlaceHolderSign() {
receiver_phone: signerMail[i]?.Phone || "",
expiry_date: localExpireDate,
company_name: orgName,
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>`
signing_url: signPdf
};
replaceVar = replaceMailVaribles(mailSubject, htmlReqBody, variables);
}
@@ -1259,7 +1261,7 @@ function PlaceHolderSign() {
title: documentName,
organization: orgName,
localExpireDate: localExpireDate,
sigingUrl: signPdf
signingUrl: signPdf
};
let params = {
extUserId: owner?.objectId,
@@ -1416,7 +1418,8 @@ function PlaceHolderSign() {
deleteOption,
status,
defaultValue,
isHideLabel
isHideLabel,
layout
) => {
const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId);
if (filterSignerPos.length > 0) {
@@ -1428,21 +1431,21 @@ function PlaceHolderSign() {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
if (position.key === signKey) {
if (widgetType === radioButtonWidget) {
if (position.key === currWidgetsDetails?.key) {
if (currWidgetsDetails?.type === radioButtonWidget) {
if (addOption) {
return {
...position,
Height: position.Height
? position.Height + 15
: defaultWidthHeight(widgetType).height + 15
: defaultWidthHeight(currWidgetsDetails?.type).height + 15
};
} else if (deleteOption) {
return {
...position,
Height: position.Height
? position.Height - 15
: defaultWidthHeight(widgetType).height - 15
: defaultWidthHeight(currWidgetsDetails?.type).height - 15
};
} else {
return {
@@ -1451,6 +1454,8 @@ function PlaceHolderSign() {
...position.options,
name: dropdownName,
values: dropdownOptions,
status: status,
layout: layout,
isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false,
defaultValue: defaultValue,
@@ -1463,20 +1468,20 @@ function PlaceHolderSign() {
}
};
}
} else if (widgetType === "checkbox") {
} else if (currWidgetsDetails?.type === "checkbox") {
if (addOption) {
return {
...position,
Height: position.Height
? position.Height + 15
: defaultWidthHeight(widgetType).height + 15
: defaultWidthHeight(currWidgetsDetails?.type).height + 15
};
} else if (deleteOption) {
return {
...position,
Height: position.Height
? position.Height - 15
: defaultWidthHeight(widgetType).height - 15
: defaultWidthHeight(currWidgetsDetails?.type).height - 15
};
} else {
return {
@@ -1490,6 +1495,7 @@ function PlaceHolderSign() {
maxRequiredCount: maxCount
},
defaultValue: defaultValue,
layout: layout,
isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false,
fontSize:
@@ -1567,7 +1573,7 @@ function PlaceHolderSign() {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
if (position.key === signKey) {
if (position.key === currWidgetsDetails?.key) {
if (position.type === textInputWidget) {
return {
...position,
@@ -1588,6 +1594,30 @@ function PlaceHolderSign() {
isReadOnly: defaultdata?.isReadOnly || false
}
};
} else if (position.type === cellsWidget) {
return {
...position,
options: {
...position.options,
name: defaultdata?.name || "Cells",
status: defaultdata?.status || "required",
hint: defaultdata?.hint || "",
cellCount: parseInt(defaultdata?.cellCount || 5),
defaultValue: (defaultdata?.defaultValue || "").slice(
0,
parseInt(defaultdata?.cellCount || 5)
),
validation:
{},
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black",
isReadOnly: defaultdata?.isReadOnly || false
}
};
} else if (["signature"].includes(position.type)) {
return {
...position,
@@ -2148,7 +2178,7 @@ function PlaceHolderSign() {
navigate("/report/1MwEuxLEkF");
}}
>
<div className="h-[100%] p-[20px]">
<div className="h-[100%] p-[20px] text-base-content">
{mailStatus === "success" ? (
<div className="text-center mb-[10px]">
<LottieWithLoader />
@@ -2367,9 +2397,9 @@ function PlaceHolderSign() {
setXyPosition={setSignerPos}
allPages={allPages}
pageNumber={pageNumber}
signKey={signKey}
signKey={currWidgetsDetails?.key}
Id={uniqueId}
widgetType={widgetType}
widgetType={currWidgetsDetails?.type}
setUniqueId={setUniqueId}
tempSignerId={tempSignerId}
setTempSignerId={setTempSignerId}
@@ -2474,17 +2504,13 @@ function PlaceHolderSign() {
setZIndex={setZIndex}
setIsPageCopy={setIsPageCopy}
signersdata={signersdata}
setSignKey={setSignKey}
handleLinkUser={handleLinkUser}
setUniqueId={setUniqueId}
isDragging={isDragging}
setShowDropdown={setShowDropdown}
setWidgetType={setWidgetType}
setIsRadio={setIsRadio}
setIsCheckbox={setIsCheckbox}
setCurrWidgetsDetails={setCurrWidgetsDetails}
setSelectWidgetId={setSelectWidgetId}
selectWidgetId={selectWidgetId}
handleNameModal={setIsNameModal}
setTempSignerId={setTempSignerId}
uniqueId={uniqueId}
@@ -2499,6 +2525,7 @@ function PlaceHolderSign() {
setFontColor={setFontColor}
unSignedWidgetId={unSignedWidgetId}
divRef={divRef}
currWidgetsDetails={currWidgetsDetails}
/>
)}
</div>
@@ -2536,7 +2563,6 @@ function PlaceHolderSign() {
handleDeleteUser={handleDeleteUser}
uniqueId={uniqueId}
setSignerPos={setSignerPos}
setSelectWidgetId={setSelectWidgetId}
/>
</div>
) : (
@@ -2582,12 +2608,29 @@ function PlaceHolderSign() {
</div>
</div>
)}
{isShowModal[currWidgetsDetails?.key] && (
<WidgetsValueModal
key={currWidgetsDetails?.key}
xyPosition={signerPos}
pageNumber={pageNumber}
setXyPosition={setSignerPos}
uniqueId={uniqueId}
setPageNumber={setPageNumber}
setCurrWidgetsDetails={setCurrWidgetsDetails}
currWidgetsDetails={currWidgetsDetails}
index={pageNumber}
isSave={true}
tempSignerId={tempSignerId}
setUniqueId={setUniqueId}
signatureTypes={signatureType}
/>
)}
<ModalUi
isOpen={isAlreadyPlace.status}
title={t("document-alert")}
showClose={false}
>
<div className="h-[100%] p-[20px]">
<div className="h-[100%] p-[20px] text-base-content">
<p>{isAlreadyPlace.message}</p>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button
@@ -2610,7 +2653,7 @@ function PlaceHolderSign() {
)}
<WidgetNameModal
signatureType={signatureType}
widgetName={widgetName}
widgetName={currWidgetsDetails?.options?.name}
defaultdata={currWidgetsDetails}
isOpen={isNameModal}
handleClose={handleNameModal}
+10 -1
View File
@@ -218,7 +218,7 @@ const Preferences = () => {
setIsLoader(true);
const updateRes = tenantRes;
setTenantId(updateRes?.objectId);
const defaultRequestBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}}&nbsp;has requested you to review and sign&nbsp;{{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p>{{signing_url}}</p><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
const defaultRequestBody = `<p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}}&nbsp;has requested you to review and sign&nbsp;{{document_title}}.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p><a href='{{signing_url}}' rel='noopener noreferrer' target='_blank'>Sign here</a></p><br><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br>`;
if (updateRes?.RequestBody) {
setRequestBody(updateRes?.RequestBody);
setRequestSubject(updateRes?.RequestSubject);
@@ -296,6 +296,15 @@ const Preferences = () => {
const updateRes = JSON.parse(JSON.stringify(updateTenant));
setRequestBody(updateRes?.RequestBody);
setRequestSubject(updateRes?.RequestSubject);
let extUser =
localStorage.getItem("Extand_Class") &&
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
if (extUser && extUser?.objectId) {
extUser.TenantId.RequestBody = updateRes?.RequestBody;
extUser.TenantId.RequestSubject = updateRes?.RequestSubject;
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
setIsAlert({ type: "success", msg: t("saved-successfully") });
setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500);
}
+70 -4
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef } from "react";
import ReportTable from "../primitives/GetReportDisplay";
import Parse from "parse";
import axios from "axios";
@@ -27,12 +27,18 @@ const Report = () => {
const [isImport, setIsImport] = useState(false);
const abortController = new AbortController();
const docPerPage = 10;
const [searchTerm, setSearchTerm] = useState("");
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [isSearchResult, setIsSearchResult] = useState(false);
const debounceTimer = useRef(null);
// below useEffect is call when id param change
useEffect(() => {
setReportName("");
setList([]);
getReportData();
setSearchTerm("");
setMobileSearchOpen(false);
getReportData(0, docPerPage, "");
// Function returned from useEffect is called on unmount
return () => {
@@ -48,7 +54,7 @@ const Report = () => {
// below useEffect call when isNextRecord state is true and fetch next record
useEffect(() => {
if (isNextRecord) {
getReportData(List.length, 20);
getReportData(List.length, 20, searchTerm);
}
// eslint-disable-next-line
}, [isNextRecord]);
@@ -56,7 +62,58 @@ const Report = () => {
const handleDontShow = (isChecked) => {
setIsDontShow(isChecked);
};
const getReportData = async (skipUserRecord = 0, limit = 20) => {
const handleSearchChange = 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: id, 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 = async (
skipUserRecord = 0,
limit = 20,
term = searchTerm
) => {
// setIsLoader(true);
const json = reportJson(id);
if (json) {
@@ -77,6 +134,9 @@ const Report = () => {
const skipRecord = id === "4Hhwbp482K" ? 0 : skipUserRecord;
const limitRecord = id === "4Hhwbp482K" ? 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,
@@ -197,6 +257,12 @@ const Report = () => {
report_help={reporthelp}
tourData={tourData}
isDontShow={isDontShow}
mobileSearchOpen={mobileSearchOpen}
setMobileSearchOpen={setMobileSearchOpen}
searchTerm={searchTerm}
handleSearchChange={handleSearchChange}
handleSearchPaste={handleSearchPaste}
isSearchResult={isSearchResult}
isImport={isImport}
/>
) : (
+210 -205
View File
@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect } from "react";
import { PDFDocument } from "pdf-lib";
import "../styles/signature.css";
import Parse from "parse";
@@ -8,7 +8,6 @@ import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useDrop } from "react-dnd";
import SignPad from "../components/pdf/SignPad";
import EmailComponent from "../components/pdf/EmailComponent";
import WidgetComponent from "../components/pdf/WidgetComponent";
import {
@@ -18,13 +17,12 @@ import {
multiSignEmbed,
calculateInitialWidthHeight,
defaultWidthHeight,
onSaveImage,
onSaveSign,
contractUsers,
contactBook,
randomId,
getDate,
textWidget,
cellsWidget,
convertPdfArrayBuffer,
textInputWidget,
fetchImageBase64,
@@ -35,23 +33,23 @@ import {
onClickZoomIn,
onClickZoomOut,
rotatePdfPage,
signatureTypes,
getBase64FromUrl,
convertBase64ToFile,
generatePdfName,
handleRemoveWidgets,
compressedFileSize,
addWidgetSelfsignOptions
addWidgetSelfsignOptions,
getOriginalWH,
signatureTypes
} from "../constant/Utils";
import { useParams } from "react-router";
import Tour from "reactour";
import Tour from "../primitives/Tour";
import Signedby from "../components/pdf/Signedby";
import Header from "../components/pdf/PdfHeader";
import RenderPdf from "../components/pdf/RenderPdf";
import PlaceholderCopy from "../components/pdf/PlaceholderCopy";
import Title from "../components/Title";
import DropdownWidgetOption from "../components/pdf/DropdownWidgetOption";
import { useSelector } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import TextFontSetting from "../components/pdf/TextFontSetting";
import VerifyEmail from "../components/pdf/VerifyEmail";
import PdfZoom from "../components/pdf/PdfZoom";
@@ -63,43 +61,46 @@ import ModalUi from "../primitives/ModalUi";
import TourContentWithBtn from "../primitives/TourContentWithBtn";
import HandleError from "../primitives/HandleError";
import LoaderWithMsg from "../primitives/LoaderWithMsg";
import {
setSaveSignCheckbox,
setMyInitial,
setDefaultSignImg,
setIsShowModal,
resetWidgetState
} from "../redux/reducers/widgetSlice.js";
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
import WidgetNameModal from "../components/pdf/WidgetNameModal";
import CellsSettingModal from "../components/pdf/CellsSettingModal";
//For signYourself inProgress section signer can add sign and complete doc sign.
function SignYourSelf() {
const { t } = useTranslation();
const { docId } = useParams();
const dispatch = useDispatch();
const isShowModal = useSelector((state) => state.widget.isShowModal);
const saveSignCheckbox = useSelector(
(state) => state.widget.saveSignCheckbox
);
const appName =
"OpenSign™";
const divRef = useRef(null);
const nodeRef = useRef(null);
const imageRef = useRef(null);
const pdfRef = useRef();
const numPages = 1;
const [pdfDetails, setPdfDetails] = useState([]);
const [isSignPad, setIsSignPad] = useState(false);
const [allPages, setAllPages] = useState(null);
const [pdfUrl, setPdfUrl] = useState();
const [xyPosition, setXyPosition] = useState([]);
const [defaultSignImg, setDefaultSignImg] = useState();
const [pageNumber, setPageNumber] = useState(1);
const [image, setImage] = useState(null);
const [isImageSelect, setIsImageSelect] = useState(false);
const [signature, setSignature] = useState();
const [isStamp, setIsStamp] = useState(false);
const [isEmail, setIsEmail] = useState(false);
const [signBtnPosition, setSignBtnPosition] = useState([]);
const [xySignature, setXYSignature] = useState({});
const [dragKey, setDragKey] = useState();
const [fontSize, setFontSize] = useState();
const [fontColor, setFontColor] = useState();
const [signKey, setSignKey] = useState();
const [imgWH, setImgWH] = useState({});
const [pdfNewWidth, setPdfNewWidth] = useState();
const [pdfOriginalWH, setPdfOriginalWH] = useState([]);
const [successEmail, setSuccessEmail] = useState(false);
const [myInitial, setMyInitial] = useState("");
const [isInitial, setIsInitial] = useState(false);
const [isUiLoading, setIsUiLoading] = useState(false);
const [validateAlert, setValidateAlert] = useState(false);
const [isLoading, setIsLoading] = useState({
isLoad: true,
message: t("loading-mssg")
@@ -113,7 +114,6 @@ function SignYourSelf() {
const [contractName, setContractName] = useState("");
const [containerWH, setContainerWH] = useState({});
const [isPageCopy, setIsPageCopy] = useState(false);
const [selectWidgetId, setSelectWidgetId] = useState("");
const [otpLoader, setOtpLoader] = useState(false);
const [showAlreadySignDoc, setShowAlreadySignDoc] = useState({
status: false
@@ -121,7 +121,10 @@ function SignYourSelf() {
const [isTextSetting, setIsTextSetting] = useState(false);
const [currWidgetsDetails, setCurrWidgetsDetails] = useState({});
const [isCheckbox, setIsCheckbox] = useState(false);
const [widgetType, setWidgetType] = useState("");
const [isNameModal, setIsNameModal] = useState(false);
const [isCellsSetting, setIsCellsSetting] = useState(false);
const openNameModal = () => setIsNameModal(true);
const openCellsSettingModal = () => setIsCellsSetting(true);
const [pdfLoad, setPdfLoad] = useState(false);
const [isAlert, setIsAlert] = useState({ isShow: false, alertMessage: "" });
const [isDontShow, setIsDontShow] = useState(false);
@@ -142,10 +145,7 @@ function SignYourSelf() {
const [isDownloadModal, setIsDownloadModal] = useState(false);
const [isResize, setIsResize] = useState(false);
const [isUploadPdf, setIsUploadPdf] = useState(false);
const [saveSignCheckbox, setSaveSignCheckbox] = useState({
isVisible: false,
signId: ""
});
const [owner, setOwner] = useState({});
const [, drop] = useDrop({
accept: "BOX",
@@ -173,6 +173,7 @@ function SignYourSelf() {
const jsonSender = JSON.parse(senderUser);
useEffect(() => {
dispatch(resetWidgetState([]));
if (documentId) {
getDocumentDetails(true);
}
@@ -243,7 +244,6 @@ function SignYourSelf() {
});
} else {
setIsUiLoading(false);
setIsSignPad(false);
setIsEmail(true);
setXyPosition([]);
setSignBtnPosition([]);
@@ -266,13 +266,15 @@ function SignYourSelf() {
//function to get default signatur eof current user from `contracts_Signature` class
const defaultSignRes = await getDefaultSignature(jsonSender.objectId);
if (defaultSignRes?.status === "success") {
setSaveSignCheckbox((prev) => ({
...prev,
isVisible: true,
signId: defaultSignRes?.res?.id
}));
setDefaultSignImg(defaultSignRes?.res?.defaultSignature);
setMyInitial(defaultSignRes?.res?.defaultInitial);
dispatch(
setSaveSignCheckbox({
...saveSignCheckbox,
isVisible: true,
signId: defaultSignRes?.res?.id
})
);
dispatch(setDefaultSignImg(defaultSignRes?.res?.defaultSignature));
dispatch(setMyInitial(defaultSignRes?.res?.defaultInitial));
}
const contractUsersRes = await contractUsers();
if (contractUsersRes === "Error: Something went wrong!") {
@@ -281,7 +283,8 @@ function SignYourSelf() {
} else if (contractUsersRes[0] && contractUsersRes.length > 0) {
setContractName("_Users");
setSignerUserId(contractUsersRes[0].objectId);
setSaveSignCheckbox((prev) => ({ ...prev, isVisible: true }));
dispatch(setSaveSignCheckbox({ ...saveSignCheckbox, isVisible: true }));
const tourstatuss =
contractUsersRes[0].TourStatus && contractUsersRes[0].TourStatus;
if (tourstatuss && tourstatuss.length > 0 && !isCompleted) {
@@ -293,12 +296,9 @@ function SignYourSelf() {
setCheckTourStatus(checkTourRecipients[0].signyourself);
}
} else {
setCheckTourStatus(true);
setCheckTourStatus(false);
}
const loadObj = {
isLoad: false
};
setIsLoading(loadObj);
setIsLoading({ isLoad: false });
} else if (contractUsersRes.length === 0) {
const contractContactBook = await contactBook(jsonSender.objectId);
if (contractContactBook && contractContactBook.length > 0) {
@@ -363,9 +363,13 @@ function SignYourSelf() {
);
const dragTypeValue = item?.text ? item.text : monitor.type;
const widgetValue = getWidgetValue(dragTypeValue);
const widgetTypeExist = ["name", "company", "job title", "email"].includes(
dragTypeValue
);
const widgetTypeExist = [
"name",
"company",
"job title",
"email",
cellsWidget
].includes(dragTypeValue);
const containerScale = getContainerScale(
pdfOriginalWH,
pageNumber,
@@ -373,15 +377,19 @@ function SignYourSelf() {
);
//adding and updating drop position in array when user drop signature button in div
if (item === "onclick") {
// `getBoundingClientRect()` is used to get accurate measurement height of the div
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
const divHeight = divRef.current.getBoundingClientRect().height;
const divWidth = divRef.current.getBoundingClientRect().width;
const getWidth = widgetTypeExist
? calculateInitialWidthHeight(widgetValue).getWidth
: defaultWidthHeight(dragTypeValue).width;
const getHeight = defaultWidthHeight(dragTypeValue).height;
// Compute the pixelspace center within the PDF viewport:
const centerX_Pixels = divWidth / 2 - getWidth / 2;
const xPosition_Final = centerX_Pixels / (containerScale * scale);
dropObj = {
xPosition: getWidth / 2 + containerWH.width / 2,
xPosition: xPosition_Final,
yPosition: getHeight + divHeight / 2,
isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
@@ -436,25 +444,13 @@ function SignYourSelf() {
const xyPos = { pageNumber: pageNumber, pos: dropData };
setXyPosition((prev) => [...prev, xyPos]);
}
if (
dragTypeValue === "signature" ||
dragTypeValue === "stamp" ||
dragTypeValue === "image" ||
dragTypeValue === "initials"
) {
setIsSignPad(true);
}
if (dragTypeValue === "stamp" || dragTypeValue === "image") {
setIsStamp(true);
} else if (dragTypeValue === "initials") {
setIsInitial(true);
} else if (dragTypeValue === "checkbox") {
if (dragTypeValue === "checkbox") {
setIsCheckbox(true);
} else if (
[
textInputWidget,
textWidget,
cellsWidget,
"name",
"company",
"job title",
@@ -464,9 +460,10 @@ function SignYourSelf() {
setFontSize(12);
setFontColor("black");
}
setWidgetType(dragTypeValue);
setSelectWidgetId(key);
setSignKey(key);
dispatch(setIsShowModal({ [key]: true }));
// if (dragTypeValue !== "checkbox") {
setCurrWidgetsDetails(dropObj);
// }
};
//`handleResend` function is used to resend otp for email verification
@@ -646,16 +643,17 @@ function SignYourSelf() {
const HeaderDocId = extUserPtr?.HeaderDocId;
//embed document's object id to all pages in pdf document
if (!HeaderDocId) {
await embedDocId(pdfDoc, documentId, allPages);
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
await embedDocId(pdfOriginalWH, pdfDoc, documentId);
}
//embed multi signature in pdf
//embed all widgets in document
const pdfBytes = await multiSignEmbed(
pdfOriginalWH,
xyPosition,
pdfDoc,
isSignYourSelfFlow,
scale
);
// console.log("pdf", pdfBytes);
//function for call to embed signature in pdf and get digital signature pdf
if (!pdfBytes?.error) {
await signPdfFun(pdfBytes, documentId);
@@ -759,7 +757,6 @@ function SignYourSelf() {
getDocumentDetails(false);
}
};
//function for save x and y position and show signature tab on that position
const handleTabDrag = (key) => {
setDragKey(key);
@@ -810,14 +807,7 @@ function SignYourSelf() {
};
//function for get pdf page details
const pageDetails = async (pdf) => {
let pdfWHObj = [];
const totalPages = pdf?.numPages;
for (let index = 0; index < totalPages; index++) {
const getPage = await pdf.getPage(index + 1);
const scale = 1;
const { width, height } = getPage.getViewport({ scale });
pdfWHObj.push({ pageNumber: index + 1, width, height });
}
const pdfWHObj = await getOriginalWH(pdf);
setPdfOriginalWH(pdfWHObj);
setPdfLoad(true);
};
@@ -826,53 +816,7 @@ function SignYourSelf() {
setSignBtnPosition([]);
setPageNumber((prevPageNumber) => prevPageNumber + offset);
}
//function for image upload or update
const onImageChange = (event) => {
if (event.target.files && event.target.files[0]) {
const file = event.target.files[0];
compressedFileSize(file, setImgWH, setImage);
}
};
//function for upload stamp or image
const saveImage = () => {
const getImage = onSaveImage(xyPosition, index, signKey, imgWH, image);
setXyPosition(getImage);
};
//function for save button to save signature or image url
const saveSign = (type, isDefaultSign, width, height, typedSignature) => {
const isTypeText = width && height ? true : false;
const signatureImg = isDefaultSign
? isDefaultSign === "initials"
? myInitial
: defaultSignImg
: signature;
let imgWH = { width: width ? width : "", height: height ? height : "" };
setIsSignPad(false);
setIsImageSelect(false);
setImage();
if (isDefaultSign) {
const img = new Image();
img.src = defaultSignImg;
if (img.complete) {
imgWH = { width: img.width, height: img.height };
}
}
const getUpdatePosition = onSaveSign(
type,
xyPosition,
index,
signKey,
signatureImg,
imgWH,
isDefaultSign,
isTypeText,
typedSignature
);
setXyPosition(getUpdatePosition);
};
//function for capture position on hover or touch widgets button
const handleDivClick = (e) => {
const isTouchEvent = e.type.startsWith("touch");
@@ -932,7 +876,7 @@ function SignYourSelf() {
selector: '[data-tut="addWidgets"]',
content: () => (
<TourContentWithBtn
message={t("tour-mssg.signyour-self-2")}
message={t("tour-mssg.signyour-self-1")}
isChecked={handleDontShow}
/>
),
@@ -970,14 +914,10 @@ function SignYourSelf() {
} else {
updatedTourStatus = [{ signyourself: true }];
}
await axios
.put(
`${localStorage.getItem(
"baseUrl"
)}classes/contracts${contractName}/${signerUserId}`,
{
TourStatus: updatedTourStatus
},
try {
await axios.put(
`${localStorage.getItem("baseUrl")}classes/contracts${contractName}/${signerUserId}`,
{ TourStatus: updatedTourStatus },
{
headers: {
"Content-Type": "application/json",
@@ -985,14 +925,11 @@ function SignYourSelf() {
sessionToken: localStorage.getItem("accesstoken")
}
}
)
.then(() => {
// const json = Listdata.data;
})
.catch((err) => {
console.log("axois err ", err);
alert(t("something-went-wrong-mssg"));
});
);
} catch (err) {
console.log("axois err ", err);
alert(t("something-went-wrong-mssg"));
}
}
};
@@ -1006,7 +943,8 @@ function SignYourSelf() {
deleteOption,
status,
defaultValue,
isHideLabel
isHideLabel,
layout
) => {
const getPageNumer = xyPosition.filter(
(data) => data.pageNumber === pageNumber
@@ -1014,21 +952,23 @@ function SignYourSelf() {
if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const widgetLayout =
currWidgetsDetails?.type === "checkbox" ? { layout: layout } : {};
const addSignPos = getPosData.map((position) => {
if (position.key === signKey) {
if (position.key === currWidgetsDetails?.key) {
if (addOption) {
return {
...position,
Height: position.Height
? position.Height + 15
: defaultWidthHeight(widgetType).height + 15
: defaultWidthHeight(currWidgetsDetails?.type).height + 15
};
} else if (deleteOption) {
return {
...position,
Height: position.Height
? position.Height - 15
: defaultWidthHeight(widgetType).height - 15
: defaultWidthHeight(currWidgetsDetails?.type).height - 15
};
} else {
return {
@@ -1037,6 +977,7 @@ function SignYourSelf() {
...position.options,
name: dropdownName,
values: dropdownOptions,
...widgetLayout,
isReadOnly: isReadOnly,
isHideLabel: isHideLabel || false,
fontSize:
@@ -1078,7 +1019,7 @@ function SignYourSelf() {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
if (position.key === signKey) {
if (position.key === currWidgetsDetails?.key) {
return {
...position,
options: {
@@ -1103,6 +1044,92 @@ function SignYourSelf() {
handleTextSettingModal(false);
}
};
const setCellCount = (key, newCount) => {
setXyPosition((prev) => {
const getPageNumer = prev.filter((data) => data.pageNumber === pageNumber);
if (getPageNumer.length > 0) {
const updatePos = getPageNumer[0].pos.map((p) =>
p.key === key ? { ...p, options: { ...p.options, cellCount: newCount } } : p
);
return prev.map((obj, ind) => (ind === index ? { ...obj, pos: updatePos } : obj));
}
return prev;
});
};
const handleWidgetdefaultdata = (defaultdata) => {
const newFontSize =
defaultdata?.fontSize !== undefined ? defaultdata.fontSize : fontSize;
const newFontColor =
defaultdata?.fontColor !== undefined ? defaultdata.fontColor : fontColor;
const getPageNumer = xyPosition.filter(
(data) => data.pageNumber === pageNumber
);
if (getPageNumer.length > 0) {
const updatePos = getPageNumer[0].pos.map((position) => {
if (position.key === currWidgetsDetails?.key) {
if (position.type === cellsWidget) {
const count = parseInt(
defaultdata?.cellCount ?? position.options?.cellCount ?? 5,
10
);
return {
...position,
options: {
...position.options,
name: defaultdata?.name || position.options?.name || "Cells",
cellCount: count,
defaultValue: (defaultdata?.defaultValue || "").slice(0, count),
fontSize: newFontSize || position.options?.fontSize || 12,
fontColor: newFontColor || position.options?.fontColor || "black"
}
};
} else {
return {
...position,
options: {
...position.options,
name: defaultdata?.name || position.options?.name,
fontSize: newFontSize || position.options?.fontSize || 12,
fontColor:
newFontColor || position.options?.fontColor || "black"
}
};
}
}
return position;
});
const updateXYposition = xyPosition.map((obj, ind) =>
ind === index ? { ...obj, pos: updatePos } : obj
);
setXyPosition(updateXYposition);
}
setFontSize();
setFontColor();
setCurrWidgetsDetails({});
setIsNameModal(false);
};
const handleNameModal = () => {
setIsNameModal(false);
setCurrWidgetsDetails({});
setIsCheckbox(false);
};
const handleCellsSettingSave = (data) => {
// ensure font and color are updated before applying widget changes
if (data?.fontSize !== undefined) setFontSize(data.fontSize);
if (data?.fontColor !== undefined) setFontColor(data.fontColor);
handleWidgetdefaultdata(data);
setIsCellsSetting(false);
};
const handleCellsSettingClose = () => {
setIsCellsSetting(false);
setCurrWidgetsDetails({});
};
const clickOnZoomIn = () => {
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
};
@@ -1195,7 +1222,7 @@ function SignYourSelf() {
/>
)}
{/* this component used for UI interaction and show their functionality */}
{pdfLoad && !checkTourStatus && (
{pdfLoad && !checkTourStatus && !isCompleted && (
<Tour
onRequestClose={closeTour}
steps={tourConfig}
@@ -1258,9 +1285,8 @@ function SignYourSelf() {
title={t("document-signed")}
handleClose={() => setShowAlreadySignDoc({ status: false })}
>
<div className="p-[20px] h-full">
<div className="p-[20px] h-full text-base-content">
<p>{showAlreadySignDoc.mssg}</p>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button
className="op-btn op-btn-ghost shadow-md"
@@ -1292,39 +1318,9 @@ function SignYourSelf() {
setXyPosition={setXyPosition}
allPages={allPages}
pageNumber={pageNumber}
signKey={signKey}
widgetType={widgetType}
signKey={currWidgetsDetails?.key}
widgetType={currWidgetsDetails?.type}
/>
{/* this is modal of signature pad */}
{isSignPad && (
<SignPad
saveSignCheckbox={saveSignCheckbox}
setSaveSignCheckbox={setSaveSignCheckbox}
signatureTypes={signatureTypes}
isSignPad={isSignPad}
isStamp={isStamp}
setIsImageSelect={setIsImageSelect}
setIsSignPad={setIsSignPad}
setImage={setImage}
isImageSelect={isImageSelect}
imageRef={imageRef}
onImageChange={onImageChange}
setSignature={setSignature}
image={image}
onSaveImage={saveImage}
onSaveSign={saveSign}
defaultSign={defaultSignImg}
myInitial={myInitial}
setDefaultSignImg={setDefaultSignImg}
setMyInitial={setMyInitial}
isInitial={isInitial}
setIsInitial={setIsInitial}
setIsStamp={setIsStamp}
widgetType={widgetType}
currWidgetsDetails={currWidgetsDetails}
setCurrWidgetsDetails={setCurrWidgetsDetails}
/>
)}
{/*render email component to send email after finish signature on document */}
<EmailComponent
isEmail={isEmail}
@@ -1372,10 +1368,7 @@ function SignYourSelf() {
handleTabDrag={handleTabDrag}
handleStop={handleStop}
isDragging={isDragging}
setIsSignPad={setIsSignPad}
setIsStamp={setIsStamp}
handleDeleteSign={handleDeleteSign}
setSignKey={setSignKey}
pdfDetails={pdfDetails}
setIsDragging={setIsDragging}
xyPosition={xyPosition}
@@ -1389,24 +1382,23 @@ function SignYourSelf() {
index={index}
containerWH={containerWH}
setIsPageCopy={setIsPageCopy}
setIsInitial={setIsInitial}
setWidgetType={setWidgetType}
setSelectWidgetId={setSelectWidgetId}
selectWidgetId={selectWidgetId}
setIsCheckbox={setIsCheckbox}
setCurrWidgetsDetails={setCurrWidgetsDetails}
setValidateAlert={setValidateAlert}
handleNameModal={openNameModal}
handleCellSettingModal={openCellsSettingModal}
handleTextSettingModal={handleTextSettingModal}
setScale={setScale}
scale={scale}
pdfBase64Url={pdfBase64Url}
fontSize={fontSize}
setFontSize={setFontSize}
setCellCount={setCellCount}
fontColor={fontColor}
setFontColor={setFontColor}
isResize={isResize}
setIsResize={setIsResize}
divRef={divRef}
currWidgetsDetails={currWidgetsDetails}
/>
)}
</div>
@@ -1424,7 +1416,6 @@ function SignYourSelf() {
isSignYourself={true}
addPositionOfSignature={addPositionOfSignature}
isMailSend={false}
// setSelectWidgetId={setSelectWidgetId}
/>
</div>
) : (
@@ -1437,24 +1428,38 @@ function SignYourSelf() {
</div>
</div>
)}
<ModalUi
isOpen={validateAlert}
title={t("validation-alert")}
handleClose={() => setValidateAlert(false)}
>
<div className="p-[20px] h-full">
<p>{t("validate-alert-mssg")}</p>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button
onClick={() => setValidateAlert(false)}
type="button"
className="op-btn op-btn-ghost shadow-md"
>
{t("close")}
</button>
</div>
</ModalUi>
{!isCheckbox && isShowModal[currWidgetsDetails?.key] && (
<WidgetsValueModal
key={currWidgetsDetails?.key}
xyPosition={xyPosition} //placeholder details
pageNumber={pageNumber} //current page number
setXyPosition={setXyPosition} //placeholder details state
setCellCount={setCellCount}
setPageNumber={setPageNumber}
setCurrWidgetsDetails={setCurrWidgetsDetails}
currWidgetsDetails={currWidgetsDetails}
index={index}
isSave={true}
signatureTypes={signatureTypes}
/>
)}
<WidgetNameModal
widgetName={currWidgetsDetails?.options?.name}
defaultdata={currWidgetsDetails}
isOpen={isNameModal}
handleClose={handleNameModal}
handleData={handleWidgetdefaultdata}
fontSize={fontSize}
setFontSize={setFontSize}
fontColor={fontColor}
setFontColor={setFontColor}
/>
<CellsSettingModal
isOpen={isCellsSetting}
defaultData={currWidgetsDetails}
handleClose={handleCellsSettingClose}
handleSave={handleCellsSettingSave}
/>
<RotateAlert
showRotateAlert={showRotateAlert.status}
setShowRotateAlert={setShowRotateAlert}
+87 -77
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef } from "react";
import { useEffect, useState, useRef } from "react";
import RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import { useParams, useNavigate } from "react-router";
import axios from "axios";
@@ -7,7 +7,7 @@ import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useDrop } from "react-dnd";
import WidgetComponent from "../components/pdf/WidgetComponent";
import Tour from "reactour";
import Tour from "../primitives/Tour";
import SignerListPlace from "../components/pdf/SignerListPlace";
import Header from "../components/pdf/PdfHeader";
import WidgetNameModal from "../components/pdf/WidgetNameModal";
@@ -22,6 +22,7 @@ import {
defaultWidthHeight,
addWidgetOptions,
textInputWidget,
cellsWidget,
radioButtonWidget,
getContainerScale,
convertBase64ToFile,
@@ -38,7 +39,8 @@ import {
convertPdfArrayBuffer,
generatePdfName,
textWidget,
multiSignEmbed
multiSignEmbed,
getOriginalWH
} from "../constant/Utils";
import RenderPdf from "../components/pdf/RenderPdf";
import "../styles/AddUser.css";
@@ -100,10 +102,8 @@ const TemplatePlaceholder = () => {
const [isSigners, setIsSigners] = useState(false);
const [zIndex, setZIndex] = useState(1);
const [showDropdown, setShowDropdown] = useState(false);
const [widgetType, setWidgetType] = useState("");
const [isRadio, setIsRadio] = useState(false);
const [blockColor, setBlockColor] = useState("");
const [selectWidgetId, setSelectWidgetId] = useState("");
const [isNameModal, setIsNameModal] = useState(false);
const [isTextSetting, setIsTextSetting] = useState(false);
const [pdfLoad, setPdfLoad] = useState(false);
@@ -122,13 +122,11 @@ const TemplatePlaceholder = () => {
const [isCreateDoc, setIsCreateDoc] = useState(false);
const [isEditTemplate, setIsEditTemplate] = useState(false);
const [isPageCopy, setIsPageCopy] = useState(false);
const [signKey, setSignKey] = useState();
const [IsReceipent, setIsReceipent] = useState(true);
const [isDontShow, setIsDontShow] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [currWidgetsDetails, setCurrWidgetsDetails] = useState([]);
const [isCheckbox, setIsCheckbox] = useState(false);
const [widgetName, setWidgetName] = useState(false);
const [isAddRole, setIsAddRole] = useState(false);
const [fontSize, setFontSize] = useState();
const [fontColor, setFontColor] = useState();
@@ -137,7 +135,6 @@ const TemplatePlaceholder = () => {
const [signatureType, setSignatureType] = useState([]);
const [pdfArrayBuffer, setPdfArrayBuffer] = useState("");
const [updatedPdfUrl, setUpdatedPdfUrl] = useState("");
const [tempSignerId, setTempSignerId] = useState("");
const [unSignedWidgetId, setUnSignedWidgetId] = useState("");
const [owner, setOwner] = useState({});
useEffect(() => {
@@ -353,9 +350,6 @@ const TemplatePlaceholder = () => {
//function for setting position after drop signature button over pdf
const addPositionOfSignature = (item, monitor) => {
if (item && item.text) {
setWidgetName(item.text);
}
getSignerPos(item, monitor);
};
@@ -378,15 +372,20 @@ const TemplatePlaceholder = () => {
const widgetHeight =
defaultWidthHeight(dragTypeValue).height * containerScale;
let dropData = [],
dropObj,
currentPagePosition,
filterSignerPos;
let placeHolder;
if (item === "onclick") {
// `getBoundingClientRect()` is used to get accurate measurement height of the div
// `getBoundingClientRect()` is used to get accurate measurement width, height of the Pdf div
const divWidth = divRef.current.getBoundingClientRect().width;
const divHeight = divRef.current.getBoundingClientRect().height;
const dropObj = {
// Compute the pixelspace center within the PDF viewport:
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
const xPosition_Final = centerX_Pixels / (containerScale * scale);
dropObj = {
//onclick put placeholder center on pdf
xPosition: widgetWidth / 4 + containerWH.width / 2,
xPosition: xPosition_Final,
yPosition: widgetHeight + divHeight / 2,
isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true,
@@ -414,7 +413,7 @@ const TemplatePlaceholder = () => {
const getYPosition = signBtnPosition[0]
? y - signBtnPosition[0].yPos
: y;
const dropObj = {
dropObj = {
xPosition: getXPosition / (containerScale * scale),
yPosition: getYPosition / (containerScale * scale),
isStamp:
@@ -521,10 +520,7 @@ const TemplatePlaceholder = () => {
setFontSize(12);
setFontColor("black");
}
setCurrWidgetsDetails({});
setWidgetType(dragTypeValue);
setSignKey(key);
setSelectWidgetId(key);
setCurrWidgetsDetails(dropObj);
} else {
setIsReceipent(false);
}
@@ -544,14 +540,7 @@ const TemplatePlaceholder = () => {
//function for get pdf page details
const pageDetails = async (pdf) => {
let pdfWHObj = [];
const totalPages = pdf?.numPages;
for (let index = 0; index < totalPages; index++) {
const getPage = await pdf.getPage(index + 1);
const scale = 1;
const { width, height } = getPage.getViewport({ scale });
pdfWHObj.push({ pageNumber: index + 1, width, height });
}
const pdfWHObj = await getOriginalWH(pdf);
setPdfOriginalWH(pdfWHObj);
setPdfLoad(true);
};
@@ -842,7 +831,9 @@ const TemplatePlaceholder = () => {
});
const isSignYourSelfFlow = false;
try {
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
const pdfBase64 = await multiSignEmbed(
pdfOriginalWH,
placeholder,
pdfDoc,
isSignYourSelfFlow,
@@ -988,14 +979,14 @@ const TemplatePlaceholder = () => {
style: { fontSize: "13px" }
},
{
selector: '[data-tut="reactourFirst"]',
selector: '[data-tut="nonpresentmask"]',
content: () => (
<TourContentWithBtn
message={t("tour-mssg.template-placeholder-2")}
isChecked={handleDontShow}
/>
),
position: "top",
position: "center",
style: { fontSize: "13px" },
action: () => handleCloseRoleModal()
},
@@ -1159,25 +1150,6 @@ const TemplatePlaceholder = () => {
const handleLinkUser = (id) => {
setIsAddUser({ [id]: true });
};
//function to use unlink signer from widgets
const handleUnlinkSigner = () => {
//remove existing signer's details from 'signerPos' array
const updatePlaceHolder = signerPos.map((x) => {
if (x.Id === uniqueId) {
return { ...x, signerPtr: {}, signerObjId: "" };
}
return { ...x };
});
setSignerPos(updatePlaceHolder);
//remove existing signer's details from 'signersdata' array and keep role and id
const updateSigner = signersdata.map((item) => {
if (item.Id == uniqueId) {
return { Role: item.Role, Id: item.Id, blockColor: item.blockColor };
}
return item;
});
setSignersData(updateSigner);
};
// `handleAddUser` is used to adduser
const handleAddUser = (data) => {
const signerPtr = {
@@ -1303,7 +1275,8 @@ const TemplatePlaceholder = () => {
deleteOption,
status,
defaultValue,
isHideLabel
isHideLabel,
layout
) => {
const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId);
if (filterSignerPos.length > 0) {
@@ -1318,21 +1291,21 @@ const TemplatePlaceholder = () => {
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
if (position.key === signKey) {
if (widgetType === radioButtonWidget) {
if (position.key === currWidgetsDetails?.key) {
if (currWidgetsDetails?.type === radioButtonWidget) {
if (addOption) {
return {
...position,
Height: position.Height
? position.Height + 15
: defaultWidthHeight(widgetType).height + 15
: defaultWidthHeight(currWidgetsDetails?.type).height + 15
};
} else if (deleteOption) {
return {
...position,
Height: position.Height
? position.Height - 15
: defaultWidthHeight(widgetType).height - 15
: defaultWidthHeight(currWidgetsDetails?.type).height - 15
};
} else {
return {
@@ -1342,6 +1315,7 @@ const TemplatePlaceholder = () => {
name: dropdownName,
values: dropdownOptions,
status: status,
layout: layout,
defaultValue: defaultValue,
isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false,
@@ -1354,20 +1328,20 @@ const TemplatePlaceholder = () => {
}
};
}
} else if (widgetType === "checkbox") {
} else if (currWidgetsDetails?.type === "checkbox") {
if (addOption) {
return {
...position,
Height: position.Height
? position.Height + 15
: defaultWidthHeight(widgetType).height + 15
: defaultWidthHeight(currWidgetsDetails?.type).height + 15
};
} else if (deleteOption) {
return {
...position,
Height: position.Height
? position.Height - 15
: defaultWidthHeight(widgetType).height - 15
: defaultWidthHeight(currWidgetsDetails?.type).height - 15
};
} else {
return {
@@ -1380,6 +1354,7 @@ const TemplatePlaceholder = () => {
minRequiredCount: minCount,
maxRequiredCount: maxCount
},
layout: layout,
isReadOnly: isReadOnly || false,
defaultValue: defaultValue,
isHideLabel: isHideLabel || false,
@@ -1459,7 +1434,7 @@ const TemplatePlaceholder = () => {
const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata;
const addSignPos = getPosData.map((position) => {
if (position.key === signKey) {
if (position.key === currWidgetsDetails?.key) {
if (position.type === textInputWidget) {
return {
...position,
@@ -1480,6 +1455,36 @@ const TemplatePlaceholder = () => {
"black"
}
};
} else if (position.type === cellsWidget) {
return {
...position,
options: {
...position.options,
name: defaultdata?.name || "Cells",
status: defaultdata?.status || "required",
hint: defaultdata?.hint || "",
cellCount: parseInt(defaultdata?.cellCount || 5),
defaultValue: (defaultdata?.defaultValue || "").slice(
0,
parseInt(defaultdata?.cellCount || 5)
),
validation:
isSubscribe && inputype
? {
type: inputype,
pattern:
inputype === "regex" ? defaultdata.textvalidate : ""
}
: {},
isReadOnly: defaultdata?.isReadOnly || false,
fontSize:
fontSize || currWidgetsDetails?.options?.fontSize || 12,
fontColor:
fontColor ||
currWidgetsDetails?.options?.fontColor ||
"black"
}
};
} else if (["signature"].includes(position.type)) {
return {
...position,
@@ -1538,13 +1543,22 @@ const TemplatePlaceholder = () => {
setShowDropdown(false);
setIsRadio(false);
setIsCheckbox(false);
//condition for text widget type after set all values for text widget
//change setUniqueId which is set in tempsignerId
//because textwidget do not have signer user so for selected signers we have to do
if (currWidgetsDetails.type === textWidget) {
setUniqueId(tempSignerId);
setTempSignerId("");
}
};
const setCellCount = (key, newCount) => {
const updated = signerPos.map((signer) => {
if (signer.Id !== uniqueId) return signer;
const placeHolder = signer.placeHolder.map((ph) => {
if (ph.pageNumber !== pageNumber) return ph;
const pos = ph.pos.map((p) =>
p.key === key
? { ...p, options: { ...p.options, cellCount: newCount } }
: p
);
return { ...ph, pos };
});
return { ...signer, placeHolder };
});
setSignerPos(updated);
};
const clickOnZoomIn = () => {
@@ -1707,7 +1721,7 @@ const TemplatePlaceholder = () => {
navigate("/report/6TeaPr321t");
}}
>
<div className="h-full p-[20px] mb-2">
<div className="h-full p-[20px] mb-2 text-base-content">
<p>{t("template-created-alert")}</p>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<div className="flex gap-1 flex-col md:flex-row">
@@ -1791,12 +1805,10 @@ const TemplatePlaceholder = () => {
setXyPosition={setSignerPos}
allPages={allPages}
pageNumber={pageNumber}
signKey={signKey}
signKey={currWidgetsDetails?.key}
Id={uniqueId}
widgetType={widgetType}
widgetType={currWidgetsDetails?.type}
setUniqueId={setUniqueId}
tempSignerId={tempSignerId}
setTempSignerId={setTempSignerId}
/>
{/* pdf header which contain funish back button */}
<Header
@@ -1853,14 +1865,10 @@ const TemplatePlaceholder = () => {
setUniqueId={setUniqueId}
signersdata={signersdata}
setIsPageCopy={setIsPageCopy}
setSignKey={setSignKey}
isDragging={isDragging}
setShowDropdown={setShowDropdown}
setCurrWidgetsDetails={setCurrWidgetsDetails}
setWidgetType={setWidgetType}
setIsRadio={setIsRadio}
setSelectWidgetId={setSelectWidgetId}
selectWidgetId={selectWidgetId}
setIsCheckbox={setIsCheckbox}
handleNameModal={setIsNameModal}
pdfOriginalWH={pdfOriginalWH}
@@ -1870,13 +1878,14 @@ const TemplatePlaceholder = () => {
pdfBase64Url={pdfBase64Url}
fontSize={fontSize}
setFontSize={setFontSize}
setCellCount={setCellCount}
fontColor={fontColor}
setFontColor={setFontColor}
isResize={isResize}
divRef={divRef}
setTempSignerId={setTempSignerId}
uniqueId={uniqueId}
unSignedWidgetId={unSignedWidgetId}
currWidgetsDetails={currWidgetsDetails}
/>
)}
</div>
@@ -1913,7 +1922,6 @@ const TemplatePlaceholder = () => {
setBlockColor={setBlockColor}
setSignerPos={setSignerPos}
uniqueId={uniqueId}
setSelectWidgetId={setSelectWidgetId}
isTemplateFlow={true}
/>
</div>
@@ -1974,7 +1982,9 @@ const TemplatePlaceholder = () => {
closePopup={closePopup}
signersData={signersdata}
signerPos={signerPos}
handleUnlinkSigner={handleUnlinkSigner}
setSignerPos={setSignerPos}
setSignersData={setSignersData}
isRemove={true}
/>
)}
</div>
@@ -1991,7 +2001,7 @@ const TemplatePlaceholder = () => {
)}
<WidgetNameModal
signatureType={signatureType}
widgetName={widgetName}
widgetName={currWidgetsDetails?.options?.name}
defaultdata={currWidgetsDetails}
isOpen={isNameModal}
handleClose={handleNameModal}
+1 -1
View File
@@ -284,7 +284,7 @@ const UserList = () => {
handleClose={handleClose}
>
<div className="m-[20px]">
<div className="text-lg font-normal text-black">
<div className="text-lg font-normal text-base-content">
{t("are-you-sure")}{" "}
{item?.IsDisabled
? t("activate")
+4 -4
View File
@@ -286,7 +286,7 @@ function UserProfile() {
style={{ width: `${percentage}%` }}
></div>
</div>
<span className="text-black text-sm">{percentage}%</span>
<span className="text-base-contentk text-sm">{percentage}%</span>
</div>
)}
<div className="text-base font-semibold pt-4">
@@ -423,9 +423,9 @@ function UserProfile() {
onClick={() =>
editmode ? handleCancel() : navigate("/changepassword")
}
className={`op-btn ${
editmode ? "op-btn-ghost w-[100px]" : "op-btn-secondary"
}`}
className={
`op-btn ${editmode ? "op-btn-ghost w-[100px]" : "op-btn-secondary"}`
}
>
{editmode ? t("cancel") : t("change-password")}
</button>
+864
View File
@@ -0,0 +1,864 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { PDFDocument, PDFName, PDFSignature, PDFRef, PDFDict } from 'pdf-lib'; // Updated import
import * as asn1js from 'asn1js';
import { Certificate, ContentInfo, SignedData, IssuerAndSerialNumber } from 'pkijs';
// import * as jsrsasign from 'jsrsasign'; // Removed for dynamic loading
const VerifyDocument = () => {
const { t } = useTranslation();
const [selectedFile, setSelectedFile] = useState(null);
const [fileBuffer, setFileBuffer] = useState(null);
const [verificationResult, setVerificationResult] = useState('');
const [detailedResults, setDetailedResults] = useState([]);
const [collapsedSections, setCollapsedSections] = useState({});
// const [jsrsasignStatus, setJsrsasignStatus] = useState('loading'); // Removed
// OID to human-readable label mapping
const oidMapping = {
'2.5.4.6': 'Country',
'2.5.4.10': 'Organization',
'2.5.4.11': 'Organizational Unit',
'2.5.4.17': 'Postal Code',
'2.5.4.8': 'State',
'2.5.4.7': 'Locality', // Alternative for City
'2.5.4.9': 'City',
'2.5.4.51': 'Address',
'2.5.4.3': 'Common Name',
'2.5.4.4': 'Surname',
'2.5.4.5': 'Serial Number',
'2.5.4.12': 'Title',
'2.5.4.13': 'Description',
'2.5.4.16': 'Postal Address',
'2.5.4.18': 'Post Office Box',
'2.5.4.20': 'Telephone Number',
'1.2.840.113549.1.9.1': 'Email Address',
// Common alternative OIDs
'C': 'Country',
'O': 'Organization',
'OU': 'Organizational Unit',
'CN': 'Common Name',
'ST': 'State',
'L': 'Locality',
'STREET': 'Address',
'emailAddress': 'Email Address',
'serialNumber': 'Serial Number'
};
// Function to parse certificate subject/issuer into structured data
const parseCertificateInfo = (certString) => {
if (!certString) return {};
const parsed = {};
// Find all OID patterns and their positions
const oidPattern = /(\d+\.\d+\.\d+\.\d+|\w+)=/g;
const matches = [];
let match;
while ((match = oidPattern.exec(certString)) !== null) {
matches.push({
oid: match[1],
startIndex: match.index,
equalIndex: match.index + match[1].length
});
}
// Extract value for each OID
for (let i = 0; i < matches.length; i++) {
const currentMatch = matches[i];
const nextMatch = matches[i + 1];
const valueStart = currentMatch.equalIndex + 1; // Skip the "=" character
const valueEnd = nextMatch ? nextMatch.startIndex - 2 : certString.length; // -2 to remove ", " before next OID
const value = certString.substring(valueStart, valueEnd).trim();
const label = oidMapping[currentMatch.oid] || currentMatch.oid;
parsed[label] = value;
}
return parsed;
};
// Function to determine if status should show success icon
const isSuccessStatus = (status) => {
const successTerms = ['valid', 'success', 'parsed', 'verified'];
const errorTerms = ['error', 'invalid', 'failed', 'expired'];
const statusLower = status.toLowerCase();
// Check for explicit error terms first
if (errorTerms.some(term => statusLower.includes(term))) {
return false;
}
// Check for success terms
return successTerms.some(term => statusLower.includes(term));
};
// Function to determine if certificate validity should show success icon
const isCertificateValid = (validityText) => {
const validityLower = validityText.toLowerCase();
// If it contains "valid" and doesn't contain negative terms
return validityLower.includes('valid') &&
!validityLower.includes('expired') &&
!validityLower.includes('not yet valid') &&
!validityLower.includes('invalid');
};
// Toggle collapsible sections
const toggleSection = (signatureIndex, section) => {
const key = `${signatureIndex}-${section}`;
setCollapsedSections(prev => ({
...prev,
[key]: !prev[key]
}));
};
// useEffect for jsrsasign loading removed
const handleFileChange = (event) => {
const file = event.target.files[0];
if (file && file.type === 'application/pdf') {
setSelectedFile(file);
setVerificationResult('');
setDetailedResults([]);
const reader = new FileReader();
reader.onload = (e) => {
setFileBuffer(e.target.result);
};
reader.readAsArrayBuffer(file);
} else {
setSelectedFile(null);
setFileBuffer(null);
setDetailedResults([]);
setVerificationResult(t('please-select-pdf'));
}
};
const parseSignature = async (pdfDoc) => {
const signatureFields = pdfDoc.getForm().getFields().filter(field => field instanceof PDFSignature); // Updated filter logic
if (!signatureFields.length) {
return { error: t('no-signature-found') };
}
const results = [];
for (const field of signatureFields) {
try {
if (!field.acroField || !field.acroField.dict) {
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('error-processing-signature'),
errorDetails: t('missing-acrofield-dict'),
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
isCertificateDateValid: false,
calculatedDocumentHash: t('not-available'),
messageDigestInSignature: t('not-available'),
hashComparisonResult: t('not-performed'),
authenticatedAttributesSignatureResult: t('not-performed'),
});
continue;
}
// New logic to determine the actual signature dictionary
const fieldDict = field.acroField.dict;
const vEntry = fieldDict.get(PDFName.of('V'));
let actualSignatureDict = null;
if (vEntry) {
if (vEntry instanceof PDFRef) {
const lookedUp = pdfDoc.context.lookup(vEntry);
if (lookedUp instanceof PDFDict) {
actualSignatureDict = lookedUp;
}
} else if (vEntry instanceof PDFDict) {
actualSignatureDict = vEntry;
}
}
// Use actualSignatureDict if found, otherwise behavior might be problematic (as per existing logic)
// If actualSignatureDict is null, subsequent checks for byteRangeObject etc. will fail,
// leading to an error message for this signature, which is acceptable.
const signatureDict = actualSignatureDict;
// Check if signatureDict is null (meaning actualSignatureDict was not resolved)
// and push an error if it is, before trying to get ByteRange or Contents.
if (!signatureDict) {
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('error-processing-signature'),
errorDetails: t('signature-dictionary-not-found-or-invalid'), // New error message
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
isCertificateDateValid: false,
calculatedDocumentHash: t('not-available'),
messageDigestInSignature: t('not-available'),
hashComparisonResult: t('not-performed'),
authenticatedAttributesSignatureResult: t('not-performed'),
});
continue;
}
const byteRangeObject = signatureDict.get(PDFName.of('ByteRange'));
let byteRange; // Will be assigned after validation
// Comprehensive validation for byteRangeObject and its contents
if (!byteRangeObject ||
!byteRangeObject.array ||
!Array.isArray(byteRangeObject.array) ||
byteRangeObject.array.length === 0 ||
byteRangeObject.array.length % 2 !== 0) {
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('error-processing-signature'),
errorDetails: t('missing-or-invalid-byterange'),
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
isCertificateDateValid: false,
calculatedDocumentHash: t('not-available'),
messageDigestInSignature: t('not-available'),
hashComparisonResult: t('not-performed'),
authenticatedAttributesSignatureResult: t('not-performed'),
});
continue;
}
const byteRangeNumbers = [];
let byteRangeIsValid = true;
for (const pdfObject of byteRangeObject.array) {
if (!pdfObject || typeof pdfObject.asNumber !== 'function') {
byteRangeIsValid = false;
break;
}
const num = pdfObject.asNumber();
if (!Number.isFinite(num)) { // Checks for NaN, Infinity, -Infinity
byteRangeIsValid = false;
break;
}
byteRangeNumbers.push(num);
}
if (!byteRangeIsValid) {
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('error-processing-signature'),
errorDetails: t('missing-or-invalid-byterange'), // Or a more specific error like "ByteRange contains non-numeric values"
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
isCertificateDateValid: false,
calculatedDocumentHash: t('not-available'),
messageDigestInSignature: t('not-available'),
hashComparisonResult: t('not-performed'),
authenticatedAttributesSignatureResult: t('not-performed'),
});
continue;
}
byteRange = byteRangeNumbers; // Assign the validated numbers to byteRange
const contentsObject = signatureDict.get(PDFName.of('Contents'));
if (!contentsObject || typeof contentsObject.asString !== 'function') {
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('error-processing-signature'),
errorDetails: t('missing-or-invalid-contents'),
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
isCertificateDateValid: false,
calculatedDocumentHash: t('not-available'),
messageDigestInSignature: t('not-available'),
hashComparisonResult: t('not-performed'),
authenticatedAttributesSignatureResult: t('not-performed'),
});
continue;
}
const contents = contentsObject.asString();
// The old basic check can be removed now as the more specific checks above cover these cases.
// if (!byteRange || !contents) { ... }
// Calculate totalSignedLength for accurate buffer initialization
let totalSignedLength = 0;
for (let i = 1; i < byteRange.length; i += 2) {
totalSignedLength += byteRange[i];
}
if (totalSignedLength <= 0) {
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('error-processing-signature'),
errorDetails: t('missing-or-invalid-byterange'), // totalSignedLength being non-positive implies invalid ByteRange
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
isCertificateDateValid: false,
calculatedDocumentHash: t('not-available'),
messageDigestInSignature: t('not-available'),
hashComparisonResult: t('not-performed'),
authenticatedAttributesSignatureResult: t('not-performed'),
});
continue;
}
const pdfSignedDataBytes = new Uint8Array(totalSignedLength);
let offset = 0;
let reconstructionFailed = false;
for (let i = 0; i < byteRange.length; i += 2) {
const start = byteRange[i];
const length = byteRange[i+1];
if (start < 0 || length <= 0 || start + length > fileBuffer.byteLength) {
reconstructionFailed = true;
break;
}
pdfSignedDataBytes.set(new Uint8Array(fileBuffer.slice(start, start + length)), offset);
offset += length;
}
if (reconstructionFailed) {
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('error-processing-signature'),
errorDetails: t('missing-or-invalid-byterange'), // Error during reconstruction due to invalid segment
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
isCertificateDateValid: false,
calculatedDocumentHash: t('not-available'),
messageDigestInSignature: t('not-available'),
hashComparisonResult: t('not-performed'),
authenticatedAttributesSignatureResult: t('not-performed'),
});
continue;
}
// Remove leading/trailing null bytes from hex if present from PDF content
const pkcs7Hex = contents.trim();
if (!pkcs7Hex) {
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('signature-invalid-basic'),
errorDetails: t('missing-signature-contents'), // New i18n key
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
});
continue;
}
// Convert hex string to ArrayBuffer
let cmsContentBuffer;
try {
cmsContentBuffer = new Uint8Array(pkcs7Hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))).buffer;
} catch (hexError) {
// console.error('Error converting hex string to ArrayBuffer:', hexError); // Removed
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('signature-invalid-basic'),
errorDetails: t('invalid-signature-hex-format'), // New i18n key
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
});
continue;
}
// Parse the CMS ContentInfo
const asn1 = asn1js.fromBER(cmsContentBuffer);
if (asn1.offset === -1) {
// console.error('Error parsing ASN.1 from signature data'); // Removed
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('signature-invalid-basic'),
errorDetails: 'ASN.1 parsing error from signature data.',
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
});
continue;
}
const cmsContentInfo = new ContentInfo({ schema: asn1.result });
if (String(cmsContentInfo.contentType).trim() !== String(ContentInfo.SIGNED_DATA).trim()) {
// console.error('Not a SignedData content type. Actual type:', cmsContentInfo.contentType, 'Expected:', ContentInfo.SIGNED_DATA); // Removed
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('signature-invalid-basic'),
errorDetails: t('unsupported-signature-format-not-signeddata'), // New i18n key
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
});
continue;
}
const signedData = new SignedData({ schema: cmsContentInfo.content });
let signerInfoText = t('signer-info-not-available');
let certSubject = '';
let certIssuer = '';
let certValidity = t('cert-validity-not-checked');
let isValid = false;
if (signedData.signerInfos && signedData.signerInfos.length > 0) {
const signerInfo = signedData.signerInfos[0];
if (signedData.certificates && signedData.certificates.length > 0) {
let signerCertificate = null;
for (const cert of signedData.certificates) {
if (cert instanceof Certificate) {
const issuerAndSerialNumber = signerInfo.sid;
if (issuerAndSerialNumber instanceof IssuerAndSerialNumber) {
let certMatch = true;
if (cert.issuer.typesAndValues.length === issuerAndSerialNumber.issuer.typesAndValues.length) {
for (let i = 0; i < cert.issuer.typesAndValues.length; i++) {
if (cert.issuer.typesAndValues[i].type !== issuerAndSerialNumber.issuer.typesAndValues[i].type ||
cert.issuer.typesAndValues[i].value.valueBlock.value !== issuerAndSerialNumber.issuer.typesAndValues[i].value.valueBlock.value) {
certMatch = false;
break;
}
}
} else {
certMatch = false;
}
if (certMatch && cert.serialNumber.valueBlock.valueHexView.join('') === issuerAndSerialNumber.serialNumber.valueBlock.valueHexView.join('')) {
signerCertificate = cert;
break;
}
}
}
}
if (signerCertificate) {
certSubject = signerCertificate.subject.typesAndValues.map(tv => `${tv.type}=${tv.value.valueBlock.value}`).join(', ');
certIssuer = signerCertificate.issuer.typesAndValues.map(tv => `${tv.type}=${tv.value.valueBlock.value}`).join(', ');
signerInfoText = `${t('signer')}: ${certSubject}, ${t('issuer')}: ${certIssuer}`;
const notBefore = signerCertificate.notBefore.value;
const notAfter = signerCertificate.notAfter.value;
const currentDate = new Date();
certValidity = `${t('valid-from')} ${notBefore.toLocaleDateString()} ${t('to')} ${notAfter.toLocaleDateString()}`;
if (currentDate < notBefore || currentDate > notAfter) {
certValidity += ` (${t('expired-or-not-yet-valid')})`;
isValid = false; // Explicitly false if expired
} else {
certValidity += ` (${t('valid')})`;
isValid = true;
}
} else {
signerInfoText = t('signer-certificate-not-found'); // New i18n key
}
} else {
signerInfoText = t('no-certificates-in-signature'); // New i18n key
}
} else {
signerInfoText = t('no-signer-info-in-pkcs7'); // Re-use existing key, or make new one
}
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: isValid ? t('signature-valid-basic') : t('signature-invalid-basic'),
signerInfo: signerInfoText,
certificateSubject: certSubject,
certificateIssuer: certIssuer,
certificateValidity: certValidity,
errorDetails: !isValid && signerInfoText === t('signer-info-not-available') ? t('could-not-parse-signer-info') : undefined, // New i18n key
});
} catch (e) {
console.error('Error processing signature field with pkijs:', field.getName(), e);
results.push({
name: field.getName() || t('unnamed-signature-field'),
status: t('error-processing-signature'),
errorDetails: e.message,
signerInfo: t('signer-info-not-available'),
certificateSubject: '',
certificateIssuer: '',
certificateValidity: t('cert-validity-not-checked'),
});
}
}
return { results };
};
const handleVerifyDocument = async () => {
// Removed jsrsasignStatus check
if (!fileBuffer) {
setVerificationResult(t('please-select-file-to-verify'));
setDetailedResults([]);
return;
}
setVerificationResult(t('verification-in-progress'));
setDetailedResults([]);
try {
// Removed window.KJUR and window.X509 check
const pdfDoc = await PDFDocument.load(fileBuffer, { ignoreEncryption: true });
const signatureInfo = await parseSignature(pdfDoc);
if (signatureInfo.error) {
setVerificationResult(signatureInfo.error);
} else if (signatureInfo.results && signatureInfo.results.length > 0) {
setDetailedResults(signatureInfo.results);
// Overall status can be determined by checking if all signatures are valid
const allValid = signatureInfo.results.every(res => res.status === t('signature-valid-basic'));
setVerificationResult(allValid ? t('all-signatures-verified-convincing') : t('some-signatures-invalid-basic'));
} else {
setVerificationResult(t('no-signatures-processed')); // Should be caught by no-signature-found earlier
}
} catch (e) {
console.error('Error during PDF processing or signature verification:', e);
setVerificationResult(`${t('error-verifying-pdf')}: ${e.message}`);
setDetailedResults([]);
}
};
return (
<div className="container mx-auto p-6 bg-base-100 shadow-xl rounded-lg mt-10">
<style>{`
.checkmark__circle {
stroke-dasharray: 166;
stroke-dashoffset: 166;
stroke-width: 2;
stroke-miterlimit: 10;
stroke: #7ac142; /* Green color */
fill: none;
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.checkmark {
width: 56px;
height: 56px;
border-radius: 50%;
display: block;
stroke-width: 2;
stroke: #fff; /* White check path */
stroke-miterlimit: 10;
margin: 10px auto; /* Example margin */
box-shadow: inset 0px 0px 0px #7ac142;
animation: fill .4s ease-in-out .4s forwards, scale .3s ease-in-out .9s both;
}
.checkmark__check {
transform-origin: 50% 50%;
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}
@keyframes stroke {
100% {
stroke-dashoffset: 0;
}
}
@keyframes scale {
0%, 100% {
transform: none;
}
50% {
transform: scale3d(1.1, 1.1, 1);
}
}
@keyframes fill {
100% {
box-shadow: inset 0px 0px 0px 30px #7ac142;
}
}
`}</style>
<h1 className="text-3xl font-bold mb-6 text-center text-base-content">
{t('verify-document-signature')}
</h1>
<div className="mb-6 p-6 border border-base-300 rounded-lg bg-base-200/30 shadow-sm">
<label
htmlFor="document-upload"
className="block text-lg font-medium text-base-content mb-2"
>
{t('select-pdf-document')}
</label>
<input
type="file"
id="document-upload"
accept=".pdf"
onChange={handleFileChange}
className="file-input file-input-bordered file-input-primary w-full max-w-xs"
/>
{selectedFile && (
<p className="mt-2 text-sm text-base-content w-full truncate">
{t('selected-file')}: {selectedFile.name}
</p>
)}
</div>
<div className="text-center mb-6">
<button
onClick={handleVerifyDocument}
className="op-btn op-btn-primary op-btn-md"
disabled={!selectedFile || verificationResult === t('verification-in-progress')}
>
{/* Removed jsrsasignStatus === 'loading' condition for spinner */}
{verificationResult === t('verification-in-progress') ? (
<span className="loading loading-spinner"></span>
) : (
t('verify-signature')
)}
</button>
</div>
{verificationResult && verificationResult !== t('verification-in-progress') && (
<div className="mt-8 p-6 border border-base-300 rounded-lg bg-base-200 shadow-md min-h-[120px] flex flex-col items-center justify-center">
<h2 className="text-2xl font-bold mb-4 text-base-content text-center">
{t('verification-status')}
</h2>
{verificationResult === "Document Verified: All signatures have been successfully validated." && (
<div className="flex flex-col items-center my-4">
<svg className="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<circle className="checkmark__circle" cx="26" cy="26" r="25" fill="none"/>
<path className="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
</svg>
</div>
)}
<p className="text-lg text-base-content mb-4 text-center">{verificationResult}</p>
{detailedResults.length > 0 && (
<div className="w-full space-y-6">
{detailedResults.map((res, index) => {
const signerInfo = parseCertificateInfo(res.certificateSubject);
const issuerInfo = parseCertificateInfo(res.certificateIssuer);
return (
<div key={index} className="bg-white border border-gray-200 rounded-xl shadow-lg overflow-hidden">
{/* Header Section */}
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white p-6">
<div className="flex items-center space-x-3">
<span className="text-2xl">🔏</span>
<div>
<h4 className="text-xl font-bold">Signature Details</h4>
<p className="text-blue-100 text-sm">Digital Certificate Information</p>
</div>
</div>
</div>
{/* Basic Info Section */}
<div className="p-6 border-b border-gray-100">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<span className="text-sm font-medium text-gray-500 uppercase tracking-wide">Field Name</span>
<p className="mt-1 text-lg font-semibold text-gray-900 font-mono">{res.name}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-500 uppercase tracking-wide">Overall Status</span>
<div className="mt-1 flex items-center space-x-2">
<span className={`text-lg ${isSuccessStatus(res.status) ? 'text-green-600' : 'text-red-600'}`}>
{isSuccessStatus(res.status) ? '✅' : '❌'}
</span>
<span className="text-lg font-semibold text-gray-900">{res.status}</span>
</div>
</div>
</div>
</div>
{/* Signer Information Section */}
{Object.keys(signerInfo).length > 0 && (
<div className="border-b border-gray-100">
<button
onClick={() => toggleSection(index, 'signer')}
className="w-full px-6 py-4 text-left hover:bg-gray-50 transition-colors duration-200 focus:outline-none focus:bg-gray-50"
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<span className="text-xl">📇</span>
<h5 className="text-lg font-semibold text-gray-900">Signer Information</h5>
</div>
<svg
className={`w-5 h-5 text-gray-400 transition-transform duration-200 ${
collapsedSections[`${index}-signer`] ? 'transform rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
{!collapsedSections[`${index}-signer`] && (
<div className="px-6 pb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(signerInfo).map(([label, value]) => (
<div key={label} className="bg-gray-50 rounded-lg p-4">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide">{label}</span>
<p className="mt-1 text-sm font-mono text-gray-900 break-all">{value}</p>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Issuer Information Section */}
{Object.keys(issuerInfo).length > 0 && (
<div className="border-b border-gray-100">
<button
onClick={() => toggleSection(index, 'issuer')}
className="w-full px-6 py-4 text-left hover:bg-gray-50 transition-colors duration-200 focus:outline-none focus:bg-gray-50"
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<span className="text-xl">🏢</span>
<h5 className="text-lg font-semibold text-gray-900">Issuer Details</h5>
</div>
<svg
className={`w-5 h-5 text-gray-400 transition-transform duration-200 ${
collapsedSections[`${index}-issuer`] ? 'transform rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
{!collapsedSections[`${index}-issuer`] && (
<div className="px-6 pb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(issuerInfo).map(([label, value]) => (
<div key={label} className="bg-gray-50 rounded-lg p-4">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide">{label}</span>
<p className="mt-1 text-sm font-mono text-gray-900 break-all">{value}</p>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Certificate Validity Section */}
{res.certificateValidity && (
<div className="p-6 bg-gray-50">
<div className="flex items-center space-x-3 mb-4">
<span className="text-xl">🕒</span>
<h5 className="text-lg font-semibold text-gray-900">Certificate Validity</h5>
</div>
<div className="bg-white rounded-lg p-4 border">
<div className="flex items-center space-x-2">
<span className={`text-lg ${isCertificateValid(res.certificateValidity) ? 'text-green-600' : 'text-red-600'}`}>
{isCertificateValid(res.certificateValidity) ? '✅' : '❌'}
</span>
<span className="text-sm font-mono text-gray-900">{res.certificateValidity}</span>
</div>
</div>
</div>
)}
{/* Technical Details Section (if any) */}
{(res.calculatedDocumentHash || res.messageDigestInSignature || res.hashComparisonResult || res.authenticatedAttributesSignatureResult || res.errorDetails || res.certificateSubject || res.certificateIssuer) && (
<div className="p-6 bg-gray-50 border-t">
<details className="group">
<summary className="flex items-center justify-between cursor-pointer text-sm font-medium text-gray-700 hover:text-gray-900">
<span>🔧 Technical Details</span>
<svg className="w-4 h-4 transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</summary>
<div className="mt-4 space-y-3">
{/* Raw Certificate Data */}
{res.certificateSubject && (
<div className="bg-white rounded p-3 border">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide block mb-1">Raw Certificate Subject</span>
<code className="text-xs text-gray-800 break-all bg-gray-100 p-2 rounded block">{res.certificateSubject}</code>
</div>
)}
{res.certificateIssuer && (
<div className="bg-white rounded p-3 border">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide block mb-1">Raw Certificate Issuer</span>
<code className="text-xs text-gray-800 break-all bg-gray-100 p-2 rounded block">{res.certificateIssuer}</code>
</div>
)}
{res.calculatedDocumentHash && res.calculatedDocumentHash !== t('not-available') && res.calculatedDocumentHash !== t('not-calculated') && (
<div className="bg-white rounded p-3 border">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide block mb-1">Calculated Document Hash</span>
<code className="text-xs text-gray-800 break-all bg-gray-100 p-2 rounded block">{res.calculatedDocumentHash}</code>
</div>
)}
{res.messageDigestInSignature && res.messageDigestInSignature !== t('not-available') && res.messageDigestInSignature !== t('not-found-in-signature') && (
<div className="bg-white rounded p-3 border">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide block mb-1">Message Digest in Signature</span>
<code className="text-xs text-gray-800 break-all bg-gray-100 p-2 rounded block">{res.messageDigestInSignature}</code>
</div>
)}
{res.hashComparisonResult && res.hashComparisonResult !== t('not-performed') && (
<div className="bg-white rounded p-3 border">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide block mb-1">Hash Comparison</span>
<span className="text-sm text-gray-800">{res.hashComparisonResult}</span>
</div>
)}
{res.authenticatedAttributesSignatureResult && res.authenticatedAttributesSignatureResult !== t('not-performed') && (
<div className="bg-white rounded p-3 border">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide block mb-1">Attributes Signature Verification</span>
<span className="text-sm text-gray-800">{res.authenticatedAttributesSignatureResult}</span>
</div>
)}
{res.errorDetails && (
<div className="bg-red-50 border border-red-200 rounded p-3">
<span className="text-xs font-medium text-red-600 uppercase tracking-wide block mb-1">Error Details</span>
<span className="text-sm text-red-800">{res.errorDetails}</span>
</div>
)}
</div>
</details>
</div>
)}
</div>
);
})}
</div>
)}
</div>
)}
{verificationResult === t('verification-in-progress') && (
<div className="mt-8 p-4 border border-base-300 rounded-lg bg-base-200 min-h-[100px] flex justify-center items-center">
<span className="loading loading-lg loading-dots"></span>
</div>
)}
{!verificationResult && !selectedFile && (
<div className="mt-8 p-4 border border-base-300 rounded-lg bg-base-200 min-h-[100px] flex justify-center items-center"> {/* Added flex for centering */}
<p className="text-base-content/60 italic text-center">{t('verification-results-will-appear-here')}</p>
</div>
)}
</div>
);
};
export default VerifyDocument;
@@ -91,7 +91,7 @@ function DownloadPdfZip(props) {
title={t("download-files")}
handleClose={() => props.setIsDownloadModal(false)}
>
<div className="p-[20px] h-full">
<div className="p-[20px] h-full text-base-content">
{downloadType.map((data, ind) => {
return (
<label
+140 -64
View File
@@ -7,11 +7,12 @@ import ModalUi from "./ModalUi";
import AddSigner from "../components/AddSigner";
import {
emailRegex,
iconColor,
} from "../constant/const";
import Alert from "./Alert";
import Tooltip from "./Tooltip";
import { RWebShare } from "react-web-share";
import Tour from "reactour";
import ShareButton from "./ShareButton";
import Tour from "../primitives/Tour";
import Parse from "parse";
import {
copytoData,
@@ -38,9 +39,12 @@ import { useTranslation } from "react-i18next";
import DownloadPdfZip from "./DownloadPdfZip";
import * as XLSX from "xlsx";
import EditContactForm from "../components/EditContactForm";
import { useElSize } from "../hook/useElSize";
const ReportTable = (props) => {
const copyUrlRef = useRef(null);
const titleRef = useRef(null);
const titleElement = useElSize(titleRef);
const appName =
"OpenSign™";
const drivename = appName === "OpenSign™" ? "OpenSign™" : "";
@@ -92,6 +96,12 @@ const ReportTable = (props) => {
const startIndex = (currentPage - 1) * props.docPerPage;
const { isMoreDocs, setIsNextRecord } = props;
useEffect(() => {
if (props.isSearchResult) {
setCurrentPage(1);
}
}, [props.isSearchResult]);
const getPaginationRange = () => {
const totalPageNumbers = 7; // Adjust this value to show more/less page numbers
const pages = [];
@@ -398,7 +408,7 @@ const ReportTable = (props) => {
setIsResendMail({ [item.objectId]: true });
} else if (act.action === "bulksend") {
handleBulkSend(item);
} else if (act.action === "sharewith") {
} else if (act.action === "sharewithteam") {
if (item?.SharedWith && item?.SharedWith.length > 0) {
// below code is used to get existing sharewith teams and formated them as per react-select
const formatedList = item?.SharedWith.map((x) => ({
@@ -679,7 +689,7 @@ const ReportTable = (props) => {
receiver_phone: userDetails?.Phone || "",
expiry_date: localExpireDate,
company_name: doc.ExtUserPtr.Company,
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>`
signing_url: signPdf
};
const res = replaceMailVaribles(subject, "", variables);
setMail((prev) => ({ ...prev, subject: res.subject }));
@@ -710,7 +720,7 @@ const ReportTable = (props) => {
receiver_phone: userDetails?.Phone || "",
expiry_date: localExpireDate,
company_name: doc.ExtUserPtr.Company,
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>`
signing_url: signPdf
};
const res = replaceMailVaribles("", body, variables);
@@ -754,7 +764,7 @@ const ReportTable = (props) => {
receiver_phone: user?.signerPtr?.Phone || "",
expiry_date: localExpireDate,
company_name: doc?.ExtUserPtr?.Company || "",
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>`
signing_url: signPdf
};
const subject =
doc?.RequestSubject ||
@@ -763,7 +773,7 @@ const ReportTable = (props) => {
const body =
doc?.RequestBody ||
doc?.ExtUserPtr?.TenantId?.RequestBody ||
`<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body><p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}} has requested you to review and sign <b>"{{document_title}}"</b>.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p>{{signing_url}}</p><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br></body> </html>`;
`<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body><p>Hi {{receiver_name}},</p><br><p>We hope this email finds you well. {{sender_name}} has requested you to review and sign <b>"{{document_title}}"</b>.</p><p>Your signature is crucial to proceed with the next steps as it signifies your agreement and authorization.</p><br><p><a href='{{signing_url}}' rel='noopener noreferrer' target='_blank'>Sign here</a></p><br><br><p>If you have any questions or need further clarification regarding the document or the signing process, please contact the sender.</p><br><p>Thanks</p><p> Team ${appName}</p><br></body> </html>`;
const res = replaceMailVaribles(subject, body, variables);
setMail((prev) => ({ ...prev, subject: res.subject, body: res.body }));
setIsNextStep({ [user.Id]: true });
@@ -907,7 +917,6 @@ const ReportTable = (props) => {
setActLoader({});
}
};
const handleUpdateExpiry = async (e, item) => {
e.preventDefault();
e.stopPropagation();
@@ -999,7 +1008,7 @@ const ReportTable = (props) => {
? "op-border-primary op-text-primary"
: x.Activity === "VIEWED"
? "border-green-400 text-green-400"
: "border-black text-black"
: "border-base-content text-base-content"
} focus:outline-none border-2 w-[60px] h-[30px] text-[11px] rounded-full`}
>
{x?.Activity?.toUpperCase() || "-"}
@@ -1314,7 +1323,6 @@ const ReportTable = (props) => {
try {
const params = { docId: doc?.objectId };
const templateRes = await Parse.Cloud.run("saveastemplate", params);
// console.log("templateRes ", templateRes);
setTemplateId(templateRes?.id);
setIsSuccess({ [doc.objectId]: true });
} catch (err) {
@@ -1394,6 +1402,12 @@ const ReportTable = (props) => {
setActLoader({});
}
};
const restrictBtn = (item, act) => {
return item.IsSignyourself && act.action === "recreatedocument"
? true
: false;
};
return (
<div className="relative">
{Object.keys(actLoader)?.length > 0 && (
@@ -1411,12 +1425,15 @@ const ReportTable = (props) => {
onRequestClose={closeTour}
steps={props.tourData}
isOpen={isTour}
// rounded={5}
rounded={5}
closeWithMask={false}
/>
</>
)}
<div className="flex flex-row items-center justify-between my-2 mx-3 text-[20px] md:text-[23px]">
<div
ref={titleRef}
className="flex flex-row items-center justify-between my-2 mx-3 text-[20px] md:text-[23px]"
>
<div className="font-light">
{t(`report-name.${props.ReportName}`)}{" "}
{props.report_help && (
@@ -1428,26 +1445,68 @@ const ReportTable = (props) => {
</span>
)}
</div>
<div className="flex flex-row justify-center items-center gap-3">
{props.isImport && (
<div className="cursor-pointer" onClick={() => handleImportBtn()}>
<i className="fa-light fa-upload op-text-secondary text-[23px] md:text-[30px]"></i>
<div className="flex flex-row justify-center items-center gap-3 mb-2">
{/* Search input for report bigger in width */}
{titleElement?.width > 500 && (
<div className="flex">
<input
type="search"
value={props.searchTerm}
onChange={props.handleSearchChange}
placeholder={
props.ReportName === "Contactbook"
? t("search-contacts")
: isTemplateReport
? t("search-templates")
: t("search-documents")
}
onPaste={props.handleSearchPaste}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-64 text-xs"
/>
</div>
)}
{/* contact import */}
{props.isImport && (
<div
className="cursor-pointer flex"
onClick={() => handleImportBtn()}
>
<i className="fa-light fa-upload op-text-secondary text-[23px] md:text-[25px]"></i>
</div>
)}
{/* add contact form */}
{props.form && (
<div
className="cursor-pointer"
className="cursor-pointer flex"
onClick={() => handleContactFormModal()}
>
<i className="fa-light fa-square-plus text-accent text-[30px] md:text-[35px]"></i>
<i className="fa-light fa-square-plus text-accent text-[30px] md:text-[32px]"></i>
</div>
)}
{/* create template form */}
{isTemplateReport && (
<i
<div
data-tut="reactourFirst"
className="cursor-pointer flex"
onClick={() => navigate("/form/template")}
className="cursor-pointer fa-light fa-square-plus text-accent text-[30px] md:text-[35px]"
></i>
>
<i className="cursor-pointer fa-light fa-square-plus text-accent text-[30px] md:text-[32px]"></i>
</div>
)}
{/* search icon/magnifer icon */}
{titleElement?.width < 500 && (
<button
className="flex justify-center items-center focus:outline-none rounded-md text-[18px]"
aria-label="Search"
onClick={() =>
props.setMobileSearchOpen(!props.mobileSearchOpen)
}
>
<i
style={{ color: `${iconColor}` }}
className="fa-solid fa-magnifying-glass"
></i>
</button>
)}
<ModalUi
isOpen={isModal?.export}
@@ -1536,6 +1595,19 @@ const ReportTable = (props) => {
</ModalUi>
</div>
</div>
{/* Search input for report smalle in width */}
{titleElement?.width < 500 && props.mobileSearchOpen && (
<div className="top-full left-0 w-full bg-white px-3 pt-1 pb-3">
<input
type="search"
value={props.searchTerm}
onChange={props.handleSearchChange}
placeholder={t("search-documents")}
onPaste={props.handleSearchPaste}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
)}
<div
className={`overflow-auto w-full border-b ${
props.List?.length > 0
@@ -1602,7 +1674,7 @@ const ReportTable = (props) => {
handleClose={handleClose}
>
<div className="m-[20px]">
<div className="text-lg font-normal text-black">
<div className="text-lg font-normal text-base-content">
{t("contact-delete-alert")}
</div>
<hr className="bg-[#ccc] mt-4 " />
@@ -1837,33 +1909,39 @@ const ReportTable = (props) => {
{isOption[item.objectId] &&
act.action === "option" && (
<ul className="absolute -right-1 top-auto z-[70] w-max op-dropdown-content op-menu shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
{act.subaction?.map((subact) => (
<li
key={subact.btnId}
onClick={() =>
handleActionBtn(
subact,
item
)
}
title={t(
`btnLabel.${subact.hoverLabel}`
)}
>
<span>
<i
className={`${subact.btnIcon} mr-1.5`}
></i>
{subact.btnLabel && (
<span className="text-[13px] capitalize font-medium">
{t(
`btnLabel.${subact.btnLabel}`
{act.subaction?.map(
(subact) =>
!restrictBtn(
item,
subact
) && (
<li
key={subact.btnId}
onClick={() =>
handleActionBtn(
subact,
item
)
}
title={t(
`btnLabel.${subact.hoverLabel}`
)}
>
<span>
<i
className={`${subact.btnIcon} mr-1.5`}
></i>
{subact.btnLabel && (
<span className="text-[13px] capitalize font-medium">
{t(
`btnLabel.${subact.btnLabel}`
)}
</span>
)}
</span>
)}
</span>
</li>
))}
</li>
)
)}
</ul>
)}
</div>
@@ -1972,7 +2050,7 @@ const ReportTable = (props) => {
</div>
) : (
<div className="m-[20px]">
<div className="text-lg font-normal text-black">
<div className="text-lg font-normal text-base-content">
{t("save-as-template-?")}
</div>
<hr className="bg-[#ccc] mt-3" />
@@ -2010,7 +2088,7 @@ const ReportTable = (props) => {
</label>
<input
type="date"
className="rounded-full mb-2 bg-base-300 w-full px-4 py-2 text-black border-2 hover:border-spacing-2"
className="rounded-full mb-2 bg-base-300 w-full px-4 py-2 text-base-content border-2 hover:border-spacing-2"
defaultValue={
item?.ExpiryDate?.iso?.split("T")?.[0]
}
@@ -2120,7 +2198,7 @@ const ReportTable = (props) => {
handleClose={handleClose}
>
<div className="m-[20px]">
<div className="text-lg font-normal text-black">
<div className="text-lg font-normal text-base-content">
{t("delete-document-alert")}
</div>
<hr className="bg-[#ccc] mt-4" />
@@ -2177,23 +2255,21 @@ const ReportTable = (props) => {
{shareUrls.map((share, i) => (
<div
key={i}
className="text-sm font-normal text-black flex my-2 justify-between items-center"
className="text-sm font-normal text-base-content flex my-2 justify-between items-center"
>
<span className="w-[150px] mr-[5px] md:mr-0 md:w-[300px] whitespace-nowrap overflow-hidden text-ellipsis text-sm font-semibold">
{share.email}
</span>
<div className="flex items-center gap-2">
<RWebShare
data={{
url: share.url,
title: "Sign url"
}}
<ShareButton
title={t("sign-url")}
text={t("sign-url")}
url={share.url}
className="op-btn op-btn-primary op-btn-outline op-btn-xs md:op-btn-sm "
>
<button className="op-btn op-btn-primary op-btn-outline op-btn-xs md:op-btn-sm ">
<i className="fa-light fa-share-from-square"></i>{" "}
{t("btnLabel.Share")}
</button>
</RWebShare>
<i className="fa-light fa-share-from-square"></i>
{t("btnLabel.Share")}
</ShareButton>
<button
className="op-btn op-btn-primary op-btn-outline op-btn-xs md:op-btn-sm"
onClick={() => copytoclipboard(share)}
@@ -2221,14 +2297,14 @@ const ReportTable = (props) => {
handleClose={handleClose}
>
<div className="m-[20px]">
<div className="text-sm md:text-lg font-normal text-black">
<div className="text-sm md:text-lg font-normal text-base-content">
{t("revoke-document-alert")}
</div>
<div className="mt-2">
<textarea
rows={3}
placeholder="Reason (optional)"
className="px-4 op-textarea op-textarea-bordered focus:outline-none hover:border-base-content w-full text-xs"
className="px-4 op-textarea op-textarea-bordered text-base-content focus:outline-none hover:border-base-content w-full text-xs"
value={reason}
onChange={(e) => setReason(e.target.value)}
></textarea>
@@ -2339,7 +2415,7 @@ const ReportTable = (props) => {
)}
{Object?.keys(isNextStep) <= 0 && (
<div className="flex justify-between items-center gap-2 my-2 px-3">
<div className="text-black">
<div className="text-base-content">
{user?.signerPtr?.Name || "-"}{" "}
{`<${
user?.email
@@ -25,12 +25,10 @@ const LinkUserModal = (props) => {
handleClose={props.closePopup}
>
<SelectSigners
details={props.handleAddUser}
{...props}
closePopup={props.closePopup}
signersData={props?.signersData}
isContact={isContact}
setIsContact={setIsContact}
handleUnlinkSigner={props.handleUnlinkSigner}
isExistSigner={isExistSigner}
/>
{isContact && (
+7 -2
View File
@@ -8,13 +8,18 @@ const ModalUi = ({
handleClose,
showHeader = true,
showClose = true,
reduceWidth
reduceWidth,
position
}) => {
const width = reduceWidth;
const isBottom = position === "bottom" ? "items-end pb-2" : "";
return (
<>
{isOpen && (
<dialog id="selectSignerModal" className="op-modal op-modal-open">
<dialog
id="selectSignerModal"
className={`${isBottom} op-modal op-modal-open`}
>
<div
className={`${
width || "md:min-w-[500px]"
@@ -43,7 +43,7 @@ function CustomModal(props) {
</h3>
{!isExtendExpiry && (
<div className="p-[10px] px-[20px]">
<p className="text-[15px]">{props.bodyMssg && props.bodyMssg}</p>
<p className="text-[15px] text-base-content">{props.bodyMssg && props.bodyMssg}</p>
</div>
)}
{!isExtendExpiry && (
@@ -68,7 +68,7 @@ function CustomModal(props) {
)}
{props.footerMessage && (
<>
<div className="mx-3">
<div className="mx-3 text-base-content">
<textarea
rows={3}
placeholder="Reason (optional)"
@@ -108,7 +108,7 @@ function CustomModal(props) {
</label>
<input
type="date"
className="rounded-full bg-base-300 w-full px-4 py-2 text-black border-2 hover:border-spacing-2"
className="rounded-full bg-base-300 w-full px-4 py-2 text-base-content border-2 hover:border-spacing-2"
defaultValue={props?.doc?.ExpiryDate?.iso?.split("T")?.[0]}
onChange={(e) => setExpiryDate(e.target.value)}
/>
@@ -0,0 +1,178 @@
import { useCallback, useState } from "react";
import ModalUi from "./ModalUi";
function useShare({ title, text, url }) {
const [error, setError] = useState(null);
const isSupported = typeof navigator !== "undefined" && !!navigator.share;
const share = useCallback(async () => {
if (!isSupported) {
setError(new Error("Web Share API not supported"));
return;
}
try {
await navigator.share({ title, text, url });
} catch (err) {
// User may have cancelled, or another error occurred
setError(err);
}
}, [isSupported, title, text, url]);
return { share, isSupported, error };
}
/**
* A customizable ShareButton component.
* If `children` are provided, they are used as the trigger element;
* otherwise, a default share button is rendered.
*
* @param {object} props
* @param {string} props.title - Title for sharing
* @param {string} props.text - Text for sharing
* @param {string} props.url - URL to share
* @param {string} [props.className] - Optional styling class
* @param {React.ReactNode} [props.children] - Custom trigger element
*/
export default function ShareButton({ title, text, url, className, children }) {
const { share, isSupported, error } = useShare({ title, text, url });
const [isPopupOpen, setPopupOpen] = useState(false);
// Native Web Share API supported
if (isSupported) {
return (
<button
onClick={share}
className={className}
aria-label="Share this page"
>
{children || "🔗 Share"}
</button>
);
}
// Fallback: trigger opens popup
const subject = encodeURIComponent(title || text);
const body = encodeURIComponent(`${text}\n\n${url}`);
return (
<>
{/* React Fragment */}
<button
onClick={() => setPopupOpen(true)}
className={className}
aria-label="Open share options"
>
{children || "🔗 Share"}
</button>
{isPopupOpen && (
<ModalUi
isOpen
title={
<>
<i class="fa-solid fa-share-from-square"></i> Share
</>
}
handleClose={() => setPopupOpen(false)}
>
{error && <p style={{ color: "red" }}>Error: {error.message}</p>}
<div className="grid grid-cols-1 md:grid-cols-2 justify-items-start text-lg p-[20px]">
{/* Copy Link */}
<button
onClick={() => navigator.clipboard.writeText(url)}
className="m-2 op-btn op-btn-primary op-btn-outline op-btn-xs md:op-btn-sm w-[190px]"
>
<i className="fa-solid fa-clipboard fa-lg"></i> Copy to clipboard
</button>
{/* Twitter */}
<button
onClick={() =>
window.open(
`https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}`,
"_blank",
"noopener"
)
}
className="m-2 op-btn op-btn-primary op-btn-outline op-btn-xs md:op-btn-sm w-[190px]"
>
<i className="fa-brands fa-square-x-twitter fa-lg"></i> Share on
Twitter
</button>
{/* Facebook */}
<button
onClick={() =>
window.open(
`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
"_blank",
"noopener"
)
}
className="m-2 op-btn op-btn-primary op-btn-outline op-btn-xs md:op-btn-sm w-[190px]"
>
<i className="fa-brands fa-square-facebook fa-lg"></i> Share on
Facebook
</button>
{/* WhatsApp */}
<button
onClick={() =>
window.open(
`https://wa.me/?text=${encodeURIComponent(text + " " + url)}`,
"_blank",
"noopener"
)
}
className="m-2 op-btn op-btn-primary op-btn-outline op-btn-xs md:op-btn-sm w-[190px]"
>
<i className="fa-brands fa-square-whatsapp fa-lg"></i> Share on
WhatsApp
</button>
{/* Gmail (Web) */}
<button
onClick={() =>
window.open(
`https://mail.google.com/mail/?view=cm&fs=1&su=${subject}&body=${body}`,
"_blank",
"noopener"
)
}
className="m-2 op-btn op-btn-primary op-btn-outline op-btn-xs md:op-btn-sm w-[190px]"
>
<i className="fa-solid fa-envelope fa-lg"></i> Share via Gmail
</button>
{/* Microsoft Teams */}
<button
onClick={() =>
window.open(
`https://teams.microsoft.com/l/share?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`,
"_blank",
"noopener"
)
}
className="m-2 op-btn op-btn-primary op-btn-outline op-btn-xs md:op-btn-sm w-[190px]"
>
<i className="fa-brands fa-microsoft fa-lg"></i> Share on Teams
</button>
{/* Outlook Web */}
<button
onClick={() =>
window.open(
`https://outlook.live.com/owa/?path=/mail/action/compose&subject=${subject}&body=${body}`,
"_blank",
"noopener"
)
}
className="m-2 op-btn op-btn-primary op-btn-outline op-btn-xs md:op-btn-sm w-[190px]"
>
<i className="fa-solid fa-envelope-open-text fa-lg"></i> Share via
Outlook
</button>
</div>
</ModalUi>
)}
</>
);
}
+33
View File
@@ -0,0 +1,33 @@
import ReactTour from "reactour";
export default function Tour({
steps,
isOpen,
rounded,
className,
showNumber,
closeWithMask,
onRequestClose,
showNavigation,
showCloseButton,
showNavigationNumber,
disableKeyboardNavigation
}) {
const radius = rounded || 5;
return (
<ReactTour
className={className}
steps={steps}
isOpen={isOpen}
rounded={radius}
scrollOffset={-100}
closeWithMask={closeWithMask}
disableKeyboardNavigation={disableKeyboardNavigation}
showCloseButton={showCloseButton}
onRequestClose={onRequestClose}
showNumber={showNumber}
showNavigation={showNavigation}
showNavigationNumber={showNavigationNumber}
/>
);
}
@@ -0,0 +1,52 @@
// reducers/widgetSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
isShowModal: false,
saveSignCheckbox: {
isVisible: false,
signId: ""
},
signatureTypes: null,
defaultSignImg: null,
myInitial: null,
lastIndex: ""
};
const widgetSlice = createSlice({
name: "widget",
initialState,
reducers: {
setIsShowModal: (state, action) => {
state.isShowModal = action.payload;
},
setSaveSignCheckbox: (state, action) => {
state.saveSignCheckbox = action.payload;
},
setSignatureTypes: (state, action) => {
state.signatureTypes = action.payload;
},
setDefaultSignImg: (state, action) => {
state.defaultSignImg = action.payload;
},
setMyInitial: (state, action) => {
state.myInitial = action.payload;
},
setLastIndex: (state, action) => {
state.lastIndex = action.payload;
},
resetWidgetState: () => initialState
}
});
export const {
setIsShowModal,
setSaveSignCheckbox,
setSignatureTypes,
setMyInitial,
resetWidgetState,
setDefaultSignImg,
setLastIndex
} = widgetSlice.actions;
export default widgetSlice.reducer;
+4 -6
View File
@@ -1,18 +1,16 @@
// import { createStore, applyMiddleware } from "redux";
// import thunk from "redux-thunk";
// import reducers from "./reducers";
// export const store = createStore(reducers, applyMiddleware(thunk));
import { configureStore } from "@reduxjs/toolkit";
import infoReducer from "./reducers/infoReducer";
import ShowTenant from "./reducers/ShowTenant";
import TourStepsReducer from "./reducers/TourStepsReducer";
import showHeader from "./reducers/showHeader";
import widgetReducer from "./reducers/widgetSlice";
export const store = configureStore({
reducer: {
appInfo: infoReducer,
TourSteps: TourStepsReducer,
ShowTenant,
showHeader
showHeader,
widget: widgetReducer
}
});
+5 -1
View File
@@ -10,6 +10,10 @@
background-position: right 0.7rem top 50%;
background-size: 1rem auto;
}
[data-theme="opensigndark"] .validationlist{
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAAXNSR0IArs4c6QAAAQ1JREFUSEvtlDtKBTEARc/Fys8OXumndhl2ggt4jTuwsVJQK92DtY1Yuw3bp2jlDvxUcn2BDIQ4k2Sa1ziBNMmdHO4JGbGCoRUwmCCjLP8DXbYPgD3gVtLnkB/bm8AxsJD02Jfr1WX7EJjHD16BC0kf+QG2N4BzYDfu3Um6z3N/ILb3gbMs+AJcpo1igwDYybIh95Su9UFmwPVyrg+BCoBv4FTSexESNm1vRw1bGegNuAFOEkVdJABCi0VVVxewHS49aAve0/EDrGVrX8u1qz5AyBXfSaFRyhhs0IWqj7ECKjZohhTuqAlQ1ZU6yRo1A0ZBYqPw6I6AB0nPrX/J6p20HlTKTZBRFiddo3T9ArZOWBrGcf52AAAAAElFTkSuQmCC");
}
@media (max-width: 375px) {
.validationlist {
@@ -17,7 +21,7 @@
}
}
@media (min-width:375px) and (max-width: 767px) {
@media (min-width: 375px) and (max-width: 767px) {
.validationlist {
background-position: right 1rem top 50%;
}
@@ -0,0 +1,143 @@
/* VS Code Dark Theme Improvements for OpenSign */
/* Better disabled button styling for dark mode */
[data-theme="opensigndark"] {
/* Primary button disabled state */
.op-btn-primary:disabled {
background-color: #3C3C3C !important;
color: #CCCCCC !important;
border-color: #565656 !important;
opacity: 1 !important;
cursor: not-allowed !important;
}
.op-btn-primary:disabled:hover {
background-color: #3C3C3C !important;
color: #CCCCCC !important;
border-color: #565656 !important;
transform: none !important;
box-shadow: none !important;
}
/* Secondary button disabled state */
.op-btn-secondary:disabled {
background-color: #2A2A2A !important;
color: #999999 !important;
border-color: #444444 !important;
opacity: 1 !important;
cursor: not-allowed !important;
}
/* Ghost button disabled state */
.op-btn-ghost:disabled {
background-color: transparent !important;
color: #666666 !important;
border-color: #444444 !important;
opacity: 1 !important;
cursor: not-allowed !important;
}
/* Better icon visibility for various states */
.icon-disabled,
.fa-light.text-gray-400,
.fa-light.text-gray-500 {
color: #858585 !important;
}
.icon-visible,
.nav-icon,
.folder-icon {
color: #CCCCCC !important;
}
/* Muted/inactive icons with better visibility */
.muted-icon,
.inactive-icon {
color: #999999 !important;
}
/* Hover states for better interactivity */
.hover\\:bg-gray-200:hover {
background-color: #2A2A2A !important;
}
.hover\\:text-gray-600:hover {
color: #E5E7EB !important;
}
/* Form elements in disabled state */
.op-input:disabled,
.op-select:disabled,
.op-textarea:disabled {
background-color: #2A2A2A !important;
color: #999999 !important;
border-color: #444444 !important;
cursor: not-allowed !important;
}
/* Dropdown menu items */
.dropdown-item {
color: #E5E7EB !important;
}
.dropdown-item:hover {
background-color: #2A2A2A !important;
color: #FFFFFF !important;
}
/* Better text contrast for various elements */
.text-gray-600 {
color: #CCCCCC !important;
}
.text-gray-500 {
color: #999999 !important;
}
.text-gray-400 {
color: #858585 !important;
}
/* Status indicators with better visibility */
.status-badge {
box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1) !important;
}
/* Tooltip improvements */
.op-tooltip {
background-color: #1F2937 !important;
color: #E5E7EB !important;
border-color: #4B5563 !important;
}
/* Card and panel borders */
.op-card,
.border-gray-300 {
border-color: #2C2C2C !important;
}
/* Loading states */
.opacity-50 {
opacity: 0.7 !important;
}
/* Focus states for better accessibility */
.op-btn:focus-visible,
.op-input:focus-visible,
.op-select:focus-visible {
outline: 2px solid #007ACC !important;
outline-offset: 2px !important;
}
}
/* Ensure these styles don't affect light mode */
[data-theme="opensigncss"] {
/* Keep original colors for light mode */
.icon-disabled {
color: #9CA3AF;
}
.icon-visible {
color: #6B7280;
}
}
+23
View File
@@ -1,3 +1,26 @@
/* Dark mode support for custom warning in Managesign */
[data-theme="opensigndark"] .customwarning {
background-color: #374151 !important;
color: #E5E7EB !important;
border-color: #4B5563 !important;
}
[data-theme="opensigndark"] .customwarning::before {
border-color: transparent transparent #4B5563 transparent !important;
}
/* Dark mode support for signature management warning */
[data-theme="opensigndark"] .signWarning {
background-color: #374151 !important;
color: #E5E7EB !important;
}
/* Ensure the Managesign page background is consistent */
[data-theme="opensigndark"] .managesign-container {
background-color: #121212 !important;
color: #F3F4F6 !important;
}
.customwarning {
position: absolute;
padding: 8px;
@@ -204,6 +204,15 @@ a {
align-items: center;
}
/* Dark mode support for HoverCard */
[data-theme="opensigndark"] .HoverCardContent {
background-color: #1F2937;
color: #E5E7EB;
box-shadow:
hsl(0 0% 0% / 50%) 0px 10px 38px -10px,
hsl(0 0% 0% / 30%) 0px 10px 20px -15px;
}
.HoverCardContent[data-side="top"] {
animation-name: slideDownAndFade;
}
@@ -224,6 +233,11 @@ a {
fill: white;
}
/* Dark mode support for HoverCard arrow */
[data-theme="opensigndark"] .HoverCardArrow {
fill: #1F2937;
}
@keyframes slideUpAndFade {
0% {
opacity: 0;
+66 -4
View File
@@ -6,7 +6,7 @@
.react-datepicker__input-container {
position: initial !important;
}
.select-none-cls{
.select-none-cls {
-webkit-user-select: none;
/* Disable text selection in WebKit browsers */
-moz-user-select: none;
@@ -18,7 +18,6 @@
-moz-user-select: none;
user-select: none;
}
.radioButton {
width: 100%;
height: 100%;
@@ -31,14 +30,34 @@
}
.signatureCanvas {
width: 460px;
height: 184px;
width: 440px;
height: 167px;
}
.tabWidth {
border: 1px solid #f3f4f6;
background-color: #f3f4f6;
width: 440px;
}
[data-theme="opensigndark"] .tabWidth {
border: 1px solid #1f2937 !important;
background-color: #1f2937 !important;
}
.intialSignatureCanvas {
width: 150px;
height: 150px;
}
.checked-radio::after {
content: "";
position: absolute;
top: 25%;
left: 25%;
width: 50%;
height: 50%;
border-radius: 9999px;
background-color: #111111; /* blue-500 */
}
.intialSignature {
border: 2px solid #888;
@@ -47,6 +66,40 @@
height: 183px;
}
/* Dark mode support for initials box in /managesign */
[data-theme="opensigndark"] .intialSignature {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
[data-theme="opensigndark"] .intialSignatureCanvas {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
/* Also support signature canvas for consistency */
[data-theme="opensigndark"] .signatureCanvas {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
/* Dark mode support for initials box in /managesign */
[data-theme="opensigndark"] .intialSignature {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
[data-theme="opensigndark"] .intialSignatureCanvas {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
/* Also support signature canvas for consistency */
[data-theme="opensigndark"] .signatureCanvas {
background-color: #1f2937 !important;
border-color: #4b5563 !important;
}
.penContainerDefault {
width: 460px;
}
@@ -388,6 +441,9 @@ option {
width: 300px;
height: 120px;
}
.tabWidth {
width: 300px;
}
.penContainerDefault {
width: 300px;
@@ -411,6 +467,9 @@ option {
width: 280px;
height: 112px;
}
.tabWidth {
width: 280px;
}
.penContainerDefault {
width: 280px;
@@ -432,6 +491,9 @@ option {
width: 230px;
height: 92px;
}
.tabWidth {
width: 230px;
}
.penContainerDefault {
width: 230px;
+99 -2
View File
@@ -6,11 +6,67 @@ module.exports = {
},
plugins: [
require("daisyui"),
function ({ addUtilities }) {
function ({ addUtilities, theme }) {
addUtilities({
// Prevent iOS long-press popup
".touch-callout-none": {
"-webkit-touch-callout": "none"
},
// VS Code-style disabled button for all themes
".op-btn-vscode-disabled": {
"background-color": "#3C3C3C !important",
color: "#CCCCCC !important",
"border-color": "#565656 !important",
cursor: "not-allowed !important",
opacity: "1 !important",
"&:hover": {
"background-color": "#3C3C3C !important",
color: "#CCCCCC !important",
"border-color": "#565656 !important",
transform: "none !important"
}
},
// Dark mode icon improvements using DaisyUI theme detection
'[data-theme="opensigndark"] .icon-improved': {
color: "#CCCCCC !important"
},
'[data-theme="opensigndark"] .icon-muted': {
color: "#999999 !important"
},
'[data-theme="opensigndark"] .icon-disabled': {
color: "#858585 !important"
},
// Gray text improvements for dark mode
'[data-theme="opensigndark"] .text-gray-500': {
color: "#CCCCCC !important"
},
'[data-theme="opensigndark"] .text-gray-400': {
color: "#999999 !important"
},
'[data-theme="opensigndark"] .text-gray-600': {
color: "#CCCCCC !important"
},
// CSS variable utilities that work with arbitrary values
".icon-themed": {
color: "var(--icon-color)"
},
".icon-themed-muted": {
color: "var(--icon-color-muted)"
},
".icon-themed-disabled": {
color: "var(--icon-color-disabled)"
},
".btn-themed-disabled": {
"background-color": "var(--btn-disabled-bg)",
color: "var(--btn-disabled-color)",
"border-color": "var(--btn-disabled-border)",
cursor: "not-allowed",
"&:hover": {
"background-color": "var(--btn-disabled-bg)",
color: "var(--btn-disabled-color)",
"border-color": "var(--btn-disabled-border)",
transform: "none"
}
}
});
}
@@ -18,7 +74,48 @@ module.exports = {
daisyui: {
// themes: true,
themes: [
"dark",
{
opensigndark: {
primary: "#007ACC", // VS Code blue - CTA & highlight color
"primary-content": "#FFFFFF",
secondary: "#1F2937", // Sidebar background (darker slate)
"secondary-content": "#E5E7EB",
accent: "#4A9EFF", // Lighter VS Code blue for hover, minor CTA
"accent-content": "#FFFFFF",
neutral: "#3C3C3C", // VS Code inactive/disabled element background
"neutral-content": "#CCCCCC", // VS Code inactive text color
"base-100": "#121212", // App background
"base-200": "#181818", // Slight elevation (cards)
"base-300": "#1E1E1E", // Further elevated items (panels)
"base-content": "#F3F4F6", // Main text color (soft white)
info: "#2563EB", // For info panels like "Out for signature"
success: "#22C55E", // Optional: for completed docs or alerts
warning: "#FBBF24",
error: "#EF4444",
"--rounded-btn": "1.9rem",
"--tab-border": "2px",
"--tab-radius": "0.7rem",
// Custom CSS variables for icon and button states
"--icon-color": "#CCCCCC",
"--icon-color-muted": "#999999",
"--icon-color-disabled": "#858585",
"--btn-disabled-bg": "#3C3C3C",
"--btn-disabled-color": "#CCCCCC",
"--btn-disabled-border": "#565656",
// Optional polish
"--navbar-padding": "0.8rem",
"--border-color": "#2C2C2C", // Card/table separation
"--tooltip-color": "#1F2937"
}
},
{
opensigncss: {
primary: "#002864",
@@ -1,41 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
import rollupNodePolyFill from "rollup-plugin-node-polyfills";
export default defineConfig({
plugins: [react()],
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
process: JSON.stringify({
env: {
NODE_ENV: "production"
}
})
},
build: {
lib: {
entry: path.resolve(__dirname, "src/ee/script/PublicTemplate.jsx"),
name: "PublicTemplate",
fileName: () => `public-template.bundle.js`,
formats: ["iife"]
},
outDir: "public/static/js",
emptyOutDir: false,
assetsInlineLimit: 0,
copyPublicDir: false,
rollupOptions: {
plugins: [rollupNodePolyFill()],
output: {
// 👇 Controls how asset files (like CSS) are named
assetFileNames: (assetInfo) => {
if (assetInfo.name && assetInfo.name.endsWith(".css")) {
return "public-template.bundle.css"; // Custom CSS filename
}
return "[name].[ext]";
},
globals: {}
}
}
}
});
-27
View File
@@ -1,27 +0,0 @@
# Use an official Node runtime as the base image
FROM node:20
# 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
# If you have native dependencies, you'll need extra tools. Uncomment the following line if needed.
# RUN apk add --no-cache make gcc g++ python3
# Copy the current directory contents into the container
COPY ./ .
# Make port 8080 available to the world outside this container
EXPOSE 8080
# Define environment variables if needed
# ENV NODE_ENV production
# ENV DATABASE_URL mongodb://db:27017
# Run the application
ENTRYPOINT npm start
+5
View File
@@ -2,6 +2,11 @@
FROM node:22.14.0
# Install LibreOffice for DOCX to PDF conversions
RUN apt-get update \
&& apt-get install -y libreoffice \
&& rm -rf /var/lib/apt/lists/*
# Set the working directory inside the container
WORKDIR /usr/src/app
+1 -1
View File
@@ -252,7 +252,7 @@ export const mailTemplate = param => {
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Note</td><td></td><td style='color:#626363;font-weight:bold'>" +
param.note +
"</td></tr><tr><td></td><td></td></tr></table></div> <div style='margin-left:70px'><a target=_blank href=" +
param.sigingUrl +
param.signingUrl +
"><button style='padding:12px;background-color:#d46b0f;color:white;border:0px;font-weight:bold;margin-top:30px'>Sign here</button></a></div><div style='display:flex;justify-content:center;margin-top:10px'></div></div></div><div><p> This is an automated email from " +
AppName +
'. For any queries regarding this email, please contact the sender ' +
@@ -2,6 +2,8 @@ import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import uploadFile from './uploadFile.js';
import docxtopdf, { upload as docxUpload } from './docxtopdf.js';
import decryptpdf, { upload as decryptUpload } from './decryptpdf.js';
export const app = express();
@@ -11,4 +13,5 @@ app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
app.post('/file_upload', uploadFile);
app.post('/docxtopdf', docxUpload.single('file'), docxtopdf);
app.post('/decryptpdf', decryptUpload.single('file'), decryptpdf);
@@ -0,0 +1,43 @@
import fs from 'node:fs';
import multer from 'multer';
import Coherentpdf from 'coherentpdf';
const storage = multer.diskStorage({
destination(req, file, cb) {
cb(null, 'exports');
},
filename(req, file, cb) {
cb(null, file.originalname);
},
});
export const upload = multer({ storage });
export default async function decryptpdf(req, res) {
const inputPath = req.file.path;
const password = req.body.password || '';
try {
const file = fs.readFileSync(inputPath);
const pdf = await Coherentpdf.fromMemory(file, password);
await Coherentpdf.decryptPdf(pdf, password);
// Get decrypted buffer directly from memory (no file I/O)
const buffer = await Coherentpdf.toMemory(pdf, false, false);
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'inline; filename="decrypted.pdf"',
'Content-Length': buffer.length,
});
res.send(buffer);
fs.unlink(inputPath, () => {});
} catch (err) {
fs.unlink(inputPath, () => {});
console.log('Error in decrypt file: ', err);
let code = err?.code ? err.code : 400;
let message = err?.[2]?.c ? err[2].c : 'Something went wrong.';
if (err?.[2]?.c?.includes('Bad password') || err?.[2]?.c?.includes('decrypt_pdf_inner')) {
code = 401;
message = 'Incorrect password.';
}
return res.status(code).json({ error: message });
}
}
@@ -0,0 +1,166 @@
import fs from 'node:fs';
import axios from 'axios';
import multer from 'multer';
import libre from 'libreoffice-convert';
import util from 'node:util';
import { cloudServerUrl, getSecureUrl } from '../../Utils.js';
libre.convertAsync = util.promisify(libre.convert);
const storage = multer.diskStorage({
destination(req, file, cb) {
cb(null, 'exports');
},
filename(req, file, cb) {
cb(null, file.originalname);
},
});
export const upload = multer({ storage });
function generatePdfName(length) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
export default async function docxtopdf(req, res) {
const serverUrl = cloudServerUrl;
const appId = process.env.APP_ID;
const masterKey = process.env.MASTER_KEY;
const inputPath = req.file.path;
const name = generatePdfName(16);
const fileName = `${name}.pdf`;
const outputPath = './exports/output.pdf';
try {
const userRes = await axios.get(serverUrl + '/users/me', {
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': req.headers['sessiontoken'],
},
});
const userId = JSON.stringify({
UserId: {
__type: 'Pointer',
className: '_User',
objectId: userRes.data.objectId,
},
});
const resUser = await axios.get(
serverUrl + `/classes/contracts_Users?where=${userId}&limit=1&include=TenantId`,
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
if (resUser?.data?.results?.length > 0) {
const tenantId = resUser.data.results[0].TenantId?.objectId;
const ext = '.pdf';
const outPath = `./exports/output${ext}`;
const docxBuf = fs.readFileSync(inputPath);
const pdfBuffer = await libre.convertAsync(docxBuf, ext, undefined);
fs.writeFileSync(outPath, pdfBuffer);
const file = fs.readFileSync(outPath);
const size = fs.statSync(outPath).size;
const PartnersTenant = JSON.stringify({
PartnersTenant: {
__type: 'Pointer',
className: 'partners_Tenant',
objectId: tenantId,
},
});
const resTenantCredit = await axios.get(
serverUrl + `/classes/partners_TenantCredits?where=${PartnersTenant}&limit=1`,
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
if (resTenantCredit.data?.results?.length > 0) {
const tenantCreditsId = resTenantCredit.data.results[0].objectId;
const activeFileAdapter = resUser.data.results[0].TenantId?.ActiveFileAdapter;
let fileUrl;
if (activeFileAdapter) {
const params = {
fileBase64: file.toString('base64'),
fileName,
id: activeFileAdapter,
};
const url = serverUrl + '/functions/savetofileadapter';
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': appId,
'X-Parse-Session-Token': req.headers['sessiontoken'],
};
try {
const savetos3 = await axios.post(url, params, { headers });
fileUrl = savetos3?.data?.result?.url;
} catch (err) {
console.log('err in save to customfile', err);
}
} else {
const parsefile = await axios.post(serverUrl + `/files/${fileName}`, file, {
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
'Content-Type': 'application/pdf',
},
});
const fileRes = getSecureUrl(parsefile.data.url);
fileUrl = fileRes.url;
}
const usedStorage = resTenantCredit.data.results[0].usedStorage
? resTenantCredit.data.results[0].usedStorage + size
: size;
await axios.put(
serverUrl + `/classes/partners_TenantCredits/${tenantCreditsId}`,
{ usedStorage },
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
await axios.post(
serverUrl + '/classes/partners_DataFiles',
{
FileSize: size,
FileUrl: fileUrl,
TenantPtr: {
__type: 'Pointer',
className: 'partners_Tenant',
objectId: tenantId,
},
},
{
headers: {
'X-Parse-Application-Id': appId,
'X-Parse-Master-Key': masterKey,
},
}
);
[inputPath, outPath].forEach(p => fs.existsSync(p) && fs.unlinkSync(p));
return res.status(200).json({ message: 'success.', url: fileUrl });
}
}
} catch (err) {
[inputPath, outputPath].forEach(p => fs.existsSync(p) && fs.unlinkSync(p));
const msg =
err?.response?.data?.error || err?.response?.data || err?.message || 'Something went wrong.';
console.log(`Error converting file: ${msg}`);
const message =
'We are currently experiencing some issues with processing DOCX files. Please upload the PDF version or contact us on support@opensignlabs.com';
return res.status(400).json({ error: message });
}
}
@@ -36,17 +36,24 @@ export default async function GetTemplate(request) {
let teamsArr = [];
_extUser?.TeamIds?.forEach(x => (teamsArr = [...teamsArr, ...x.Ancestors]));
// Create the first query
const sharedWithQuery = new Parse.Query('contracts_Template');
sharedWithQuery.containedIn('SharedWith', teamsArr);
const sharedWithTeamQuery = new Parse.Query('contracts_Template');
sharedWithTeamQuery.containedIn('SharedWith', teamsArr);
// Create the second query
const sharedWithJsersQuery = new Parse.Query('contracts_Template');
sharedWithJsersQuery.equalTo('SharedWithUsers', {
__type: 'Pointer',
className: 'contracts_Users',
objectId: extUser.id,
});
// Create the third query
const createdByQuery = new Parse.Query('contracts_Template');
createdByQuery.equalTo('ExtUserPtr', {
__type: 'Pointer',
className: 'contracts_Users',
objectId: extUser.id,
});
template = Parse.Query.or(sharedWithQuery, createdByQuery);
template = Parse.Query.or(sharedWithTeamQuery, sharedWithJsersQuery, createdByQuery);
template.equalTo('objectId', templateId);
template.notEqualTo('IsArchive', true);
template.include('ExtUserPtr');

Some files were not shown because too many files have changed in this diff Show More