Compare commits

...
28 Commits
Author SHA1 Message Date
prafull-opensignlabsandGitHub 1cc017bb05 Merge pull request #1829 from OpenSignLabs/updates-16070658635
fix: quicksend screen get black out while sending document
2025-07-04 15:06:16 +05:30
prafull-opensignlabs 531dd11185 Merge pull request #942 from nxglabs/staging
fix: quicksend screen get black out while sending document
2025-07-04 09:32:50 +00:00
prafull-opensignlabsandGitHub 1e80754750 Merge pull request #1827 from OpenSignLabs/updates-16068582405
revert: css of drive
2025-07-04 13:14:27 +05:30
prafull-opensignlabs 0d286938c7 Merge pull request #940 from nxglabs/staging
revert: css of drive
2025-07-04 07:43:32 +00:00
prafull-opensignlabsandGitHub 360c13df25 Merge pull request #1825 from OpenSignLabs/updates-16055306436
v2.26.0
2025-07-04 10:35:33 +05:30
nxglabs 71b9bd6367 Merge pull request #937 from nxglabs/tenant_mailtemplate
fix: if user not provide default value use it from template in createdocument from template API
2025-07-03 16:04:35 +00:00
prafull-opensignlabsandGitHub 4a5b35e147 Merge pull request #1818 from OpenSignLabs/updates-15966812948
fix: handle undefine appId
2025-06-30 13:09:02 +05:30
prafull-opensignlabs 7930a8f439 Merge pull request #917 from nxglabs/handle_appid
fix: handle undefine appId
2025-06-30 07:37:32 +00:00
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 d888897b62 Update README.md 2025-05-20 20:10:44 +05:30
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
119 changed files with 12282 additions and 6687 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 - The simplest way to install OpenSign on your own server is using official docker images by running the following command -
**Command for linux/MacOS**
``` ```
export HOST_URL=https://opensign.yourdomain.com && curl --remote-name-all https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && mv .env.local_dev .env.prod && docker compose up --force-recreate export HOST_URL=https://opensign.yourdomain.com && curl --remote-name-all https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && mv .env.local_dev .env.prod && docker compose up --force-recreate
``` ```
**Command for Windows (Powershell)**
```
$env:HOST_URL="https://opensign.yourdomain.com"; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml -OutFile docker-compose.yml; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile -OutFile Caddyfile; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev -OutFile .env.local_dev; Rename-Item -Path .env.local_dev -NewName .env.prod; docker compose up --force-recreate
```
**Command for Windows (CMD/Terminal)**
```
set HOST_URL=https://opensign.yourdomain.com && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && rename .env.local_dev .env.prod && docker compose up --force-recreate
```
Make sure that you have `Docker` and `git` installed before you run this command - Make sure that you have `Docker` and `git` installed before you run this command -
Please refer to the [Installation Guide](https://docs.opensignlabs.com/docs/self-host/docker/run-locally/) for detailed instructions on how to install OpenSign on your system. Please refer to the [Installation Guide](https://docs.opensignlabs.com/docs/self-host/docker/run-locally/) for detailed instructions on how to install OpenSign on your system.
-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 the current directory contents into the container
COPY apps/OpenSign/ . COPY apps/OpenSign/ .
COPY apps/OpenSign/.husky . COPY apps/OpenSign/.husky .
COPY apps/OpenSign/entrypoint.sh .
# make the entrypoint.sh file executable
RUN chmod +x entrypoint.sh
# Define environment variables if needed # Define environment variables if needed
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -20,8 +24,13 @@ ENV GENERATE_SOURCEMAP=false
# build # build
RUN npm run build RUN npm run build
# Inject env.js loader into index.html
RUN sed -i '/<head>/a\<script src="/env.js"></script>' build/index.html
# Make port 3000 available to the world outside this container # Make port 3000 available to the world outside this container
EXPOSE 3000 EXPOSE 3000
ENTRYPOINT ["./entrypoint.sh"]
# Run the application # Run the application
CMD ["npm", "start"] CMD ["npm", "start"]
+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
};
+1096 -319
View File
File diff suppressed because it is too large Load Diff
+28 -24
View File
@@ -4,29 +4,31 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@formkit/auto-animate": "^0.8.2", "@formkit/auto-animate": "^0.8.2",
"@lottiefiles/dotlottie-react": "^0.13.5", "@imgly/background-removal": "^1.6.0",
"@lottiefiles/dotlottie-react": "^0.14.2",
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.2.1", "@radix-ui/themes": "^3.2.1",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"@imgly/background-removal": "^1.6.0", "axios": "^1.10.0",
"axios": "^1.9.0",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^23.16.8", "i18next": "^25.3.0",
"i18next-browser-languagedetector": "^8.1.0", "i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"parse": "^6.1.1", "parse": "^6.1.1",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pkijs": "^3.0.8",
"print-js": "^1.6.0", "print-js": "^1.6.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"quill-html-edit-button": "^3.0.0",
"radix-ui": "^1.4.2", "radix-ui": "^1.4.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-confetti": "^6.4.0", "react-confetti": "^6.4.0",
"react-datepicker": "^8.3.0", "react-datepicker": "^8.4.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dnd-multi-backend": "^9.0.0", "react-dnd-multi-backend": "^9.0.0",
@@ -34,24 +36,24 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-gtm-module": "^2.0.11", "react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-i18next": "^15.5.1", "react-i18next": "^15.5.3",
"react-konva": "^18.2.10", "react-konva": "^18.2.10",
"react-pdf": "^9.2.1", "react-pdf": "^9.2.1",
"react-quill-new": "^3.4.6", "react-quill-new": "^3.4.6",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-rnd": "^10.5.2", "react-rnd": "^10.5.2",
"react-router": "^7.6.0", "react-router": "^7.6.3",
"react-scrollbars-custom": "^4.1.1", "react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.1", "react-select": "^5.10.1",
"react-signature-canvas": "^1.1.0-alpha.2", "react-signature-canvas": "^1.1.0-alpha.2",
"react-timezone-select": "^3.2.8", "react-timezone-select": "^3.2.8",
"react-tooltip": "^5.28.1", "react-tooltip": "^5.29.1",
"reactour": "^1.19.4", "reactour": "^1.19.4",
"redux": "^5.0.1", "redux": "^5.0.1",
"regex-parser": "^2.3.1", "regex-parser": "^2.3.1",
"serve": "^14.2.4", "serve": "^14.2.4",
"styled-components": "^5.3.11", "styled-components": "^5.3.11",
"web-vitals": "^5.0.1", "web-vitals": "^5.0.3",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
}, },
"scripts": { "scripts": {
@@ -92,36 +94,38 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.27.1", "@babel/core": "^7.27.7",
"@babel/preset-env": "^7.27.2", "@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1", "@babel/preset-react": "^7.27.1",
"@babel/runtime-corejs2": "^7.27.1", "@babel/runtime-corejs2": "^7.27.6",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/react": "^18.3.22", "@types/react": "^18.3.23",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.6.0",
"@vitejs/plugin-react-swc": "^3.9.0", "@vitejs/plugin-react-swc": "^3.10.2",
"@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0", "babel-loader": "^10.0.0",
"commitizen": "^4.3.1", "commitizen": "^4.3.1",
"concurrently": "^9.1.2", "concurrently": "^9.2.0",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"daisyui": "^4.12.24", "daisyui": "^4.12.24",
"dotenv": "^16.5.0", "dotenv": "^16.6.1",
"eslint": "^9.27.0", "eslint": "^9.30.0",
"eslint-plugin-prettier": "^5.4.0", "eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"lint-staged": "^16.0.0", "jsdom": "^26.1.0",
"postcss": "^8.5.3", "lint-staged": "^16.1.2",
"prettier": "^3.5.3", "postcss": "^8.5.6",
"pretty-quick": "^4.1.1", "prettier": "^3.6.2",
"pretty-quick": "^4.2.2",
"rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-plugin-svgr": "^4.3.0", "vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.4" "vitest": "^3.2.4"
}, },
"engines": { "engines": {
"node": "18 || 20 || 22" "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": "Neue Funktion: Benutzer des Teams-Plans können jetzt ihre eigenen AWS S3-Buckets für die Dateispeicherung integrieren",
"header-news-btn": "Jetzt einrichten", "header-news-btn": "Jetzt einrichten",
"sandbox-news": "Dies ist eine Sandbox-Umgebung. Bitte nicht für produktive Zwecke verwenden.",
"create-account": "Konto erstellen", "create-account": "Konto erstellen",
"login": "Anmelden", "login": "Anmelden",
"language": "Sprache", "language": "Sprache",
"dark-mode": "Dunkelmodus",
"name": "Name", "name": "Name",
"phone": "Telefon", "phone": "Telefon",
"phone-optional": "optional", "phone-optional": "optional",
@@ -360,6 +362,7 @@
"date": "Datum", "date": "Datum",
"text": "Text", "text": "Text",
"text input": "Texteingabe", "text input": "Texteingabe",
"cells": "Zellen",
"checkbox": "Checkbox", "checkbox": "Checkbox",
"dropdown": "Dropdown", "dropdown": "Dropdown",
"radio button": "Radiobutton", "radio button": "Radiobutton",
@@ -414,10 +417,12 @@
"options": "Optionen", "options": "Optionen",
"minimun-check": "Minimale Anzahl", "minimun-check": "Minimale Anzahl",
"maximum-check": "Maximale Anzahl", "maximum-check": "Maximale Anzahl",
"cell-count": "Zellzahl",
"default-value": "Standardwert", "default-value": "Standardwert",
"select": "Auswählen", "select": "Auswählen",
"read-only": "Nur lesen", "read-only": "Ist schreibgeschützt",
"hide-labels": "Labels ausblenden", "hide-labels": "Labels ausblenden",
"layout": "Layout",
"checkbox": "Checkbox", "checkbox": "Checkbox",
"alert": "Warnung", "alert": "Warnung",
"zoom-in": "Vergrößern", "zoom-in": "Vergrößern",
@@ -675,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-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-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-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-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-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.", "public-template-mssg-7": "Bevor Sie einen öffentlichen Link generieren können, müssen Sie diese Vorlage öffentlich machen.",
@@ -749,12 +754,15 @@
"delete-page": "Seite löschen", "delete-page": "Seite löschen",
"merge-pdf": "PDFs zusammenführen", "merge-pdf": "PDFs zusammenführen",
"add-pages": "Seiten hinzufügen", "add-pages": "Seiten hinzufügen",
"reorder-pages": "Seiten neu anordnen",
"delete-alert": "Einzelne Seite kann nicht gelöscht werden.", "delete-alert": "Einzelne Seite kann nicht gelöscht werden.",
"delete-alert-2": "Sind Sie sicher, dass Sie diese Seite löschen möchten?", "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.", "delete-note": "Hinweis: Sobald Sie diese Seite löschen, kann dies nicht rückgängig gemacht werden.",
"Rotation-alert": "Seite drehen", "Rotation-alert": "Seite drehen",
"bulk-import": "Massenimport", "bulk-import": "Massenimport",
"contacts-file": "Kontaktdatei (xlsx, csv)", "contacts-file": "Kontaktdatei (xlsx, csv)",
"import-guideline": "Laden Sie eine CSV- oder Excel-Datei mit den Spalten Name, Email und optional Phone hoch. Es werden nur die ersten 100 Kontakte importiert.",
"download-sample": "Beispieldatei herunterladen",
"100-records-only": "Derzeit können Sie nur bis zu 100 Datensätze importieren.", "100-records-only": "Derzeit können Sie nur bis zu 100 Datensätze importieren.",
"csv-excel-support-only": "Laden Sie eine Datei in einem der folgenden Formate hoch: CSV, XLSX oder XLS.", "csv-excel-support-only": "Laden Sie eine Datei in einem der folgenden Formate hoch: CSV, XLSX oder XLS.",
"contact-imported": "{{imported}} Kontakte wurden importiert. {{failed}} Kontakte konnten nicht importiert werden.", "contact-imported": "{{imported}} Kontakte wurden importiert. {{failed}} Kontakte konnten nicht importiert werden.",
@@ -980,5 +988,58 @@
"finish-mssg": "Sind Sie sicher, dass Sie das Dokument abschließen möchten?", "finish-mssg": "Sind Sie sicher, dass Sie das Dokument abschließen möchten?",
"review": "Überprüfen", "review": "Überprüfen",
"next-field": "Nächstes Feld", "next-field": "Nächstes Feld",
"required-mssg": "{{leftRequiredWidget}} von {{totalWidget}} Feldern übrig" "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…",
"invalid-email-found": "Ungültige E-Mail gefunden: {{email}}",
"duplicate-email-found": "Doppelte E-Mail gefunden: {{email}}",
"vertical": "Vertikal",
"horizontal": "Horizontal",
"billing": "Abrechnung",
"console": "Konsole"
} }
@@ -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": "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", "header-news-btn": "Setup now",
"sandbox-news": "This is a sandbox environment. Please do not use it for production purposes.",
"create-account": "Create account", "create-account": "Create account",
"login": "Login", "login": "Login",
"language": "Language", "language": "Language",
"dark-mode": "Dark mode",
"name": "Name", "name": "Name",
"phone": "Phone", "phone": "Phone",
"phone-optional": "optional", "phone-optional": "optional",
@@ -198,7 +200,7 @@
}, },
"file-type": "pdf, png, jpg, jpeg", "file-type": "pdf, png, jpg, jpeg",
"docx": "docx", "docx": "docx",
"file-selected": "file selected", "file-selected": "file(s) selected",
"template-title": "Template title", "template-title": "Template title",
"document-title": "Document title", "document-title": "Document title",
"description": "Description", "description": "Description",
@@ -360,6 +362,7 @@
"date": "date", "date": "date",
"text": "text", "text": "text",
"text input": "text input", "text input": "text input",
"cells": "cells",
"checkbox": "checkbox", "checkbox": "checkbox",
"dropdown": "dropdown", "dropdown": "dropdown",
"radio button": "radio button", "radio button": "radio button",
@@ -414,10 +417,12 @@
"options": "Options", "options": "Options",
"minimun-check": "Minimun check", "minimun-check": "Minimun check",
"maximum-check": "Maximum check", "maximum-check": "Maximum check",
"cell-count": "Cell count",
"default-value": "Default value", "default-value": "Default value",
"select": "Select", "select": "Select",
"read-only": "Is read only", "read-only": "read only",
"hide-labels": "Hide labels", "hide-labels": "Hide labels",
"layout": "Layout",
"checkbox": "Checkbox", "checkbox": "Checkbox",
"alert": "Alert", "alert": "Alert",
"zoom-in": "Zoom in", "zoom-in": "Zoom in",
@@ -525,7 +530,7 @@
"new-password": "New password", "new-password": "New password",
"confirm-password": "Confirm password", "confirm-password": "Confirm password",
"file-alert-1": "The selected file size is too large. Please select a file less than", "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.", "file-alert-3": "Please wait while the document is being uploaded.",
"enter-pdf-password": "Enter Pdf password", "enter-pdf-password": "Enter Pdf password",
"correct-password": "Please provide correct password", "correct-password": "Please provide correct password",
@@ -675,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-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-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-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-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-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.", "public-template-mssg-7": "Before you can generate a public link you must make this template public.",
@@ -749,12 +754,15 @@
"delete-page": "Delete page", "delete-page": "Delete page",
"merge-pdf": "Merge pdf", "merge-pdf": "Merge pdf",
"add-pages": "Add pages", "add-pages": "Add pages",
"reorder-pages": "Reorder pages",
"delete-alert": "Can not delete single page", "delete-alert": "Can not delete single page",
"delete-alert-2": "Are you sure you want to delete this page?", "delete-alert-2": "Are you sure you want to delete this page?",
"delete-note": "Note: Once you delete this page, you cannot undo.", "delete-note": "Note: Once you delete this page, you cannot undo.",
"Rotation-alert": "Rotate page", "Rotation-alert": "Rotate page",
"bulk-import": "Bulk import", "bulk-import": "Bulk import",
"contacts-file": "Contacts file (xlsx, csv)", "contacts-file": "Contacts file (xlsx, csv)",
"import-guideline": "Upload a CSV or Excel file with columns Name, Email and optional Phone. Only the first 100 records will be imported.",
"download-sample": "Download sample file",
"100-records-only": "Currently you can only import up to 100 records.", "100-records-only": "Currently you can only import up to 100 records.",
"csv-excel-support-only": "Upload a file in one of the following formats: CSV, XLSX or XLS.", "csv-excel-support-only": "Upload a file in one of the following formats: CSV, XLSX or XLS.",
"contact-imported": "{{imported}} contacts were imported. {{failed}} contacts failed to import.", "contact-imported": "{{imported}} contacts were imported. {{failed}} contacts failed to import.",
@@ -884,7 +892,7 @@
"you-will-receive-email-shortly": "✅ That's it! You'll receive a confirmation email shortly.", "you-will-receive-email-shortly": "✅ That's it! You'll receive a confirmation email shortly.",
"please-provide-templateid": "Please provide templateid", "please-provide-templateid": "Please provide templateid",
"this-template-is-not-public": "This template is not public", "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", "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.", "title-length-alert": "Title must be at most 250 characters long.",
"note-length-alert": "Note must be at most 200 characters long.", "note-length-alert": "Note must be at most 200 characters long.",
@@ -980,5 +988,58 @@
"finish-mssg":" Are you sure you want to finish the document ?", "finish-mssg":" Are you sure you want to finish the document ?",
"review":"Review", "review":"Review",
"next-field":"Next Field", "next-field":"Next Field",
"required-mssg":"{{leftRequiredWidget}} of {{totalWidget}} fields left" "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…",
"invalid-email-found": "Invalid email found: {{email}}",
"duplicate-email-found": "Duplicate email found: {{email}}",
"vertical": "Vertical",
"horizontal": "Horizontal",
"billing": "Billing",
"console": "Console"
} }
@@ -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": "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", "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", "create-account": "Crear cuenta",
"login": "Iniciar sesión", "login": "Iniciar sesión",
"language": "Idioma", "language": "Idioma",
"dark-mode": "Modo oscuro",
"name": "Nombre", "name": "Nombre",
"phone": "Teléfono", "phone": "Teléfono",
"phone-optional": "opcional", "phone-optional": "opcional",
@@ -361,6 +363,7 @@
"date": "fecha", "date": "fecha",
"text": "texto", "text": "texto",
"text input": "entrada de texto", "text input": "entrada de texto",
"cells": "células",
"checkbox": "casilla", "checkbox": "casilla",
"dropdown": "desplegable", "dropdown": "desplegable",
"radio button": "botón de radio", "radio button": "botón de radio",
@@ -415,10 +418,12 @@
"options": "Opciones", "options": "Opciones",
"minimun-check": "Chequeo mínimo", "minimun-check": "Chequeo mínimo",
"maximum-check": "Chequeo máximo", "maximum-check": "Chequeo máximo",
"cell-count": "recuento de células",
"default-value": "Valor por defecto", "default-value": "Valor por defecto",
"select": "Seleccionar", "select": "Seleccionar",
"read-only": "Es de solo lectura", "read-only": "Es de solo lectura",
"hide-labels": "Esconder etiquetas", "hide-labels": "Esconder etiquetas",
"layout": "Diseño",
"checkbox": "Casilla", "checkbox": "Casilla",
"alert": "Alerta", "alert": "Alerta",
"zoom-in": "Acercar", "zoom-in": "Acercar",
@@ -675,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-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-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-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-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-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.", "public-template-mssg-7": "Antes de poder generar un enlace público, debes hacer que esta plantilla sea pública.",
@@ -749,12 +754,15 @@
"delete-page": "eliminar página", "delete-page": "eliminar página",
"merge-pdf": "fusionar pdf", "merge-pdf": "fusionar pdf",
"add-pages": "Agregar páginas", "add-pages": "Agregar páginas",
"reorder-pages": "Reordenar páginas",
"delete-alert": "No se puede eliminar una sola página", "delete-alert": "No se puede eliminar una sola página",
"delete-alert-2": "¿Está seguro de que desea eliminar esta 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", "delete-note": "Nota: una vez que elimines esta página, no podrás deshacerla",
"Rotation-alert": "Girar página", "Rotation-alert": "Girar página",
"bulk-import": "Importación masiva", "bulk-import": "Importación masiva",
"contacts-file": "Archivo de contactos (xlsx, csv)", "contacts-file": "Archivo de contactos (xlsx, csv)",
"import-guideline": "Sube un archivo CSV o Excel con las columnas Name, Email y opcionalmente Phone. Solo se importarán los primeros 100 contactos.",
"download-sample": "Descargar archivo de ejemplo",
"100-records-only": "Currently you can only import up to 100 records.", "100-records-only": "Currently you can only import up to 100 records.",
"csv-excel-support-only": "Cargue un archivo en uno de los siguientes formatos: CSV, XLSX o XLS.", "csv-excel-support-only": "Cargue un archivo en uno de los siguientes formatos: CSV, XLSX o XLS.",
"contact-imported": "Se importaron {{imported}} contactos. {{failed}} contactos no pudieron importarse.", "contact-imported": "Se importaron {{imported}} contactos. {{failed}} contactos no pudieron importarse.",
@@ -980,5 +988,58 @@
"finish-mssg": "¿Está seguro de que desea finalizar el documento?", "finish-mssg": "¿Está seguro de que desea finalizar el documento?",
"review": "Revisar", "review": "Revisar",
"next-field": "Siguiente campo", "next-field": "Siguiente campo",
"required-mssg": "{{leftRequiredWidget}} de {{totalWidget}} campos restantes" "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…",
"invalid-email-found": "Correo electrónico no válido encontrado: {{email}}",
"duplicate-email-found": "Correo electrónico duplicado encontrado: {{email}}",
"vertical": "Vertical",
"horizontal": "Horizontal",
"billing": "Facturación",
"console": "Consola"
} }
@@ -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": "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", "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", "create-account": "Créer un compte",
"login": "Se Connecter", "login": "Se Connecter",
"language": "Langue", "language": "Langue",
"dark-mode": "Mode sombre",
"name": "Nom et Prénom", "name": "Nom et Prénom",
"phone": "Téléphone", "phone": "Téléphone",
"phone-optional": "(facultatif)", "phone-optional": "(facultatif)",
@@ -360,6 +362,7 @@
"date": "date", "date": "date",
"text": "texte", "text": "texte",
"text input": "saisie de texte", "text input": "saisie de texte",
"cells": "cellules",
"checkbox": "case à cocher", "checkbox": "case à cocher",
"dropdown": "dérouler", "dropdown": "dérouler",
"radio button": "bouton radio", "radio button": "bouton radio",
@@ -414,10 +417,12 @@
"options": "Possibilités", "options": "Possibilités",
"minimun-check": "Vérification minimale", "minimun-check": "Vérification minimale",
"maximum-check": "Contrôle maximum", "maximum-check": "Contrôle maximum",
"cell-count": "numération cellulaire",
"default-value": "Valeur par défaut", "default-value": "Valeur par défaut",
"select": "Sélectionner", "select": "Sélectionner",
"read-only": "Est en lecture seule", "read-only": "Est en lecture seule",
"hide-labels": "Masquer les étiquettes", "hide-labels": "Masquer les étiquettes",
"layout": "Disposition",
"checkbox": "Case à cocher", "checkbox": "Case à cocher",
"alert": "Alerte", "alert": "Alerte",
"zoom-in": "Agrandir", "zoom-in": "Agrandir",
@@ -675,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-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-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-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-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-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.", "public-template-mssg-7": "Avant de pouvoir générer un lien public, vous devez rendre ce modèle public.",
@@ -749,12 +754,15 @@
"delete-page": "supprimer la page", "delete-page": "supprimer la page",
"merge-pdf": "Fusionner le pdf", "merge-pdf": "Fusionner le pdf",
"add-pages": "Ajouter des pages", "add-pages": "Ajouter des pages",
"reorder-pages": "Réorganiser les pages",
"delete-alert": "Impossible de supprimer une seule page", "delete-alert": "Impossible de supprimer une seule page",
"delete-alert-2": "Etes-vous sûr de vouloir supprimer cette 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", "delete-note": "Remarque : une fois cette page supprimée, vous ne pouvez plus l'annuler",
"Rotation-alert": "Faire pivoter la page", "Rotation-alert": "Faire pivoter la page",
"bulk-import": "Importation en masse", "bulk-import": "Importation en masse",
"contacts-file": "Fichier de contacts (xlsx, csv)", "contacts-file": "Fichier de contacts (xlsx, csv)",
"import-guideline": "Téléchargez un fichier CSV ou Excel avec les colonnes Name, Email et éventuellement Phone. Seuls les 100 premiers contacts seront importés.",
"download-sample": "Télécharger un fichier d'exemple",
"100-records-only": "Actuellement, vous ne pouvez importer que 100 enregistrements.", "100-records-only": "Actuellement, vous ne pouvez importer que 100 enregistrements.",
"csv-excel-support-only": "Veuillez télécharger un fichier dans l'un des formats suivants : CSV, XLSX ou XLS.", "csv-excel-support-only": "Veuillez télécharger un fichier dans l'un des formats suivants : CSV, XLSX ou XLS.",
"contact-imported": "{{imported}} contacts ont été importés. {{failed}} contacts n'ont pas pu être importés.", "contact-imported": "{{imported}} contacts ont été importés. {{failed}} contacts n'ont pas pu être importés.",
@@ -980,5 +988,58 @@
"finish-mssg": "Êtes-vous sûr de vouloir terminer le document ?", "finish-mssg": "Êtes-vous sûr de vouloir terminer le document ?",
"review": "Revoir", "review": "Revoir",
"next-field": "Champ suivant", "next-field": "Champ suivant",
"required-mssg":"{{leftRequiredWidget}} champs sur {{totalWidget}} restants" "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…",
"invalid-email-found": "Adresse e-mail invalide trouvée : {{email}}",
"duplicate-email-found": "Adresse e-mail en double trouvée : {{email}}",
"vertical": "Vertical",
"horizontal": "Horizontal",
"billing": "Facturation",
"console": "Console"
} }
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": "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", "header-news-btn": "Configura Ora",
"sandbox-news": "Questo è un ambiente sandbox. Si prega di non utilizzarlo per scopi di produzione.",
"create-account": "Crea Account", "create-account": "Crea Account",
"login": "Accedi", "login": "Accedi",
"language": "Lingua", "language": "Lingua",
"dark-mode": "Modalità scura",
"name": "Nome", "name": "Nome",
"phone": "Telefono", "phone": "Telefono",
"phone-optional": "facoltativo", "phone-optional": "facoltativo",
@@ -360,6 +362,7 @@
"date": "data", "date": "data",
"text": "testo", "text": "testo",
"text input": "campo di testo", "text input": "campo di testo",
"cells": "cellule",
"checkbox": "casella di controllo", "checkbox": "casella di controllo",
"dropdown": "menu a tendina", "dropdown": "menu a tendina",
"radio button": "pulsante di opzione", "radio button": "pulsante di opzione",
@@ -414,10 +417,12 @@
"options": "Opzioni", "options": "Opzioni",
"minimun-check": "Controllo minimo", "minimun-check": "Controllo minimo",
"maximum-check": "Controllo massimo", "maximum-check": "Controllo massimo",
"cell-count": "conteggio delle cellule",
"default-value": "Valore predefinita", "default-value": "Valore predefinita",
"select": "Seleziona", "select": "Seleziona",
"read-only": "È solo lettura", "read-only": di sola lettura",
"hide-labels": "Nascondi etichette", "hide-labels": "Nascondi etichette",
"layout": "Layout",
"checkbox": "Casella di controllo", "checkbox": "Casella di controllo",
"alert": "Avviso", "alert": "Avviso",
"zoom-in": "Ingrandisci", "zoom-in": "Ingrandisci",
@@ -675,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-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-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-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-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-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.", "public-template-mssg-7": "Prima di poter generare un link pubblico, devi rendere questo modello pubblico.",
@@ -749,12 +754,15 @@
"delete-page": "Elimina pagina", "delete-page": "Elimina pagina",
"merge-pdf": "Unisci PDF", "merge-pdf": "Unisci PDF",
"add-pages": "Aggiungi pagine", "add-pages": "Aggiungi pagine",
"reorder-pages": "Riordina pagine",
"delete-alert": "Non è possibile eliminare una singola pagina", "delete-alert": "Non è possibile eliminare una singola pagina",
"delete-alert-2": "Sei sicuro di voler eliminare questa pagina?", "delete-alert-2": "Sei sicuro di voler eliminare questa pagina?",
"delete-note": "Nota: Una volta eliminata questa pagina, non potrai annullare l'operazione.", "delete-note": "Nota: Una volta eliminata questa pagina, non potrai annullare l'operazione.",
"Rotation-alert": "Ruota pagina", "Rotation-alert": "Ruota pagina",
"bulk-import": "Importazione massiva", "bulk-import": "Importazione massiva",
"contacts-file": "File contatti (xlsx, csv)", "contacts-file": "File contatti (xlsx, csv)",
"import-guideline": "Carica un file CSV o Excel con le colonne Name, Email e opzionale Phone. Verranno importati solo i primi 100 contatti.",
"download-sample": "Scarica file di esempio",
"100-records-only": "Attualmente puoi importare solo fino a 100 record.", "100-records-only": "Attualmente puoi importare solo fino a 100 record.",
"csv-excel-support-only": "Carica un file nei seguenti formati: CSV, XLSX o XLS.", "csv-excel-support-only": "Carica un file nei seguenti formati: CSV, XLSX o XLS.",
"contact-imported": "{{imported}} contatti importati. {{failed}} contatti non sono stati importati.", "contact-imported": "{{imported}} contatti importati. {{failed}} contatti non sono stati importati.",
@@ -980,5 +988,58 @@
"finish-mssg": "Sei sicuro di voler completare il documento?", "finish-mssg": "Sei sicuro di voler completare il documento?",
"review": "Rivedere", "review": "Rivedere",
"next-field": "Campo successivo", "next-field": "Campo successivo",
"required-mssg":"{{leftRequiredWidget}} di {{totalWidget}} campi rimanenti" "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…",
"invalid-email-found": "Email non valida trovata: {{email}}",
"duplicate-email-found": "Email duplicata trovata: {{email}}",
"vertical": "Verticale",
"horizontal": "Orizzontale",
"billing": "Fatturazione",
"console": "Console"
} }
+4
View File
@@ -0,0 +1,4 @@
Name,Email,Phone
John Doe,john@example.com,1234567890
Jane Smith,jane@example.com,9876543210
Foo Bar,foo@example.com,5555555555
1 Name Email Phone
2 John Doe john@example.com 1234567890
3 Jane Smith jane@example.com 9876543210
4 Foo Bar foo@example.com 5555555555
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

+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 { Routes, Route, BrowserRouter } from "react-router";
import { pdfjs } from "react-pdf"; import { pdfjs } from "react-pdf";
import Form from "./pages/Form"; import Form from "./pages/Form";
@@ -30,7 +30,7 @@ const AddAdmin = lazy(() => import("./pages/AddAdmin"));
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin")); const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
const Preferences = lazy(() => import("./pages/Preferences")); const Preferences = lazy(() => import("./pages/Preferences"));
const Login = lazy(() => import("./pages/Login")); 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`; pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
const AppLoader = () => { const AppLoader = () => {
return ( return (
@@ -106,10 +106,10 @@ function App() {
element={<LazyPage Page={GuestLogin} />} element={<LazyPage Page={GuestLogin} />}
/> />
<Route path="/debugpdf" element={<LazyPage Page={DebugPdf} />} /> <Route path="/debugpdf" element={<LazyPage Page={DebugPdf} />} />
<Route <Route
path="/forgetpassword" path="/forgetpassword"
element={<LazyPage Page={ForgetPassword} />} element={<LazyPage Page={ForgetPassword} />}
/> />
<Route <Route
element={ element={
<ValidateSession> <ValidateSession>
@@ -117,10 +117,10 @@ function App() {
</ValidateSession> </ValidateSession>
} }
> >
<Route <Route
path="/changepassword" path="/changepassword"
element={<LazyPage Page={ChangePassword} />} element={<LazyPage Page={ChangePassword} />}
/> />
<Route path="/form/:id" element={<Form />} /> <Route path="/form/:id" element={<Form />} />
<Route path="/report/:id" element={<Report />} /> <Route path="/report/:id" element={<Report />} />
<Route path="/dashboard/:id" element={<Dashboard />} /> <Route path="/dashboard/:id" element={<Dashboard />} />
@@ -142,7 +142,7 @@ function App() {
/> />
{/* signyouself route with no rowlevel data using docId from url */} {/* signyouself route with no rowlevel data using docId from url */}
<Route path="/signaturePdf/:docId" element={<SignYourSelf />} /> <Route path="/signaturePdf/:docId" element={<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 />} /> <Route path="/draftDocument" element={<DraftDocument />} />
{/* recipient placeholder set route with no rowlevel data using docId from url*/} {/* recipient placeholder set route with no rowlevel data using docId from url*/}
<Route <Route
@@ -165,7 +165,11 @@ function App() {
path="/recipientSignPdf/:docId" path="/recipientSignPdf/:docId"
element={<PdfRequestFiles />} element={<PdfRequestFiles />}
/> />
<Route path="/users" element={<UserList />} /> <Route path="/users" element={<UserList />} />
<Route
path="/verify-document"
element={<LazyPage Page={VerifyDocument} />}
/>
<Route <Route
path="/preferences" path="/preferences"
element={<LazyPage Page={Preferences} />} element={<LazyPage Page={Preferences} />}
+23 -6
View File
@@ -15,12 +15,14 @@ const BulkSendUi = (props) => {
const [isSignatureExist, setIsSignatureExist] = useState(); const [isSignatureExist, setIsSignatureExist] = useState();
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false); const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
const [isLoader, setIsLoader] = useState(false); const [isLoader, setIsLoader] = useState(false);
const [signers, setSigners] = useState([]);
const [emails, setEmails] = useState([]);
useEffect(() => { useEffect(() => {
signatureExist(); signatureExist();
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 () => { const signatureExist = async () => {
setIsDisableBulkSend(false); setIsDisableBulkSend(false);
const getPlaceholder = props?.Placeholders; const getPlaceholder = props?.Placeholders;
@@ -47,7 +49,14 @@ const BulkSendUi = (props) => {
(() => { (() => {
if (props?.Placeholders?.length > 0) { if (props?.Placeholders?.length > 0) {
let users = []; let users = [];
let emails = [];
props?.Placeholders?.forEach((element) => { props?.Placeholders?.forEach((element) => {
const signerEmail = element?.email || element?.signerPtr?.Email;
// only add when there's a non-empty signerEmail
if (signerEmail) {
emails = [...emails, signerEmail];
}
if (!element.signerObjId) { if (!element.signerObjId) {
users = [ users = [
...users, ...users,
@@ -60,7 +69,10 @@ const BulkSendUi = (props) => {
]; ];
} }
}); });
setEmails(emails);
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]); setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
const signer = props.item?.Signers?.filter((x) => x?.objectId);
setSigners(signer);
} }
})(); })();
// eslint-disable-next-line // eslint-disable-next-line
@@ -82,10 +94,16 @@ const BulkSendUi = (props) => {
function validateEmails(data) { function validateEmails(data) {
for (const item of data) { for (const item of data) {
let email = "";
for (const field of item.fields) { for (const field of item.fields) {
if (!emailRegex.test(field.email)) { if (!emailRegex.test(field.email)) {
alert(`Invalid email found: ${field.email}`); alert(t("invalid-email-found", { email: field.email }));
return false; return false;
} else if (email === field.email || emails?.includes(field.email)) {
alert(t("duplicate-email-found", { email: field.email }));
return false;
} else {
email = field.email;
} }
} }
} }
@@ -143,15 +161,14 @@ const BulkSendUi = (props) => {
Documents.push({ Documents.push({
...props.item, ...props.item,
Placeholders: updatedPlaceholders, Placeholders: updatedPlaceholders,
Signers: props.item.Signers Signers: signers ? [...signers, ...existSigner] : [...existSigner]
? [...props.item.Signers, ...existSigner]
: [...existSigner]
}); });
} else { } else {
Documents.push({ Documents.push({
...props.item, ...props.item,
Placeholders: updatedPlaceholders, Placeholders: updatedPlaceholders,
SignatureType: props.signatureType SignatureType: props.signatureType,
Signers: signers
}); });
} }
}); });
+56 -6
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 dp from "../assets/images/dp.png";
import FullScreenButton from "./FullScreenButton"; import FullScreenButton from "./FullScreenButton";
import ThemeToggle from "./ThemeToggle";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import Parse from "parse"; import Parse from "parse";
import { useWindowSize } from "../hook/useWindowSize"; import { useWindowSize } from "../hook/useWindowSize";
@@ -85,9 +86,37 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
}; };
}, [isOpen]); }, [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 ( return (
<div> <div>
<div className="op-navbar bg-base-100 shadow"> <div className="op-navbar bg-base-100 shadow touch-none">
<div className="flex-none"> <div className="flex-none">
<button <button
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation" className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
@@ -147,7 +176,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
</div> </div>
<ul <ul
tabIndex={0} tabIndex={0}
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-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" isOpen ? "" : "hidden"
}`} }`}
> >
@@ -172,15 +201,36 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
<i className="fa-light fa-user"></i> {t("profile")} <i className="fa-light fa-user"></i> {t("profile")}
</span> </span>
</li> </li>
<li
onClick={() => {
setIsOpen(false);
navigate("/changepassword");
}}
>
<span>
<i className="fa-light fa-lock"></i>{" "}
{t("change-password")}
</span>
</li>
<li <li
onClick={() => { onClick={() => {
setIsOpen(false); setIsOpen(false);
navigate("/changepassword"); navigate("/verify-document");
}} }}
> >
<span> <span>
<i className="fa-light fa-lock"></i>{" "} <i className="fa-light fa-check-square"></i>{" "}
{t("change-password")} {t("verify-document")}
</span>
</li>
<li>
<span>
<i className="fa-light fa-moon"></i>
{t("dark-mode")}
<span className="text-[10px] font-semibold bg-base-300 text-base-content px-1 rounded-md">
BETA
</span>
<ThemeToggle />
</span> </span>
</li> </li>
</> </>
@@ -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;
@@ -29,7 +29,7 @@ const DashboardButton = (props) => {
: "cursor-default" : "cursor-default"
} w-full shadow-md px-3 py-2 op-card bg-base-100`} } w-full shadow-md px-3 py-2 op-card bg-base-100`}
> >
<div className="flex flex-row items-center"> <div className="flex flex-row items-center text-base-content">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center"> <span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center">
<i <i
@@ -39,7 +39,7 @@ const DashboardButton = (props) => {
></i> ></i>
</span> </span>
</div> </div>
<div className="text-lg ml-3 text-base-content"> <div className="text-lg ml-3">
{t(`sidebar.${props.Label}`)} {t(`sidebar.${props.Label}`)}
{props.Label === "Sign yourself" && ( {props.Label === "Sign yourself" && (
<div className="text-gray-500 text-xs mt-1"> <div className="text-gray-500 text-xs mt-1">
@@ -148,7 +148,7 @@ const DashboardCard = (props) => {
); );
let arr = []; let arr = [];
for (const obj of listData) { for (const obj of listData) {
const isSigner = obj.Signers.some( const isSigner = obj.Signers?.some(
(item) => item.UserId.objectId === currentUser.id (item) => item.UserId.objectId === currentUser.id
); );
if (isSigner) { if (isSigner) {
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useRef } from "react";
import Parse from "parse"; import Parse from "parse";
import ReportTable from "../../primitives/GetReportDisplay"; import ReportTable from "../../primitives/GetReportDisplay";
import reportJson from "../../json/ReportJson"; import reportJson from "../../json/ReportJson";
@@ -17,10 +17,16 @@ function DashboardReport(props) {
const [isMoreDocs, setIsMoreDocs] = useState(true); const [isMoreDocs, setIsMoreDocs] = useState(true);
const abortController = new AbortController(); const abortController = new AbortController();
const docPerPage = 5; const docPerPage = 5;
const [searchTerm, setSearchTerm] = useState("");
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [isSearchResult, setIsSearchResult] = useState(false);
const debounceTimer = useRef(null);
useEffect(() => { useEffect(() => {
setReportName(""); setReportName("");
getReportData(props.Record.reportId); setSearchTerm("");
setMobileSearchOpen(false);
getReportData(props.Record.reportId, 0, 20, "");
// Function returned from useEffect is called on unmount // Function returned from useEffect is called on unmount
return () => { return () => {
@@ -36,12 +42,69 @@ function DashboardReport(props) {
// below useEffect call when isNextRecord state is true and fetch next record // below useEffect call when isNextRecord state is true and fetch next record
useEffect(() => { useEffect(() => {
if (isNextRecord) { if (isNextRecord) {
getReportData(props.Record.reportId, List.length, 20); getReportData(props.Record.reportId, List.length, 20, searchTerm);
} }
// eslint-disable-next-line // eslint-disable-next-line
}, [isNextRecord]); }, [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); setIsLoader(true);
const json = reportJson(id); const json = reportJson(id);
if (json) { if (json) {
@@ -59,6 +122,9 @@ function DashboardReport(props) {
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord; const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit; const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
const params = { reportId: id, skip: skipRecord, limit: limitRecord }; const params = { reportId: id, skip: skipRecord, limit: limitRecord };
if (term) {
params.searchTerm = term;
}
const url = `${localStorage.getItem("baseUrl")}functions/getReport`; const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
const res = await axios.post(url, params, { const res = await axios.post(url, params, {
headers: headers, headers: headers,
@@ -68,7 +134,7 @@ function DashboardReport(props) {
const listData = res.data?.result.filter((x) => x.Signers.length > 0); const listData = res.data?.result.filter((x) => x.Signers.length > 0);
let arr = []; let arr = [];
for (const obj of listData) { for (const obj of listData) {
const isSigner = obj.Signers.some( const isSigner = obj.Signers?.some(
(item) => item.UserId.objectId === currentUser (item) => item.UserId.objectId === currentUser
); );
if (isSigner) { if (isSigner) {
@@ -141,11 +207,17 @@ function DashboardReport(props) {
setIsNextRecord={setIsNextRecord} setIsNextRecord={setIsNextRecord}
isMoreDocs={isMoreDocs} isMoreDocs={isMoreDocs}
docPerPage={docPerPage} 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="flex items-center justify-center h-[100px] w-full bg-white rounded">
<div className="text-center"> <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>
</div> </div>
)} )}
@@ -405,7 +405,7 @@ function DriveBody(props) {
</tr> </tr>
) )
) : listType === "list" && data.Type === "Folder" ? ( ) : listType === "list" && data.Type === "Folder" ? (
<div key={ind} className="relative w-[100px] h-[100px] mx-2 my-3"> <div className="relative w-[100px] h-[100px] mx-2 my-3">
<ContextMenu.Root> <ContextMenu.Root>
<ContextMenu.Trigger className="flex flex-col justify-center items-center select-none-cls"> <ContextMenu.Trigger className="flex flex-col justify-center items-center select-none-cls">
{/* folder */} {/* folder */}
@@ -447,7 +447,6 @@ function DriveBody(props) {
)} )}
</div> </div>
</ContextMenu.Trigger> </ContextMenu.Trigger>
<ContextMenu.Portal> <ContextMenu.Portal>
<ContextMenu.Content <ContextMenu.Content
className="ContextMenuContent" className="ContextMenuContent"
@@ -641,7 +640,7 @@ function DriveBody(props) {
) : ( ) : (
<div className="flex flex-row flex-wrap items-center mt-1 pb-[20px] mx-[5px]"> <div className="flex flex-row flex-wrap items-center mt-1 pb-[20px] mx-[5px]">
{props.pdfData.map((data, ind) => { {props.pdfData.map((data, ind) => {
return <div key={ind}>{handleFolderData(data, ind, "list")}</div>; return <React.Fragment key={ind}>{handleFolderData(data, ind, "list")}</React.Fragment>;
})} })}
</div> </div>
)} )}
@@ -10,7 +10,7 @@ const AddRoleModal = (props) => {
isOpen={props.isModalRole} isOpen={props.isModalRole}
handleClose={props.handleCloseRoleModal} 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}> <form className="flex flex-col" onSubmit={props.handleAddRole}>
<input <input
value={props.roleName} value={props.roleName}
@@ -12,20 +12,33 @@ function AgreementSign(props) {
<div className="op-modal op-modal-open absolute z-[448]"> <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="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"> <div className="flex flex-row items-center">
<input <label className="inline-flex justify-center items-center cursor-pointer mb-0">
data-tut="IsAgree" {/* 1) This div becomes the “fake” checkbox */}
className="mr-3 op-checkbox op-checkbox-m" <div
type="checkbox" data-tut="IsAgree"
value={isChecked} 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"}`}
onChange={(e) => { >
setIsChecked(e.target.checked); {isChecked ? (
if (e.target.checked) { <span className="op-text-primary text-sm font-bold"></span>
props.setIsAgreeTour(false); ) : (
} <span className="text-red-500 text-sm font-bold">X</span>
props.showFirstWidget(); )}
}} </div>
/> {/* 2) Visually hide the native checkbox but keep it in the DOM */}
<div className="text-[11px] md:text-base"> <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>{t("agree-p1")}</span>
<span <span
className="font-bold text-blue-600 cursor-pointer" className="font-bold text-blue-600 cursor-pointer"
@@ -54,7 +67,7 @@ function AgreementSign(props) {
{t("agrre-button")} {t("agrre-button")}
</button> </button>
</div> </div>
<div className="mt-2"> <div className="mt-2 text-base-content">
<span className="text-[11px]">{t("agreement-note")}</span> <span className="text-[11px]">{t("agreement-note")}</span>
</div> </div>
</div> </div>
@@ -12,12 +12,9 @@ function BorderResize(props) {
return ( return (
<div <div
style={{ style={{ width: getHeight() || "14px", height: getHeight() || "14px" }}
width: getHeight() || "14px",
height: getHeight() || "14px"
}}
className={`${props.right ? `-right-[12px]` : "-right-[2px]"} ${ 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]`} } absolute inline-block hover:cursor-sw-resize border-r-[3px] border-b-[3px] border-[#188ae2]`}
></div> ></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>
);
}
@@ -7,8 +7,8 @@ import { fontColorArr, fontsizeArr } from "../../constant/Utils";
function DropdownWidgetOption(props) { function DropdownWidgetOption(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const [dropdownOptionList, setDropdownOptionList] = useState([ const [dropdownOptionList, setDropdownOptionList] = useState([
"option-1", "Option-1",
"option-2" "Option-2"
]); ]);
const [minCount, setMinCount] = useState(0); const [minCount, setMinCount] = useState(0);
const [maxCount, setMaxCount] = useState(0); const [maxCount, setMaxCount] = useState(0);
@@ -17,18 +17,21 @@ function DropdownWidgetOption(props) {
const [isHideLabel, setIsHideLabel] = useState(false); const [isHideLabel, setIsHideLabel] = useState(false);
const [status, setStatus] = useState("required"); const [status, setStatus] = useState("required");
const [defaultValue, setDefaultValue] = useState(""); const [defaultValue, setDefaultValue] = useState("");
const statusArr = ["required", "optional"];
const [defaultCheckbox, setDefaultCheckbox] = useState([]); const [defaultCheckbox, setDefaultCheckbox] = useState([]);
const [layout, setLayout] = useState("vertical");
const statusArr = ["required", "optional"];
const layoutArr = ["vertical", "horizontal"];
const resetState = () => { const resetState = () => {
setDropdownOptionList(["option-1", "option-2"]); setDropdownOptionList(["Option-1", "Option-2"]);
setDropdownName( props.currWidgetsDetails?.options?.name || props.type); setDropdownName(props.currWidgetsDetails?.options?.name || props.type);
setIsReadOnly(false); setIsReadOnly(false);
setIsHideLabel(false); setIsHideLabel(false);
setMinCount(0); setMinCount(0);
setMaxCount(0); setMaxCount(0);
setDefaultCheckbox([]); setDefaultCheckbox([]);
setDefaultValue(""); setDefaultValue("");
setLayout("vertical");
}; };
useEffect(() => { useEffect(() => {
if ( if (
@@ -48,6 +51,7 @@ function DropdownWidgetOption(props) {
setStatus(props.currWidgetsDetails?.options?.status || "required"); setStatus(props.currWidgetsDetails?.options?.status || "required");
setDefaultValue(props.currWidgetsDetails?.options?.defaultValue || ""); setDefaultValue(props.currWidgetsDetails?.options?.defaultValue || "");
setDefaultCheckbox(props.currWidgetsDetails?.options?.defaultValue || []); setDefaultCheckbox(props.currWidgetsDetails?.options?.defaultValue || []);
setLayout(props.currWidgetsDetails?.options?.layout || "vertical");
} else { } else {
setStatus("required"); setStatus("required");
resetState(); resetState();
@@ -103,6 +107,30 @@ function DropdownWidgetOption(props) {
? defaultCheckbox ? defaultCheckbox
: defaultValue; : 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( props.handleSaveWidgetsOptions(
dropdownName, dropdownName,
dropdownOptionList, dropdownOptionList,
@@ -113,9 +141,10 @@ function DropdownWidgetOption(props) {
null, null,
status, status,
defaultData, defaultData,
isHideLabel isHideLabel,
WidgetLayout
); );
resetState() resetState();
}; };
@@ -137,17 +166,18 @@ function DropdownWidgetOption(props) {
}} }}
> >
<div> <div>
<label className="text-[13px] font-semibold"> <label htmlFor="title" className="text-[13px] font-semibold">
{t("name")} {t("name")}
<span className="text-[red] text-[13px]"> *</span> <span className="text-[red] text-[13px]"> *</span>
</label> </label>
<input <input
id="title"
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))} onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")} onInput={(e) => e.target.setCustomValidity("")}
required
value={dropdownName} value={dropdownName}
onChange={(e) => setDropdownName(e.target.value)} 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" 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]"> <label className="text-[13px] font-semibold mt-[5px]">
@@ -228,28 +258,26 @@ function DropdownWidgetOption(props) {
</select> </select>
</> </>
)} )}
{props.type !== "checkbox" && props.type !== radioButtonWidget && ( {props.type !== "checkbox" && (
<> <div className="flex flex-row gap-[10px] mt-[0.5rem]">
<div className="flex flex-row gap-[10px] mt-[0.5rem]"> {statusArr.map((data, ind) => (
{statusArr.map((data, ind) => { <div
return ( key={ind}
<div className="flex flex-row gap-[5px] items-center"
key={ind} >
className="flex flex-row gap-[5px] items-center" <input
> className="op-radio op-radio-xs my-1"
<input type="radio"
className="op-radio op-radio-xs my-1" name="status"
type="radio" onChange={() => setStatus(data.toLowerCase())}
name="status" checked={status.toLowerCase() === data.toLowerCase()}
onChange={() => setStatus(data.toLowerCase())} />
checked={status.toLowerCase() === data.toLowerCase()} <div className="text-[13px] font-500 capitalize">
/> {data}
<div className="text-[13px] font-500">{data}</div> </div>
</div> </div>
); ))}
})} </div>
</div>
</>
)} )}
<div className="flex items-center mt-3 mb-3"> <div className="flex items-center mt-3 mb-3">
<span>{t("font-size")} :</span> <span>{t("font-size")} :</span>
@@ -270,8 +298,8 @@ function DropdownWidgetOption(props) {
); );
})} })}
</select> </select>
<div className="flex flex-row gap-1 items-center ml-4 "> <div className="flex flex-row gap-1 items-center ml-4">
<span>{t("color")} : </span> <span className="capitalize">{t("color")} : </span>
<select <select
value={ value={
props.fontColor || props.fontColor ||
@@ -313,7 +341,10 @@ function DropdownWidgetOption(props) {
className="op-checkbox op-checkbox-sm" className="op-checkbox op-checkbox-sm"
onChange={(e) => setIsReadOnly(e.target.checked)} 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")} {t("read-only")}
</label> </label>
</div> </div>
@@ -328,15 +359,45 @@ function DropdownWidgetOption(props) {
onChange={(e) => setIsHideLabel(e.target.checked)} 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")} {t("hide-labels")}
</label> </label>
</div> </div>
)} )}
</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">
{t(data)}
</label>
</div>
))}
</div>
</>
)}
</div> </div>
<div <div
className={`${ className={`${
props.type === "checkbox" && props.isShowAdvanceFeature props.type === "checkbox" && props.isShowAdvanceFeature
@@ -1,4 +1,4 @@
import React, { import {
useState, useState,
useRef, useRef,
} from "react"; } from "react";
@@ -1,5 +1,10 @@
import React from "react"; import React from "react";
import { Quill } from "react-quill-new"; 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 // 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 // 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", container: "#toolbar1",
handlers: { undo: undoChange, redo: redoChange } 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 // Modules object for setting up the Quill editor
@@ -65,7 +72,9 @@ export const module2 = {
container: "#toolbar2", container: "#toolbar2",
handlers: { undo: undoChange, redo: redoChange } 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 // Formats objects for setting up the Quill editor
@@ -186,7 +186,7 @@ function EmailComponent({
<input <input
type="email" type="email"
value={emailValue} 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} onChange={handleEmailValue}
onKeyDown={handleEnterPress} onKeyDown={handleEnterPress}
placeholder={t("enter-email-plaholder")} placeholder={t("enter-email-plaholder")}
@@ -204,19 +204,6 @@ function EmailComponent({
{t("email-error-1")} {t("email-error-1")}
</p> </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"> <div className="mt-2">
<button <button
type="button" type="button"
@@ -227,7 +214,7 @@ function EmailComponent({
</button> </button>
<button <button
type="button" type="button"
className="op-btn op-btn-ghost ml-2" className="op-btn op-btn-ghost text-base-content ml-2"
onClick={() => handleClose()} onClick={() => handleClose()}
> >
{t("close")} {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>
);
}
+76 -3
View File
@@ -2,16 +2,21 @@ import React, { useRef, useState } from "react";
import PrevNext from "./PrevNext"; import PrevNext from "./PrevNext";
import { import {
base64ToArrayBuffer, base64ToArrayBuffer,
decryptPdf,
deletePdfPage, deletePdfPage,
flattenPdf,
getFileAsArrayBuffer,
handleDownloadCertificate, handleDownloadCertificate,
handleDownloadPdf, handleDownloadPdf,
handleRemoveWidgets, handleRemoveWidgets,
handleToPrint handleToPrint,
reorderPdfPages
} from "../../constant/Utils"; } from "../../constant/Utils";
import "../../styles/signature.css"; import "../../styles/signature.css";
import { DropdownMenu } from "radix-ui"; import { DropdownMenu } from "radix-ui";
import ModalUi from "../../primitives/ModalUi"; import ModalUi from "../../primitives/ModalUi";
import Loader from "../../primitives/Loader"; import Loader from "../../primitives/Loader";
import PageReorderModal from "./PageReorderModal";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PDFDocument } from "pdf-lib"; import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const"; import { maxFileSize } from "../../constant/const";
@@ -24,6 +29,7 @@ function Header(props) {
const isMobile = window.innerWidth < 767; const isMobile = window.innerWidth < 767;
const [isDownloading, setIsDownloading] = useState(""); const [isDownloading, setIsDownloading] = useState("");
const [isDeletePage, setIsDeletePage] = useState(false); const [isDeletePage, setIsDeletePage] = useState(false);
const [isReorderModal, setIsReorderModal] = useState(false);
const mergePdfInputRef = useRef(null); const mergePdfInputRef = useRef(null);
const enabledBackBtn = props?.disabledBackBtn === true ? false : true; const enabledBackBtn = props?.disabledBackBtn === true ? false : true;
//function for show decline alert //function for show decline alert
@@ -80,7 +86,42 @@ function Header(props) {
return; return;
} }
try { 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, { const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true ignoreEncryption: true
}); });
@@ -106,12 +147,27 @@ function Header(props) {
console.error("Error merging PDF:", error); 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 ( return (
<div className="flex py-[5px]"> <div className="flex py-[5px]">
{isMobile && props?.isShowHeader ? ( {isMobile && props?.isShowHeader ? (
<div <div
id="navbar" id="navbar"
className="stickyHead" className="stickyHead touch-none"
style={{ style={{
width: window.innerWidth + "px" width: window.innerWidth + "px"
}} }}
@@ -334,6 +390,17 @@ function Header(props) {
</span> </span>
</div> </div>
</DropdownMenu.Item> </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 <DropdownMenu.Item
className="DropdownMenuItem" className="DropdownMenuItem"
@@ -696,6 +763,12 @@ function Header(props) {
</button> </button>
</div> </div>
</ModalUi> </ModalUi>
<PageReorderModal
isOpen={isReorderModal}
handleClose={() => setIsReorderModal(false)}
totalPages={props.allPages}
onSave={handleReorderSave}
/>
</div> </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 { useTranslation } from "react-i18next";
import { import {
base64ToArrayBuffer, base64ToArrayBuffer,
decryptPdf,
deletePdfPage, deletePdfPage,
handleRemoveWidgets flattenPdf,
getFileAsArrayBuffer,
handleRemoveWidgets,
reorderPdfPages
} from "../../constant/Utils"; } from "../../constant/Utils";
import ModalUi from "../../primitives/ModalUi"; import ModalUi from "../../primitives/ModalUi";
import { PDFDocument } from "pdf-lib"; import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const"; import { maxFileSize } from "../../constant/const";
import PageReorderModal from "./PageReorderModal";
function PdfZoom(props) { function PdfZoom(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const mergePdfInputRef = useRef(null); const mergePdfInputRef = useRef(null);
const [isDeletePage, setIsDeletePage] = useState(false); const [isDeletePage, setIsDeletePage] = useState(false);
const [isReorderModal, setIsReorderModal] = useState(false);
const handleDetelePage = async () => { const handleDetelePage = async () => {
props.setIsUploadPdf && props.setIsUploadPdf(true); props.setIsUploadPdf && props.setIsUploadPdf(true);
try { try {
@@ -67,7 +73,42 @@ function PdfZoom(props) {
return; return;
} }
try { 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, { const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true 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 ( return (
<> <>
<span className="hidden md:flex flex-col gap-1 text-center md:w-[5%] mt-[42px]"> <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> <i className="fa-light fa-trash text-gray-500 2xl:text-[25px]"></i>
</span> </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 <span
@@ -185,6 +248,12 @@ function PdfZoom(props) {
</button> </button>
</div> </div>
</ModalUi> </ModalUi>
<PageReorderModal
isOpen={isReorderModal}
handleClose={() => setIsReorderModal(false)}
totalPages={props.allPages}
onSave={handleReorderSave}
/>
</> </>
); );
} }
+203 -155
View File
@@ -1,6 +1,5 @@
import React, { useState, useEffect, useRef } from "react"; import { useState, useEffect } from "react";
import BorderResize from "./BorderResize"; import BorderResize from "./BorderResize";
import PlaceholderBorder from "./PlaceholderBorder";
import { Rnd } from "react-rnd"; import { Rnd } from "react-rnd";
import { import {
changeDateToMomentFormat, changeDateToMomentFormat,
@@ -14,7 +13,9 @@ import {
onChangeInput, onChangeInput,
radioButtonWidget, radioButtonWidget,
textInputWidget, textInputWidget,
textWidget cellsWidget,
textWidget,
selectFormat
} from "../../constant/Utils"; } from "../../constant/Utils";
import PlaceholderType from "./PlaceholderType"; import PlaceholderType from "./PlaceholderType";
import moment from "moment"; import moment from "moment";
@@ -23,37 +24,7 @@ import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { setIsShowModal } from "../../redux/reducers/widgetSlice"; import { setIsShowModal } from "../../redux/reducers/widgetSlice";
import { themeColor } from "../../constant/const";
const selectFormat = (data) => {
switch (data) {
case "L":
return "MM/dd/yyyy";
case "MM/DD/YYYY":
return "MM/dd/yyyy";
case "DD-MM-YYYY":
return "dd-MM-yyyy";
case "DD/MM/YYYY":
return "dd/MM/yyyy";
case "LL":
return "MMMM dd, yyyy";
case "DD MMM, YYYY":
return "dd MMM, yyyy";
case "YYYY-MM-DD":
return "yyyy-MM-dd";
case "MM-DD-YYYY":
return "MM-dd-yyyy";
case "MM.DD.YYYY":
return "MM.dd.yyyy";
case "MMM DD, YYYY":
return "MMM dd, yyyy";
case "MMMM DD, YYYY":
return "MMMM dd, yyyy";
case "DD MMMM, YYYY":
return "dd MMMM, yyyy";
default:
return "MM/dd/yyyy";
}
};
//function to get default format //function to get default format
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy"; const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
@@ -78,14 +49,14 @@ function Placeholder(props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const widgetData = const widgetData =
props.pos?.options?.defaultValue || props.pos?.options?.response; props.pos?.options?.defaultValue || props.pos?.options?.response;
const [placeholderBorder, setPlaceholderBorder] = useState({ w: 0, h: 0 });
const [isDateModal, setIsDateModal] = useState(false); const [isDateModal, setIsDateModal] = useState(false);
const [containerScale, setContainerScale] = useState(); const [containerScale, setContainerScale] = useState();
const holdTimeout = useRef(null);
const startTime = useRef(null); // Track when the user starts holdings
const [selectDate, setSelectDate] = useState({}); const [selectDate, setSelectDate] = useState({});
const [dateFormat, setDateFormat] = useState([]); const [dateFormat, setDateFormat] = useState([]);
const [clickonWidget, setClickonWidget] = useState({}); const [clickonWidget, setClickonWidget] = useState({});
const [isDateReadOnly, setIsDateReadOnly] = useState(
props?.pos?.options?.isReadOnly || false
);
const startDate = props?.pos?.options?.response const startDate = props?.pos?.options?.response
? getDefaultDate( ? getDefaultDate(
props?.pos?.options?.response, props?.pos?.options?.response,
@@ -93,10 +64,6 @@ function Placeholder(props) {
) )
: new Date(); : new Date();
const [getCheckboxRenderWidth, setGetCheckboxRenderWidth] = useState({
width: null,
height: null
});
useEffect(() => { useEffect(() => {
const getPdfPageWidth = props.pdfOriginalWH.find( const getPdfPageWidth = props.pdfOriginalWH.find(
(data) => data.pageNumber === props.pageNumber (data) => data.pageNumber === props.pageNumber
@@ -113,7 +80,9 @@ function Placeholder(props) {
"MMM DD, YYYY", "MMM DD, YYYY",
"LL", "LL",
"DD MMM, YYYY", "DD MMM, YYYY",
"DD MMMM, YYYY" "DD MMMM, YYYY",
"DD.MM.YYYY",
"DD/MM/YYYY"
]; ];
useEffect(() => { useEffect(() => {
@@ -127,18 +96,6 @@ function Placeholder(props) {
} }
}, [widgetData]); }, [widgetData]);
const handleGetDaynamicWH = () => {
if (
props?.pos?.type === "checkbox" ||
props?.pos?.type === radioButtonWidget
) {
const rndElement = document.getElementById(props.pos.key);
if (rndElement) {
const { width, height } = rndElement.getBoundingClientRect();
setGetCheckboxRenderWidth({ width: width, height: height });
}
}
};
//function change format array list with selected date and format //function change format array list with selected date and format
const changeDateFormat = () => { const changeDateFormat = () => {
const updateDate = []; const updateDate = [];
@@ -147,15 +104,18 @@ function Placeholder(props) {
if (selectDate && selectDate.format === "dd-MM-yyyy") { if (selectDate && selectDate.format === "dd-MM-yyyy") {
const [day, month, year] = selectDate.date.split("-"); const [day, month, year] = selectDate.date.split("-");
date = new Date(`${year}-${month}-${day}`); date = new Date(`${year}-${month}-${day}`);
} else if (selectDate && selectDate.format === "dd.MM.yyyy") {
const [day, month, year] = selectDate.date.split(".");
date = new Date(`${year}.${month}.${day}`);
} else if (selectDate && selectDate.format === "dd/MM/yyyy") {
const [day, month, year] = selectDate.date.split("/");
date = new Date(`${year}/${month}/${day}`);
} else { } else {
date = new Date(selectDate?.date); date = new Date(selectDate?.date);
} }
const milliseconds = date.getTime(); const milliseconds = date.getTime();
const newDate = moment(milliseconds).format(data); const newDate = moment(milliseconds).format(data);
const dateObj = { const dateObj = { date: newDate, format: selectFormat(data) };
date: newDate,
format: selectFormat(data)
};
updateDate.push(dateObj); updateDate.push(dateObj);
}); });
setDateFormat(updateDate); setDateFormat(updateDate);
@@ -309,7 +269,6 @@ function Placeholder(props) {
//The else condition is used to handle the case when the user clicks on a widget and open signature pad to draw sign //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.setCurrWidgetsDetails && props.setCurrWidgetsDetails(props.pos);
handleWidgetIdandPopup(); handleWidgetIdandPopup();
handleGetDaynamicWH();
} }
}; };
//`handleOnClickSettingIcon` is used set current widget details and open setting of it //`handleOnClickSettingIcon` is used set current widget details and open setting of it
@@ -321,6 +280,13 @@ function Placeholder(props) {
} else if (props.pos.type === "checkbox") { } else if (props.pos.type === "checkbox") {
props?.setIsCheckbox(true); 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 //condition to handle setting icon for signyour-self flow for all type text widgets
else if ( else if (
[ [
@@ -400,9 +366,12 @@ function Placeholder(props) {
//function to save date and format on local array onchange date and onclick format //function to save date and format on local array onchange date and onclick format
const handleSaveDate = (data, isDateChange) => { const handleSaveDate = (data, isDateChange) => {
const isSpecialDateFormat =
data?.format &&
["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(data?.format);
let updateDate = data.date; let updateDate = data.date;
let date; let date;
if (data?.format === "dd-MM-yyyy") { if (isSpecialDateFormat) {
date = isDateChange date = isDateChange
? moment(updateDate).format(changeDateToMomentFormat(data.format)) ? moment(updateDate).format(changeDateToMomentFormat(data.format))
: updateDate; : updateDate;
@@ -424,11 +393,54 @@ function Placeholder(props) {
false, false,
data?.format, data?.format,
props.fontSize || props.pos?.options?.fontSize || 12, 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 }); 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 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 // 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: // 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. // 1- When isAlllowModify is true, show border's icon.
@@ -452,10 +464,15 @@ function Placeholder(props) {
"name", "name",
"company", "company",
"job title", "job title",
"email" "email",
...isSettingForCells
].includes(props.pos.type) && ].includes(props.pos.type) &&
(props.isSignYourself || props.isSelfSign) ? ( (props.isSignYourself || props.isSelfSign) ? (
<i <i
onPointerDown={(e) => {
e.stopPropagation();
handleOnClickSettingIcon();
}}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleOnClickSettingIcon(); handleOnClickSettingIcon();
@@ -465,7 +482,14 @@ function Placeholder(props) {
handleOnClickSettingIcon(); handleOnClickSettingIcon();
}} }}
className="fa-light fa-gear icon" 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> ></i>
) : ( ) : (
/* condition to add setting icon for placeholder & template flow for all widgets except signature and date */ /* condition to add setting icon for placeholder & template flow for all widgets except signature and date */
@@ -475,6 +499,10 @@ function Placeholder(props) {
!props.isSignYourself && !props.isSignYourself &&
!props.isSelfSign)) && ( !props.isSelfSign)) && (
<i <i
onPointerDown={(e) => {
e.stopPropagation();
handleOnClickSettingIcon();
}}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleOnClickSettingIcon(); handleOnClickSettingIcon();
@@ -487,7 +515,10 @@ function Placeholder(props) {
style={{ style={{
color: "#188ae2", color: "#188ae2",
right: props?.pos?.type === textWidget ? "32px" : "51px", right: props?.pos?.type === textWidget ? "32px" : "51px",
top: "-19px" top: "-19px",
cursor: "pointer",
zIndex: 99,
pointerEvents: "auto"
}} }}
></i> ></i>
) )
@@ -525,6 +556,7 @@ function Placeholder(props) {
{/* setting icon only for date widgets */} {/* setting icon only for date widgets */}
{props.pos.type === "date" && selectDate && ( {props.pos.type === "date" && selectDate && (
<i <i
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => { onClick={(e) => {
props.setCurrWidgetsDetails(props.pos); props.setCurrWidgetsDetails(props.pos);
setIsDateModal(!isDateModal); setIsDateModal(!isDateModal);
@@ -557,7 +589,9 @@ function Placeholder(props) {
top: "-18px", top: "-18px",
right: props.isPlaceholder ? "50px" : "30px", right: props.isPlaceholder ? "50px" : "30px",
color: "#188ae2", color: "#188ae2",
fontSize: "14px" fontSize: "14px",
cursor: "pointer",
pointerEvents: "auto"
}} }}
className="fa-light fa-gear icon" className="fa-light fa-gear icon"
></i> ></i>
@@ -684,7 +718,7 @@ function Placeholder(props) {
return "not-allowed"; return "not-allowed";
} }
} else { } else {
return "all-scroll"; return "move";
} }
}; };
@@ -704,21 +738,6 @@ function Placeholder(props) {
return "rgba(203, 233, 237, 0.69)"; 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 fontSize = calculateFont(props.pos.options?.fontSize); const fontSize = calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black"; const fontColor = props.pos.options?.fontColor || "black";
@@ -769,20 +788,16 @@ function Placeholder(props) {
id={props.pos.key} id={props.pos.key}
data-tut={props.pos.key === props.unSignedWidgetId ? "IsSigned" : ""} data-tut={props.pos.key === props.unSignedWidgetId ? "IsSigned" : ""}
key={props.pos.key} key={props.pos.key}
cancel=".cell-size-handle, .icon"
lockAspectRatio={ lockAspectRatio={
!props.isFreeResize && props?.isAlllowModify &&
![ !props?.assignedWidgetId.includes(props.pos.key)
textWidget, ? false
"email", : !props.isFreeResize &&
"name", (props.pos.Width
"company", ? props.pos.Width / props.pos.Height
"job title", : defaultWidthHeight(props.pos.type).width /
textInputWidget defaultWidthHeight(props.pos.type).height)
].includes(props.pos.type) &&
(props.pos.Width
? props.pos.Width / props.pos.Height
: defaultWidthHeight(props.pos.type).width /
defaultWidthHeight(props.pos.type).height)
} }
enableResizing={{ enableResizing={{
top: false, top: false,
@@ -822,9 +837,16 @@ function Placeholder(props) {
props.isNeedSign && props.data?.Id !== props?.uniqueId && "0.4", props.isNeedSign && props.data?.Id !== props?.uniqueId && "0.4",
background: handleBackground() background: handleBackground()
}} }}
onDrag={() => { onDrag={(_, d) => {
handleGetDaynamicWH();
props.handleTabDrag && props.handleTabDrag(props.pos.key); props.handleTabDrag && props.handleTabDrag(props.pos.key);
props.showGuidelines &&
props.showGuidelines(
true,
d.x,
d.y,
d.node.offsetWidth,
d.node.offsetHeight
);
}} }}
size={{ size={{
width: width:
@@ -838,10 +860,33 @@ function Placeholder(props) {
? "auto" ? "auto"
: props.posHeight(props.pos, props.isSignYourself) : 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" maxHeight="auto"
onResizeStart={() => { onResizeStart={(e, dir, ref) => {
props.setIsResize && props.setIsResize(true); props.setIsResize && props.setIsResize(true);
props.showGuidelines &&
props.showGuidelines(
true,
xPos(props.pos, props.isSignYourself),
yPos(props.pos, props.isSignYourself),
ref.offsetWidth,
ref.offsetHeight
);
}}
onResize={(e, dir, ref, delta, position) => {
props.showGuidelines &&
props.showGuidelines(
true,
position.x,
position.y,
ref.offsetWidth,
ref.offsetHeight
);
}} }}
onResizeStop={(e, direction, ref) => { onResizeStop={(e, direction, ref) => {
setTimeout(() => { setTimeout(() => {
@@ -859,6 +904,7 @@ function Placeholder(props) {
props.data && props.data.Id, props.data && props.data.Id,
props.isResize props.isResize
); );
props.showGuidelines && props.showGuidelines(false);
}} }}
onDragStop={(event, dragElement) => { onDragStop={(event, dragElement) => {
props.handleStop && props.handleStop &&
@@ -868,17 +914,14 @@ function Placeholder(props) {
props.data?.Id, props.data?.Id,
props.pos?.key props.pos?.key
); );
props.isDragging &&
props.showGuidelines &&
props.showGuidelines(false);
}} }}
position={{ position={{
x: xPos(props.pos, props.isSignYourself), x: xPos(props.pos, props.isSignYourself),
y: yPos(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)
});
}}
disableDragging={handleDragging()} disableDragging={handleDragging()}
> >
{props.pos.key === props?.currWidgetsDetails?.key && {props.pos.key === props?.currWidgetsDetails?.key &&
@@ -910,41 +953,28 @@ function Placeholder(props) {
![radioButtonWidget, "checkbox"].includes(props.pos.type) && ![radioButtonWidget, "checkbox"].includes(props.pos.type) &&
props.pos.key === props?.currWidgetsDetails?.key && <BorderResize /> props.pos.key === props?.currWidgetsDetails?.key && <BorderResize />
)} )}
{/* 1- Show a border if props.pos.key === props?.currWidgetsDetails?.key, indicating the current user's selected widget. {/* 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 borders for all widgets. 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 borders: 3- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show border:
1- When isAlllowModify is true, show borders. 3.1- When isAlllowModify is true, show border.
2- Do not display border for widgets already assigned (props.assignedWidgetId.includes(props.pos.key) is true). 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.pos.key === props?.currWidgetsDetails?.key &&
(props.isShowBorder || (props.isShowBorder ||
(props?.isAlllowModify && (props?.isAlllowModify &&
!props?.assignedWidgetId.includes(props.pos.key))) && ( !props?.assignedWidgetId.includes(props.pos.key))) && (
<PlaceholderBorder <div
pos={props.pos} style={{ borderColor: themeColor }}
isPlaceholder={props.isPlaceholder} className="w-[calc(100%+21px)] h-[calc(100%+21px)] cursor-move absolute inline-block border-[1px] border-dashed"
getCheckboxRenderWidth={getCheckboxRenderWidth} ></div>
scale={props.scale}
containerScale={containerScale}
placeholderBorder={placeholderBorder}
/>
)} )}
<div <div
className="flex items-stretch justify-center" className="flex items-stretch justify-center"
style={{ style={{
left: xPos(props.pos, props.isSignYourself), left: xPos(props.pos, props.isSignYourself),
top: yPos(props.pos, props.isSignYourself), top: yPos(props.pos, props.isSignYourself),
width: width: "100%",
props.pos.type === radioButtonWidget || height: "100%",
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),
zIndex: "10" zIndex: "10"
}} }}
onTouchEnd={() => handleOnClickPlaceholder()} onTouchEnd={() => handleOnClickPlaceholder()}
@@ -973,18 +1003,19 @@ function Placeholder(props) {
handleSaveDate={handleSaveDate} handleSaveDate={handleSaveDate}
xPos={props.xPos} xPos={props.xPos}
calculateFont={calculateFont} calculateFont={calculateFont}
setCellCount={setCellCount}
/> />
</div> </div>
</Rnd> </Rnd>
)} )}
<ModalUi isOpen={isDateModal} title={t("widget-info")} showClose={false}> <ModalUi isOpen={isDateModal} title={t("widget-info")} showClose={false}>
<div className="h-[100%] p-[20px]"> <div className="text-base-content h-[100%] p-[20px]">
<div className="flex flex-row items-center"> <div className="flex flex-col md:flex-row md:items-center gap-y-3">
<span>{t("format")} : </span> <div className="flex flex-row items-center gap-x-1">
<div className="flex"> <span className="capitalize">{t("format")} :</span>
<select <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={""} defaultValue={""}
onChange={(e) => { onChange={(e) => {
const selectedIndex = e.target.value; const selectedIndex = e.target.value;
@@ -1008,23 +1039,23 @@ function Placeholder(props) {
{selectDate.format} {selectDate.format}
</span> </span>
</div> </div>
<div className="flex items-center mt-4 md:mt-2"> <div className="flex flex-col md:flex-row gap-y-2 md:gap-y-0 gap-x-2 mt-3">
<span>{t("font-size")} :</span> <div className="flex flex-row items-center">
<select <span className="capitalize">{t("font-size")} :</span>
className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs" <select
value={props.fontSize || clickonWidget.options?.fontSize || 12} className="ml-[3px] md:ml:[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
onChange={(e) => props.setFontSize(parseInt(e.target.value))} value={props.fontSize || clickonWidget.options?.fontSize || 12}
> onChange={(e) => props.setFontSize(parseInt(e.target.value))}
{fontsizeArr.map((size, ind) => { >
return ( {fontsizeArr.map((size, ind) => (
<option className="text-[13px]" value={size} key={ind}> <option className="text-[13px]" value={size} key={ind}>
{size} {size}
</option> </option>
); ))}
})} </select>
</select> </div>
<div className="flex flex-row gap-1 items-center ml-2 md:ml-4 "> <div className="flex flex-row gap-1 items-center">
<span>{t("color")}: </span> <span className="capitalize">{t("color")} :</span>
<select <select
value={ value={
props.fontColor || clickonWidget.options?.fontColor || "black" props.fontColor || clickonWidget.options?.fontColor || "black"
@@ -1032,13 +1063,11 @@ function Placeholder(props) {
onChange={(e) => props.setFontColor(e.target.value)} 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" 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) => { {fontColorArr.map((color, ind) => (
return ( <option value={color} key={ind}>
<option value={color} key={ind}> {t(`color-type.${color}`)}
{t(`color-type.${color}`)} </option>
</option> ))}
);
})}
</select> </select>
<span <span
style={{ style={{
@@ -1049,7 +1078,26 @@ function Placeholder(props) {
></span> ></span>
</div> </div>
</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> <div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button <button
type="button" type="button"
@@ -1,56 +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
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;
@@ -1,9 +1,10 @@
import React, { useEffect, useState, forwardRef, useRef } from "react"; import React, { useEffect, useState, forwardRef } from "react";
import { import {
getMonth, getMonth,
getYear, getYear,
radioButtonWidget, radioButtonWidget,
textInputWidget, textInputWidget,
cellsWidget,
textWidget, textWidget,
months, months,
years, years,
@@ -14,8 +15,9 @@ import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css"; import "react-datepicker/dist/react-datepicker.css";
import "../../styles/signature.css"; import "../../styles/signature.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import CellsWidget from "./CellsWidget";
const textWidgetCls = 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 = const selectWidgetCls =
"w-full h-full absolute left-0 top-0 border-[1px] border-[#007bff] rounded-[2px] focus:outline-none text-base-content"; "w-full h-full absolute left-0 top-0 border-[1px] border-[#007bff] rounded-[2px] focus:outline-none text-base-content";
const widgetCls = const widgetCls =
@@ -27,10 +29,13 @@ function PlaceholderType(props) {
props.isSignYourself || props.isSignYourself ||
((props.isSelfSign || props.isNeedSign) && ((props.isSelfSign || props.isNeedSign) &&
props.data?.signerObjId === props.signerObjId); 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 = 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 widgetTypeTranslation = t(`widgets-name.${props?.pos?.type}`);
const inputRef = useRef(null);
const [widgetValue, setwidgetValue] = useState(); const [widgetValue, setwidgetValue] = useState();
const [selectedCheckbox, setSelectedCheckbox] = useState([]); const [selectedCheckbox, setSelectedCheckbox] = useState([]);
const [hint, setHint] = useState(""); const [hint, setHint] = useState("");
@@ -56,16 +61,13 @@ function PlaceholderType(props) {
[] []
); );
} else { } else {
if (widgetData) { // keep displayed value in sync with the stored response
setwidgetValue(widgetData); setwidgetValue(widgetData);
}
} }
if (props.pos?.options?.hint) { if (props.pos?.options?.hint) {
setHint(props.pos?.options.hint); setHint(props.pos?.options.hint);
} else if (props.pos?.options?.validation?.type) { } else if (props.pos?.options?.validation?.type) {
checkRegularExpress(props.pos?.options?.validation?.type, setHint); checkRegularExpress(props.pos?.options?.validation?.type, setHint);
} else {
setHint(props.pos?.type);
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -77,7 +79,8 @@ function PlaceholderType(props) {
color: fontColor, color: fontColor,
fontFamily: "Arial, sans-serif" fontFamily: "Arial, sans-serif"
}} }}
className={`${selectWidgetCls} overflow-hidden`} className={`${isReadOnly ? `select-none` : ``} ${selectWidgetCls} overflow-hidden`}
disabled={isReadOnly}
onClick={onClick} onClick={onClick}
ref={ref} ref={ref}
> >
@@ -98,22 +101,6 @@ function PlaceholderType(props) {
} }
}; };
//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) { switch (type) {
case "signature": case "signature":
return props.pos.SignUrl ? ( return props.pos.SignUrl ? (
@@ -164,68 +151,59 @@ function PlaceholderType(props) {
</div> </div>
); );
case "checkbox": 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-[3px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
return ( return (
<div style={{ zIndex: props.isSignYourself && "99" }}> <div
{props.pos.options?.values?.map((data, ind) => { className={checkBoxWrapperClass}
return ( style={{ zIndex: props.isSignYourself && "99" }}
<div >
key={ind} {props.pos.options?.values?.map((data, ind) => (
className="select-none-cls flex items-center text-center gap-0.5 pointer-events-none" <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 <input
id={`checkbox-${props.pos.key + ind}`} id={`checkbox-${props.pos.key + ind}`}
style={{ width: fontSize, height: fontSize }} style={{ width: fontSize, height: fontSize }}
className={`${ className="op-checkbox rounded-[1px]"
ind === 0 ? "mt-0" : "mt-[5px]" disabled={props.isNeedSign && isReadOnly}
} flex justify-center op-checkbox rounded-[1px] `}
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
type="checkbox" type="checkbox"
readOnly readOnly
checked={!!selectCheckbox(ind, selectedCheckbox)} checked={!!selectCheckbox(ind, selectedCheckbox)}
/> />
{!props.pos.options?.isHideLabel && ( {!props.pos.options?.isHideLabel && (
<label <span className="leading-none">{data}</span>
htmlFor={`checkbox-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0 text-center"
>
{data}
</label>
)} )}
</div> </label>
); </div>
})} ))}
</div> </div>
); );
case textInputWidget: case textInputWidget:
return props.isSignYourself || iswidgetEnable ? ( return props.isSignYourself || iswidgetEnable ? (
<textarea <textarea
ref={inputRef}
placeholder={hint || t("widgets-name.text")} placeholder={hint || t("widgets-name.text")}
rows={1} rows={1}
value={widgetValue} value={widgetValue}
className={`${ className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId
? "select-none"
: textWidgetCls
}`}
style={{ style={{
fontSize: fontSize, fontSize: fontSize,
color: fontColor, color: fontColor,
background: props.data?.blockColor, background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none" pointerEvents: "none"
}} }}
readOnly readOnly
disabled={ disabled={props.isNeedSign && isReadOnly}
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
cols="50" cols="50"
/> />
) : ( ) : (
@@ -233,17 +211,42 @@ function PlaceholderType(props) {
<span>{hint || widgetTypeTranslation}</span> <span>{hint || widgetTypeTranslation}</span>
</div> </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": case "dropdown":
return ( return (
<div <div
style={textWidgetStyle} style={textWidgetStyle}
className="select-none-cls flex justify-between items-center" className="select-none-cls flex justify-between items-center"
> >
{widgetData || hint || widgetTypeTranslation} {widgetData || t("choose-one")}
<i className="fa-light fa-circle-chevron-down mr-1 "></i> <i className="fa-light fa-circle-chevron-down mr-1 "></i>
</div> </div>
); );
case "initials": case "initials":
return props.pos.SignUrl ? ( return props.pos.SignUrl ? (
<img <img
@@ -272,17 +275,18 @@ function PlaceholderType(props) {
return iswidgetEnable ? ( return iswidgetEnable ? (
<textarea <textarea
readOnly readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation} placeholder={hint || widgetTypeTranslation}
rows={1} rows={1}
value={widgetValue} value={widgetValue}
className={textWidgetCls} className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{ style={{
fontSize: fontSize, fontSize: fontSize,
color: fontColor, color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none" pointerEvents: "none"
}} }}
cols="50" cols="50"
disabled={props.isNeedSign && isReadOnly}
/> />
) : ( ) : (
<div className="flex h-full select-none-cls" style={textWidgetStyle}> <div className="flex h-full select-none-cls" style={textWidgetStyle}>
@@ -293,17 +297,18 @@ function PlaceholderType(props) {
return iswidgetEnable ? ( return iswidgetEnable ? (
<textarea <textarea
readOnly readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation} placeholder={hint || widgetTypeTranslation}
rows={1} rows={1}
value={widgetValue} value={widgetValue}
className={textWidgetCls} className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{ style={{
fontSize: fontSize, fontSize: fontSize,
color: fontColor, color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none" pointerEvents: "none"
}} }}
cols="50" cols="50"
disabled={props.isNeedSign && isReadOnly}
/> />
) : ( ) : (
<div style={textWidgetStyle} className="select-none-cls"> <div style={textWidgetStyle} className="select-none-cls">
@@ -314,17 +319,18 @@ function PlaceholderType(props) {
return iswidgetEnable ? ( return iswidgetEnable ? (
<textarea <textarea
readOnly readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation} placeholder={hint || widgetTypeTranslation}
rows={1} rows={1}
value={widgetValue} value={widgetValue}
className={textWidgetCls} className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{ style={{
fontSize: fontSize, fontSize: fontSize,
color: fontColor, color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none" pointerEvents: "none"
}} }}
cols="50" cols="50"
disabled={props.isNeedSign && isReadOnly}
/> />
) : ( ) : (
<div style={textWidgetStyle} className="select-none-cls"> <div style={textWidgetStyle} className="select-none-cls">
@@ -418,19 +424,18 @@ function PlaceholderType(props) {
return iswidgetEnable ? ( return iswidgetEnable ? (
<textarea <textarea
readOnly readOnly
ref={inputRef}
placeholder={hint || widgetTypeTranslation} placeholder={hint || widgetTypeTranslation}
rows={1} rows={1}
value={widgetValue} value={widgetValue}
className={textWidgetCls} className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{ style={{
fontSize: fontSize, fontSize: fontSize,
color: fontColor, color: fontColor,
fontFamily: "Arial, sans-serif", background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none" pointerEvents: "none"
}} }}
disabled
cols="1" cols="1"
disabled={props.isNeedSign && isReadOnly}
/> />
) : ( ) : (
<div style={textWidgetStyle} className="select-none-cls"> <div style={textWidgetStyle} className="select-none-cls">
@@ -438,45 +443,39 @@ function PlaceholderType(props) {
</div> </div>
); );
case radioButtonWidget: 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-[10px]" : ""}`
: `flex-col ${isOnlyOneBtn ? "gap-y-[5px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
return ( return (
<div> <div className={radioWrapperClass}>
{props.pos.options?.values.map((data, ind) => { {props.pos.options?.values.map((data, ind) => (
return ( <div key={ind} className="select-none-cls pointer-events-none">
<div <label
key={ind} htmlFor={`radio-${props.pos.key + ind}`}
className="select-none-cls flex items-center text-center gap-0.5 pointer-events-none" style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0 flex items-center gap-1"
> >
<input <input
readOnly readOnly
id={`radio-${props.pos.key + ind}`} id={`radio-${props.pos.key + ind}`}
style={{ style={{ width: fontSize, height: fontSize, lineHeight: 2 }}
width: fontSize, className={`op-radio rounded-full border-black appearance-none bg-white inline-block align-middle relative ${
height: fontSize,
marginTop: ind > 0 ? "10px" : "0px"
}}
className={`op-radio rounded-full border- border-black appearance-none bg-white inline-block align-middle relative ${
handleRadioCheck(data) ? "checked-radio" : "" handleRadioCheck(data) ? "checked-radio" : ""
}`} }`}
type="radio" type="radio"
disabled={ disabled={props.isNeedSign && isReadOnly}
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
checked={handleRadioCheck(data)} checked={handleRadioCheck(data)}
/> />
{!props.pos.options?.isHideLabel && ( {!props.pos.options?.isHideLabel && (
<label <span className="leading-none">{data}</span>
htmlFor={`radio-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0"
>
{data}
</label>
)} )}
</div> </label>
); </div>
})} ))}
</div> </div>
); );
case textWidget: case textWidget:
@@ -490,7 +489,8 @@ function PlaceholderType(props) {
style={{ style={{
fontFamily: "Arial, sans-serif", fontFamily: "Arial, sans-serif",
fontSize: fontSize, fontSize: fontSize,
color: fontColor color: fontColor,
background: "white"
}} }}
cols="50" cols="50"
/> />
@@ -31,7 +31,7 @@ const RecipientList = (props) => {
e.preventDefault(); 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) => { const handleChangeSequence = (e, ind, isUp, isDown, obj) => {
e.preventDefault(); e.preventDefault();
let draggedItemId; let draggedItemId;
@@ -82,7 +82,7 @@ const RecipientList = (props) => {
}; };
return ( return (
<> <>
{props.signersdata.length > 0 && {props.signersdata?.length > 0 &&
props.signersdata.map((obj, ind) => { props.signersdata.map((obj, ind) => {
return ( return (
<div <div
@@ -3,7 +3,12 @@ import { useTranslation } from "react-i18next";
import { Document, Page } from "react-pdf"; import { Document, Page } from "react-pdf";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { PDFDocument } from "pdf-lib"; import { PDFDocument } from "pdf-lib";
import { base64ToArrayBuffer } from "../../constant/Utils"; import {
base64ToArrayBuffer,
decryptPdf,
flattenPdf,
getFileAsArrayBuffer
} from "../../constant/Utils";
import { maxFileSize } from "../../constant/const"; import { maxFileSize } from "../../constant/const";
function RenderAllPdfPage(props) { function RenderAllPdfPage(props) {
@@ -87,7 +92,42 @@ function RenderAllPdfPage(props) {
return; return;
} }
try { 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, { const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true ignoreEncryption: true
}); });
@@ -169,8 +209,8 @@ function RenderAllPdfPage(props) {
onChange={handleFileUpload} onChange={handleFileUpload}
/> />
<i className="fa-light fa-plus text-gray-500"></i> <i className="fa-light fa-plus text-gray-500"></i>
<span className="text-xs lg:text-sm text-base-content "> <span className="text-xs lg:text-sm text-base-content">
Add pages {t("add-pages")}
</span> </span>
</button> </button>
)} )}
+284 -431
View File
@@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState, useRef } from "react";
import RSC from "react-scrollbars-custom"; import RSC from "react-scrollbars-custom";
import { Document, Page } from "react-pdf"; import { Document, Page } from "react-pdf";
import { import {
@@ -11,13 +11,45 @@ import {
import Placeholder from "./Placeholder"; import Placeholder from "./Placeholder";
import Alert from "../../primitives/Alert"; import Alert from "../../primitives/Alert";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import usePdfPinchZoom from "../../hook/usePdfPinchZoom";
function RenderPdf(props) { function RenderPdf(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const [scaledHeight, setScaledHeight] = useState(); const [scaledHeight, setScaledHeight] = useState();
const [guideline, setGuideline] = useState({
show: false,
x1: 0,
x2: 0,
y1: 0,
y2: 0
});
//check isGuestSigner is present in local if yes than handle login flow header in mobile view //check isGuestSigner is present in local if yes than handle login flow header in mobile view
const isGuestSigner = localStorage.getItem("isGuestSigner"); const isGuestSigner = localStorage.getItem("isGuestSigner");
const pdfContainerRef = useRef(null);
// enable pinch to zoom only on actual pdf wrapper
usePdfPinchZoom(
pdfContainerRef,
props.scale,
props.setScale,
props.setZoomPercent
);
const handleGuideline = (isShow, x = 0, y = 0, width = 0, height = 0) => {
if (isShow) {
setGuideline({
show: true,
x1: x,
x2: x + width,
y1: y,
y2: y + height
});
} else {
setGuideline({ show: false, x1: 0, x2: 0, y1: 0, y2: 0 });
}
};
// handle signature block width and height according to screen // handle signature block width and height according to screen
const posWidth = (pos, signYourself) => { const posWidth = (pos, signYourself) => {
const containerScale = getContainerScale( const containerScale = getContainerScale(
@@ -79,7 +111,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) => { const checkSignedSigners = (data) => {
let checkSign = []; let checkSign = [];
//condition to handle quick send flow and using normal request sign flow //condition to handle quick send flow and using normal request sign flow
@@ -91,73 +123,73 @@ function RenderPdf(props) {
: []; : [];
return ( return (
checkSign.length === 0 && checkSign.length === 0 &&
data?.placeHolder?.map((placeData, key) => { data?.placeHolder?.map((placeData, key) => (
return ( <React.Fragment key={key}>
<React.Fragment key={key}> {placeData.pageNumber === props.pageNumber &&
{placeData.pageNumber === props.pageNumber && placeData.pos.map(
placeData.pos.map((pos, ind) => { (pos, ind) =>
return ( pos && (
pos && ( <React.Fragment key={ind}>
<React.Fragment key={ind}> <Placeholder
<Placeholder pos={pos}
pos={pos} handleSignYourselfImageResize={handleImageResize}
handleSignYourselfImageResize={handleImageResize} index={props.pageNumber}
index={props.pageNumber} xyPosition={props.signerPos}
xyPosition={props.signerPos} setXyPosition={props.setSignerPos}
setXyPosition={props.setSignerPos} data={data}
data={data} setIsResize={props.setIsResize}
setIsResize={props.setIsResize} isShowBorder={props.isSelfSign}
isShowBorder={props.isSelfSign} isAlllowModify={props.isAlllowModify}
isAlllowModify={props.isAlllowModify} signerObjId={props.signerObjectId}
signerObjId={props.signerObjectId} isShowDropdown={true}
isShowDropdown={true} isNeedSign={props.pdfRequest}
isNeedSign={props.pdfRequest} isSelfSign={true}
isSelfSign={true} isSignYourself={false}
isSignYourself={false} posWidth={posWidth}
posWidth={posWidth} posHeight={posHeight}
posHeight={posHeight} showGuidelines={handleGuideline}
isDragging={props.isDragging} isDragging={props.isDragging}
pdfDetails={props.pdfDetails} pdfDetails={props.pdfDetails}
unSignedWidgetId={props.unSignedWidgetId} unSignedWidgetId={props.unSignedWidgetId}
setCurrWidgetsDetails={props.setCurrWidgetsDetails} setCurrWidgetsDetails={props.setCurrWidgetsDetails}
uniqueId={props.uniqueId} uniqueId={props.uniqueId}
scale={props.scale} scale={props.scale}
containerWH={props.containerWH} containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH} pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber} pageNumber={props.pageNumber}
ispublicTemplate={props.ispublicTemplate} ispublicTemplate={props.ispublicTemplate}
handleUserDetails={props.handleUserDetails} handleUserDetails={props.handleUserDetails}
isResize={props.isResize} isResize={props.isResize}
setIsAgreeTour={props.setIsAgreeTour} setIsAgreeTour={props.setIsAgreeTour}
isAgree={props.isAgree} isAgree={props.isAgree}
handleTabDrag={props.handleTabDrag} handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop} handleStop={props.handleStop}
setUniqueId={props.setUniqueId} setUniqueId={props.setUniqueId}
setIsSelectId={props.setIsSelectId} setIsSelectId={props.setIsSelectId}
handleDeleteSign={props.handleDeleteSign} handleDeleteSign={props.handleDeleteSign}
setIsPageCopy={props.setIsPageCopy} setIsPageCopy={props.setIsPageCopy}
handleTextSettingModal={props.handleTextSettingModal} handleTextSettingModal={props.handleTextSettingModal}
setIsCheckbox={props.setIsCheckbox} handleCellSettingModal={props.handleCellSettingModal}
isFreeResize={false} setIsCheckbox={props.setIsCheckbox}
isOpenSignPad={true} isFreeResize={props.isSelfSign ? true : false}
assignedWidgetId={props.assignedWidgetId} isOpenSignPad={true}
isApplyAll={true} assignedWidgetId={props.assignedWidgetId}
setFontSize={props.setFontSize} isApplyAll={true}
fontSize={props.fontSize} setCellCount={props.setCellCount}
fontColor={props.fontColor} setFontSize={props.setFontSize}
setFontColor={props.setFontColor} fontSize={props.fontSize}
setRequestSignTour={props.setRequestSignTour} fontColor={props.fontColor}
calculateFontsize={calculateFontsize} setFontColor={props.setFontColor}
currWidgetsDetails={props?.currWidgetsDetails} setRequestSignTour={props.setRequestSignTour}
setTempSignerId={props.setTempSignerId} calculateFontsize={calculateFontsize}
/> currWidgetsDetails={props?.currWidgetsDetails}
</React.Fragment> setTempSignerId={props.setTempSignerId}
) />
); </React.Fragment>
})} )
</React.Fragment> )}
); </React.Fragment>
}) ))
); );
}; };
@@ -172,148 +204,145 @@ function RenderPdf(props) {
} }
}; };
const pdfDataBase64 = `data:application/pdf;base64,${props.pdfBase64Url}`; 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 handlePageLoadSuccess = (page) => {
const containerWidth = props.divRef.current.offsetWidth; // Get container width if (isMobile) {
const viewport = page.getViewport({ scale: 1 }); const containerWidth = props.divRef.current.offsetWidth; // Get container width
const scale = containerWidth / viewport.width; // Scale to fit container width const viewport = page.getViewport({ scale: 1 });
const scaleHeight = viewport.height * scale; const scale = containerWidth / viewport.width; // Scale to fit container width
setScaledHeight(scaleHeight); const scaleHeight = viewport.height * scale;
setScaledHeight(scaleHeight);
}
}; };
return ( return (
<> <>
{props.successEmail && ( {props.successEmail && (
<Alert type={"success"}>{t("success-email-alert")}</Alert> <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 relative`
: "relative"
}
style={{ style={{
position: "relative", width:
boxShadow: "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px", props.containerWH?.width && props.containerWH?.width * props.scale
//49 is height of header
height: isGuestSigner ? window.innerHeight - 49 : scaledHeight,
zIndex: 0
}} }}
noScrollY={props.scale === 1 ? true : false} ref={(node) => {
noScrollX={props.scale === 1 ? true : false} pdfContainerRef.current = node;
props.drop && props.drop(node);
}}
id="container"
> >
<div {props.pdfLoad !== false &&
data-tut="reactourForth" props.containerWH?.width &&
className={`${ props.pdfOriginalWH.length > 0 && (
isGuestSigner ? "30px" : "" <>
} border-[0.1px] border-[#ebe8e8] overflow-x-auto`} {props.pdfRequest || props.isSelfSign
style={{ ? // request sign, guest sign,
width: props.signerPos?.map((data, key) => (
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 (
<React.Fragment key={key}> <React.Fragment key={key}>
{checkSignedSigners(data)} {checkSignedSigners(data)}
</React.Fragment> </React.Fragment>
); ))
}) : props.placeholder // placeholdersign document, draft document, create template, draft template
: props.placeholder // placeholder mobile ? props.signerPos?.map((data, ind) => (
? props.signerPos?.map((data, ind) => {
return (
<React.Fragment key={ind}> <React.Fragment key={ind}>
{data?.placeHolder && {data?.placeHolder &&
data?.placeHolder.map((placeData, index) => { data?.placeHolder.map((placeData, index) => (
return ( <React.Fragment key={index}>
<React.Fragment key={index}> {placeData.pageNumber === props.pageNumber &&
{placeData.pageNumber === props.pageNumber && placeData.pos.map((pos) => (
placeData.pos.map((pos) => { <React.Fragment key={pos.key}>
return ( <Placeholder
<React.Fragment key={pos.key}> pos={pos}
<Placeholder setIsPageCopy={props.setIsPageCopy}
pos={pos} handleDeleteSign={
setIsPageCopy={props.setIsPageCopy} props.handleDeleteSign
handleDeleteSign={ }
props.handleDeleteSign handleTabDrag={props.handleTabDrag}
} handleStop={props.handleStop}
handleTabDrag={props.handleTabDrag} handleSignYourselfImageResize={
handleStop={props.handleStop} handleImageResize
handleSignYourselfImageResize={ }
handleImageResize index={props.pageNumber}
} xyPosition={props.signerPos}
index={props.pageNumber} setXyPosition={props.setSignerPos}
xyPosition={props.signerPos} data={data}
setXyPosition={props.setSignerPos} setIsResize={props.setIsResize}
data={data} setShowDropdown={props.setShowDropdown}
setIsResize={props.setIsResize} isShowBorder={true}
setShowDropdown={ isPlaceholder={true}
props.setShowDropdown setUniqueId={props.setUniqueId}
} handleLinkUser={props.handleLinkUser}
isShowBorder={true} isSignYourself={false}
isPlaceholder={true} posWidth={posWidth}
setUniqueId={props.setUniqueId} posHeight={posHeight}
handleLinkUser={ showGuidelines={handleGuideline}
props.handleLinkUser isDragging={props.isDragging}
} setIsValidate={props.setIsValidate}
isSignYourself={false} setIsRadio={props.setIsRadio}
posWidth={posWidth} setIsCheckbox={props.setIsCheckbox}
posHeight={posHeight} setCurrWidgetsDetails={
isDragging={props.isDragging} props.setCurrWidgetsDetails
setIsValidate={props.setIsValidate} }
setIsRadio={props.setIsRadio} handleNameModal={props.handleNameModal}
setIsCheckbox={props.setIsCheckbox} setTempSignerId={props.setTempSignerId}
setCurrWidgetsDetails={ uniqueId={props.uniqueId}
props.setCurrWidgetsDetails handleTextSettingModal={
} props.handleTextSettingModal
handleNameModal={ }
props.handleNameModal handleCellSettingModal={
} props.handleCellSettingModal
setTempSignerId={ }
props.setTempSignerId scale={props.scale}
} containerWH={props.containerWH}
uniqueId={props.uniqueId} pdfOriginalWH={props.pdfOriginalWH}
handleTextSettingModal={ pageNumber={props.pageNumber}
props.handleTextSettingModal setIsSelectId={props.setIsSelectId}
} fontSize={props.fontSize}
scale={props.scale} setFontSize={props.setFontSize}
containerWH={props.containerWH} setCellCount={props.setCellCount}
pdfOriginalWH={props.pdfOriginalWH} fontColor={props.fontColor}
pageNumber={props.pageNumber} setFontColor={props.setFontColor}
setIsSelectId={props.setIsSelectId} isResize={props.isResize}
fontSize={props.fontSize} unSignedWidgetId={
setFontSize={props.setFontSize} props.unSignedWidgetId
fontColor={props.fontColor} }
setFontColor={props.setFontColor} isFreeResize={true}
isResize={props.isResize} calculateFontsize={calculateFontsize}
unSignedWidgetId={ currWidgetsDetails={
props.unSignedWidgetId props?.currWidgetsDetails
} }
isFreeResize={true} />
calculateFontsize={ </React.Fragment>
calculateFontsize ))}
} </React.Fragment>
currWidgetsDetails={ ))}
props?.currWidgetsDetails
}
/>
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</React.Fragment> </React.Fragment>
); ))
}) : !props.pdfDetails?.[0]?.IsCompleted && // signyourself flow
: !props.pdfDetails?.[0]?.IsCompleted && props.xyPosition?.map((data, ind) => (
props.xyPosition?.map((data, ind) => {
return (
<React.Fragment key={ind}> <React.Fragment key={ind}>
{data.pageNumber === props.pageNumber && {data.pageNumber === props.pageNumber &&
data.pos.map((pos, id) => { data.pos.map(
return ( (pos, id) =>
pos && ( pos && (
<Placeholder <Placeholder
key={id} key={id}
@@ -333,6 +362,7 @@ function RenderPdf(props) {
isSignYourself={true} isSignYourself={true}
posWidth={posWidth} posWidth={posWidth}
posHeight={posHeight} posHeight={posHeight}
showGuidelines={handleGuideline}
pdfDetails={props.pdfDetails[0]} pdfDetails={props.pdfDetails[0]}
isDragging={props.isDragging} isDragging={props.isDragging}
setIsCheckbox={props.setIsCheckbox} setIsCheckbox={props.setIsCheckbox}
@@ -342,6 +372,9 @@ function RenderPdf(props) {
handleTextSettingModal={ handleTextSettingModal={
props.handleTextSettingModal props.handleTextSettingModal
} }
handleCellSettingModal={
props.handleCellSettingModal
}
scale={props.scale} scale={props.scale}
pdfOriginalWH={props.pdfOriginalWH} pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber} pageNumber={props.pageNumber}
@@ -351,7 +384,7 @@ function RenderPdf(props) {
setFontColor={props.setFontColor} setFontColor={props.setFontColor}
isResize={props.isResize} isResize={props.isResize}
setIsResize={props.setIsResize} setIsResize={props.setIsResize}
isFreeResize={false} isFreeResize={true}
isOpenSignPad={true} isOpenSignPad={true}
calculateFontsize={calculateFontsize} calculateFontsize={calculateFontsize}
currWidgetsDetails={ currWidgetsDetails={
@@ -359,247 +392,67 @@ function RenderPdf(props) {
} }
/> />
) )
); )}
})}
</React.Fragment> </React.Fragment>
); ))}
}))} </>
{/* Mobile */} )}
<Document <Document
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>} error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
onLoadError={() => props.setPdfLoad(false)} onLoadError={(e) => {
loading={t("loading-doc")} console.log("PDF load error", e);
onLoadSuccess={props.pageDetails} props.setPdfLoad(false);
onClick={() =>
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails({})
}
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
}} }}
ref={props.drop} loading={t("loading-doc")}
id="container" onLoadSuccess={(pdf) => {
props.setPdfLoad(true);
props.pageDetails(pdf);
}}
onClick={() =>
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails({})
}
file={pdfDataBase64}
> >
{props.pdfLoad && <Page
props.containerWH?.width && key={props.index}
props.pdfOriginalWH.length > 0 && onLoadSuccess={handlePageLoadSuccess}
(props.pdfRequest || props.isSelfSign //pdf request sign flow width={props.containerWH.width}
? props.signerPos?.map((data, key) => { scale={props.scale || 1}
return ( className={isMobile ? "select-none touch-callout-none" : "-z-[1]"}
<React.Fragment key={key}> pageNumber={props.pageNumber}
{checkSignedSigners(data)} renderAnnotationLayer={false}
</React.Fragment> renderTextLayer={false}
); onGetAnnotationsError={(error) => {
}) console.log("annotation error", error);
: props.placeholder //placeholder and template flow }}
? props.signerPos.map((data, ind) => { />
return ( </Document>
<React.Fragment key={ind}> {guideline.show && (
{data?.placeHolder && <>
data?.placeHolder.map((placeData, index) => { {/* top guide */}
return ( <div
<React.Fragment key={index}> className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
{placeData.pageNumber === props.pageNumber && style={{ top: guideline.y1 }}
placeData.pos.map((pos) => {
return (
<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
}
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
}
currWidgetsDetails={
props?.currWidgetsDetails
}
/>
</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}
handleDeleteSign={props.handleDeleteSign}
handleTabDrag={props.handleTabDrag}
handleStop={(event, dragElement) =>
props.handleStop(
event,
dragElement,
pos.type
)
}
handleSignYourselfImageResize={
handleSignYourselfImageResize
}
index={props.index}
xyPosition={props.xyPosition}
setXyPosition={props.setXyPosition}
isShowBorder={true}
isSignYourself={true}
posWidth={posWidth}
posHeight={posHeight}
pdfDetails={props.pdfDetails[0]}
isDragging={props.isDragging}
setIsCheckbox={props.setIsCheckbox}
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}
currWidgetsDetails={
props?.currWidgetsDetails
}
/>
</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.setCurrWidgetsDetails && props.setCurrWidgetsDetails({})
}
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> {/* bottom guide */}
</div> <div
</RSC> className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
)} style={{ top: guideline.y2 }}
/>
{/* left guide */}
<div
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: guideline.x1 }}
/>
{/* right guide */}
<div
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: guideline.x2 }}
/>
</>
)}
</div>
</RSC>
</> </>
); );
} }
@@ -9,7 +9,8 @@ function SelectLanguage(props) {
{ value: "es", text: "Española" }, //spanish { value: "es", text: "Española" }, //spanish
{ value: "fr", text: "Français" }, //french { value: "fr", text: "Français" }, //french
{ value: "it", text: "Italiano" }, //italian { 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 defaultLanguage = i18next.language || "en";
const [lang, setLang] = useState(defaultLanguage); const [lang, setLang] = useState(defaultLanguage);
@@ -23,14 +24,14 @@ function SelectLanguage(props) {
<div <div
className={`${ className={`${
!props.isProfile && " mt-[9px] pb-2 md:pb-0 " !props.isProfile && " mt-[9px] pb-2 md:pb-0 "
} flex justify-center items-center `} } flex justify-center items-center text-base-content`}
> >
<select <select
value={lang} value={lang}
onChange={handleChangeLang} onChange={handleChangeLang}
className={`${ className={`${
!props.isProfile ? " md:w-[15%] w-[50%]" : "w-[180px]" !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> <option disabled>select</option>
{languages.map((item) => { {languages.map((item) => {
@@ -13,7 +13,7 @@ function TextFontSetting(props) {
title={t("text-field")} title={t("text-field")}
handleClose={() => props.setIsTextSetting(false)} 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"> <div className="flex flex-col md:flex-row md:items-center gap-3">
{/* Font Size Selector */} {/* Font Size Selector */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -8,6 +8,7 @@ import {
isMobile, isMobile,
radioButtonWidget, radioButtonWidget,
textInputWidget, textInputWidget,
cellsWidget,
textWidget, textWidget,
widgets widgets
} from "../../constant/Utils"; } from "../../constant/Utils";
@@ -37,6 +38,10 @@ function WidgetComponent(props) {
type: "BOX", type: "BOX",
item: { type: "BOX", id: 7, text: textInputWidget } item: { type: "BOX", id: 7, text: textInputWidget }
}); });
const [, cells] = useDrag({
type: "BOX",
item: { type: "BOX", id: 17, text: cellsWidget }
});
const [, initials] = useDrag({ const [, initials] = useDrag({
type: "BOX", type: "BOX",
item: { type: "BOX", id: 8, text: "initials" } item: { type: "BOX", id: 8, text: "initials" }
@@ -89,6 +94,7 @@ function WidgetComponent(props) {
date, date,
text, text,
textInput, textInput,
cells,
checkbox, checkbox,
dropdown, dropdown,
radioButton, radioButton,
@@ -132,7 +138,9 @@ function WidgetComponent(props) {
); );
const filterWidgets = widget.filter( const filterWidgets = widget.filter(
(data) => (data) =>
!["dropdown", radioButtonWidget, textInputWidget].includes(data.type) !["dropdown", radioButtonWidget, textInputWidget].includes(
data.type
)
); );
const textWidgetData = widget.filter((data) => data.type !== textWidget); const textWidgetData = widget.filter((data) => data.type !== textWidget);
const updateWidgets = props.isSignYourself const updateWidgets = props.isSignYourself
@@ -241,6 +249,7 @@ function WidgetComponent(props) {
handleDivClick={props.handleDivClick} handleDivClick={props.handleDivClick}
handleMouseLeave={props.handleMouseLeave} handleMouseLeave={props.handleMouseLeave}
signRef={signRef} signRef={signRef}
addPositionOfSignature={props.addPositionOfSignature}
/> />
</div> </div>
</div> </div>
@@ -5,6 +5,7 @@ import RegexParser from "regex-parser";
import { import {
signatureTypes, signatureTypes,
textInputWidget, textInputWidget,
cellsWidget,
textWidget textWidget
} from "../../constant/Utils"; } from "../../constant/Utils";
import { fontColorArr, fontsizeArr } from "../../constant/Utils"; import { fontColorArr, fontsizeArr } from "../../constant/Utils";
@@ -19,7 +20,8 @@ const WidgetNameModal = (props) => {
status: "required", status: "required",
hint: "", hint: "",
textvalidate: "", textvalidate: "",
isReadOnly: false isReadOnly: false,
cellCount: 5
}); });
const [isValid, setIsValid] = useState(true); const [isValid, setIsValid] = useState(true);
const statusArr = ["Required", "Optional"]; const statusArr = ["Required", "Optional"];
@@ -51,12 +53,14 @@ const WidgetNameModal = (props) => {
props.defaultdata?.options?.validation?.type === "regex" props.defaultdata?.options?.validation?.type === "regex"
? props.defaultdata?.options?.validation?.pattern ? props.defaultdata?.options?.validation?.pattern
: props.defaultdata?.options?.validation?.type || "", : props.defaultdata?.options?.validation?.type || "",
isReadOnly: props.defaultdata?.options?.isReadOnly || false isReadOnly: props.defaultdata?.options?.isReadOnly || false,
cellCount: props.defaultdata?.options?.cellCount || 5
}); });
} else { } else {
setFormdata({ setFormdata({
...formdata, ...formdata,
name: props.defaultdata?.options?.name || "" name: props.defaultdata?.options?.name || "",
cellCount: props.defaultdata?.options?.cellCount || 5
}); });
} }
@@ -83,6 +87,20 @@ const WidgetNameModal = (props) => {
props.handleData(data, props.defaultdata?.type); props.handleData(data, props.defaultdata?.type);
} }
} else { } 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); props.handleData(formdata);
} }
setFormdata({ setFormdata({
@@ -91,7 +109,8 @@ const WidgetNameModal = (props) => {
defaultValue: "", defaultValue: "",
status: "required", status: "required",
hint: "", hint: "",
textvalidate: "" textvalidate: "",
cellCount: 5
}); });
setSignatureType(signTypes); setSignatureType(signTypes);
} }
@@ -104,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) => { const handledefaultChange = (e) => {
if (formdata.textvalidate) { if (formdata.textvalidate) {
const regexObject = RegexParser(handleValidation(formdata.textvalidate)); const regexObject = RegexParser(handleValidation(formdata.textvalidate));
@@ -112,7 +148,11 @@ const WidgetNameModal = (props) => {
} else { } else {
setIsValid(true); 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) { function handleValidation(type) {
@@ -122,7 +162,10 @@ const WidgetNameModal = (props) => {
case "number": case "number":
return "/^\\d+$/"; return "/^\\d+$/";
case "text": 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: default:
return type; return type;
} }
@@ -150,7 +193,7 @@ const WidgetNameModal = (props) => {
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={`${ className={`${
props.defaultdata?.type === textInputWidget [textInputWidget, cellsWidget].includes(props.defaultdata?.type)
? "pt-0" ? "pt-0"
: ["signature", "initials"].includes(props.defaultdata?.type) : ["signature", "initials"].includes(props.defaultdata?.type)
? "pt-2" ? "pt-2"
@@ -174,7 +217,21 @@ const WidgetNameModal = (props) => {
/> />
</div> </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]"> <div className="mb-[0.75rem]">
<label htmlFor="name" className="text-[13px]"> <label htmlFor="name" className="text-[13px]">
@@ -186,6 +243,11 @@ const WidgetNameModal = (props) => {
value={formdata.defaultValue} value={formdata.defaultValue}
onChange={(e) => handledefaultChange(e)} onChange={(e) => handledefaultChange(e)}
autoComplete="off" autoComplete="off"
maxLength={
props.defaultdata?.type === cellsWidget
? formdata.cellCount
: undefined
}
onBlur={() => { onBlur={() => {
if (isValid === false) { if (isValid === false) {
setFormdata({ ...formdata, defaultValue: "" }); setFormdata({ ...formdata, defaultValue: "" });
@@ -237,7 +299,9 @@ const WidgetNameModal = (props) => {
); );
})} })}
</div> </div>
{[textInputWidget].includes(props.defaultdata?.type) && ( {[textInputWidget, cellsWidget].includes(
props.defaultdata?.type
) && (
<div className="flex items-center"> <div className="flex items-center">
<input <input
id="isReadOnly" id="isReadOnly"
@@ -252,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")} {t("read-only")}
</label> </label>
</div> </div>
@@ -304,6 +371,7 @@ const WidgetNameModal = (props) => {
{[ {[
textInputWidget, textInputWidget,
textWidget, textWidget,
cellsWidget,
"name", "name",
"company", "company",
"job title", "job title",
File diff suppressed because it is too large Load Diff
@@ -195,7 +195,7 @@ const SelectFolder = ({ required, onSuccess, folderCls, isReset }) => {
? selectFolder.Name ? selectFolder.Name
: t("OpenSign-drive", { appName: drivename })} : t("OpenSign-drive", { appName: drivename })}
</p> </p>
<div className="text-black text-sm"> <div className="text-sm">
<i <i
className="fa-light fa-pencil cursor-pointer" className="fa-light fa-pencil cursor-pointer"
title={t("select-folder")} title={t("select-folder")}
@@ -2,9 +2,19 @@ import React, { useEffect, useState } from "react";
import AsyncSelect from "react-select/async"; import AsyncSelect from "react-select/async";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import axios from "axios"; import axios from "axios";
import { handleUnlinkSigner } from "../../../constant/Utils";
const SelectSigners = (props) => { const SelectSigners = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const {
signerPos,
setSignerPos,
signersData,
setSignersData,
uniqueId,
isRemove,
handleAddUser
} = props;
const [userList, setUserList] = useState([]); const [userList, setUserList] = useState([]);
const [selected, setSelected] = useState(); const [selected, setSelected] = useState();
const [userData, setUserData] = useState({}); const [userData, setUserData] = useState({});
@@ -30,7 +40,7 @@ const SelectSigners = (props) => {
//checking if user select no signer option from dropdown //checking if user select no signer option from dropdown
if (item) { if (item) {
//checking selected signer is already assign to the document or not //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 (item2) => item2.objectId === item.value
); );
if (alreadyAssign) { if (alreadyAssign) {
@@ -49,7 +59,7 @@ const SelectSigners = (props) => {
}; };
const handleAdd = () => { const handleAdd = () => {
if (userData && userData.objectId) { if (userData && userData.objectId) {
props.details(userData); handleAddUser(userData);
if (props.closePopup) { if (props.closePopup) {
props.closePopup(); props.closePopup();
} }
@@ -60,7 +70,13 @@ const SelectSigners = (props) => {
}; };
//function to use remove signer from assigned widgets in create template flow //function to use remove signer from assigned widgets in create template flow
const handleRemove = () => { const handleRemove = () => {
props.handleUnlinkSigner(); handleUnlinkSigner(
signerPos,
setSignerPos,
signersData,
setSignersData,
uniqueId
);
if (props.closePopup) { if (props.closePopup) {
props.closePopup(); props.closePopup();
} }
@@ -81,7 +97,7 @@ const SelectSigners = (props) => {
const contactRes = axiosRes?.data?.result || []; const contactRes = axiosRes?.data?.result || [];
if (contactRes) { if (contactRes) {
const res = JSON.parse(JSON.stringify(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 //and filter signers from total signer's list which already present in document's signers list
// const compareArrays = (res, signerObj) => { // const compareArrays = (res, signerObj) => {
// return res.filter( // return res.filter(
@@ -161,7 +177,7 @@ const SelectSigners = (props) => {
<button className="op-btn op-btn-primary" onClick={() => handleAdd()}> <button className="op-btn op-btn-primary" onClick={() => handleAdd()}>
{t("submit")} {t("submit")}
</button> </button>
{props.isExistSigner && props.handleUnlinkSigner && ( {props.isExistSigner && isRemove && (
<button <button
className="op-btn op-btn-accent op-btn-outline" className="op-btn op-btn-accent op-btn-outline"
onClick={() => handleRemove()} onClick={() => handleRemove()}
@@ -122,7 +122,7 @@ const SignersInput = (props) => {
); );
if (contactRes) { if (contactRes) {
const res = JSON.parse(JSON.stringify(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 //and filter signers from total signer's list which already present in document's signers list
const compareArrays = (res, signerObj) => { const compareArrays = (res, signerObj) => {
return res.filter( return res.filter(
@@ -18,16 +18,16 @@ const Menu = ({ item, isOpen, closeSidebar }) => {
className={({ isActive }) => className={({ isActive }) =>
`${ `${
isActive ? " bg-base-300 text-base-content" : "" isActive ? " bg-base-300 text-base-content" : ""
} flex items-center justify-start text-left p-3 lg:p-4 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none` } flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
} }
onClick={closeSidebar} onClick={closeSidebar}
tabIndex={isOpen ? 0 : -1} tabIndex={isOpen ? 0 : -1}
role="menuitem" role="menuitem"
> >
<span className="w-[20px] h-[20px] flex justify-center"> <span className="w-[20px] h-[20px] flex justify-center">
<i className={`${item.icon} text-[18px]`} aria-hidden="true"></i> <i className={`${item.icon} text-[20px]`} aria-hidden="true"></i>
</span> </span>
<span className="ml-3 lg:ml-4"> <span className="flex items-center mb-0.5">
{t(`sidebar.${item.title}`, { appName: drivename })} {t(`sidebar.${item.title}`, { appName: drivename })}
</span> </span>
</NavLink> </NavLink>
@@ -12,16 +12,16 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
<li role="none" className="my-0.5"> <li role="none" className="my-0.5">
<button <button
onClick={() => toggleSubmenu(item.title)} onClick={() => toggleSubmenu(item.title)}
className="flex items-center justify-start text-left p-3 lg:p-4 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none " className="flex gap-x-5 items-center justify-start text-left p-3 text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none"
aria-expanded={submenuOpen} aria-expanded={submenuOpen}
aria-haspopup="true" aria-haspopup="true"
aria-controls={`submenu-${title}`} aria-controls={`submenu-${title}`}
> >
<span className="w-[20px] h-[20px] flex justify-center"> <span className="w-[20px] h-[20px] flex justify-center">
<i className={`${icon} text-[18px]`}></i> <i className={`${icon} text-[20px]`}></i>
</span> </span>
<div className="flex justify-between items-center w-full"> <div className="flex justify-between items-center w-full">
<span className="ml-3 lg:ml-4 text-start"> <span className="flex items-center mb-0.5">
{t(`sidebar.${item.title}`, { appName })} {t(`sidebar.${item.title}`, { appName })}
</span> </span>
<i <i
@@ -46,20 +46,20 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
} }
className={({ isActive }) => className={({ isActive }) =>
`${ `${
isActive ? "bg-base-300 text-base-content" : "" isActive ? "bg-base-300 text-base-content " : ""
} flex items-center justify-start text-left pl-6 md:pl-8 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none` } pl-4 flex items-center gap-x-5 py-2 text-sm cursor-pointer text-base-content hover:text-base-content focus:bg-base-300 hover:bg-base-300 hover:no-underline focus:outline-none`
} }
onClick={closeSidebar} onClick={closeSidebar}
role="menuitem" role="menuitem"
tabIndex={submenuOpen ? 0 : -1} tabIndex={submenuOpen ? 0 : -1}
> >
<span className="w-[15px] h-[15px] flex justify-center"> <span className="w-[18px] h-[18px] flex justify-center">
<i <i
className={`${childItem.icon} text-[18px]`} className={`${childItem.icon} text-[18px]`}
aria-hidden="true" aria-hidden="true"
></i> ></i>
</span> </span>
<span className="ml-3 lg:ml-4"> <span className="mb-0.5">
{t(`sidebar.${item.title}-Children.${childItem.title}`, { {t(`sidebar.${item.title}-Children.${childItem.title}`, {
appName: drivename appName: drivename
})} })}
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 logo from "../assets/images/logo.png";
import { getEnv } from "./Utils";
export function serverUrl_fn() { export function serverUrl_fn() {
let baseUrl = process.env.REACT_APP_SERVERURL const env = getEnv();
? process.env.REACT_APP_SERVERURL const serverurl = env?.REACT_APP_SERVERURL
: window.location.origin + "/api/app"; ? 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; return baseUrl;
} }
export const appInfo = { export const appInfo = {
+5
View File
@@ -3,6 +3,11 @@ export const templateCls = "contracts_Template";
export const documentCls = "contracts_Document"; export const documentCls = "contracts_Document";
export const themeColor = "#47a3ad"; export const themeColor = "#47a3ad";
export const iconColor = "#686968"; 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 emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
export const maxFileSize = 10; // 10MB export const maxFileSize = 10; // 10MB
export const maxTitleLength = 250; // 250 characters 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;
}
+110
View File
@@ -0,0 +1,110 @@
import { useEffect, useRef } from "react";
export default function usePdfPinchZoom(ref, scale, setScale, setZoomPercent) {
const initialDistanceRef = useRef(null);
const startScaleRef = useRef(scale);
const currentScaleRef = useRef(scale);
const pinchScaleRef = useRef(null); // stores relative scale during a gesture
const frameRef = useRef(null);
// keep a reference to the latest scale value
useEffect(() => {
const element = ref.current;
currentScaleRef.current = scale;
// once React commits the new scale after a pinch we clear the transform
if (element && !pinchScaleRef.current) {
element.style.transform = "";
}
}, [scale, ref]);
useEffect(() => {
const element = ref.current;
if (!element) return;
const getDistance = (touches) => {
const [touch1, touch2] = touches;
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.hypot(dx, dy);
};
const handleTouchStart = (e) => {
if (e.touches.length === 2) {
initialDistanceRef.current = getDistance(e.touches);
startScaleRef.current = currentScaleRef.current;
element.style.transformOrigin = "0 0";
}
};
const handleTouchMove = (e) => {
if (e.touches.length === 2 && initialDistanceRef.current) {
e.preventDefault();
const newDistance = getDistance(e.touches);
const pinchScale = newDistance / initialDistanceRef.current;
const newScale = Math.max(1, startScaleRef.current * pinchScale);
// store relative scale so it can be applied on top of current width
pinchScaleRef.current = newScale / startScaleRef.current;
if (!frameRef.current) {
frameRef.current = requestAnimationFrame(() => {
element.style.transform = `scale(${pinchScaleRef.current})`;
frameRef.current = null;
});
}
}
};
const handleTouchEnd = () => {
if (pinchScaleRef.current) {
let relativeScale = pinchScaleRef.current;
const inlineTransform = element.style.transform;
if (inlineTransform && inlineTransform.startsWith("scale(")) {
const value = parseFloat(inlineTransform.slice(6, -1));
if (!isNaN(value)) {
relativeScale = value;
}
}
const finalScale = Math.max(1, startScaleRef.current * relativeScale);
const percent = parseFloat(((finalScale - 1) * 100).toFixed(2));
const unchanged =
Math.abs(finalScale - currentScaleRef.current) < 0.001;
pinchScaleRef.current = null;
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
frameRef.current = null;
}
if (unchanged) {
// if scale didn't actually change, reset transform right away
element.style.transform = "";
} else {
// keep transform applied until state update reflects the new scale
element.style.transform = `scale(${relativeScale})`;
}
setScale(finalScale);
setZoomPercent(percent);
// ensure refs reflect the latest value before next gesture starts
currentScaleRef.current = finalScale;
startScaleRef.current = finalScale;
}
initialDistanceRef.current = null;
};
element.addEventListener("touchstart", handleTouchStart, {
passive: false
});
element.addEventListener("touchmove", handleTouchMove, { passive: false });
element.addEventListener("touchend", handleTouchEnd);
element.addEventListener("touchcancel", handleTouchEnd);
return () => {
element.removeEventListener("touchstart", handleTouchStart);
element.removeEventListener("touchmove", handleTouchMove);
element.removeEventListener("touchend", handleTouchEnd);
element.removeEventListener("touchcancel", handleTouchEnd);
};
}, [ref, setScale, setZoomPercent]);
}
+1 -2
View File
@@ -1,6 +1,6 @@
import { useEffect } from "react"; 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) => { export const useScript = (url, onload) => {
useEffect(() => { useEffect(() => {
const script = document.createElement("script"); const script = document.createElement("script");
@@ -15,4 +15,3 @@ export const useScript = (url, onload) => {
}; };
}, [url, onload]); }, [url, onload]);
}; };
+1 -1
View File
@@ -25,7 +25,7 @@ i18n
interpolation: { interpolation: {
escapeValue: false // Not needed for react as it escapes by default 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; export default i18n;
+160
View File
@@ -99,3 +99,163 @@ body {
background-color: gray; background-color: gray;
border-radius: 10px; 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;
}
}
+6
View File
@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import "./index.css"; import "./index.css";
import "./styles/dark-theme-improvements.css";
import App from "./App"; import App from "./App";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { store } from "./redux/store"; import { store } from "./redux/store";
@@ -24,6 +25,11 @@ const serverUrl = serverUrl_fn();
Parse.initialize(appId); Parse.initialize(appId);
Parse.serverURL = serverUrl; Parse.serverURL = serverUrl;
const savedTheme = localStorage.getItem("theme");
if (savedTheme === "dark") {
document.documentElement.setAttribute("data-theme", "opensigndark");
}
const HTML5toTouch = { const HTML5toTouch = {
backends: [ backends: [
{ {
+8
View File
@@ -186,6 +186,14 @@ export default function reportJson(id) {
btnIcon: "fa-light fa-envelope", btnIcon: "fa-light fa-envelope",
redirectUrl: "", redirectUrl: "",
action: "saveastemplate" action: "saveastemplate"
},
{
btnId: "8440",
btnLabel: "Fix & resend",
hoverLabel: "Fix & resend",
btnIcon: "fa-light fa-paper-plane",
redirectUrl: "",
action: "recreatedocument"
} }
] ]
} }
+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 = [ export const subSetting = [
{ {
icon: "fa-light fa-sliders", icon: "fa-light fa-sliders",
@@ -7,14 +17,7 @@ export const subSetting = [
description: "", description: "",
objectId: "preferences" objectId: "preferences"
}, },
{ ...userssetting
icon: "fa-light fa-users fa-fw",
title: "Users",
target: "_self",
pageType: "",
description: "",
objectId: "users"
}
]; ];
const sidebarList = [ const sidebarList = [
@@ -65,7 +68,7 @@ const sidebarList = [
pageType: "report", pageType: "report",
description: "", description: "",
objectId: "6TeaPr321t" objectId: "6TeaPr321t"
}, }
] ]
}, },
{ {
+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 { useNavigate, useParams } from "react-router";
import { formJson } from "../json/FormJson"; import { formJson } from "../json/FormJson";
import Parse from "parse"; import Parse from "parse";
@@ -12,9 +12,12 @@ import {
flattenPdf, flattenPdf,
generatePdfName, generatePdfName,
generateTitleFromFilename, generateTitleFromFilename,
getFileName,
getSecureUrl, getSecureUrl,
toDataUrl toDataUrl,
decryptPdf,
base64ToFile,
getFileAsArrayBuffer,
removeTrailingSegment
} from "../constant/Utils"; } from "../constant/Utils";
import { PDFDocument } from "pdf-lib"; import { PDFDocument } from "pdf-lib";
import axios from "axios"; import axios from "axios";
@@ -68,6 +71,7 @@ const Forms = (props) => {
AllowModifications: false AllowModifications: false
}); });
const [fileupload, setFileUpload] = useState(""); const [fileupload, setFileUpload] = useState("");
const [selectedFiles, setSelectedFiles] = useState([]);
const [fileload, setfileload] = useState(false); const [fileload, setfileload] = useState(false);
const [percentage, setpercentage] = useState(0); const [percentage, setpercentage] = useState(0);
const [isReset, setIsReset] = useState(false); 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 // `removeFile` is used to reset progress, percentage and remove file if exists
const removeFile = (e) => { const removeFile = (e) => {
setfileload(false); setfileload(false);
@@ -134,219 +129,212 @@ const Forms = (props) => {
const handleFileInput = async (e) => { const handleFileInput = async (e) => {
setpercentage(0); setpercentage(0);
try { try {
let files = e.target.files; const files = Array.from(e.target.files);
setFormData((prev) => ({ ...prev, file: e.target.files[0] })); const filesNameArr = files.map((f) => f.name);
if (typeof files[0] !== "undefined") { setSelectedFiles(filesNameArr);
const mb = Math.round(files[0].size / Math.pow(1024, 2)); if (!files.length) {
if (mb > maxFileSize) { alert(t("file-alert-2"));
alert(`${t("file-alert-1")} ${maxFileSize} MB`); return;
setFileUpload(""); }
removeFile(e); // setFormData((prev) => ({ ...prev, file: files[0] }));
return; const totalMb = Math.round(
} else { files.reduce((sum, f) => sum + f.size, 0) / Math.pow(1024, 2)
if (files?.[0]?.type === "application/pdf") { );
const size = files?.[0]?.size; if (totalMb > maxFileSize) {
const name = generatePdfName(16); alert(`${t("file-alert-1")} ${maxFileSize} MB`);
const pdfName = `${name?.split(".")[0]}.pdf`; setFileUpload("");
setfileload(true); setSelectedFiles([]);
try { removeFile(e);
const res = await getFileAsArrayBuffer(files[0]); return;
const flatPdf = await flattenPdf(res); }
const parseFile = new Parse.File(
pdfName,
[...flatPdf],
"application/pdf"
);
try { const pdfBuffers = [];
const response = await parseFile.save({ for (const file of files) {
progress: (progressValue, loaded, total, { type }) => { setFormData((prev) => ({ ...prev, file: file }));
if (type === "upload" && progressValue !== null) { if (file.type === "application/pdf") {
const percentCompleted = Math.round( try {
(loaded * 100) / total const buffer = await getFileAsArrayBuffer(file);
); const flat = await flattenPdf(buffer);
setpercentage(percentCompleted); pdfBuffers.push(flat);
} } catch (err) {
} if (err?.message?.includes("is encrypted")) {
}); try {
// The response object will contain information about the uploaded file setIsDecrypting(true);
// You can access the URL of the uploaded file using response.url() const pdfFile = await decryptPdf(file, "");
if (response.url()) { setIsDecrypting(false);
const fileRes = await getSecureUrl(response.url()); setfileload(true);
if (fileRes.url) { const res = await getFileAsArrayBuffer(pdfFile);
setFileUpload(fileRes.url); const flatPdf = await flattenPdf(res);
setfileload(false); // Upload the file to Parse Server
const tenantId = localStorage.getItem("TenantId"); pdfBuffers.push(flatPdf);
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);
}
} catch (err) { } catch (err) {
if (err?.message?.includes("is encrypted")) { removeFile(e);
try { if (err?.response?.status === 401) {
setIsDecrypting(true); // setIsPassword(true);
const size = files?.[0].size; const password = prompt(
const name = generatePdfName(16); `PDF "${file.name}" is password-protected. Enter password:`
const url = "https://ai.nxglabs.in/decryptpdf"; // );
let formData = new FormData();
formData.append("file", files[0]); if (password) {
formData.append("password", ""); try {
const config = { const pdfFile = await decryptPdf(file, password);
headers: { "content-type": "multipart/form-data" }, setIsDecrypting(false);
responseType: "blob" setfileload(true);
};
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);
const res = await getFileAsArrayBuffer(pdfFile); const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res); const flatPdf = await flattenPdf(res);
// Upload the file to Parse Server // Upload the file to Parse Server
const parseFile = new Parse.File( pdfBuffers.push(flatPdf);
name, } catch (err) {
[...flatPdf], console.error(
"application/pdf" "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); 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); 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); removeFile(e);
alert(`Incorrect password for file: ${file.name}`);
} }
} else { } else {
console.error("password not provided");
setSelectedFiles(
filesNameArr.filter((f) => f !== file.name)
);
setIsDecrypting(false);
setfileload(false);
removeFile(e); removeFile(e);
} }
} catch (error) { } else {
console.log("Error uploading file: ", err?.response);
setIsDecrypting(false);
e.target.value = "";
removeFile(e); 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")); 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) { } catch (error) {
alert(error.message); alert(error.message);
return false; setSelectedFiles([]);
} }
}; };
// `isValidURL` is used to check valid webhook url // `isValidURL` is used to check valid webhook url
@@ -446,16 +434,13 @@ const Forms = (props) => {
className: "contracts_Users", className: "contracts_Users",
objectId: ExtCls[0].objectId objectId: ExtCls[0].objectId
}); });
if (extUserData?.TenantId?.ActiveFileAdapter) {
object.set("FileAdapterId", extUserData?.TenantId?.ActiveFileAdapter);
}
const res = await object.save(); const res = await object.save();
if (res) { if (res) {
setSigners([]); setSigners([]);
setBcc([]); setBcc([]);
setFolder({ ObjectId: "", Name: "" }); setFolder({ ObjectId: "", Name: "" });
const notifySign = const notifySign =
extUserData?.NotifyOnSignatures extUserData?.NotifyOnSignatures !== undefined
? extUserData?.NotifyOnSignatures ? extUserData?.NotifyOnSignatures
: true; : true;
setFormData({ setFormData({
@@ -478,6 +463,7 @@ const Forms = (props) => {
AllowModifications: false AllowModifications: false
}); });
setFileUpload(""); setFileUpload("");
setSelectedFiles([]);
setpercentage(0); setpercentage(0);
navigate(`/${props?.redirectRoute}/${res.id}`); navigate(`/${props?.redirectRoute}/${res.id}`);
} }
@@ -534,7 +520,7 @@ const Forms = (props) => {
setBcc([]); setBcc([]);
setFolder({ ObjectId: "", Name: "" }); setFolder({ ObjectId: "", Name: "" });
const notifySign = const notifySign =
extUserData?.NotifyOnSignatures extUserData?.NotifyOnSignatures !== undefined
? extUserData?.NotifyOnSignatures ? extUserData?.NotifyOnSignatures
: true; : true;
let obj = { let obj = {
@@ -559,6 +545,7 @@ const Forms = (props) => {
setFormData(obj); setFormData(obj);
removeFile(); removeFile();
setFileUpload(""); setFileUpload("");
setSelectedFiles([]);
setTimeout(() => setIsReset(false), 50); setTimeout(() => setIsReset(false), 50);
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -571,24 +558,7 @@ const Forms = (props) => {
try { try {
const size = formData?.file?.size; const size = formData?.file?.size;
const name = generatePdfName(16); const name = generatePdfName(16);
const url = "https://ai.nxglabs.in/decryptpdf"; // const pdfFile = await decryptPdf(formData?.file, formData?.password);
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"
});
setIsDecrypting(false); setIsDecrypting(false);
const res = await getFileAsArrayBuffer(pdfFile); const res = await getFileAsArrayBuffer(pdfFile);
const flatPdf = await flattenPdf(res); const flatPdf = await flattenPdf(res);
@@ -749,19 +719,20 @@ const Forms = (props) => {
)} )}
<div className="text-xs"> <div className="text-xs">
<label className="block"> <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> <span className="text-red-500 text-[13px]">*</span>
</label> </label>
{fileupload.length > 0 ? ( {fileupload.length > 0 ? (
<div className="flex gap-1 justify-center items-center"> <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="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"> <div className="break-all cursor-default">
{t("file-selected")}: {getFileName(fileupload)} {t("file-selected")}: {selectedFiles.join(", ")}
</div> </div>
<div <div
onClick={() => setFileUpload("")} onClick={() => {
setFileUpload("");
setSelectedFiles([]);
}}
className="cursor-pointer px-[10px] text-[20px] font-bold text-red-500" className="cursor-pointer px-[10px] text-[20px] font-bold text-red-500"
> >
<i className="fa-light fa-xmark"></i> <i className="fa-light fa-xmark"></i>
@@ -772,12 +743,11 @@ const Forms = (props) => {
<div className="flex gap-1 justify-center items-center"> <div className="flex gap-1 justify-center items-center">
<input <input
type="file" 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" 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)} onChange={(e) => handleFileInput(e)}
ref={inputFileRef} ref={inputFileRef}
accept={ accept="application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/png,image/jpeg"
"application/pdf,image/png,image/jpeg"
}
onInvalid={(e) => onInvalid={(e) =>
e.target.setCustomValidity(t("input-required")) e.target.setCustomValidity(t("input-required"))
} }
+24 -23
View File
@@ -68,6 +68,7 @@ function Login() {
setState({ ...state, loading: false, alertType: type, alertMsg: msg }); setState({ ...state, loading: false, alertType: type, alertMsg: msg });
setTimeout(() => setState({ ...state, alertMsg: "" }), 2000); setTimeout(() => setState({ ...state, alertMsg: "" }), 2000);
}; };
const checkUserExt = async () => { const checkUserExt = async () => {
const app = await getAppLogo(); const app = await getAppLogo();
if (app?.error === "invalid_json") { if (app?.error === "invalid_json") {
@@ -82,11 +83,11 @@ function Login() {
} else { } else {
setImage(appInfo?.applogo || undefined); setImage(appInfo?.applogo || undefined);
} }
dispatch(fetchAppInfo());
if (localStorage.getItem("accesstoken")) { if (localStorage.getItem("accesstoken")) {
setState({ ...state, loading: true }); setState({ ...state, loading: true });
GetLoginData(); GetLoginData();
} }
dispatch(fetchAppInfo());
}; };
const handleChange = (event) => { const handleChange = (event) => {
let { name, value } = event.target; let { name, value } = event.target;
@@ -96,20 +97,15 @@ function Login() {
setState({ ...state, [name]: value }); setState({ ...state, [name]: value });
}; };
const handleSubmit = async (event) => { const handleLogin = async (
localStorage.removeItem("accesstoken"); ) => {
event.preventDefault(); 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) { if (!email || !password) {
return; return;
} }
localStorage.removeItem("accesstoken");
try { try {
setState({ ...state, loading: true }); setState({ ...state, loading: true });
localStorage.setItem("appLogo", appInfo.applogo); localStorage.setItem("appLogo", appInfo.applogo);
@@ -132,6 +128,14 @@ function Login() {
showToast("danger", "Invalid username/password or region"); 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) => { const setThirdpartyLoader = (value) => {
setState({ ...state, thirdpartyLoader: value }); setState({ ...state, thirdpartyLoader: value });
@@ -275,7 +279,6 @@ function Login() {
const userInformation = JSON.parse( const userInformation = JSON.parse(
localStorage.getItem("UserInformation") localStorage.getItem("UserInformation")
); );
// console.log("payload ", payload);
if (payload && payload.sessionToken) { if (payload && payload.sessionToken) {
const params = { const params = {
userDetails: { userDetails: {
@@ -289,7 +292,6 @@ function Login() {
} }
}; };
const userSignUp = await Parse.Cloud.run("usersignup", params); const userSignUp = await Parse.Cloud.run("usersignup", params);
// console.log("userSignUp ", userSignUp);
if (userSignUp && userSignUp.sessionToken) { if (userSignUp && userSignUp.sessionToken) {
const LocalUserDetails = { const LocalUserDetails = {
name: userInformation.name, name: userInformation.name,
@@ -430,7 +432,7 @@ function Login() {
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-2"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-2">
<div> <div>
<form onSubmit={handleSubmit} aria-label="Login Form"> <form onSubmit={handleLoginBtn} aria-label="Login Form">
<h1 className="text-[30px] mt-6">{t("welcome")}</h1> <h1 className="text-[30px] mt-6">{t("welcome")}</h1>
<fieldset> <fieldset>
<legend className="text-[12px] text-[#878787]"> <legend className="text-[12px] text-[#878787]">
@@ -488,15 +490,14 @@ function Login() {
)} )}
</span> </span>
</div> </div>
<div className="relative mt-1">
<div className="relative mt-1"> <NavLink
<NavLink to="/forgetpassword"
to="/forgetpassword" className="text-[13px] op-link op-link-primary underline-offset-1 focus:outline-none ml-1"
className="text-[13px] op-link op-link-primary underline-offset-1 focus:outline-none ml-1" >
> {t("forgot-password")}
{t("forgot-password")} </NavLink>
</NavLink> </div>
</div>
</div> </div>
</fieldset> </fieldset>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-center text-xs font-bold mt-2"> <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 hidden
/> />
<div className="relative"> <div className="relative">
<div> {isLoader && (
{image ? ( <div className="absolute bg-black bg-opacity-30 z-50 w-full h-full flex justify-center items-center">
<div className="signatureCanvas relative border-[2px] border-[#888] rounded-box overflow-hidden"> <Loader />
<img </div>
alt="signature" )}
src={image} {image ? (
className="w-full h-full object-contain" <div className="signatureCanvas relative border-[2px] border-[#888] rounded-box overflow-hidden">
/> <img
</div> alt="signature"
) : ( src={image}
<SignatureCanvas className="w-full h-full object-contain"
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}
/> />
)} </div>
<div className="penContainerDefault flex flex-row justify-between"> ) : (
<div> <SignatureCanvas
{!image && ( ref={canvasRef}
<div className="flex flex-row gap-1.5 m-[5px]"> penColor={penColor}
{allColor.map((data, key) => { canvasProps={{
return ( width: "456px",
<i height: "180px",
key={key} className:
onClick={() => setPenColor(allColor[key])} "signatureCanvas border-[2px] border-[#888] rounded-box"
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> onEnd={() =>
); handleSignatureChange(canvasRef.current.toDataURL())
})} }
</div> 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>
<div className="flex flex-row gap-2 text-sm md:text-base mr-1"> <div
<div type="button"
type="button" className="op-link"
className="op-link" onClick={() => handleClear()}
onClick={() => handleUploadBtn()} >
> {t("clear")}
{t("upload")}
</div>
<div
type="button"
className="op-link"
onClick={() => handleClear()}
>
{t("clear")}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -420,7 +422,6 @@ const ManageSign = () => {
canvasProps={{ canvasProps={{
className: "intialSignature rounded-box" className: "intialSignature rounded-box"
}} }}
// backgroundColor="rgb(255, 255, 255)"
onEnd={() => handleInitialsChange()} onEnd={() => handleInitialsChange()}
dotSize={1} dotSize={1}
/> />
+17 -9
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useState, useRef } from "react"; import React, { useEffect, useState, useRef } from "react";
import "../styles/opensigndrive.css"; import "../styles/opensigndrive.css";
import { import {
iconColor, getThemeIconColor,
} from "../constant/const"; } from "../constant/const";
import { import {
getDrive getDrive
@@ -539,6 +539,12 @@ function Opensigndrive() {
} }
}, 300); }, 300);
}; };
const handleSearchPaste = (e) => {
setTimeout(() => {
handleSearchChange({ target: { value: e.target.value } });
}, 0);
};
// Cleanup on unmount // Cleanup on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -657,7 +663,8 @@ function Opensigndrive() {
type="search" type="search"
value={searchTerm} value={searchTerm}
onChange={handleSearchChange} 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" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-64 text-xs"
/> />
</div> </div>
@@ -668,7 +675,7 @@ function Opensigndrive() {
onClick={() => setMobileSearchOpen((open) => !open)} onClick={() => setMobileSearchOpen((open) => !open)}
> >
<i <i
style={{ color: `${iconColor}` }} style={{ color: `${getThemeIconColor()}` }}
className="fa-solid fa-magnifying-glass" className="fa-solid fa-magnifying-glass"
></i> ></i>
</button> </button>
@@ -681,7 +688,7 @@ function Opensigndrive() {
<i <i
className="fa-light fa-plus-square text-[24px]" className="fa-light fa-plus-square text-[24px]"
aria-hidden="true" aria-hidden="true"
style={{ color: `${iconColor}` }} style={{ color: `${getThemeIconColor()}` }}
></i> ></i>
</div> </div>
<div <div
@@ -730,9 +737,9 @@ function Opensigndrive() {
<i <i
className="fa-light fa-sort-amount-asc mr-[5px] text-[19px]" className="fa-light fa-sort-amount-asc mr-[5px] text-[19px]"
aria-hidden="true" aria-hidden="true"
style={{ color: `${iconColor}` }} style={{ color: `${getThemeIconColor()}` }}
></i> ></i>
<span style={{ fontSize: "15px", color: `${iconColor}` }}> <span style={{ fontSize: "15px", color: `${getThemeIconColor()}` }}>
{selectedSort} {selectedSort}
</span> </span>
</div> </div>
@@ -793,7 +800,7 @@ function Opensigndrive() {
> >
<i <i
className={`${isList ? "fa-light fa-th-large" : "fa-light fa-list"} text-[20px]`} className={`${isList ? "fa-light fa-th-large" : "fa-light fa-list"} text-[20px]`}
style={{ color: `${iconColor}` }} style={{ color: `${getThemeIconColor()}` }}
aria-hidden="true" aria-hidden="true"
></i> ></i>
</div> </div>
@@ -809,7 +816,7 @@ function Opensigndrive() {
<i <i
className="fa-light fa-ellipsis-vertical fa-lg" className="fa-light fa-ellipsis-vertical fa-lg"
aria-hidden="true" aria-hidden="true"
style={{ color: `${iconColor}` }} style={{ color: `${getThemeIconColor()}` }}
></i> ></i>
</div> </div>
<div <div
@@ -854,7 +861,8 @@ function Opensigndrive() {
type="search" type="search"
value={searchTerm} value={searchTerm}
onChange={handleSearchChange} 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" className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/> />
</div> </div>
+64 -192
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 { PDFDocument } from "pdf-lib";
import "../styles/signature.css"; import "../styles/signature.css";
import Parse from "parse"; import Parse from "parse";
@@ -23,7 +23,6 @@ import {
pdfNewWidthFun, pdfNewWidthFun,
signPdfFun, signPdfFun,
addDefaultSignatureImg, addDefaultSignatureImg,
radioButtonWidget,
replaceMailVaribles, replaceMailVaribles,
convertPdfArrayBuffer, convertPdfArrayBuffer,
contractUsers, contractUsers,
@@ -46,7 +45,9 @@ import {
textWidget, textWidget,
mailTemplate, mailTemplate,
updateDateWidgetsRes, updateDateWidgetsRes,
widgetDataValue widgetDataValue,
getOriginalWH,
handleCheckResponse,
} from "../constant/Utils"; } from "../constant/Utils";
import Header from "../components/pdf/PdfHeader"; import Header from "../components/pdf/PdfHeader";
import RenderPdf from "../components/pdf/RenderPdf"; import RenderPdf from "../components/pdf/RenderPdf";
@@ -276,6 +277,15 @@ function PdfRequestFiles(
let currUserId; let currUserId;
//getting document details //getting document details
const documentData = await contractDocument(docId); 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) { if (documentData && documentData.length > 0) {
const userSignatureType = const userSignatureType =
documentData[0]?.ExtUserPtr?.SignatureType || signatureTypes; documentData[0]?.ExtUserPtr?.SignatureType || signatureTypes;
@@ -623,6 +633,7 @@ function PdfRequestFiles(
} catch (err) { } catch (err) {
console.log("err in get email verification ", err); console.log("err in get email verification ", err);
setHandleError(t("something-went-wrong-mssg")); setHandleError(t("something-went-wrong-mssg"));
setIsUiLoading(false);
} }
} }
//check if isEmailVerified then go on next step //check if isEmailVerified then go on next step
@@ -632,153 +643,13 @@ function PdfRequestFiles(
(data) => data.signerObjId === signerObjectId (data) => data.signerObjId === signerObjectId
); );
if (checkUser && checkUser.length > 0) { if (checkUser && checkUser.length > 0) {
let checkboxExist, const status = handleCheckResponse(checkUser,setminRequiredCount)
requiredRadio, if (status?.showAlert) {
showAlert = false, setUnSignedWidgetId(status?.widgetKey);
widgetKey, setPageNumber(status?.tourPageNumber);
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);
setWidgetsTour(true); setWidgetsTour(true);
setIsUiLoading(false);
} else { } else {
setIsUiLoading(true);
// `widgets` is Used to return widgets details with page number of current user // `widgets` is Used to return widgets details with page number of current user
const widgets = checkUser?.[0]?.placeHolder; const widgets = checkUser?.[0]?.placeHolder;
let pdfArrBuffer; let pdfArrBuffer;
@@ -816,17 +687,17 @@ function PdfRequestFiles(
//embed document's object id to all pages in pdf document //embed document's object id to all pages in pdf document
if (!HeaderDocId) { if (!HeaderDocId) {
if (!isDocId) { 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( const pdfBytes = await multiSignEmbed(
widgets, widgets,
pdfDoc, pdfDoc,
isSignYourSelfFlow, isSignYourSelfFlow,
scale scale
); );
// console.log("pdfte", pdfBytes);
//get ExistUserPtr object id of user class to get tenantDetails //get ExistUserPtr object id of user class to get tenantDetails
if (!pdfBytes?.error) { if (!pdfBytes?.error) {
const objectId = pdfDetails?.[0]?.ExtUserPtr?.UserId?.objectId; const objectId = pdfDetails?.[0]?.ExtUserPtr?.UserId?.objectId;
@@ -852,17 +723,23 @@ function PdfRequestFiles(
isSuccessRoute, isSuccessRoute,
contactId contactId
); );
const index = pdfDetails?.[0]?.Signers.findIndex( const index =
(x) => x.objectId === signerObjectId updatedDoc.updatedPdfDetails?.[0]?.Signers.findIndex(
); (x) => x.objectId === contactId
);
const newIndex = index + 1; const newIndex = index + 1;
const usermail = { const usermail = {
Email: pdfDetails?.[0]?.Placeholders[newIndex]?.email || "" Email:
updatedDoc.updatedPdfDetails?.[0]?.Placeholders[newIndex]
?.email || ""
}; };
const user = usermail?.Email const user = usermail?.Email
? usermail ? usermail
: pdfDetails?.[0]?.Signers[newIndex]; : updatedDoc.updatedPdfDetails?.[0]?.Signers[newIndex];
if (sendmail !== "false" && sendInOrder) { if (
sendmail !== "false" &&
sendInOrder
) {
const requestBody = const requestBody =
updatedDoc.updatedPdfDetails?.[0]?.RequestBody; updatedDoc.updatedPdfDetails?.[0]?.RequestBody;
const requestSubject = const requestSubject =
@@ -916,7 +793,7 @@ function PdfRequestFiles(
const htmlReqBody = const htmlReqBody =
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>" + "<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body>" +
replacedRequestBody + replacedRequestBody +
"</body> </html>"; "</body></html>";
const variables = { const variables = {
document_title: documentName, document_title: documentName,
@@ -929,7 +806,7 @@ function PdfRequestFiles(
receiver_phone: user?.Phone || "", receiver_phone: user?.Phone || "",
expiry_date: localExpireDate, expiry_date: localExpireDate,
company_name: orgName, company_name: orgName,
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>` signing_url: signPdf
}; };
replaceVar = replaceMailVaribles( replaceVar = replaceMailVaribles(
requestSubject, requestSubject,
@@ -944,7 +821,7 @@ function PdfRequestFiles(
title: documentName, title: documentName,
organization: orgName, organization: orgName,
localExpireDate: localExpireDate, localExpireDate: localExpireDate,
sigingUrl: signPdf signingUrl: signPdf
}; };
let params = { let params = {
replyto: senderEmail || "", replyto: senderEmail || "",
@@ -1017,6 +894,7 @@ function PdfRequestFiles(
isShow: true, isShow: true,
alertMessage: t("something-went-wrong-mssg") alertMessage: t("something-went-wrong-mssg")
}); });
setIsUiLoading(false);
} }
} catch (err) { } catch (err) {
console.log("err in embedsign", err); console.log("err in embedsign", err);
@@ -1028,8 +906,8 @@ function PdfRequestFiles(
} }
} }
} }
const handleSignPdf = async () => { const handleSignPdf = async () => {
setIsUiLoading(true);
await embedWidgetsData(); await embedWidgetsData();
}; };
@@ -1053,15 +931,15 @@ function PdfRequestFiles(
let filterSignerPos = []; let filterSignerPos = [];
if (signerObjId) { if (signerObjId) {
//get current signerObjId placeholder details //get current signerObjId placeholder details
filterSignerPos = updateSignPos.filter( filterSignerPos = updateSignPos?.filter(
(data) => data.Id === signerObjId (data) => data.Id === signerObjId
); );
} }
if (filterSignerPos.length > 0) { if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder; const getPlaceHolder = filterSignerPos[0]?.placeHolder;
//get position of current pagenumber //get position of current pagenumber
const getPageNumer = getPlaceHolder.filter( const getPageNumer = getPlaceHolder?.filter(
(data) => data.pageNumber === pageNumber (data) => data.pageNumber === pageNumber
); );
if (getPageNumer.length > 0) { if (getPageNumer.length > 0) {
@@ -1105,9 +983,9 @@ function PdfRequestFiles(
setIsTextSetting(value); setIsTextSetting(value);
}; };
const handleSaveFontSize = () => { const handleSaveFontSize = () => {
const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId); const filterSignerPos = signerPos?.filter((data) => data.Id === uniqueId);
if (filterSignerPos) { if (filterSignerPos) {
const placehoder = filterSignerPos[0].placeHolder; const placehoder = filterSignerPos[0]?.placeHolder;
const getPageNumer = placehoder.filter( const getPageNumer = placehoder.filter(
(data) => data.pageNumber === pageNumber (data) => data.pageNumber === pageNumber
); );
@@ -1169,14 +1047,7 @@ function PdfRequestFiles(
]; ];
//function for get pdf page details //function for get pdf page details
const pageDetails = async (pdf) => { const pageDetails = async (pdf) => {
let pdfWHObj = []; const pdfWHObj = await getOriginalWH(pdf);
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 });
}
setPdfOriginalWH(pdfWHObj); setPdfOriginalWH(pdfWHObj);
setPdfLoad(true); setPdfLoad(true);
}; };
@@ -1230,7 +1101,7 @@ function PdfRequestFiles(
const addDefaultSignature = () => { const addDefaultSignature = () => {
const type = defaultSignAlert?.type; const type = defaultSignAlert?.type;
//get current signers placeholder position data //get current signers placeholder position data
const currentSignerPosition = signerPos.filter( const currentSignerPosition = signerPos?.filter(
(data) => data.signerObjId === signerObjectId (data) => data.signerObjId === signerObjectId
); );
const defaultSign = type === "signature" ? defaultSignImg : myInitial; const defaultSign = type === "signature" ? defaultSignImg : myInitial;
@@ -1255,7 +1126,8 @@ function PdfRequestFiles(
setRequestSignTour(true); setRequestSignTour(true);
if (isDontShow) { if (isDontShow) {
const isEnableOTP = pdfDetails?.[0]?.IsEnableOTP || false; const isEnableOTP = pdfDetails?.[0]?.IsEnableOTP || false;
if (!isEnableOTP) { const sessionToken = localStorage.getItem("accesstoken");
if (!isEnableOTP && !sessionToken) {
try { try {
await axios.post( await axios.post(
`${localStorage.getItem("baseUrl")}functions/updatecontacttour`, `${localStorage.getItem("baseUrl")}functions/updatecontacttour`,
@@ -1568,11 +1440,15 @@ function PdfRequestFiles(
const widgetValue = widgetDataValue(dragTypeValue, parseUser); const widgetValue = widgetDataValue(dragTypeValue, parseUser);
//adding and updating drop position in array when user drop signature button in div //adding and updating drop position in array when user drop signature button in div
if (item === "onclick") { 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; const divHeight = divRef.current.getBoundingClientRect().height;
// `getBoundingClientRect()` is used to get accurate measurement height of the div // Compute the pixelspace center within the PDF viewport:
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
const xPosition_Final = centerX_Pixels / (containerScale * scale);
dropObj = { dropObj = {
//onclick put placeholder center on pdf //onclick put placeholder center on pdf
xPosition: widgetWidth / 4 + containerWH.width / 2, xPosition: xPosition_Final,
yPosition: widgetHeight + divHeight / 2, yPosition: widgetHeight + divHeight / 2,
isStamp: isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true, (dragTypeValue === "stamp" || dragTypeValue === "image") && true,
@@ -1616,11 +1492,11 @@ function PdfRequestFiles(
} }
if (uniqueId) { if (uniqueId) {
let filterSignerPos, currentPagePosition; let filterSignerPos, currentPagePosition;
filterSignerPos = signerPos.find((data) => data.Id === uniqueId); filterSignerPos = signerPos?.find((data) => data.Id === uniqueId);
const getPlaceHolder = filterSignerPos?.placeHolder; const getPlaceHolder = filterSignerPos?.placeHolder;
if (getPlaceHolder) { if (getPlaceHolder) {
//checking exist placeholder on same page //checking exist placeholder on same page
currentPagePosition = getPlaceHolder.find( currentPagePosition = getPlaceHolder?.find(
(data) => data.pageNumber === pageNumber (data) => data.pageNumber === pageNumber
); );
} }
@@ -1648,12 +1524,6 @@ function PdfRequestFiles(
); );
setSignerPos(updatesignerPos); setSignerPos(updatesignerPos);
} }
// if (dragTypeValue === "dropdown") {
// setShowDropdown(true);
// } else if (dragTypeValue === "checkbox") {
// setIsCheckbox(true);
// } else
if ( if (
[textWidget, "name", "company", "job title", "email"].includes( [textWidget, "name", "company", "job title", "email"].includes(
dragTypeValue dragTypeValue
@@ -1669,7 +1539,7 @@ function PdfRequestFiles(
//function for delete signature block //function for delete signature block
const handleDeleteSign = (key, Id) => { const handleDeleteSign = (key, Id) => {
const updateData = []; const updateData = [];
const filterSignerPos = signerPos.filter((data) => data.Id === Id); const filterSignerPos = signerPos?.filter((data) => data.Id === Id);
if (filterSignerPos.length > 0) { if (filterSignerPos.length > 0) {
const getPlaceHolder = filterSignerPos[0].placeHolder; const getPlaceHolder = filterSignerPos[0].placeHolder;
const getPageNumer = getPlaceHolder.filter( const getPageNumer = getPlaceHolder.filter(
@@ -1697,7 +1567,7 @@ function PdfRequestFiles(
}); });
setSignerPos(newUpdateSigner); setSignerPos(newUpdateSigner);
} else { } else {
const getRemainPage = filterSignerPos[0].placeHolder.filter( const getRemainPage = filterSignerPos[0]?.placeHolder?.filter(
(data) => data.pageNumber !== pageNumber (data) => data.pageNumber !== pageNumber
); );
//condition to check placeholder length is greater than 1 do not need to remove whole placeholder //condition to check placeholder length is greater than 1 do not need to remove whole placeholder
@@ -1710,11 +1580,11 @@ function PdfRequestFiles(
return obj; return obj;
}); });
let signerupdate = []; let signerupdate = [];
signerupdate = signerPos.filter((data) => data.Id !== Id); signerupdate = signerPos?.filter((data) => data.Id !== Id);
signerupdate.push(newUpdatePos[0]); signerupdate.push(newUpdatePos[0]);
setSignerPos(signerupdate); setSignerPos(signerupdate);
} else { } else {
const updatedData = signerPos.map((item) => { const updatedData = signerPos?.map((item) => {
if (item.Id === Id) { if (item.Id === Id) {
// Create a copy of the item object and delete the placeHolder field // Create a copy of the item object and delete the placeHolder field
const updatedItem = { ...item }; const updatedItem = { ...item };
@@ -1732,7 +1602,7 @@ function PdfRequestFiles(
//function to get first widget and page number to assign currect signer and tour message //function to get first widget and page number to assign currect signer and tour message
const showFirstWidget = () => { const showFirstWidget = () => {
if (!requestSignTour) { if (!requestSignTour) {
const getCurrentUserPlaceholder = signerPos.find( const getCurrentUserPlaceholder = signerPos?.find(
(x) => x.Id === uniqueId (x) => x.Id === uniqueId
); );
const placeholder = getCurrentUserPlaceholder.placeHolder; const placeholder = getCurrentUserPlaceholder.placeHolder;
@@ -1753,7 +1623,6 @@ function PdfRequestFiles(
setShowSignPagenumber(sortedPagenumber); setShowSignPagenumber(sortedPagenumber);
} }
}; };
return ( return (
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<Title <Title
@@ -1825,6 +1694,7 @@ function PdfRequestFiles(
{!requestSignTour && {!requestSignTour &&
isAgree && isAgree &&
signerObjectId && signerObjectId &&
!alreadySign &&
requestSignTourFunction()} requestSignTourFunction()}
<Tour <Tour
showNumber={false} showNumber={false}
@@ -2263,6 +2133,8 @@ function PdfRequestFiles(
index={pageNumber} index={pageNumber}
setUniqueId={setUniqueId} setUniqueId={setUniqueId}
tempSignerId={tempSignerId} tempSignerId={tempSignerId}
signatureTypes={signatureType}
allowCellResize={pdfDetails[0]?.AllowModifications ?? false}
/> />
)} )}
<DownloadPdfZip <DownloadPdfZip
+55 -22
View File
@@ -27,6 +27,7 @@ import {
multiSignEmbed, multiSignEmbed,
addWidgetOptions, addWidgetOptions,
textInputWidget, textInputWidget,
cellsWidget,
textWidget, textWidget,
radioButtonWidget, radioButtonWidget,
color, color,
@@ -44,7 +45,8 @@ import {
handleSignatureType, handleSignatureType,
getBase64FromUrl, getBase64FromUrl,
generatePdfName, generatePdfName,
mailTemplate mailTemplate,
getOriginalWH
} from "../constant/Utils"; } from "../constant/Utils";
import RenderPdf from "../components/pdf/RenderPdf"; import RenderPdf from "../components/pdf/RenderPdf";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
@@ -192,7 +194,7 @@ function PlaceHolderSign() {
); );
if (user) { if (user) {
try { 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}}`; const defaultSubject = `{{sender_name}} has requested you to sign {{document_title}}`;
setDefaultBody(defaultRequestBody); setDefaultBody(defaultRequestBody);
setDefaultSubject(defaultSubject); setDefaultSubject(defaultSubject);
@@ -523,11 +525,15 @@ function PlaceHolderSign() {
defaultWidthHeight(dragTypeValue).height * containerScale; defaultWidthHeight(dragTypeValue).height * containerScale;
//adding and updating drop position in array when user drop signature button in div //adding and updating drop position in array when user drop signature button in div
if (item === "onclick") { 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; const divHeight = divRef.current.getBoundingClientRect().height;
// `getBoundingClientRect()` is used to get accurate measurement height of the div // Compute the pixelspace center within the PDF viewport:
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
const xPosition_Final = centerX_Pixels / (containerScale * scale);
dropObj = { dropObj = {
//onclick put placeholder center on pdf //onclick put placeholder center on pdf
xPosition: widgetWidth / 4 + containerWH.width / 2, xPosition: xPosition_Final,
yPosition: widgetHeight + divHeight / 2, yPosition: widgetHeight + divHeight / 2,
isStamp: isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true, (dragTypeValue === "stamp" || dragTypeValue === "image") && true,
@@ -671,14 +677,7 @@ function PlaceHolderSign() {
//function for get pdf page details //function for get pdf page details
const pageDetails = async (pdf) => { const pageDetails = async (pdf) => {
let pdfWHObj = []; const pdfWHObj = await getOriginalWH(pdf);
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 });
}
setPdfOriginalWH(pdfWHObj); setPdfOriginalWH(pdfWHObj);
setPdfLoad(true); setPdfLoad(true);
}; };
@@ -862,6 +861,7 @@ function PlaceHolderSign() {
}); });
const isSignYourSelfFlow = false; const isSignYourSelfFlow = false;
try { try {
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
const pdfBase64 = await multiSignEmbed( const pdfBase64 = await multiSignEmbed(
placeholder, placeholder,
pdfDoc, pdfDoc,
@@ -1220,7 +1220,7 @@ function PlaceHolderSign() {
receiver_phone: signerMail[i]?.Phone || "", receiver_phone: signerMail[i]?.Phone || "",
expiry_date: localExpireDate, expiry_date: localExpireDate,
company_name: orgName, company_name: orgName,
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>` signing_url: signPdf
}; };
replaceVar = replaceMailVaribles( replaceVar = replaceMailVaribles(
requestSubject, requestSubject,
@@ -1249,7 +1249,7 @@ function PlaceHolderSign() {
receiver_phone: signerMail[i]?.Phone || "", receiver_phone: signerMail[i]?.Phone || "",
expiry_date: localExpireDate, expiry_date: localExpireDate,
company_name: orgName, company_name: orgName,
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>` signing_url: signPdf
}; };
replaceVar = replaceMailVaribles(mailSubject, htmlReqBody, variables); replaceVar = replaceMailVaribles(mailSubject, htmlReqBody, variables);
} }
@@ -1260,7 +1260,7 @@ function PlaceHolderSign() {
title: documentName, title: documentName,
organization: orgName, organization: orgName,
localExpireDate: localExpireDate, localExpireDate: localExpireDate,
sigingUrl: signPdf signingUrl: signPdf
}; };
let params = { let params = {
extUserId: owner?.objectId, extUserId: owner?.objectId,
@@ -1417,7 +1417,8 @@ function PlaceHolderSign() {
deleteOption, deleteOption,
status, status,
defaultValue, defaultValue,
isHideLabel isHideLabel,
layout
) => { ) => {
const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId); const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId);
if (filterSignerPos.length > 0) { if (filterSignerPos.length > 0) {
@@ -1452,6 +1453,8 @@ function PlaceHolderSign() {
...position.options, ...position.options,
name: dropdownName, name: dropdownName,
values: dropdownOptions, values: dropdownOptions,
status: status,
layout: layout,
isReadOnly: isReadOnly || false, isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false, isHideLabel: isHideLabel || false,
defaultValue: defaultValue, defaultValue: defaultValue,
@@ -1491,6 +1494,7 @@ function PlaceHolderSign() {
maxRequiredCount: maxCount maxRequiredCount: maxCount
}, },
defaultValue: defaultValue, defaultValue: defaultValue,
layout: layout,
isReadOnly: isReadOnly || false, isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false, isHideLabel: isHideLabel || false,
fontSize: fontSize:
@@ -1589,6 +1593,30 @@ function PlaceHolderSign() {
isReadOnly: defaultdata?.isReadOnly || false 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)) { } else if (["signature"].includes(position.type)) {
return { return {
...position, ...position,
@@ -1716,14 +1744,14 @@ function PlaceHolderSign() {
objectId: data.objectId objectId: data.objectId
}; };
const updatePlaceHolder = signerPos.map((x) => { const updatePlaceHolder = signerPos.map((x) => {
if (x.Id === id) { if (x.Id === id || x.signerObjId === id) {
return { ...x, signerPtr: signerPtr, signerObjId: data.objectId }; return { ...x, signerPtr: signerPtr, signerObjId: data.objectId };
} }
return { ...x }; return { ...x };
}); });
setSignerPos(updatePlaceHolder); setSignerPos(updatePlaceHolder);
const updateSigner = signersdata.map((x) => { const updateSigner = signersdata.map((x) => {
if (x.Id === id) { if (x.Id === id || x.objectId === id) {
return { ...x, ...data, className: "contracts_Contactbook" }; return { ...x, ...data, className: "contracts_Contactbook" };
} }
return { ...x }; return { ...x };
@@ -1738,7 +1766,9 @@ function PlaceHolderSign() {
} }
} }
setSignersData(updateSigner); setSignersData(updateSigner);
const index = signersdata.findIndex((x) => x.Id === id); const index = signersdata.findIndex(
(x) => x.Id === id || x.objectId === id
);
setIsSelectId(index); setIsSelectId(index);
} }
if (isNewContact.status) { if (isNewContact.status) {
@@ -1902,7 +1932,9 @@ function PlaceHolderSign() {
}; };
//`handleInputChange` function to get signers list from dropdown //`handleInputChange` function to get signers list from dropdown
const handleInputChange = (item, id) => { const handleInputChange = (item, id) => {
const signerExist = forms.some((x) => x.value === item.value); const signerExist = signersdata?.some(
(x) => x.objectId && x.objectId === item.value
);
if (signerExist) { if (signerExist) {
alert(t("already-exist-signer")); alert(t("already-exist-signer"));
} else { } else {
@@ -2149,7 +2181,7 @@ function PlaceHolderSign() {
navigate("/report/1MwEuxLEkF"); navigate("/report/1MwEuxLEkF");
}} }}
> >
<div className="h-[100%] p-[20px]"> <div className="h-[100%] p-[20px] text-base-content">
{mailStatus === "success" ? ( {mailStatus === "success" ? (
<div className="text-center mb-[10px]"> <div className="text-center mb-[10px]">
<LottieWithLoader /> <LottieWithLoader />
@@ -2593,6 +2625,7 @@ function PlaceHolderSign() {
isSave={true} isSave={true}
tempSignerId={tempSignerId} tempSignerId={tempSignerId}
setUniqueId={setUniqueId} setUniqueId={setUniqueId}
signatureTypes={signatureType}
/> />
)} )}
<ModalUi <ModalUi
@@ -2600,7 +2633,7 @@ function PlaceHolderSign() {
title={t("document-alert")} title={t("document-alert")}
showClose={false} showClose={false}
> >
<div className="h-[100%] p-[20px]"> <div className="h-[100%] p-[20px] text-base-content">
<p>{isAlreadyPlace.message}</p> <p>{isAlreadyPlace.message}</p>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div> <div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button <button
+72 -5
View File
@@ -56,6 +56,10 @@ const Preferences = () => {
const [dateFormat, setDateFormat] = useState("MM/DD/YYYY"); const [dateFormat, setDateFormat] = useState("MM/DD/YYYY");
const [is12HourTime, setIs12HourTime] = useState(false); const [is12HourTime, setIs12HourTime] = useState(false);
const [isLTVEnabled, setIsLTVEnabled] = useState(false); const [isLTVEnabled, setIsLTVEnabled] = useState(false);
const [isDefaultMail, setIsDefaultMail] = useState({
requestMail: false,
completionMail: false
});
useEffect(() => { useEffect(() => {
fetchSignType(); fetchSignType();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -218,7 +222,7 @@ const Preferences = () => {
setIsLoader(true); setIsLoader(true);
const updateRes = tenantRes; const updateRes = tenantRes;
setTenantId(updateRes?.objectId); 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) { if (updateRes?.RequestBody) {
setRequestBody(updateRes?.RequestBody); setRequestBody(updateRes?.RequestBody);
setRequestSubject(updateRes?.RequestSubject); setRequestSubject(updateRes?.RequestSubject);
@@ -227,6 +231,7 @@ const Preferences = () => {
setRequestSubject( setRequestSubject(
`{{sender_name}} has requested you to sign {{document_title}}` `{{sender_name}} has requested you to sign {{document_title}}`
); );
setIsDefaultMail((prev) => ({ ...prev, requestMail: true }));
} }
setDefaultReqHtml({ setDefaultReqHtml({
body: defaultRequestBody, body: defaultRequestBody,
@@ -241,6 +246,7 @@ const Preferences = () => {
setCompletionSubject( setCompletionSubject(
`Document {{document_title}} has been signed by all parties` `Document {{document_title}} has been signed by all parties`
); );
setIsDefaultMail((prev) => ({ ...prev, completionMail: true }));
} }
setDefaultCompHtml({ setDefaultCompHtml({
body: defaultCompletionBody, body: defaultCompletionBody,
@@ -301,7 +307,7 @@ const Preferences = () => {
JSON.parse(localStorage.getItem("Extand_Class"))?.[0]; JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
if (extUser && extUser?.objectId) { if (extUser && extUser?.objectId) {
extUser.TenantId.RequestBody = updateRes?.RequestBody; extUser.TenantId.RequestBody = updateRes?.RequestBody;
extUser.TenantId.RequestBody = updateRes?.RequestSubject; extUser.TenantId.RequestSubject = updateRes?.RequestSubject;
const _extUser = JSON.parse(JSON.stringify(extUser)); const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser])); localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
} }
@@ -318,13 +324,45 @@ const Preferences = () => {
}; };
//function to use reset form //function to use reset form
const handleReset = (request, completion) => { const handleReset = async (request, completion) => {
if (request) { let extUser =
localStorage.getItem("Extand_Class") &&
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
handleModifyMail(request);
if (request && !isDefaultMail?.requestMail) {
setRequestBody(defaultReqHtml?.body); setRequestBody(defaultReqHtml?.body);
setRequestSubject(defaultReqHtml?.subject); setRequestSubject(defaultReqHtml?.subject);
} else if (completion) { try {
await Parse.Cloud.run("updatetenant", {
tenantId: tenantId,
details: { RequestBody: "", RequestSubject: "" }
});
if (extUser && extUser?.objectId) {
extUser.TenantId.RequestBody = "";
extUser.TenantId.RequestSubject = "";
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
} catch (err) {
console.log("Err in reseting request mail", err);
}
} else if (completion && !isDefaultMail?.completionMail) {
setCompletionSubject(defaultCompHtml?.subject); setCompletionSubject(defaultCompHtml?.subject);
SetCompletionBody(defaultCompHtml?.body); SetCompletionBody(defaultCompHtml?.body);
try {
await Parse.Cloud.run("updatetenant", {
tenantId: tenantId,
details: { CompletionBody: "", CompletionSubject: "" }
});
if (extUser && extUser?.objectId) {
extUser.TenantId.CompletionBody = "";
extUser.TenantId.CompletionSubject = "";
const _extUser = JSON.parse(JSON.stringify(extUser));
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
}
} catch (err) {
console.log("Err in reseting completion mail", err);
}
} }
}; };
//function for handle ontext change and save again text in delta //function for handle ontext change and save again text in delta
@@ -344,6 +382,11 @@ const Preferences = () => {
const handleTourInput = () => setIsTourEnabled(!isTourEnabled); const handleTourInput = () => setIsTourEnabled(!isTourEnabled);
const handleSendinOrderInput = () => setSendinOrder(!sendinOrder); const handleSendinOrderInput = () => setSendinOrder(!sendinOrder);
const tabName = (ind) => tab.find((t, i) => i === ind)?.name; const tabName = (ind) => tab.find((t, i) => i === ind)?.name;
const handleModifyMail = (mode) => {
mode === "request"
? setIsDefaultMail((p) => ({ ...p, requestMail: !p?.requestMail }))
: setIsDefaultMail((p) => ({ ...p, completionMail: !p?.completionMail }));
};
return ( return (
<React.Fragment> <React.Fragment>
<Title title={t("Preferences")} /> <Title title={t("Preferences")} />
@@ -737,6 +780,18 @@ const Preferences = () => {
{t("request-email")} {t("request-email")}
</h1> </h1>
<div className="relative mt-2 mb-4"> <div className="relative mt-2 mb-4">
{
isDefaultMail?.requestMail && (
<div className="absolute backdrop-blur-[2px] flex w-full h-full justify-center items-center bg-black/10 rounded-box select-none z-20">
<button
onClick={() => handleModifyMail("request")}
className="op-btn op-btn-primary shadow-lg"
>
Modify
</button>
</div>
)
}
<form <form
onSubmit={handleSaveRequestEmail} onSubmit={handleSaveRequestEmail}
className="p-3 border-[1px] border-base-content rounded-box" className="p-3 border-[1px] border-base-content rounded-box"
@@ -801,6 +856,18 @@ const Preferences = () => {
{t("completion-email")} {t("completion-email")}
</h1> </h1>
<div className="relative my-2"> <div className="relative my-2">
{
isDefaultMail?.completionMail && (
<div className="absolute backdrop-blur-[2px] flex w-full h-full justify-center items-center bg-black/10 rounded-box select-none z-20">
<button
onClick={() => handleModifyMail("completion")}
className="op-btn op-btn-primary shadow-lg"
>
Modify
</button>
</div>
)
}
<form <form
onSubmit={handleSaveCompletionEmail} onSubmit={handleSaveCompletionEmail}
className="p-3 border-[1px] border-base-content rounded-box" className="p-3 border-[1px] border-base-content rounded-box"
+71 -5
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 ReportTable from "../primitives/GetReportDisplay";
import Parse from "parse"; import Parse from "parse";
import axios from "axios"; import axios from "axios";
@@ -27,12 +27,18 @@ const Report = () => {
const [isImport, setIsImport] = useState(false); const [isImport, setIsImport] = useState(false);
const abortController = new AbortController(); const abortController = new AbortController();
const docPerPage = 10; 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 // below useEffect is call when id param change
useEffect(() => { useEffect(() => {
setReportName(""); setReportName("");
setList([]); setList([]);
getReportData(); setSearchTerm("");
setMobileSearchOpen(false);
getReportData(0, docPerPage, "");
// Function returned from useEffect is called on unmount // Function returned from useEffect is called on unmount
return () => { return () => {
@@ -48,7 +54,7 @@ const Report = () => {
// below useEffect call when isNextRecord state is true and fetch next record // below useEffect call when isNextRecord state is true and fetch next record
useEffect(() => { useEffect(() => {
if (isNextRecord) { if (isNextRecord) {
getReportData(List.length, 20); getReportData(List.length, 20, searchTerm);
} }
// eslint-disable-next-line // eslint-disable-next-line
}, [isNextRecord]); }, [isNextRecord]);
@@ -56,7 +62,58 @@ const Report = () => {
const handleDontShow = (isChecked) => { const handleDontShow = (isChecked) => {
setIsDontShow(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); // setIsLoader(true);
const json = reportJson(id); const json = reportJson(id);
if (json) { if (json) {
@@ -77,6 +134,9 @@ const Report = () => {
const skipRecord = id === "4Hhwbp482K" ? 0 : skipUserRecord; const skipRecord = id === "4Hhwbp482K" ? 0 : skipUserRecord;
const limitRecord = id === "4Hhwbp482K" ? 200 : limit; const limitRecord = id === "4Hhwbp482K" ? 200 : limit;
const params = { reportId: id, skip: skipRecord, limit: limitRecord }; const params = { reportId: id, skip: skipRecord, limit: limitRecord };
if (term) {
params.searchTerm = term;
}
const url = `${localStorage.getItem("baseUrl")}functions/getReport`; const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
const res = await axios.post(url, params, { const res = await axios.post(url, params, {
headers: headers, headers: headers,
@@ -120,7 +180,7 @@ const Report = () => {
const listData = res.data?.result.filter((x) => x.Signers.length > 0); const listData = res.data?.result.filter((x) => x.Signers.length > 0);
let arr = []; let arr = [];
for (const obj of listData) { for (const obj of listData) {
const isSigner = obj.Signers.some( const isSigner = obj?.Signers?.some(
(item) => item.UserId.objectId === currentUser (item) => item.UserId.objectId === currentUser
); );
if (isSigner) { if (isSigner) {
@@ -197,6 +257,12 @@ const Report = () => {
report_help={reporthelp} report_help={reporthelp}
tourData={tourData} tourData={tourData}
isDontShow={isDontShow} isDontShow={isDontShow}
mobileSearchOpen={mobileSearchOpen}
setMobileSearchOpen={setMobileSearchOpen}
searchTerm={searchTerm}
handleSearchChange={handleSearchChange}
handleSearchPaste={handleSearchPaste}
isSearchResult={isSearchResult}
isImport={isImport} isImport={isImport}
/> />
) : ( ) : (
+143 -22
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 { PDFDocument } from "pdf-lib";
import "../styles/signature.css"; import "../styles/signature.css";
import Parse from "parse"; import Parse from "parse";
@@ -22,6 +22,7 @@ import {
randomId, randomId,
getDate, getDate,
textWidget, textWidget,
cellsWidget,
convertPdfArrayBuffer, convertPdfArrayBuffer,
textInputWidget, textInputWidget,
fetchImageBase64, fetchImageBase64,
@@ -32,12 +33,13 @@ import {
onClickZoomIn, onClickZoomIn,
onClickZoomOut, onClickZoomOut,
rotatePdfPage, rotatePdfPage,
signatureTypes,
getBase64FromUrl, getBase64FromUrl,
convertBase64ToFile, convertBase64ToFile,
generatePdfName, generatePdfName,
handleRemoveWidgets, handleRemoveWidgets,
addWidgetSelfsignOptions addWidgetSelfsignOptions,
getOriginalWH,
signatureTypes
} from "../constant/Utils"; } from "../constant/Utils";
import { useParams } from "react-router"; import { useParams } from "react-router";
import Tour from "../primitives/Tour"; import Tour from "../primitives/Tour";
@@ -67,6 +69,8 @@ import {
resetWidgetState resetWidgetState
} from "../redux/reducers/widgetSlice.js"; } from "../redux/reducers/widgetSlice.js";
import WidgetsValueModal from "../components/pdf/WidgetsValueModal"; 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. //For signYourself inProgress section signer can add sign and complete doc sign.
function SignYourSelf() { function SignYourSelf() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -117,6 +121,10 @@ function SignYourSelf() {
const [isTextSetting, setIsTextSetting] = useState(false); const [isTextSetting, setIsTextSetting] = useState(false);
const [currWidgetsDetails, setCurrWidgetsDetails] = useState({}); const [currWidgetsDetails, setCurrWidgetsDetails] = useState({});
const [isCheckbox, setIsCheckbox] = useState(false); const [isCheckbox, setIsCheckbox] = useState(false);
const [isNameModal, setIsNameModal] = useState(false);
const [isCellsSetting, setIsCellsSetting] = useState(false);
const openNameModal = () => setIsNameModal(true);
const openCellsSettingModal = () => setIsCellsSetting(true);
const [pdfLoad, setPdfLoad] = useState(false); const [pdfLoad, setPdfLoad] = useState(false);
const [isAlert, setIsAlert] = useState({ isShow: false, alertMessage: "" }); const [isAlert, setIsAlert] = useState({ isShow: false, alertMessage: "" });
const [isDontShow, setIsDontShow] = useState(false); const [isDontShow, setIsDontShow] = useState(false);
@@ -355,9 +363,13 @@ function SignYourSelf() {
); );
const dragTypeValue = item?.text ? item.text : monitor.type; const dragTypeValue = item?.text ? item.text : monitor.type;
const widgetValue = getWidgetValue(dragTypeValue); const widgetValue = getWidgetValue(dragTypeValue);
const widgetTypeExist = ["name", "company", "job title", "email"].includes( const widgetTypeExist = [
dragTypeValue "name",
); "company",
"job title",
"email",
cellsWidget
].includes(dragTypeValue);
const containerScale = getContainerScale( const containerScale = getContainerScale(
pdfOriginalWH, pdfOriginalWH,
pageNumber, pageNumber,
@@ -365,15 +377,19 @@ function SignYourSelf() {
); );
//adding and updating drop position in array when user drop signature button in div //adding and updating drop position in array when user drop signature button in div
if (item === "onclick") { 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 divHeight = divRef.current.getBoundingClientRect().height;
const divWidth = divRef.current.getBoundingClientRect().width;
const getWidth = widgetTypeExist const getWidth = widgetTypeExist
? calculateInitialWidthHeight(widgetValue).getWidth ? calculateInitialWidthHeight(widgetValue).getWidth
: defaultWidthHeight(dragTypeValue).width; : defaultWidthHeight(dragTypeValue).width;
const getHeight = defaultWidthHeight(dragTypeValue).height; 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 = { dropObj = {
xPosition: getWidth / 2 + containerWH.width / 2, xPosition: xPosition_Final,
yPosition: getHeight + divHeight / 2, yPosition: getHeight + divHeight / 2,
isStamp: isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true, (dragTypeValue === "stamp" || dragTypeValue === "image") && true,
@@ -434,6 +450,7 @@ function SignYourSelf() {
[ [
textInputWidget, textInputWidget,
textWidget, textWidget,
cellsWidget,
"name", "name",
"company", "company",
"job title", "job title",
@@ -626,9 +643,10 @@ function SignYourSelf() {
const HeaderDocId = extUserPtr?.HeaderDocId; const HeaderDocId = extUserPtr?.HeaderDocId;
//embed document's object id to all pages in pdf document //embed document's object id to all pages in pdf document
if (!HeaderDocId) { 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( const pdfBytes = await multiSignEmbed(
xyPosition, xyPosition,
pdfDoc, pdfDoc,
@@ -738,7 +756,6 @@ function SignYourSelf() {
getDocumentDetails(false); getDocumentDetails(false);
} }
}; };
//function for save x and y position and show signature tab on that position //function for save x and y position and show signature tab on that position
const handleTabDrag = (key) => { const handleTabDrag = (key) => {
setDragKey(key); setDragKey(key);
@@ -789,14 +806,7 @@ function SignYourSelf() {
}; };
//function for get pdf page details //function for get pdf page details
const pageDetails = async (pdf) => { const pageDetails = async (pdf) => {
let pdfWHObj = []; const pdfWHObj = await getOriginalWH(pdf);
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 });
}
setPdfOriginalWH(pdfWHObj); setPdfOriginalWH(pdfWHObj);
setPdfLoad(true); setPdfLoad(true);
}; };
@@ -932,7 +942,8 @@ function SignYourSelf() {
deleteOption, deleteOption,
status, status,
defaultValue, defaultValue,
isHideLabel isHideLabel,
layout
) => { ) => {
const getPageNumer = xyPosition.filter( const getPageNumer = xyPosition.filter(
(data) => data.pageNumber === pageNumber (data) => data.pageNumber === pageNumber
@@ -940,6 +951,8 @@ function SignYourSelf() {
if (getPageNumer.length > 0) { if (getPageNumer.length > 0) {
const getXYdata = getPageNumer[0].pos; const getXYdata = getPageNumer[0].pos;
const getPosData = getXYdata; const getPosData = getXYdata;
const widgetLayout =
currWidgetsDetails?.type === "checkbox" ? { layout: layout } : {};
const addSignPos = getPosData.map((position) => { const addSignPos = getPosData.map((position) => {
if (position.key === currWidgetsDetails?.key) { if (position.key === currWidgetsDetails?.key) {
if (addOption) { if (addOption) {
@@ -963,6 +976,7 @@ function SignYourSelf() {
...position.options, ...position.options,
name: dropdownName, name: dropdownName,
values: dropdownOptions, values: dropdownOptions,
...widgetLayout,
isReadOnly: isReadOnly, isReadOnly: isReadOnly,
isHideLabel: isHideLabel || false, isHideLabel: isHideLabel || false,
fontSize: fontSize:
@@ -1029,6 +1043,92 @@ function SignYourSelf() {
handleTextSettingModal(false); 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 = () => { const clickOnZoomIn = () => {
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent); onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
}; };
@@ -1184,9 +1284,8 @@ function SignYourSelf() {
title={t("document-signed")} title={t("document-signed")}
handleClose={() => setShowAlreadySignDoc({ status: false })} handleClose={() => setShowAlreadySignDoc({ status: false })}
> >
<div className="p-[20px] h-full"> <div className="p-[20px] h-full text-base-content">
<p>{showAlreadySignDoc.mssg}</p> <p>{showAlreadySignDoc.mssg}</p>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div> <div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<button <button
className="op-btn op-btn-ghost shadow-md" className="op-btn op-btn-ghost shadow-md"
@@ -1284,12 +1383,15 @@ function SignYourSelf() {
setIsPageCopy={setIsPageCopy} setIsPageCopy={setIsPageCopy}
setIsCheckbox={setIsCheckbox} setIsCheckbox={setIsCheckbox}
setCurrWidgetsDetails={setCurrWidgetsDetails} setCurrWidgetsDetails={setCurrWidgetsDetails}
handleNameModal={openNameModal}
handleCellSettingModal={openCellsSettingModal}
handleTextSettingModal={handleTextSettingModal} handleTextSettingModal={handleTextSettingModal}
setScale={setScale} setScale={setScale}
scale={scale} scale={scale}
pdfBase64Url={pdfBase64Url} pdfBase64Url={pdfBase64Url}
fontSize={fontSize} fontSize={fontSize}
setFontSize={setFontSize} setFontSize={setFontSize}
setCellCount={setCellCount}
fontColor={fontColor} fontColor={fontColor}
setFontColor={setFontColor} setFontColor={setFontColor}
isResize={isResize} isResize={isResize}
@@ -1331,13 +1433,32 @@ function SignYourSelf() {
xyPosition={xyPosition} //placeholder details xyPosition={xyPosition} //placeholder details
pageNumber={pageNumber} //current page number pageNumber={pageNumber} //current page number
setXyPosition={setXyPosition} //placeholder details state setXyPosition={setXyPosition} //placeholder details state
setCellCount={setCellCount}
setPageNumber={setPageNumber} setPageNumber={setPageNumber}
setCurrWidgetsDetails={setCurrWidgetsDetails} setCurrWidgetsDetails={setCurrWidgetsDetails}
currWidgetsDetails={currWidgetsDetails} currWidgetsDetails={currWidgetsDetails}
index={index} index={index}
isSave={true} 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 <RotateAlert
showRotateAlert={showRotateAlert.status} showRotateAlert={showRotateAlert.status}
setShowRotateAlert={setShowRotateAlert} setShowRotateAlert={setShowRotateAlert}
+67 -34
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 RenderAllPdfPage from "../components/pdf/RenderAllPdfPage";
import { useParams, useNavigate } from "react-router"; import { useParams, useNavigate } from "react-router";
import axios from "axios"; import axios from "axios";
@@ -22,6 +22,7 @@ import {
defaultWidthHeight, defaultWidthHeight,
addWidgetOptions, addWidgetOptions,
textInputWidget, textInputWidget,
cellsWidget,
radioButtonWidget, radioButtonWidget,
getContainerScale, getContainerScale,
convertBase64ToFile, convertBase64ToFile,
@@ -38,7 +39,8 @@ import {
convertPdfArrayBuffer, convertPdfArrayBuffer,
generatePdfName, generatePdfName,
textWidget, textWidget,
multiSignEmbed multiSignEmbed,
getOriginalWH
} from "../constant/Utils"; } from "../constant/Utils";
import RenderPdf from "../components/pdf/RenderPdf"; import RenderPdf from "../components/pdf/RenderPdf";
import "../styles/AddUser.css"; import "../styles/AddUser.css";
@@ -375,11 +377,15 @@ const TemplatePlaceholder = () => {
filterSignerPos; filterSignerPos;
let placeHolder; let placeHolder;
if (item === "onclick") { 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 divHeight = divRef.current.getBoundingClientRect().height;
// Compute the pixelspace center within the PDF viewport:
const centerX_Pixels = divWidth / 2 - widgetWidth / 2;
const xPosition_Final = centerX_Pixels / (containerScale * scale);
dropObj = { dropObj = {
//onclick put placeholder center on pdf //onclick put placeholder center on pdf
xPosition: widgetWidth / 4 + containerWH.width / 2, xPosition: xPosition_Final,
yPosition: widgetHeight + divHeight / 2, yPosition: widgetHeight + divHeight / 2,
isStamp: isStamp:
(dragTypeValue === "stamp" || dragTypeValue === "image") && true, (dragTypeValue === "stamp" || dragTypeValue === "image") && true,
@@ -534,14 +540,7 @@ const TemplatePlaceholder = () => {
//function for get pdf page details //function for get pdf page details
const pageDetails = async (pdf) => { const pageDetails = async (pdf) => {
let pdfWHObj = []; const pdfWHObj = await getOriginalWH(pdf);
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 });
}
setPdfOriginalWH(pdfWHObj); setPdfOriginalWH(pdfWHObj);
setPdfLoad(true); setPdfLoad(true);
}; };
@@ -832,6 +831,7 @@ const TemplatePlaceholder = () => {
}); });
const isSignYourSelfFlow = false; const isSignYourSelfFlow = false;
try { try {
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
const pdfBase64 = await multiSignEmbed( const pdfBase64 = await multiSignEmbed(
placeholder, placeholder,
pdfDoc, pdfDoc,
@@ -1149,25 +1149,6 @@ const TemplatePlaceholder = () => {
const handleLinkUser = (id) => { const handleLinkUser = (id) => {
setIsAddUser({ [id]: true }); 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 // `handleAddUser` is used to adduser
const handleAddUser = (data) => { const handleAddUser = (data) => {
const signerPtr = { const signerPtr = {
@@ -1293,7 +1274,8 @@ const TemplatePlaceholder = () => {
deleteOption, deleteOption,
status, status,
defaultValue, defaultValue,
isHideLabel isHideLabel,
layout
) => { ) => {
const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId); const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId);
if (filterSignerPos.length > 0) { if (filterSignerPos.length > 0) {
@@ -1332,6 +1314,7 @@ const TemplatePlaceholder = () => {
name: dropdownName, name: dropdownName,
values: dropdownOptions, values: dropdownOptions,
status: status, status: status,
layout: layout,
defaultValue: defaultValue, defaultValue: defaultValue,
isReadOnly: isReadOnly || false, isReadOnly: isReadOnly || false,
isHideLabel: isHideLabel || false, isHideLabel: isHideLabel || false,
@@ -1370,6 +1353,7 @@ const TemplatePlaceholder = () => {
minRequiredCount: minCount, minRequiredCount: minCount,
maxRequiredCount: maxCount maxRequiredCount: maxCount
}, },
layout: layout,
isReadOnly: isReadOnly || false, isReadOnly: isReadOnly || false,
defaultValue: defaultValue, defaultValue: defaultValue,
isHideLabel: isHideLabel || false, isHideLabel: isHideLabel || false,
@@ -1470,6 +1454,36 @@ const TemplatePlaceholder = () => {
"black" "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)) { } else if (["signature"].includes(position.type)) {
return { return {
...position, ...position,
@@ -1529,6 +1543,22 @@ const TemplatePlaceholder = () => {
setIsRadio(false); setIsRadio(false);
setIsCheckbox(false); setIsCheckbox(false);
}; };
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 = () => { const clickOnZoomIn = () => {
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent); onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
@@ -1690,7 +1720,7 @@ const TemplatePlaceholder = () => {
navigate("/report/6TeaPr321t"); 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> <p>{t("template-created-alert")}</p>
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div> <div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
<div className="flex gap-1 flex-col md:flex-row"> <div className="flex gap-1 flex-col md:flex-row">
@@ -1847,6 +1877,7 @@ const TemplatePlaceholder = () => {
pdfBase64Url={pdfBase64Url} pdfBase64Url={pdfBase64Url}
fontSize={fontSize} fontSize={fontSize}
setFontSize={setFontSize} setFontSize={setFontSize}
setCellCount={setCellCount}
fontColor={fontColor} fontColor={fontColor}
setFontColor={setFontColor} setFontColor={setFontColor}
isResize={isResize} isResize={isResize}
@@ -1950,7 +1981,9 @@ const TemplatePlaceholder = () => {
closePopup={closePopup} closePopup={closePopup}
signersData={signersdata} signersData={signersdata}
signerPos={signerPos} signerPos={signerPos}
handleUnlinkSigner={handleUnlinkSigner} setSignerPos={setSignerPos}
setSignersData={setSignersData}
isRemove={true}
/> />
)} )}
</div> </div>
+1 -1
View File
@@ -284,7 +284,7 @@ const UserList = () => {
handleClose={handleClose} handleClose={handleClose}
> >
<div className="m-[20px]"> <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")}{" "} {t("are-you-sure")}{" "}
{item?.IsDisabled {item?.IsDisabled
? t("activate") ? t("activate")
+4 -4
View File
@@ -286,7 +286,7 @@ function UserProfile() {
style={{ width: `${percentage}%` }} style={{ width: `${percentage}%` }}
></div> ></div>
</div> </div>
<span className="text-black text-sm">{percentage}%</span> <span className="text-base-contentk text-sm">{percentage}%</span>
</div> </div>
)} )}
<div className="text-base font-semibold pt-4"> <div className="text-base font-semibold pt-4">
@@ -423,9 +423,9 @@ function UserProfile() {
onClick={() => onClick={() =>
editmode ? handleCancel() : navigate("/changepassword") editmode ? handleCancel() : navigate("/changepassword")
} }
className={`op-btn ${ className={
editmode ? "op-btn-ghost w-[100px]" : "op-btn-secondary" `op-btn ${editmode ? "op-btn-ghost w-[100px]" : "op-btn-secondary"}`
}`} }
> >
{editmode ? t("cancel") : t("change-password")} {editmode ? t("cancel") : t("change-password")}
</button> </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")} title={t("download-files")}
handleClose={() => props.setIsDownloadModal(false)} handleClose={() => props.setIsDownloadModal(false)}
> >
<div className="p-[20px] h-full"> <div className="p-[20px] h-full text-base-content">
{downloadType.map((data, ind) => { {downloadType.map((data, ind) => {
return ( return (
<label <label
+180 -54
View File
@@ -7,6 +7,7 @@ import ModalUi from "./ModalUi";
import AddSigner from "../components/AddSigner"; import AddSigner from "../components/AddSigner";
import { import {
emailRegex, emailRegex,
iconColor,
} from "../constant/const"; } from "../constant/const";
import Alert from "./Alert"; import Alert from "./Alert";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
@@ -38,9 +39,12 @@ import { useTranslation } from "react-i18next";
import DownloadPdfZip from "./DownloadPdfZip"; import DownloadPdfZip from "./DownloadPdfZip";
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
import EditContactForm from "../components/EditContactForm"; import EditContactForm from "../components/EditContactForm";
import { useElSize } from "../hook/useElSize";
const ReportTable = (props) => { const ReportTable = (props) => {
const copyUrlRef = useRef(null); const copyUrlRef = useRef(null);
const titleRef = useRef(null);
const titleElement = useElSize(titleRef);
const appName = const appName =
"OpenSign™"; "OpenSign™";
const drivename = appName === "OpenSign™" ? "OpenSign™" : ""; const drivename = appName === "OpenSign™" ? "OpenSign™" : "";
@@ -87,11 +91,18 @@ const ReportTable = (props) => {
const [contact, setContact] = useState({ Name: "", Email: "", Phone: "" }); const [contact, setContact] = useState({ Name: "", Email: "", Phone: "" });
const [isSuccess, setIsSuccess] = useState({}); const [isSuccess, setIsSuccess] = useState({});
const [templateId, setTemplateId] = useState(""); const [templateId, setTemplateId] = useState("");
const [sortOrder, setSortOrder] = useState("asc");
const isTemplateReport = props.ReportName === "Templates"; const isTemplateReport = props.ReportName === "Templates";
const recordsPerPage = 5; const recordsPerPage = 5;
const startIndex = (currentPage - 1) * props.docPerPage; const startIndex = (currentPage - 1) * props.docPerPage;
const { isMoreDocs, setIsNextRecord } = props; const { isMoreDocs, setIsNextRecord } = props;
useEffect(() => {
if (props.isSearchResult) {
setCurrentPage(1);
}
}, [props.isSearchResult]);
const getPaginationRange = () => { const getPaginationRange = () => {
const totalPageNumbers = 7; // Adjust this value to show more/less page numbers const totalPageNumbers = 7; // Adjust this value to show more/less page numbers
const pages = []; const pages = [];
@@ -427,7 +438,22 @@ const ReportTable = (props) => {
// Get current list // Get current list
const indexOfLastDoc = currentPage * props.docPerPage; const indexOfLastDoc = currentPage * props.docPerPage;
const indexOfFirstDoc = indexOfLastDoc - props.docPerPage; const indexOfFirstDoc = indexOfLastDoc - props.docPerPage;
const currentList = props.List?.slice(indexOfFirstDoc, indexOfLastDoc); const sortedList = React.useMemo(() => {
if (props.ReportName === "Contactbook") {
const contacts = [...props.List];
contacts.sort((a, b) => {
const nameA = a?.Name?.toLowerCase() || "";
const nameB = b?.Name?.toLowerCase() || "";
if (nameA < nameB) return sortOrder === "asc" ? -1 : 1;
if (nameA > nameB) return sortOrder === "asc" ? 1 : -1;
return 0;
});
return contacts;
}
return props.List;
}, [props.List, sortOrder, props.ReportName]);
const currentList = sortedList?.slice(indexOfFirstDoc, indexOfLastDoc);
// Change page // Change page
const paginateFront = () => { const paginateFront = () => {
@@ -447,6 +473,10 @@ const ReportTable = (props) => {
setIsContactform(!isContactform); setIsContactform(!isContactform);
}; };
const toggleSortOrder = () => {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
};
const handleUserData = (data) => { const handleUserData = (data) => {
props.setList((prevData) => [data, ...prevData]); props.setList((prevData) => [data, ...prevData]);
}; };
@@ -679,7 +709,7 @@ const ReportTable = (props) => {
receiver_phone: userDetails?.Phone || "", receiver_phone: userDetails?.Phone || "",
expiry_date: localExpireDate, expiry_date: localExpireDate,
company_name: doc.ExtUserPtr.Company, company_name: doc.ExtUserPtr.Company,
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>` signing_url: signPdf
}; };
const res = replaceMailVaribles(subject, "", variables); const res = replaceMailVaribles(subject, "", variables);
setMail((prev) => ({ ...prev, subject: res.subject })); setMail((prev) => ({ ...prev, subject: res.subject }));
@@ -710,7 +740,7 @@ const ReportTable = (props) => {
receiver_phone: userDetails?.Phone || "", receiver_phone: userDetails?.Phone || "",
expiry_date: localExpireDate, expiry_date: localExpireDate,
company_name: doc.ExtUserPtr.Company, company_name: doc.ExtUserPtr.Company,
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>` signing_url: signPdf
}; };
const res = replaceMailVaribles("", body, variables); const res = replaceMailVaribles("", body, variables);
@@ -754,7 +784,7 @@ const ReportTable = (props) => {
receiver_phone: user?.signerPtr?.Phone || "", receiver_phone: user?.signerPtr?.Phone || "",
expiry_date: localExpireDate, expiry_date: localExpireDate,
company_name: doc?.ExtUserPtr?.Company || "", company_name: doc?.ExtUserPtr?.Company || "",
signing_url: `<a href=${signPdf} target=_blank>Sign here</a>` signing_url: signPdf
}; };
const subject = const subject =
doc?.RequestSubject || doc?.RequestSubject ||
@@ -763,7 +793,7 @@ const ReportTable = (props) => {
const body = const body =
doc?.RequestBody || doc?.RequestBody ||
doc?.ExtUserPtr?.TenantId?.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); const res = replaceMailVaribles(subject, body, variables);
setMail((prev) => ({ ...prev, subject: res.subject, body: res.body })); setMail((prev) => ({ ...prev, subject: res.subject, body: res.body }));
setIsNextStep({ [user.Id]: true }); setIsNextStep({ [user.Id]: true });
@@ -907,7 +937,6 @@ const ReportTable = (props) => {
setActLoader({}); setActLoader({});
} }
}; };
const handleUpdateExpiry = async (e, item) => { const handleUpdateExpiry = async (e, item) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -999,7 +1028,7 @@ const ReportTable = (props) => {
? "op-border-primary op-text-primary" ? "op-border-primary op-text-primary"
: x.Activity === "VIEWED" : x.Activity === "VIEWED"
? "border-green-400 text-green-400" ? "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`} } focus:outline-none border-2 w-[60px] h-[30px] text-[11px] rounded-full`}
> >
{x?.Activity?.toUpperCase() || "-"} {x?.Activity?.toUpperCase() || "-"}
@@ -1314,7 +1343,6 @@ const ReportTable = (props) => {
try { try {
const params = { docId: doc?.objectId }; const params = { docId: doc?.objectId };
const templateRes = await Parse.Cloud.run("saveastemplate", params); const templateRes = await Parse.Cloud.run("saveastemplate", params);
// console.log("templateRes ", templateRes);
setTemplateId(templateRes?.id); setTemplateId(templateRes?.id);
setIsSuccess({ [doc.objectId]: true }); setIsSuccess({ [doc.objectId]: true });
} catch (err) { } catch (err) {
@@ -1394,6 +1422,12 @@ const ReportTable = (props) => {
setActLoader({}); setActLoader({});
} }
}; };
const restrictBtn = (item, act) => {
return item.IsSignyourself && act.action === "recreatedocument"
? true
: false;
};
return ( return (
<div className="relative"> <div className="relative">
{Object.keys(actLoader)?.length > 0 && ( {Object.keys(actLoader)?.length > 0 && (
@@ -1416,7 +1450,10 @@ const ReportTable = (props) => {
/> />
</> </>
)} )}
<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"> <div className="font-light">
{t(`report-name.${props.ReportName}`)}{" "} {t(`report-name.${props.ReportName}`)}{" "}
{props.report_help && ( {props.report_help && (
@@ -1428,26 +1465,68 @@ const ReportTable = (props) => {
</span> </span>
)} )}
</div> </div>
<div className="flex flex-row justify-center items-center gap-3"> <div className="flex flex-row justify-center items-center gap-3 mb-2">
{props.isImport && ( {/* Search input for report bigger in width */}
<div className="cursor-pointer" onClick={() => handleImportBtn()}> {titleElement?.width > 500 && (
<i className="fa-light fa-upload op-text-secondary text-[23px] md:text-[30px]"></i> <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> </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 && ( {props.form && (
<div <div
className="cursor-pointer" className="cursor-pointer flex"
onClick={() => handleContactFormModal()} 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> </div>
)} )}
{/* create template form */}
{isTemplateReport && ( {isTemplateReport && (
<i <div
data-tut="reactourFirst" data-tut="reactourFirst"
className="cursor-pointer flex"
onClick={() => navigate("/form/template")} 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 <ModalUi
isOpen={isModal?.export} isOpen={isModal?.export}
@@ -1477,6 +1556,17 @@ const ReportTable = (props) => {
required required
className="op-file-input op-file-input-bordered op-file-input-sm focus:outline-none hover:border-base-content w-full text-xs" className="op-file-input op-file-input-bordered op-file-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/> />
<p className="mt-1 ml-2 text-[11px] text-gray-600">
{t("import-guideline")}{" "}
<a
href="/sample_contacts.csv"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
>
{t("download-sample")}
</a>
</p>
</div> </div>
<div className="text-md m-2"> <div className="text-md m-2">
<div className="flex flex-col md:flex-row gap-1"> <div className="flex flex-col md:flex-row gap-1">
@@ -1536,6 +1626,19 @@ const ReportTable = (props) => {
</ModalUi> </ModalUi>
</div> </div>
</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 <div
className={`overflow-auto w-full border-b ${ className={`overflow-auto w-full border-b ${
props.List?.length > 0 props.List?.length > 0
@@ -1552,7 +1655,24 @@ const ReportTable = (props) => {
<tr className="border-y-[1px]"> <tr className="border-y-[1px]">
{props.heading?.map((item, index) => ( {props.heading?.map((item, index) => (
<React.Fragment key={index}> <React.Fragment key={index}>
<th className="p-2">{t(`report-heading.${item}`)}</th> <th className="p-2">
{t(`report-heading.${item}`)}
{props.ReportName === "Contactbook" && item === "Name" && (
<button
type="button"
onClick={toggleSortOrder}
className="ml-1"
>
<i
className={
sortOrder === "asc"
? "fa-light fa-arrow-down-a-z"
: "fa-light fa-arrow-up-a-z"
}
></i>
</button>
)}
</th>
</React.Fragment> </React.Fragment>
))} ))}
{props.actions?.length > 0 && ( {props.actions?.length > 0 && (
@@ -1602,7 +1722,7 @@ const ReportTable = (props) => {
handleClose={handleClose} handleClose={handleClose}
> >
<div className="m-[20px]"> <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")} {t("contact-delete-alert")}
</div> </div>
<hr className="bg-[#ccc] mt-4 " /> <hr className="bg-[#ccc] mt-4 " />
@@ -1745,7 +1865,7 @@ const ReportTable = (props) => {
{/* template report */} {/* template report */}
{isOption[item.objectId] && {isOption[item.objectId] &&
act.action === "option" && ( act.action === "option" && (
<ul className="absolute -right-1 top-auto z-[70] w-52 op-dropdown-content op-menu shadow-black/20 shadow bg-base-100 text-base-content rounded-box"> <ul className="absolute -right-1 top-auto z-[70] w-52 op-dropdown-content op-menu op-menu-sm shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
{act.subaction?.map((subact) => ( {act.subaction?.map((subact) => (
<li <li
key={subact.btnId} key={subact.btnId}
@@ -1836,34 +1956,40 @@ const ReportTable = (props) => {
{/* doc report */} {/* doc report */}
{isOption[item.objectId] && {isOption[item.objectId] &&
act.action === "option" && ( 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"> <ul className="absolute -right-1 top-auto z-[70] w-max op-dropdown-content op-menu op-menu-sm shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
{act.subaction?.map((subact) => ( {act.subaction?.map(
<li (subact) =>
key={subact.btnId} !restrictBtn(
onClick={() => item,
handleActionBtn( subact
subact, ) && (
item <li
) key={subact.btnId}
} onClick={() =>
title={t( handleActionBtn(
`btnLabel.${subact.hoverLabel}` subact,
)} item
> )
<span> }
<i title={t(
className={`${subact.btnIcon} mr-1.5`} `btnLabel.${subact.hoverLabel}`
></i> )}
{subact.btnLabel && ( >
<span className="text-[13px] capitalize font-medium"> <span>
{t( <i
`btnLabel.${subact.btnLabel}` className={`${subact.btnIcon} mr-1.5`}
></i>
{subact.btnLabel && (
<span className="text-[13px] capitalize font-medium">
{t(
`btnLabel.${subact.btnLabel}`
)}
</span>
)} )}
</span> </span>
)} </li>
</span> )
</li> )}
))}
</ul> </ul>
)} )}
</div> </div>
@@ -1972,7 +2098,7 @@ const ReportTable = (props) => {
</div> </div>
) : ( ) : (
<div className="m-[20px]"> <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-?")} {t("save-as-template-?")}
</div> </div>
<hr className="bg-[#ccc] mt-3" /> <hr className="bg-[#ccc] mt-3" />
@@ -2010,7 +2136,7 @@ const ReportTable = (props) => {
</label> </label>
<input <input
type="date" 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={ defaultValue={
item?.ExpiryDate?.iso?.split("T")?.[0] item?.ExpiryDate?.iso?.split("T")?.[0]
} }
@@ -2120,7 +2246,7 @@ const ReportTable = (props) => {
handleClose={handleClose} handleClose={handleClose}
> >
<div className="m-[20px]"> <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")} {t("delete-document-alert")}
</div> </div>
<hr className="bg-[#ccc] mt-4" /> <hr className="bg-[#ccc] mt-4" />
@@ -2177,7 +2303,7 @@ const ReportTable = (props) => {
{shareUrls.map((share, i) => ( {shareUrls.map((share, i) => (
<div <div
key={i} 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"> <span className="w-[150px] mr-[5px] md:mr-0 md:w-[300px] whitespace-nowrap overflow-hidden text-ellipsis text-sm font-semibold">
{share.email} {share.email}
@@ -2219,14 +2345,14 @@ const ReportTable = (props) => {
handleClose={handleClose} handleClose={handleClose}
> >
<div className="m-[20px]"> <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")} {t("revoke-document-alert")}
</div> </div>
<div className="mt-2"> <div className="mt-2">
<textarea <textarea
rows={3} rows={3}
placeholder="Reason (optional)" 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} value={reason}
onChange={(e) => setReason(e.target.value)} onChange={(e) => setReason(e.target.value)}
></textarea> ></textarea>
@@ -2337,7 +2463,7 @@ const ReportTable = (props) => {
)} )}
{Object?.keys(isNextStep) <= 0 && ( {Object?.keys(isNextStep) <= 0 && (
<div className="flex justify-between items-center gap-2 my-2 px-3"> <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?.signerPtr?.Name || "-"}{" "}
{`<${ {`<${
user?.email user?.email
@@ -25,12 +25,10 @@ const LinkUserModal = (props) => {
handleClose={props.closePopup} handleClose={props.closePopup}
> >
<SelectSigners <SelectSigners
details={props.handleAddUser} {...props}
closePopup={props.closePopup} closePopup={props.closePopup}
signersData={props?.signersData}
isContact={isContact} isContact={isContact}
setIsContact={setIsContact} setIsContact={setIsContact}
handleUnlinkSigner={props.handleUnlinkSigner}
isExistSigner={isExistSigner} isExistSigner={isExistSigner}
/> />
{isContact && ( {isContact && (
+7 -2
View File
@@ -8,13 +8,18 @@ const ModalUi = ({
handleClose, handleClose,
showHeader = true, showHeader = true,
showClose = true, showClose = true,
reduceWidth reduceWidth,
position
}) => { }) => {
const width = reduceWidth; const width = reduceWidth;
const isBottom = position === "bottom" ? "items-end pb-2" : "";
return ( return (
<> <>
{isOpen && ( {isOpen && (
<dialog id="selectSignerModal" className="op-modal op-modal-open"> <dialog
id="selectSignerModal"
className={`${isBottom} op-modal op-modal-open`}
>
<div <div
className={`${ className={`${
width || "md:min-w-[500px]" width || "md:min-w-[500px]"
@@ -43,7 +43,7 @@ function CustomModal(props) {
</h3> </h3>
{!isExtendExpiry && ( {!isExtendExpiry && (
<div className="p-[10px] px-[20px]"> <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> </div>
)} )}
{!isExtendExpiry && ( {!isExtendExpiry && (
@@ -68,7 +68,7 @@ function CustomModal(props) {
)} )}
{props.footerMessage && ( {props.footerMessage && (
<> <>
<div className="mx-3"> <div className="mx-3 text-base-content">
<textarea <textarea
rows={3} rows={3}
placeholder="Reason (optional)" placeholder="Reason (optional)"
@@ -108,7 +108,7 @@ function CustomModal(props) {
</label> </label>
<input <input
type="date" 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]} defaultValue={props?.doc?.ExpiryDate?.iso?.split("T")?.[0]}
onChange={(e) => setExpiryDate(e.target.value)} onChange={(e) => setExpiryDate(e.target.value)}
/> />
+5 -1
View File
@@ -10,6 +10,10 @@
background-position: right 0.7rem top 50%; background-position: right 0.7rem top 50%;
background-size: 1rem auto; 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) { @media (max-width: 375px) {
.validationlist { .validationlist {
@@ -17,7 +21,7 @@
} }
} }
@media (min-width:375px) and (max-width: 767px) { @media (min-width: 375px) and (max-width: 767px) {
.validationlist { .validationlist {
background-position: right 1rem top 50%; 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 { .customwarning {
position: absolute; position: absolute;
padding: 8px; padding: 8px;
@@ -204,6 +204,15 @@ a {
align-items: center; 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"] { .HoverCardContent[data-side="top"] {
animation-name: slideDownAndFade; animation-name: slideDownAndFade;
} }
@@ -224,6 +233,11 @@ a {
fill: white; fill: white;
} }
/* Dark mode support for HoverCard arrow */
[data-theme="opensigndark"] .HoverCardArrow {
fill: #1F2937;
}
@keyframes slideUpAndFade { @keyframes slideUpAndFade {
0% { 0% {
opacity: 0; opacity: 0;
+46 -6
View File
@@ -6,7 +6,7 @@
.react-datepicker__input-container { .react-datepicker__input-container {
position: initial !important; position: initial !important;
} }
.select-none-cls{ .select-none-cls {
-webkit-user-select: none; -webkit-user-select: none;
/* Disable text selection in WebKit browsers */ /* Disable text selection in WebKit browsers */
-moz-user-select: none; -moz-user-select: none;
@@ -33,10 +33,17 @@
width: 440px; width: 440px;
height: 167px; height: 167px;
} }
.tabWidth{ .tabWidth {
border: 1px solid #f3f4f6;
background-color: #f3f4f6;
width: 440px; width: 440px;
} }
[data-theme="opensigndark"] .tabWidth {
border: 1px solid #1f2937 !important;
background-color: #1f2937 !important;
}
.intialSignatureCanvas { .intialSignatureCanvas {
width: 150px; width: 150px;
height: 150px; height: 150px;
@@ -52,7 +59,6 @@
background-color: #111111; /* blue-500 */ background-color: #111111; /* blue-500 */
} }
.intialSignature { .intialSignature {
border: 2px solid #888; border: 2px solid #888;
background-color: rgb(255, 255, 255); background-color: rgb(255, 255, 255);
@@ -60,6 +66,40 @@
height: 183px; 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 { .penContainerDefault {
width: 460px; width: 460px;
} }
@@ -401,7 +441,7 @@ option {
width: 300px; width: 300px;
height: 120px; height: 120px;
} }
.tabWidth{ .tabWidth {
width: 300px; width: 300px;
} }
@@ -427,7 +467,7 @@ option {
width: 280px; width: 280px;
height: 112px; height: 112px;
} }
.tabWidth{ .tabWidth {
width: 280px; width: 280px;
} }
@@ -451,7 +491,7 @@ option {
width: 230px; width: 230px;
height: 92px; height: 92px;
} }
.tabWidth{ .tabWidth {
width: 230px; width: 230px;
} }
+99 -2
View File
@@ -6,11 +6,67 @@ module.exports = {
}, },
plugins: [ plugins: [
require("daisyui"), require("daisyui"),
function ({ addUtilities }) { function ({ addUtilities, theme }) {
addUtilities({ addUtilities({
// Prevent iOS long-press popup // Prevent iOS long-press popup
".touch-callout-none": { ".touch-callout-none": {
"-webkit-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: { daisyui: {
// themes: true, // themes: true,
themes: [ 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: { opensigncss: {
primary: "#002864", primary: "#002864",
+3 -2
View File
@@ -1,4 +1,4 @@
import { defineConfig } from "vite"; import { defineConfig, splitVendorChunkPlugin } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr"; import svgr from "vite-plugin-svgr";
import { resolve } from "path"; import { resolve } from "path";
@@ -12,7 +12,8 @@ export default defineConfig(({ command, mode }) => {
return { return {
plugins: [ plugins: [
react(), react(),
svgr() // Transform SVGs into React components svgr(), // Transform SVGs into React components
splitVendorChunkPlugin()
], ],
resolve: { resolve: {
alias: { alias: {
-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 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 # Set the working directory inside the container
WORKDIR /usr/src/app WORKDIR /usr/src/app
+4 -1
View File
@@ -7,6 +7,7 @@ import axios from 'axios';
dotenv.config(); dotenv.config();
export const cloudServerUrl = 'http://localhost:8080/app'; export const cloudServerUrl = 'http://localhost:8080/app';
export const serverAppId = process.env.APP_ID || 'opensign';
export const appName = 'OpenSign™'; export const appName = 'OpenSign™';
export const MAX_NAME_LENGTH = 250; export const MAX_NAME_LENGTH = 250;
@@ -252,7 +253,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'>" + "</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 + param.note +
"</td></tr><tr><td></td><td></td></tr></table></div> <div style='margin-left:70px'><a target=_blank href=" + "</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 " + "><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 + AppName +
'. For any queries regarding this email, please contact the sender ' + '. For any queries regarding this email, please contact the sender ' +
@@ -288,6 +289,8 @@ export const selectFormat = data => {
return 'MMMM dd, yyyy'; return 'MMMM dd, yyyy';
case 'DD MMMM, YYYY': case 'DD MMMM, YYYY':
return 'dd MMMM, yyyy'; return 'dd MMMM, yyyy';
case 'DD.MM.YYYY':
return 'dd.MM.yyyy';
default: default:
return 'MM/dd/yyyy'; return 'MM/dd/yyyy';
} }
@@ -2,6 +2,8 @@ import express from 'express';
import cors from 'cors'; import cors from 'cors';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import uploadFile from './uploadFile.js'; 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(); export const app = express();
@@ -11,4 +13,5 @@ app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true })); app.use(express.urlencoded({ limit: '50mb', extended: true }));
app.post('/file_upload', uploadFile); 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, serverAppId } 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 = serverAppId;
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 });
}
}
@@ -3,7 +3,7 @@ import multer from 'multer';
import multerS3 from 'multer-s3'; import multerS3 from 'multer-s3';
import aws from 'aws-sdk'; import aws from 'aws-sdk';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { cloudServerUrl, useLocal } from '../../Utils.js'; import { cloudServerUrl, serverAppId, useLocal } from '../../Utils.js';
dotenv.config(); dotenv.config();
function sanitizeFileName(fileName) { function sanitizeFileName(fileName) {
@@ -50,7 +50,7 @@ async function uploadFile(req, res) {
const DO_SPACE = process.env.DO_SPACE; const DO_SPACE = process.env.DO_SPACE;
const parseBaseUrl = cloudServerUrl; //process.env.SERVER_URL; const parseBaseUrl = cloudServerUrl; //process.env.SERVER_URL;
const parseAppId = process.env.APP_ID; const parseAppId = serverAppId;
let fileStorage; let fileStorage;
if (useLocal === 'true') { if (useLocal === 'true') {
fileStorage = multer.diskStorage({ fileStorage = multer.diskStorage({
@@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import { cloudServerUrl } from '../../Utils.js'; import { cloudServerUrl, serverAppId } from '../../Utils.js';
const serverUrl = cloudServerUrl; //process.env.SERVER_URL; const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const APPID = process.env.APP_ID; const APPID = serverAppId;
const masterKEY = process.env.MASTER_KEY; const masterKEY = process.env.MASTER_KEY;
async function addTeamAndOrg(extUser) { async function addTeamAndOrg(extUser) {
try { try {
@@ -1,10 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { cloudServerUrl } from '../../Utils.js'; import { cloudServerUrl, serverAppId } from '../../Utils.js';
async function AuthLoginAsMail(request) { async function AuthLoginAsMail(request) {
try { try {
//function for login user using user objectId without touching user's password //function for login user using user objectId without touching user's password
const serverUrl = cloudServerUrl; //process.env.SERVER_URL; const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
const APPID = process.env.APP_ID; const APPID = serverAppId;
const masterKEY = process.env.MASTER_KEY; const masterKEY = process.env.MASTER_KEY;
let otpN = request.params.otp; let otpN = request.params.otp;
@@ -92,7 +92,7 @@ async function AuthLoginAsMail(request) {
} catch (err) { } catch (err) {
console.log('err in Auth'); console.log('err in Auth');
console.log(err); console.log(err);
return 'Result not found', err; return 'Result not found';
} }
} }
export default AuthLoginAsMail; export default AuthLoginAsMail;
@@ -1,5 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { appName, cloudServerUrl } from '../../Utils.js'; import { appName, cloudServerUrl, serverAppId } from '../../Utils.js';
export default async function forwardDoc(request) { export default async function forwardDoc(request) {
try { try {
@@ -58,7 +58,7 @@ export default async function forwardDoc(request) {
mailRes = await axios.post(`${cloudServerUrl}/functions/sendmailv3`, params, { mailRes = await axios.post(`${cloudServerUrl}/functions/sendmailv3`, params, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Parse-Application-Id': process.env.APP_ID, 'X-Parse-Application-Id': serverAppId,
'X-Parse-Master-Key': process.env.MASTER_KEY, 'X-Parse-Master-Key': process.env.MASTER_KEY,
}, },
}); });
@@ -1,5 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { cloudServerUrl } from '../../Utils.js'; import { cloudServerUrl, serverAppId } from '../../Utils.js';
export default async function GetTemplate(request) { export default async function GetTemplate(request) {
const serverUrl = cloudServerUrl; //process.env.SERVER_URL; const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
@@ -10,7 +10,7 @@ export default async function GetTemplate(request) {
if (sessiontoken) { if (sessiontoken) {
const userRes = await axios.get(serverUrl + '/users/me', { const userRes = await axios.get(serverUrl + '/users/me', {
headers: { headers: {
'X-Parse-Application-Id': process.env.APP_ID, 'X-Parse-Application-Id': serverAppId,
'X-Parse-Session-Token': sessiontoken, 'X-Parse-Session-Token': sessiontoken,
}, },
}); });

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