mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-09-04 16:58:05 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e87aa4c38 |
@@ -1,213 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
Generated
+265
-311
File diff suppressed because it is too large
Load Diff
+21
-22
@@ -5,15 +5,15 @@
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@imgly/background-removal": "^1.6.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.14.2",
|
||||
"@lottiefiles/dotlottie-react": "^0.14.0",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"axios": "^1.10.0",
|
||||
"axios": "^1.9.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.3.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
@@ -23,7 +23,6 @@
|
||||
"pkijs": "^3.0.8",
|
||||
"print-js": "^1.6.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"quill-html-edit-button": "^3.0.0",
|
||||
"radix-ui": "^1.4.2",
|
||||
"react": "^18.3.1",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
@@ -36,24 +35,24 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-gtm-module": "^2.0.11",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-i18next": "^15.5.2",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-pdf": "^9.2.1",
|
||||
"react-quill-new": "^3.4.6",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
"react-router": "^7.6.3",
|
||||
"react-router": "^7.6.1",
|
||||
"react-scrollbars-custom": "^4.1.1",
|
||||
"react-select": "^5.10.1",
|
||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||
"react-timezone-select": "^3.2.8",
|
||||
"react-tooltip": "^5.29.1",
|
||||
"react-tooltip": "^5.28.1",
|
||||
"reactour": "^1.19.4",
|
||||
"redux": "^5.0.1",
|
||||
"regex-parser": "^2.3.1",
|
||||
"serve": "^14.2.4",
|
||||
"styled-components": "^5.3.11",
|
||||
"web-vitals": "^5.0.3",
|
||||
"web-vitals": "^5.0.2",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -94,38 +93,38 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.7",
|
||||
"@babel/core": "^7.27.4",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/runtime-corejs2": "^7.27.6",
|
||||
"@babel/runtime-corejs2": "^7.27.4",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^18.3.23",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"@vitejs/plugin-react": "^4.5.1",
|
||||
"@vitejs/plugin-react-swc": "^3.10.1",
|
||||
"@vitest/ui": "^3.2.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-loader": "^10.0.0",
|
||||
"commitizen": "^4.3.1",
|
||||
"concurrently": "^9.2.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"css-loader": "^7.1.2",
|
||||
"daisyui": "^4.12.24",
|
||||
"dotenv": "^16.6.1",
|
||||
"eslint": "^9.30.0",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"dotenv": "^16.5.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.2",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"lint-staged": "^16.1.0",
|
||||
"postcss": "^8.5.4",
|
||||
"prettier": "^3.5.3",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || 22"
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"create-account": "Konto erstellen",
|
||||
"login": "Anmelden",
|
||||
"language": "Sprache",
|
||||
"dark-mode": "Dunkelmodus",
|
||||
"name": "Name",
|
||||
"phone": "Telefon",
|
||||
"phone-optional": "optional",
|
||||
@@ -754,15 +753,12 @@
|
||||
"delete-page": "Seite löschen",
|
||||
"merge-pdf": "PDFs zusammenführen",
|
||||
"add-pages": "Seiten hinzufügen",
|
||||
"reorder-pages": "Seiten neu anordnen",
|
||||
"delete-alert": "Einzelne Seite kann nicht gelöscht werden.",
|
||||
"delete-alert-2": "Sind Sie sicher, dass Sie diese Seite löschen möchten?",
|
||||
"delete-note": "Hinweis: Sobald Sie diese Seite löschen, kann dies nicht rückgängig gemacht werden.",
|
||||
"Rotation-alert": "Seite drehen",
|
||||
"bulk-import": "Massenimport",
|
||||
"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.",
|
||||
"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.",
|
||||
@@ -989,7 +985,6 @@
|
||||
"review": "Überprüfen",
|
||||
"next-field": "Nächstes Feld",
|
||||
"required-mssg": "{{leftRequiredWidget}} von {{totalWidget}} Feldern übrig",
|
||||
"verify-document": "Dokument verifizieren",
|
||||
"verify-document-signature": "Dokumentensignatur überprüfen",
|
||||
"select-pdf-document": "PDF-Dokument auswählen",
|
||||
"selected-file": "Ausgewählte Datei",
|
||||
@@ -1031,15 +1026,8 @@
|
||||
"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"
|
||||
"readonly-textinput-error": "Schreibgeschütztes Text-Widget muss einen Standardwert haben oder optional sein.",
|
||||
"readonly-dropdown-error": "Schreibgeschütztes Dropdown-Widget muss einen Standardwert haben oder optional sein.",
|
||||
"readonly-radiobtn-error": "Schreibgeschütztes Optionsfeld-Widget muss einen Standardwert haben oder optional sein.",
|
||||
"choose-one":"Wählen Sie eine aus"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"create-account": "Create account",
|
||||
"login": "Login",
|
||||
"language": "Language",
|
||||
"dark-mode": "Dark mode",
|
||||
"name": "Name",
|
||||
"phone": "Phone",
|
||||
"phone-optional": "optional",
|
||||
@@ -200,7 +199,7 @@
|
||||
},
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "file(s) selected",
|
||||
"file-selected": "file selected",
|
||||
"template-title": "Template title",
|
||||
"document-title": "Document title",
|
||||
"description": "Description",
|
||||
@@ -530,7 +529,7 @@
|
||||
"new-password": "New password",
|
||||
"confirm-password": "Confirm password",
|
||||
"file-alert-1": "The selected file size is too large. Please select a file less than",
|
||||
"file-alert-2": "Please select file(s).",
|
||||
"file-alert-2": "Please select file.",
|
||||
"file-alert-3": "Please wait while the document is being uploaded.",
|
||||
"enter-pdf-password": "Enter Pdf password",
|
||||
"correct-password": "Please provide correct password",
|
||||
@@ -754,15 +753,12 @@
|
||||
"delete-page": "Delete page",
|
||||
"merge-pdf": "Merge pdf",
|
||||
"add-pages": "Add pages",
|
||||
"reorder-pages": "Reorder pages",
|
||||
"delete-alert": "Can not delete single page",
|
||||
"delete-alert-2": "Are you sure you want to delete this page?",
|
||||
"delete-note": "Note: Once you delete this page, you cannot undo.",
|
||||
"Rotation-alert": "Rotate page",
|
||||
"bulk-import": "Bulk import",
|
||||
"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.",
|
||||
"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.",
|
||||
@@ -892,7 +888,7 @@
|
||||
"you-will-receive-email-shortly": "✅ That's it! You'll receive a confirmation email shortly.",
|
||||
"please-provide-templateid": "Please provide templateid",
|
||||
"this-template-is-not-public": "This template is not public",
|
||||
"invalid-templateid": "Invalid templateid",
|
||||
"invalid-templateid": "Invaldi templateid",
|
||||
"contact-billing-at-opensign": "To add more seats, please contact OpenSign™ at <1>billing@opensignlabs.com</1> for assistance",
|
||||
"title-length-alert": "Title must be at most 250 characters long.",
|
||||
"note-length-alert": "Note must be at most 200 characters long.",
|
||||
@@ -989,7 +985,6 @@
|
||||
"review":"Review",
|
||||
"next-field":"Next Field",
|
||||
"required-mssg":"{{leftRequiredWidget}} of {{totalWidget}} fields left",
|
||||
"verify-document": "Verify document",
|
||||
"verify-document-signature": "Verify Document Signature",
|
||||
"select-pdf-document": "Select PDF Document",
|
||||
"selected-file": "Selected file",
|
||||
@@ -1031,15 +1026,8 @@
|
||||
"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"
|
||||
"readonly-textinput-error": "Read-only text widget must have a default value or you can make it optional.",
|
||||
"readonly-dropdown-error": "Read-only dropdown widget must have a default value or you can make it optional.",
|
||||
"readonly-radiobtn-error": "Read-only radio button widget must have a default value or you can make it optional.",
|
||||
"choose-one":"Choose One"
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
"create-account": "Crear cuenta",
|
||||
"login": "Iniciar sesión",
|
||||
"language": "Idioma",
|
||||
"dark-mode": "Modo oscuro",
|
||||
"name": "Nombre",
|
||||
"phone": "Teléfono",
|
||||
"phone-optional": "opcional",
|
||||
@@ -754,15 +753,12 @@
|
||||
"delete-page": "eliminar página",
|
||||
"merge-pdf": "fusionar pdf",
|
||||
"add-pages": "Agregar páginas",
|
||||
"reorder-pages": "Reordenar páginas",
|
||||
"delete-alert": "No se puede eliminar una sola página",
|
||||
"delete-alert-2": "¿Está seguro de que desea eliminar esta página?",
|
||||
"delete-note": "Nota: una vez que elimines esta página, no podrás deshacerla",
|
||||
"Rotation-alert": "Girar página",
|
||||
"bulk-import": "Importación masiva",
|
||||
"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.",
|
||||
"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.",
|
||||
@@ -989,7 +985,6 @@
|
||||
"review": "Revisar",
|
||||
"next-field": "Siguiente campo",
|
||||
"required-mssg": "{{leftRequiredWidget}} de {{totalWidget}} campos restantes",
|
||||
"verify-document": "Verificar documento",
|
||||
"verify-document-signature": "Verificar firma del documento",
|
||||
"select-pdf-document": "Seleccionar documento PDF",
|
||||
"selected-file": "Archivo seleccionado",
|
||||
@@ -1031,15 +1026,8 @@
|
||||
"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"
|
||||
"readonly-textinput-error": "El widget de texto de solo lectura debe tener un valor predeterminado o puede hacerse opcional.",
|
||||
"readonly-dropdown-error": "El widget desplegable de solo lectura debe tener un valor predeterminado o puede hacerse opcional.",
|
||||
"readonly-radiobtn-error": "El widget de botón de opción de solo lectura debe tener un valor predeterminado o puede hacerse opcional.",
|
||||
"choose-one":"Elige uno"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"create-account": "Créer un compte",
|
||||
"login": "Se Connecter",
|
||||
"language": "Langue",
|
||||
"dark-mode": "Mode sombre",
|
||||
"name": "Nom et Prénom",
|
||||
"phone": "Téléphone",
|
||||
"phone-optional": "(facultatif)",
|
||||
@@ -754,15 +753,12 @@
|
||||
"delete-page": "supprimer la page",
|
||||
"merge-pdf": "Fusionner le pdf",
|
||||
"add-pages": "Ajouter des pages",
|
||||
"reorder-pages": "Réorganiser les pages",
|
||||
"delete-alert": "Impossible de supprimer une seule page",
|
||||
"delete-alert-2": "Etes-vous sûr de vouloir supprimer cette page ?",
|
||||
"delete-note": "Remarque : une fois cette page supprimée, vous ne pouvez plus l'annuler",
|
||||
"Rotation-alert": "Faire pivoter la page",
|
||||
"bulk-import": "Importation en masse",
|
||||
"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.",
|
||||
"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.",
|
||||
@@ -989,7 +985,6 @@
|
||||
"review": "Revoir",
|
||||
"next-field": "Champ suivant",
|
||||
"required-mssg":"{{leftRequiredWidget}} champs sur {{totalWidget}} restants",
|
||||
"verify-document": "Vérifier le document",
|
||||
"verify-document-signature": "Vérifier la signature du document",
|
||||
"select-pdf-document": "Sélectionner le document PDF",
|
||||
"selected-file": "Fichier sélectionné",
|
||||
@@ -1031,15 +1026,8 @@
|
||||
"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"
|
||||
"readonly-textinput-error": "Le widget de texte en lecture seule doit avoir une valeur par défaut ou être rendu optionnel.",
|
||||
"readonly-dropdown-error": "Le widget déroulant en lecture seule doit avoir une valeur par défaut ou être rendu optionnel.",
|
||||
"readonly-radiobtn-error": "Le widget bouton radio en lecture seule doit avoir une valeur par défaut ou être rendu optionnel.",
|
||||
"choose-one":"Choisissez-en un"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"create-account": "खाता बनाएं",
|
||||
"login": "लॉग इन करें",
|
||||
"language": "भाषा",
|
||||
"dark-mode": "डार्क मोड",
|
||||
"name": "नाम",
|
||||
"phone": "फ़ोन",
|
||||
"phone-optional": "वैकल्पिक",
|
||||
@@ -355,7 +354,7 @@
|
||||
"widgets-name": {
|
||||
"signature": "हस्ताक्षर",
|
||||
"stamp": "मोहर",
|
||||
"initials": "प्रारंभिक अक्षर",
|
||||
"initials": "हस्ताक्षर",
|
||||
"name": "नाम",
|
||||
"job title": "पद",
|
||||
"company": "कंपनी",
|
||||
@@ -389,7 +388,7 @@
|
||||
"Email-verified-alert-2": "ईमेल पहले से ही सत्यापित है।",
|
||||
"upload-stamp-image": "मोहर छवि अपलोड करें",
|
||||
"draw": "आरेखित करें",
|
||||
"type": "टाइप",
|
||||
"type": "प्रकार",
|
||||
"color-type": {
|
||||
"red": "लाल",
|
||||
"blue": "नीला",
|
||||
@@ -754,15 +753,12 @@
|
||||
"delete-page": "पृष्ठ हटाएं",
|
||||
"merge-pdf": "पीडीएफ मर्ज करें",
|
||||
"add-pages": "पृष्ठ जोड़ें",
|
||||
"reorder-pages": "पृष्ठ पुनः व्यवस्थित करें",
|
||||
"delete-alert": "एकल पृष्ठ नहीं हटाया जा सकता",
|
||||
"delete-alert-2": "क्या आप वाकई इस पृष्ठ को हटाना चाहते हैं?",
|
||||
"delete-note": "ध्यान दें: एक बार जब आप इस पृष्ठ को हटा देते हैं, तो आप इसे पूर्ववत नहीं कर सकते।",
|
||||
"Rotation-alert": "पृष्ठ घुमाएँ",
|
||||
"bulk-import": "थोक आयात",
|
||||
"contacts-file": "संपर्क फ़ाइल (xlsx, csv)",
|
||||
"import-guideline": "नाम, ईमेल और वैकल्पिक फोन कॉलम वाली CSV या Excel फ़ाइल अपलोड करें। केवल पहले 100 संपर्क आयात किए जाएंगे.",
|
||||
"download-sample": "उदाहरण फ़ाइल डाउनलोड करें",
|
||||
"100-records-only": "वर्तमान में आप केवल 100 रिकॉर्ड तक आयात कर सकते हैं।",
|
||||
"csv-excel-support-only": "निम्नलिखित प्रारूपों में से किसी एक में फ़ाइल अपलोड करें: CSV, XLSX या XLS।",
|
||||
"contact-imported": "{{imported}} संपर्क आयात किए गए। {{failed}} संपर्क आयात करने में विफल रहे।",
|
||||
@@ -989,7 +985,6 @@
|
||||
"review": "समीक्षा",
|
||||
"next-field": "अगला फ़ील्ड",
|
||||
"required-mssg": "{{totalWidget}} में से {{leftRequiredWidget}} फ़ील्ड शेष हैं",
|
||||
"verify-document": "दस्तावेज़ सत्यापित करें",
|
||||
"verify-document-signature": "दस्तावेज़ हस्ताक्षर सत्यापित करें",
|
||||
"select-pdf-document": "पीडीएफ दस्तावेज़ चुनें",
|
||||
"selected-file": "चयनित फ़ाइल",
|
||||
@@ -1031,15 +1026,8 @@
|
||||
"could-not-parse-signer-info": "हस्ताक्षरकर्ता जानकारी पार्स नहीं की जा सकी",
|
||||
"not-calculated": "गणना नहीं की गई",
|
||||
"not-found-in-signature": "हस्ताक्षर में नहीं मिला",
|
||||
"readonly-error": "रीड-ओनली {{widgetName}} विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।",
|
||||
"choose-one":"एक का चयन",
|
||||
"search-templates": "टेम्पलेट खोजें…",
|
||||
"search-documents": "दस्तावेज़ खोजें…",
|
||||
"search-contacts": "संपर्क खोजें…",
|
||||
"invalid-email-found": "अमान्य ईमेल पाया गया: {{email}}",
|
||||
"duplicate-email-found": "डुप्लिकेट ईमेल पाया गया: {{email}}",
|
||||
"vertical": "वर्टिकल",
|
||||
"horizontal": "हॉरिज़ॉन्टल",
|
||||
"billing": "बिलिंग",
|
||||
"console": "कंसोल"
|
||||
"readonly-textinput-error": "रीड-ओनली टेक्स्ट विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।",
|
||||
"readonly-dropdown-error": "रीड-ओनली ड्रॉपडाउन विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।",
|
||||
"readonly-radiobtn-error": "रीड-ओनली रेडियो बटन विजेट में एक डिफ़ॉल्ट मान होना चाहिए या आप इसे वैकल्पिक बना सकते हैं।",
|
||||
"choose-one":"एक का चयन"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"create-account": "Crea Account",
|
||||
"login": "Accedi",
|
||||
"language": "Lingua",
|
||||
"dark-mode": "Modalità scura",
|
||||
"name": "Nome",
|
||||
"phone": "Telefono",
|
||||
"phone-optional": "facoltativo",
|
||||
@@ -754,15 +753,12 @@
|
||||
"delete-page": "Elimina pagina",
|
||||
"merge-pdf": "Unisci PDF",
|
||||
"add-pages": "Aggiungi pagine",
|
||||
"reorder-pages": "Riordina pagine",
|
||||
"delete-alert": "Non è possibile eliminare una singola pagina",
|
||||
"delete-alert-2": "Sei sicuro di voler eliminare questa pagina?",
|
||||
"delete-note": "Nota: Una volta eliminata questa pagina, non potrai annullare l'operazione.",
|
||||
"Rotation-alert": "Ruota pagina",
|
||||
"bulk-import": "Importazione massiva",
|
||||
"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.",
|
||||
"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.",
|
||||
@@ -989,7 +985,6 @@
|
||||
"review": "Rivedere",
|
||||
"next-field": "Campo successivo",
|
||||
"required-mssg":"{{leftRequiredWidget}} di {{totalWidget}} campi rimanenti",
|
||||
"verify-document": "Verifica documento",
|
||||
"verify-document-signature": "Verifica firma documento",
|
||||
"select-pdf-document": "Seleziona documento PDF",
|
||||
"selected-file": "File selezionato",
|
||||
@@ -1031,15 +1026,8 @@
|
||||
"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"
|
||||
"readonly-textinput-error": "Il widget di testo di sola lettura deve avere un valore predefinito oppure può essere reso opzionale.",
|
||||
"readonly-dropdown-error": "Il widget a discesa di sola lettura deve avere un valore predefinito oppure può essere reso opzionale.",
|
||||
"readonly-radiobtn-error": "Il widget pulsante di opzione di sola lettura deve avere un valore predefinito oppure può essere reso opzionale.",
|
||||
"choose-one":"Scegline uno"
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
Name,Email,Phone
|
||||
John Doe,john@example.com,1234567890
|
||||
Jane Smith,jane@example.com,9876543210
|
||||
Foo Bar,foo@example.com,5555555555
|
||||
|
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB |
@@ -142,7 +142,7 @@ function App() {
|
||||
/>
|
||||
{/* signyouself route with no rowlevel data using docId from url */}
|
||||
<Route path="/signaturePdf/:docId" element={<SignYourSelf />} />
|
||||
{/* draft document route to handle and navigate route page according to document status */}
|
||||
{/* draft document route to handle and navigate route page accordiing to document status */}
|
||||
<Route path="/draftDocument" element={<DraftDocument />} />
|
||||
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
|
||||
<Route
|
||||
|
||||
@@ -15,14 +15,12 @@ const BulkSendUi = (props) => {
|
||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
||||
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [signers, setSigners] = useState([]);
|
||||
const [emails, setEmails] = useState([]);
|
||||
useEffect(() => {
|
||||
signatureExist();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
//function to check at least one signature field exist
|
||||
//function to check atleast one signature field exist
|
||||
const signatureExist = async () => {
|
||||
setIsDisableBulkSend(false);
|
||||
const getPlaceholder = props?.Placeholders;
|
||||
@@ -49,14 +47,7 @@ const BulkSendUi = (props) => {
|
||||
(() => {
|
||||
if (props?.Placeholders?.length > 0) {
|
||||
let users = [];
|
||||
let emails = [];
|
||||
props?.Placeholders?.forEach((element) => {
|
||||
const signerEmail = element?.email || element?.signerPtr?.Email;
|
||||
|
||||
// only add when there's a non-empty signerEmail
|
||||
if (signerEmail) {
|
||||
emails = [...emails, signerEmail];
|
||||
}
|
||||
if (!element.signerObjId) {
|
||||
users = [
|
||||
...users,
|
||||
@@ -69,10 +60,7 @@ const BulkSendUi = (props) => {
|
||||
];
|
||||
}
|
||||
});
|
||||
setEmails(emails);
|
||||
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
|
||||
const signer = props.item?.Signers?.filter((x) => x?.objectId);
|
||||
setSigners(signer);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line
|
||||
@@ -94,16 +82,10 @@ const BulkSendUi = (props) => {
|
||||
|
||||
function validateEmails(data) {
|
||||
for (const item of data) {
|
||||
let email = "";
|
||||
for (const field of item.fields) {
|
||||
if (!emailRegex.test(field.email)) {
|
||||
alert(t("invalid-email-found", { email: field.email }));
|
||||
alert(`Invalid email found: ${field.email}`);
|
||||
return false;
|
||||
} else if (email === field.email || emails?.includes(field.email)) {
|
||||
alert(t("duplicate-email-found", { email: field.email }));
|
||||
return false;
|
||||
} else {
|
||||
email = field.email;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,14 +143,15 @@ const BulkSendUi = (props) => {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
|
||||
Signers: props.item.Signers
|
||||
? [...props.item.Signers, ...existSigner]
|
||||
: [...existSigner]
|
||||
});
|
||||
} else {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
SignatureType: props.signatureType,
|
||||
Signers: signers
|
||||
SignatureType: props.signatureType
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import dp from "../assets/images/dp.png";
|
||||
import FullScreenButton from "./FullScreenButton";
|
||||
import ThemeToggle from "./ThemeToggle";
|
||||
import { useNavigate } from "react-router";
|
||||
import Parse from "parse";
|
||||
import { useWindowSize } from "../hook/useWindowSize";
|
||||
@@ -86,37 +85,9 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const updateLogoForTheme = () => {
|
||||
const isDarkMode =
|
||||
document.documentElement.getAttribute("data-theme") === "opensigndark";
|
||||
const logo = isDarkMode
|
||||
? "/static/js/assets/images/logo-dark.png" // Path to the dark mode logo
|
||||
: appInfo.applogo; // Use current logo for light mode
|
||||
if (applogo !== logo) {
|
||||
setAppLogo(logo);
|
||||
}
|
||||
};
|
||||
|
||||
// Set the logo immediately based on the current theme
|
||||
updateLogoForTheme();
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
updateLogoForTheme();
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme"]
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [applogo]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="op-navbar bg-base-100 shadow touch-none">
|
||||
<div className="op-navbar bg-base-100 shadow">
|
||||
<div className="flex-none">
|
||||
<button
|
||||
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
|
||||
@@ -176,7 +147,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
|
||||
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-52 ${
|
||||
isOpen ? "" : "hidden"
|
||||
}`}
|
||||
>
|
||||
@@ -223,16 +194,6 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
{t("verify-document")}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>
|
||||
<i className="fa-light fa-moon"></i>
|
||||
{t("dark-mode")}
|
||||
<span className="text-[10px] font-semibold bg-base-300 text-base-content px-1 rounded-md">
|
||||
BETA
|
||||
</span>
|
||||
<ThemeToggle />
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
<li onClick={closeDropdown}>
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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"
|
||||
} w-full shadow-md px-3 py-2 op-card bg-base-100`}
|
||||
>
|
||||
<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">
|
||||
<i
|
||||
@@ -39,7 +39,7 @@ const DashboardButton = (props) => {
|
||||
></i>
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-lg ml-3">
|
||||
<div className="text-lg ml-3 text-base-content">
|
||||
{t(`sidebar.${props.Label}`)}
|
||||
{props.Label === "Sign yourself" && (
|
||||
<div className="text-gray-500 text-xs mt-1">
|
||||
|
||||
@@ -148,7 +148,7 @@ const DashboardCard = (props) => {
|
||||
);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers?.some(
|
||||
const isSigner = obj.Signers.some(
|
||||
(item) => item.UserId.objectId === currentUser.id
|
||||
);
|
||||
if (isSigner) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Parse from "parse";
|
||||
import ReportTable from "../../primitives/GetReportDisplay";
|
||||
import reportJson from "../../json/ReportJson";
|
||||
@@ -17,16 +17,10 @@ function DashboardReport(props) {
|
||||
const [isMoreDocs, setIsMoreDocs] = useState(true);
|
||||
const abortController = new AbortController();
|
||||
const docPerPage = 5;
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
|
||||
const [isSearchResult, setIsSearchResult] = useState(false);
|
||||
const debounceTimer = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setReportName("");
|
||||
setSearchTerm("");
|
||||
setMobileSearchOpen(false);
|
||||
getReportData(props.Record.reportId, 0, 20, "");
|
||||
getReportData(props.Record.reportId);
|
||||
|
||||
// Function returned from useEffect is called on unmount
|
||||
return () => {
|
||||
@@ -42,69 +36,12 @@ function DashboardReport(props) {
|
||||
// below useEffect call when isNextRecord state is true and fetch next record
|
||||
useEffect(() => {
|
||||
if (isNextRecord) {
|
||||
getReportData(props.Record.reportId, List.length, 20, searchTerm);
|
||||
getReportData(props.Record.reportId, List.length, 20);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isNextRecord]);
|
||||
|
||||
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
|
||||
) => {
|
||||
const getReportData = async (id, skipUserRecord = 0, limit = 20) => {
|
||||
setIsLoader(true);
|
||||
const json = reportJson(id);
|
||||
if (json) {
|
||||
@@ -122,9 +59,6 @@ function DashboardReport(props) {
|
||||
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
||||
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||
if (term) {
|
||||
params.searchTerm = term;
|
||||
}
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(url, params, {
|
||||
headers: headers,
|
||||
@@ -134,7 +68,7 @@ function DashboardReport(props) {
|
||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers?.some(
|
||||
const isSigner = obj.Signers.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
);
|
||||
if (isSigner) {
|
||||
@@ -207,17 +141,11 @@ function DashboardReport(props) {
|
||||
setIsNextRecord={setIsNextRecord}
|
||||
isMoreDocs={isMoreDocs}
|
||||
docPerPage={docPerPage}
|
||||
mobileSearchOpen={mobileSearchOpen}
|
||||
setMobileSearchOpen={setMobileSearchOpen}
|
||||
searchTerm={searchTerm}
|
||||
handleSearchChange={handleSearchChange}
|
||||
handleSearchPaste={handleSearchPaste}
|
||||
isSearchResult={isSearchResult}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded">
|
||||
<div className="text-center">
|
||||
<p className="text-xl text-base-content">{t("report-not-found")}</p>
|
||||
<p className="text-xl text-black">{t("report-not-found")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -405,7 +405,7 @@ function DriveBody(props) {
|
||||
</tr>
|
||||
)
|
||||
) : listType === "list" && data.Type === "Folder" ? (
|
||||
<div className="relative w-[100px] h-[100px] mx-2 my-3">
|
||||
<div key={ind} className="relative w-[100px] h-[100px] mx-2 my-3">
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger className="flex flex-col justify-center items-center select-none-cls">
|
||||
{/* folder */}
|
||||
@@ -447,6 +447,7 @@ function DriveBody(props) {
|
||||
)}
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content
|
||||
className="ContextMenuContent"
|
||||
@@ -640,7 +641,7 @@ function DriveBody(props) {
|
||||
) : (
|
||||
<div className="flex flex-row flex-wrap items-center mt-1 pb-[20px] mx-[5px]">
|
||||
{props.pdfData.map((data, ind) => {
|
||||
return <React.Fragment key={ind}>{handleFolderData(data, ind, "list")}</React.Fragment>;
|
||||
return <div key={ind}>{handleFolderData(data, ind, "list")}</div>;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,7 @@ const AddRoleModal = (props) => {
|
||||
isOpen={props.isModalRole}
|
||||
handleClose={props.handleCloseRoleModal}
|
||||
>
|
||||
<div className="text-base-content h-full py-[10px] px-[20px]">
|
||||
<div className="h-full py-[10px] px-[20px]">
|
||||
<form className="flex flex-col" onSubmit={props.handleAddRole}>
|
||||
<input
|
||||
value={props.roleName}
|
||||
|
||||
@@ -38,7 +38,7 @@ function AgreementSign(props) {
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div className="text-[11px] md:text-base text-base-content">
|
||||
<div className="text-[11px] md:text-base">
|
||||
<span>{t("agree-p1")}</span>
|
||||
<span
|
||||
className="font-bold text-blue-600 cursor-pointer"
|
||||
@@ -67,7 +67,7 @@ function AgreementSign(props) {
|
||||
{t("agrre-button")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 text-base-content">
|
||||
<div className="mt-2">
|
||||
<span className="text-[11px]">{t("agreement-note")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,12 @@ function BorderResize(props) {
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ width: getHeight() || "14px", height: getHeight() || "14px" }}
|
||||
style={{
|
||||
width: getHeight() || "14px",
|
||||
height: getHeight() || "14px"
|
||||
}}
|
||||
className={`${props.right ? `-right-[12px]` : "-right-[2px]"} ${
|
||||
props.top ? `-bottom-[12px]` : "-bottom-[2px]"
|
||||
props.top ? `-bottom-[12px]` : "-bottom-[2px] "
|
||||
} absolute inline-block hover:cursor-sw-resize border-r-[3px] border-b-[3px] border-[#188ae2]`}
|
||||
></div>
|
||||
);
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +1,42 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
const Cell = ({
|
||||
isEnabled,
|
||||
count,
|
||||
h,
|
||||
value,
|
||||
editable,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onBlur,
|
||||
inputRef,
|
||||
index,
|
||||
fontSize,
|
||||
fontColor,
|
||||
hint
|
||||
index
|
||||
}) => (
|
||||
<div
|
||||
className={`${isEnabled ? "bg-white border-gray-400" : "select-none-cls pointer-events-none border-gray-500"} flex items-center justify-center border-[1px]`}
|
||||
className="flex items-center justify-center border border-gray-800 bg-white"
|
||||
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 }}
|
||||
className="w-full text-center uppercase focus:outline-none bg-transparent text-[12px]"
|
||||
style={{ fontFamily: "Arial, sans-serif" }}
|
||||
/>
|
||||
</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 = ""
|
||||
resizable = false
|
||||
}) {
|
||||
const [cellCount, setCellCount] = useState(count);
|
||||
|
||||
@@ -93,7 +79,6 @@ export default function CellsWidget({
|
||||
};
|
||||
|
||||
const cells = Array.from({ length: cellCount }).map((_, i) => value[i] || "");
|
||||
const hints = Array.from({ length: cellCount }).map((_, i) => hint[i] || "");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -102,7 +87,7 @@ export default function CellsWidget({
|
||||
>
|
||||
{resizable && (
|
||||
<div
|
||||
className="cell-size-handle absolute left-1/2 -translate-x-1/2 -bottom-4 rotate-180 cursor-ew-resize touch-none"
|
||||
className="cell-size-handle absolute left-1/2 -translate-x-1/2 -top-3 cursor-ew-resize touch-none"
|
||||
onPointerDown={onTopHandlePointerDown}
|
||||
>
|
||||
<svg
|
||||
@@ -123,19 +108,14 @@ export default function CellsWidget({
|
||||
{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>
|
||||
|
||||
@@ -111,22 +111,17 @@ function DropdownWidgetOption(props) {
|
||||
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 it’s a dropdown and it’s 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 }));
|
||||
alert(
|
||||
props?.type === "dropdown"
|
||||
? t("readonly-dropdown-error")
|
||||
: t("readonly-radiobtn-error")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -272,9 +267,7 @@ function DropdownWidgetOption(props) {
|
||||
onChange={() => setStatus(data.toLowerCase())}
|
||||
checked={status.toLowerCase() === data.toLowerCase()}
|
||||
/>
|
||||
<div className="text-[13px] font-500 capitalize">
|
||||
{data}
|
||||
</div>
|
||||
<div className="text-[13px] font-500 capitalize">{data}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -341,10 +334,7 @@ function DropdownWidgetOption(props) {
|
||||
className="op-checkbox op-checkbox-sm"
|
||||
onChange={(e) => setIsReadOnly(e.target.checked)}
|
||||
/>
|
||||
<label
|
||||
className="ml-2 mb-0 capitalize"
|
||||
htmlFor="isreadonly"
|
||||
>
|
||||
<label className="ml-2 mb-0 capitalize" htmlFor="isreadonly">
|
||||
{t("read-only")}
|
||||
</label>
|
||||
</div>
|
||||
@@ -359,10 +349,7 @@ function DropdownWidgetOption(props) {
|
||||
onChange={(e) => setIsHideLabel(e.target.checked)}
|
||||
/>
|
||||
|
||||
<label
|
||||
className="ml-2 mb-0 capitalize"
|
||||
htmlFor="ishidelabel"
|
||||
>
|
||||
<label className="ml-2 mb-0 capitalize" htmlFor="ishidelabel">
|
||||
{t("hide-labels")}
|
||||
</label>
|
||||
</div>
|
||||
@@ -389,8 +376,8 @@ function DropdownWidgetOption(props) {
|
||||
checked={layout.toLowerCase() === data.toLowerCase()}
|
||||
onChange={() => setLayout(data.toLowerCase())}
|
||||
/>
|
||||
<label className="text-[13px] font-500 mb-0">
|
||||
{t(data)}
|
||||
<label className="text-[13px] font-500 mb-0 capitalize">
|
||||
{data}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import React from "react";
|
||||
import { Quill } from "react-quill-new";
|
||||
// Use a third-party plugin so editors can toggle raw HTML view.
|
||||
import htmlEditButton from "quill-html-edit-button";
|
||||
|
||||
// Register the module which adds a "<>" button for HTML editing.
|
||||
Quill.register("modules/htmlEditButton", htmlEditButton);
|
||||
|
||||
// Custom Undo button icon component for Quill editor. You can import it directly
|
||||
// from 'quill/assets/icons/undo.svg' but I found that a number of loaders do not
|
||||
@@ -61,9 +56,7 @@ export const module1 = {
|
||||
container: "#toolbar1",
|
||||
handlers: { undo: undoChange, redo: redoChange }
|
||||
},
|
||||
history: { delay: 500, maxStack: 100, userOnly: true },
|
||||
// Enable the "<>" button registered above
|
||||
htmlEditButton: {}
|
||||
history: { delay: 500, maxStack: 100, userOnly: true }
|
||||
};
|
||||
|
||||
// Modules object for setting up the Quill editor
|
||||
@@ -72,9 +65,7 @@ export const module2 = {
|
||||
container: "#toolbar2",
|
||||
handlers: { undo: undoChange, redo: redoChange }
|
||||
},
|
||||
history: { delay: 500, maxStack: 100, userOnly: true },
|
||||
// Enable the "<>" button registered above
|
||||
htmlEditButton: {}
|
||||
history: { delay: 500, maxStack: 100, userOnly: true }
|
||||
};
|
||||
|
||||
// Formats objects for setting up the Quill editor
|
||||
|
||||
@@ -186,7 +186,7 @@ function EmailComponent({
|
||||
<input
|
||||
type="email"
|
||||
value={emailValue}
|
||||
className="p-[10px] pb-[20px] text-base-content rounded-md w-full text-[15px] outline-none bg-transparent border-[1px] op-border-primary"
|
||||
className="p-[10px] pb-[20px] rounded-md w-full text-[15px] outline-none bg-transparent border-[1px] op-border-primary"
|
||||
onChange={handleEmailValue}
|
||||
onKeyDown={handleEnterPress}
|
||||
placeholder={t("enter-email-plaholder")}
|
||||
@@ -204,6 +204,19 @@ function EmailComponent({
|
||||
{t("email-error-1")}
|
||||
</p>
|
||||
)}
|
||||
{/* <button
|
||||
className={`${
|
||||
emailValue ? "cursor-pointer" : "cursor-default"
|
||||
} op-btn op-btn-primary op-btn-sm m-2 shadow-md`}
|
||||
onClick={() => emailValue && handleEnterPress("add")}
|
||||
>
|
||||
<i className="fa-light fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div className="bg-[#e3e2e1] mt-[10px] p-[5px] rounded">
|
||||
<span className="font-bold">{t("report-heading.Note")}: </span>
|
||||
<span className="text-[15px]">{t("email-error-2")}</span>
|
||||
</div>
|
||||
<hr className="w-full my-[15px] bg-base-content" /> */}
|
||||
<div className="mt-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -214,7 +227,7 @@ function EmailComponent({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content ml-2"
|
||||
className="op-btn op-btn-ghost ml-2"
|
||||
onClick={() => handleClose()}
|
||||
>
|
||||
{t("close")}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -2,21 +2,16 @@ import React, { useRef, useState } from "react";
|
||||
import PrevNext from "./PrevNext";
|
||||
import {
|
||||
base64ToArrayBuffer,
|
||||
decryptPdf,
|
||||
deletePdfPage,
|
||||
flattenPdf,
|
||||
getFileAsArrayBuffer,
|
||||
handleDownloadCertificate,
|
||||
handleDownloadPdf,
|
||||
handleRemoveWidgets,
|
||||
handleToPrint,
|
||||
reorderPdfPages
|
||||
handleToPrint
|
||||
} from "../../constant/Utils";
|
||||
import "../../styles/signature.css";
|
||||
import { DropdownMenu } from "radix-ui";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import PageReorderModal from "./PageReorderModal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PDFDocument } from "pdf-lib";
|
||||
import { maxFileSize } from "../../constant/const";
|
||||
@@ -29,7 +24,6 @@ function Header(props) {
|
||||
const isMobile = window.innerWidth < 767;
|
||||
const [isDownloading, setIsDownloading] = useState("");
|
||||
const [isDeletePage, setIsDeletePage] = useState(false);
|
||||
const [isReorderModal, setIsReorderModal] = useState(false);
|
||||
const mergePdfInputRef = useRef(null);
|
||||
const enabledBackBtn = props?.disabledBackBtn === true ? false : true;
|
||||
//function for show decline alert
|
||||
@@ -86,42 +80,7 @@ function Header(props) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
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 uploadedPdfBytes = await file.arrayBuffer();
|
||||
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
|
||||
ignoreEncryption: true
|
||||
});
|
||||
@@ -147,27 +106,12 @@ function Header(props) {
|
||||
console.error("Error merging PDF:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReorderSave = async (order) => {
|
||||
try {
|
||||
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
|
||||
if (pdfupdatedData) {
|
||||
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
|
||||
props.setPdfBase64Url(pdfupdatedData.base64);
|
||||
props.setAllPages(pdfupdatedData.totalPages);
|
||||
props.setPageNumber(1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error in reorder pdf pages", e);
|
||||
}
|
||||
setIsReorderModal(false);
|
||||
};
|
||||
return (
|
||||
<div className="flex py-[5px]">
|
||||
{isMobile && props?.isShowHeader ? (
|
||||
<div
|
||||
id="navbar"
|
||||
className="stickyHead touch-none"
|
||||
className="stickyHead"
|
||||
style={{
|
||||
width: window.innerWidth + "px"
|
||||
}}
|
||||
@@ -390,17 +334,6 @@ function Header(props) {
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => setIsReorderModal(true)}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[30px] mr-[3px]"></i>
|
||||
<span className="font-[500]">
|
||||
{t("reorder-pages")}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
@@ -763,12 +696,6 @@ function Header(props) {
|
||||
</button>
|
||||
</div>
|
||||
</ModalUi>
|
||||
<PageReorderModal
|
||||
isOpen={isReorderModal}
|
||||
handleClose={() => setIsReorderModal(false)}
|
||||
totalPages={props.allPages}
|
||||
onSave={handleReorderSave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import { useRef, useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
base64ToArrayBuffer,
|
||||
decryptPdf,
|
||||
deletePdfPage,
|
||||
flattenPdf,
|
||||
getFileAsArrayBuffer,
|
||||
handleRemoveWidgets,
|
||||
reorderPdfPages
|
||||
handleRemoveWidgets
|
||||
} from "../../constant/Utils";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { PDFDocument } from "pdf-lib";
|
||||
import { maxFileSize } from "../../constant/const";
|
||||
import PageReorderModal from "./PageReorderModal";
|
||||
|
||||
function PdfZoom(props) {
|
||||
const { t } = useTranslation();
|
||||
const mergePdfInputRef = useRef(null);
|
||||
const [isDeletePage, setIsDeletePage] = useState(false);
|
||||
const [isReorderModal, setIsReorderModal] = useState(false);
|
||||
const handleDetelePage = async () => {
|
||||
props.setIsUploadPdf && props.setIsUploadPdf(true);
|
||||
try {
|
||||
@@ -73,42 +67,7 @@ function PdfZoom(props) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
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 uploadedPdfBytes = await file.arrayBuffer();
|
||||
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
|
||||
ignoreEncryption: true
|
||||
});
|
||||
@@ -135,21 +94,6 @@ function PdfZoom(props) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReorderSave = async (order) => {
|
||||
try {
|
||||
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
|
||||
if (pdfupdatedData) {
|
||||
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
|
||||
props.setPdfBase64Url(pdfupdatedData.base64);
|
||||
props.setAllPages(pdfupdatedData.totalPages);
|
||||
props.setPageNumber(1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error in reorder pdf pages", e);
|
||||
}
|
||||
setIsReorderModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="hidden md:flex flex-col gap-1 text-center md:w-[5%] mt-[42px]">
|
||||
@@ -176,13 +120,6 @@ function PdfZoom(props) {
|
||||
>
|
||||
<i className="fa-light fa-trash text-gray-500 2xl:text-[25px]"></i>
|
||||
</span>
|
||||
<span
|
||||
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
|
||||
onClick={() => setIsReorderModal(true)}
|
||||
title={t("reorder-pages")}
|
||||
>
|
||||
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[25px]"></i>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span
|
||||
@@ -248,12 +185,6 @@ function PdfZoom(props) {
|
||||
</button>
|
||||
</div>
|
||||
</ModalUi>
|
||||
<PageReorderModal
|
||||
isOpen={isReorderModal}
|
||||
handleClose={() => setIsReorderModal(false)}
|
||||
totalPages={props.allPages}
|
||||
onSave={handleReorderSave}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import BorderResize from "./BorderResize";
|
||||
import { Rnd } from "react-rnd";
|
||||
import {
|
||||
@@ -14,8 +14,7 @@ import {
|
||||
radioButtonWidget,
|
||||
textInputWidget,
|
||||
cellsWidget,
|
||||
textWidget,
|
||||
selectFormat
|
||||
textWidget
|
||||
} from "../../constant/Utils";
|
||||
import PlaceholderType from "./PlaceholderType";
|
||||
import moment from "moment";
|
||||
@@ -26,6 +25,37 @@ import { useDispatch } from "react-redux";
|
||||
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
|
||||
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
|
||||
|
||||
@@ -51,6 +81,8 @@ function Placeholder(props) {
|
||||
props.pos?.options?.defaultValue || props.pos?.options?.response;
|
||||
const [isDateModal, setIsDateModal] = useState(false);
|
||||
const [containerScale, setContainerScale] = useState();
|
||||
const holdTimeout = useRef(null);
|
||||
const startTime = useRef(null); // Track when the user starts holdings
|
||||
const [selectDate, setSelectDate] = useState({});
|
||||
const [dateFormat, setDateFormat] = useState([]);
|
||||
const [clickonWidget, setClickonWidget] = useState({});
|
||||
@@ -80,9 +112,7 @@ function Placeholder(props) {
|
||||
"MMM DD, YYYY",
|
||||
"LL",
|
||||
"DD MMM, YYYY",
|
||||
"DD MMMM, YYYY",
|
||||
"DD.MM.YYYY",
|
||||
"DD/MM/YYYY"
|
||||
"DD MMMM, YYYY"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
@@ -104,18 +134,15 @@ function Placeholder(props) {
|
||||
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 if (selectDate && selectDate.format === "dd/MM/yyyy") {
|
||||
const [day, month, year] = selectDate.date.split("/");
|
||||
date = new Date(`${year}/${month}/${day}`);
|
||||
} else {
|
||||
date = new Date(selectDate?.date);
|
||||
}
|
||||
const milliseconds = date.getTime();
|
||||
const newDate = moment(milliseconds).format(data);
|
||||
const dateObj = { date: newDate, format: selectFormat(data) };
|
||||
const dateObj = {
|
||||
date: newDate,
|
||||
format: selectFormat(data)
|
||||
};
|
||||
updateDate.push(dateObj);
|
||||
});
|
||||
setDateFormat(updateDate);
|
||||
@@ -280,13 +307,6 @@ function Placeholder(props) {
|
||||
} else if (props.pos.type === "checkbox") {
|
||||
props?.setIsCheckbox(true);
|
||||
}
|
||||
// cells widget settings in sign yourself flow
|
||||
else if (
|
||||
props.pos.type === cellsWidget &&
|
||||
(props.isSignYourself || props.isSelfSign)
|
||||
) {
|
||||
props.handleCellSettingModal && props.handleCellSettingModal();
|
||||
}
|
||||
//condition to handle setting icon for signyour-self flow for all type text widgets
|
||||
else if (
|
||||
[
|
||||
@@ -366,12 +386,9 @@ function Placeholder(props) {
|
||||
|
||||
//function to save date and format on local array onchange date and onclick format
|
||||
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 date;
|
||||
if (isSpecialDateFormat) {
|
||||
if (data?.format === "dd-MM-yyyy") {
|
||||
date = isDateChange
|
||||
? moment(updateDate).format(changeDateToMomentFormat(data.format))
|
||||
: updateDate;
|
||||
@@ -400,47 +417,41 @@ function Placeholder(props) {
|
||||
};
|
||||
|
||||
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
|
||||
const isSignerList = props.xyPosition.some((d) => d.signerPtr);
|
||||
if (isSignerList) {
|
||||
const signerId = props.data?.Id || props.uniqueId;
|
||||
const filterSignerPos = props.xyPosition.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
|
||||
);
|
||||
}
|
||||
} 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 { ...ph, pos: newPos };
|
||||
});
|
||||
const newSignerPos = props.xyPosition.map((obj) =>
|
||||
obj.Id === signerId
|
||||
? { ...obj, placeHolder: updatedPlaceHolder }
|
||||
: obj
|
||||
);
|
||||
props.setXyPosition(newSignerPos);
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
} else {
|
||||
const updatePos = props.xyPosition[props.index].pos.map((p) =>
|
||||
p.key === key
|
||||
? { ...p, options: { ...p.options, cellCount: newCount } }
|
||||
: p
|
||||
);
|
||||
const updatePlaceholder = props.xyPosition.map((obj, ind) =>
|
||||
ind === props.index ? { ...obj, pos: updatePos } : obj
|
||||
);
|
||||
props.setXyPosition(updatePlaceholder);
|
||||
}
|
||||
};
|
||||
const PlaceholderIcon = () => {
|
||||
const isSettingForCells =
|
||||
props?.isAlllowModify && !props?.assignedWidgetId.includes(props.pos.key)
|
||||
? []
|
||||
: [cellsWidget];
|
||||
|
||||
// 1- If props.isShowBorder is true, display border's icon for all widgets. OR
|
||||
// 2- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show border's icon:
|
||||
// 1- When isAlllowModify is true, show border's icon.
|
||||
@@ -464,15 +475,10 @@ function Placeholder(props) {
|
||||
"name",
|
||||
"company",
|
||||
"job title",
|
||||
"email",
|
||||
...isSettingForCells
|
||||
"email"
|
||||
].includes(props.pos.type) &&
|
||||
(props.isSignYourself || props.isSelfSign) ? (
|
||||
<i
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOnClickSettingIcon();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOnClickSettingIcon();
|
||||
@@ -482,14 +488,7 @@ function Placeholder(props) {
|
||||
handleOnClickSettingIcon();
|
||||
}}
|
||||
className="fa-light fa-gear icon"
|
||||
style={{
|
||||
color: "#188ae2",
|
||||
right: "29px",
|
||||
top: "-19px",
|
||||
cursor: "pointer",
|
||||
zIndex: 99,
|
||||
pointerEvents: "auto"
|
||||
}}
|
||||
style={{ color: "#188ae2", right: "29px", top: "-19px" }}
|
||||
></i>
|
||||
) : (
|
||||
/* condition to add setting icon for placeholder & template flow for all widgets except signature and date */
|
||||
@@ -499,10 +498,6 @@ function Placeholder(props) {
|
||||
!props.isSignYourself &&
|
||||
!props.isSelfSign)) && (
|
||||
<i
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOnClickSettingIcon();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOnClickSettingIcon();
|
||||
@@ -515,10 +510,7 @@ function Placeholder(props) {
|
||||
style={{
|
||||
color: "#188ae2",
|
||||
right: props?.pos?.type === textWidget ? "32px" : "51px",
|
||||
top: "-19px",
|
||||
cursor: "pointer",
|
||||
zIndex: 99,
|
||||
pointerEvents: "auto"
|
||||
top: "-19px"
|
||||
}}
|
||||
></i>
|
||||
)
|
||||
@@ -556,7 +548,6 @@ function Placeholder(props) {
|
||||
{/* setting icon only for date widgets */}
|
||||
{props.pos.type === "date" && selectDate && (
|
||||
<i
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
props.setCurrWidgetsDetails(props.pos);
|
||||
setIsDateModal(!isDateModal);
|
||||
@@ -589,9 +580,7 @@ function Placeholder(props) {
|
||||
top: "-18px",
|
||||
right: props.isPlaceholder ? "50px" : "30px",
|
||||
color: "#188ae2",
|
||||
fontSize: "14px",
|
||||
cursor: "pointer",
|
||||
pointerEvents: "auto"
|
||||
fontSize: "14px"
|
||||
}}
|
||||
className="fa-light fa-gear icon"
|
||||
></i>
|
||||
@@ -718,7 +707,7 @@ function Placeholder(props) {
|
||||
return "not-allowed";
|
||||
}
|
||||
} else {
|
||||
return "move";
|
||||
return "all-scroll";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -738,6 +727,21 @@ function Placeholder(props) {
|
||||
return "rgba(203, 233, 237, 0.69)";
|
||||
}
|
||||
};
|
||||
const handleTouchEnd = () => {
|
||||
if (!props.isNeedSign || props.isAlllowModify) {
|
||||
const holdDuration = Date.now() - startTime.current; // Calculate hold time
|
||||
clearTimeout(holdTimeout.current); // Cancel timeout if touch ended early
|
||||
|
||||
if (holdDuration < 1000) {
|
||||
try {
|
||||
navigator.vibrate([]); // Cancel any ongoing vibration
|
||||
} catch (e) {
|
||||
console.log("error in navigator.vibrate", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!props.isNeedSign || props.isAlllowModify) handleOnClickPlaceholder();
|
||||
};
|
||||
|
||||
const fontSize = calculateFont(props.pos.options?.fontSize);
|
||||
const fontColor = props.pos.options?.fontColor || "black";
|
||||
@@ -788,16 +792,13 @@ function Placeholder(props) {
|
||||
id={props.pos.key}
|
||||
data-tut={props.pos.key === props.unSignedWidgetId ? "IsSigned" : ""}
|
||||
key={props.pos.key}
|
||||
cancel=".cell-size-handle, .icon"
|
||||
cancel=".cell-size-handle"
|
||||
lockAspectRatio={
|
||||
props?.isAlllowModify &&
|
||||
!props?.assignedWidgetId.includes(props.pos.key)
|
||||
? false
|
||||
: !props.isFreeResize &&
|
||||
(props.pos.Width
|
||||
? props.pos.Width / props.pos.Height
|
||||
: defaultWidthHeight(props.pos.type).width /
|
||||
defaultWidthHeight(props.pos.type).height)
|
||||
!props.isFreeResize &&
|
||||
(props.pos.Width
|
||||
? props.pos.Width / props.pos.Height
|
||||
: defaultWidthHeight(props.pos.type).width /
|
||||
defaultWidthHeight(props.pos.type).height)
|
||||
}
|
||||
enableResizing={{
|
||||
top: false,
|
||||
@@ -837,16 +838,8 @@ function Placeholder(props) {
|
||||
props.isNeedSign && props.data?.Id !== props?.uniqueId && "0.4",
|
||||
background: handleBackground()
|
||||
}}
|
||||
onDrag={(_, d) => {
|
||||
onDrag={() => {
|
||||
props.handleTabDrag && props.handleTabDrag(props.pos.key);
|
||||
props.showGuidelines &&
|
||||
props.showGuidelines(
|
||||
true,
|
||||
d.x,
|
||||
d.y,
|
||||
d.node.offsetWidth,
|
||||
d.node.offsetHeight
|
||||
);
|
||||
}}
|
||||
size={{
|
||||
width:
|
||||
@@ -867,26 +860,8 @@ function Placeholder(props) {
|
||||
calculateFont(props.pos.options?.fontSize, true)
|
||||
}
|
||||
maxHeight="auto"
|
||||
onResizeStart={(e, dir, ref) => {
|
||||
onResizeStart={() => {
|
||||
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) => {
|
||||
setTimeout(() => {
|
||||
@@ -904,7 +879,6 @@ function Placeholder(props) {
|
||||
props.data && props.data.Id,
|
||||
props.isResize
|
||||
);
|
||||
props.showGuidelines && props.showGuidelines(false);
|
||||
}}
|
||||
onDragStop={(event, dragElement) => {
|
||||
props.handleStop &&
|
||||
@@ -914,9 +888,6 @@ function Placeholder(props) {
|
||||
props.data?.Id,
|
||||
props.pos?.key
|
||||
);
|
||||
props.isDragging &&
|
||||
props.showGuidelines &&
|
||||
props.showGuidelines(false);
|
||||
}}
|
||||
position={{
|
||||
x: xPos(props.pos, props.isSignYourself),
|
||||
@@ -953,24 +924,24 @@ function Placeholder(props) {
|
||||
![radioButtonWidget, "checkbox"].includes(props.pos.type) &&
|
||||
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.
|
||||
2- If props.isShowBorder is true, display border for all widgets.
|
||||
3- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show border:
|
||||
3.1- When isAlllowModify is true, show border.
|
||||
3.2- Do not display border for widgets already assigned (props.assignedWidgetId.includes(props.pos.key) is true).
|
||||
|
||||
{/* 1- Show a ouline if props.pos.key === props?.currWidgetsDetails?.key, indicating the current user's selected widget.
|
||||
2- If props.isShowBorder is true, display ouline for all widgets.
|
||||
3- Use the combination of props?.isAlllowModify and !props?.assignedWidgetId.includes(props.pos.key) to determine when to show ouline:
|
||||
3.1- When isAlllowModify is true, show ouline.
|
||||
3.2- Do not display ouline for widgets already assigned (props.assignedWidgetId.includes(props.pos.key) is true).
|
||||
*/}
|
||||
{props.pos.key === props?.currWidgetsDetails?.key &&
|
||||
(props.isShowBorder ||
|
||||
(props?.isAlllowModify &&
|
||||
!props?.assignedWidgetId.includes(props.pos.key))) && (
|
||||
<div
|
||||
style={{ borderColor: themeColor }}
|
||||
className="w-[calc(100%+21px)] h-[calc(100%+21px)] cursor-move absolute inline-block border-[1px] border-dashed"
|
||||
></div>
|
||||
)}
|
||||
<div
|
||||
className="flex items-stretch justify-center"
|
||||
className={`${
|
||||
props.pos.key === props?.currWidgetsDetails?.key &&
|
||||
(props.isShowBorder ||
|
||||
(props?.isAlllowModify &&
|
||||
!props?.assignedWidgetId.includes(props.pos.key)))
|
||||
? "outline-[0.3px] outline-dashed outline-offset-[10px]"
|
||||
: ""
|
||||
} flex items-stretch justify-center`}
|
||||
style={{
|
||||
outlineColor: themeColor,
|
||||
left: xPos(props.pos, props.isSignYourself),
|
||||
top: yPos(props.pos, props.isSignYourself),
|
||||
width: "100%",
|
||||
@@ -1010,7 +981,7 @@ function Placeholder(props) {
|
||||
)}
|
||||
|
||||
<ModalUi isOpen={isDateModal} title={t("widget-info")} showClose={false}>
|
||||
<div className="text-base-content h-[100%] p-[20px]">
|
||||
<div className="h-[100%] p-[20px]">
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-y-3">
|
||||
<div className="flex flex-row items-center gap-x-1">
|
||||
<span className="capitalize">{t("format")} :</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, forwardRef } from "react";
|
||||
import React, { useEffect, useState, forwardRef, useRef } from "react";
|
||||
import {
|
||||
getMonth,
|
||||
getYear,
|
||||
@@ -36,6 +36,7 @@ function PlaceholderType(props) {
|
||||
const widgetData =
|
||||
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
|
||||
const widgetTypeTranslation = t(`widgets-name.${props?.pos?.type}`);
|
||||
const inputRef = useRef(null);
|
||||
const [widgetValue, setwidgetValue] = useState();
|
||||
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
|
||||
const [hint, setHint] = useState("");
|
||||
@@ -157,7 +158,7 @@ function PlaceholderType(props) {
|
||||
const checkBoxWrapperClass = `flex items-start ${
|
||||
checkBoxLayout === "horizontal"
|
||||
? `flex-row flex-wrap ${isMultipleCheckbox ? "gap-x-2" : ""}`
|
||||
: `flex-col ${isMultipleCheckbox ? "gap-y-[3px]" : ""}`
|
||||
: `flex-col ${isMultipleCheckbox ? "gap-y-[5px]" : ""}`
|
||||
}`; // Using gap-y-1 for consistency, adjust if needed
|
||||
|
||||
return (
|
||||
@@ -192,6 +193,7 @@ function PlaceholderType(props) {
|
||||
case textInputWidget:
|
||||
return props.isSignYourself || iswidgetEnable ? (
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
placeholder={hint || t("widgets-name.text")}
|
||||
rows={1}
|
||||
value={widgetValue}
|
||||
@@ -215,24 +217,16 @@ function PlaceholderType(props) {
|
||||
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}
|
||||
editable={props.isPlaceholder}
|
||||
resizable={props.isPlaceholder}
|
||||
onCellCountChange={handleCellResize}
|
||||
/>
|
||||
);
|
||||
@@ -275,6 +269,7 @@ function PlaceholderType(props) {
|
||||
return iswidgetEnable ? (
|
||||
<textarea
|
||||
readOnly
|
||||
ref={inputRef}
|
||||
placeholder={hint || widgetTypeTranslation}
|
||||
rows={1}
|
||||
value={widgetValue}
|
||||
@@ -297,6 +292,7 @@ function PlaceholderType(props) {
|
||||
return iswidgetEnable ? (
|
||||
<textarea
|
||||
readOnly
|
||||
ref={inputRef}
|
||||
placeholder={hint || widgetTypeTranslation}
|
||||
rows={1}
|
||||
value={widgetValue}
|
||||
@@ -319,6 +315,7 @@ function PlaceholderType(props) {
|
||||
return iswidgetEnable ? (
|
||||
<textarea
|
||||
readOnly
|
||||
ref={inputRef}
|
||||
placeholder={hint || widgetTypeTranslation}
|
||||
rows={1}
|
||||
value={widgetValue}
|
||||
@@ -424,6 +421,7 @@ function PlaceholderType(props) {
|
||||
return iswidgetEnable ? (
|
||||
<textarea
|
||||
readOnly
|
||||
ref={inputRef}
|
||||
placeholder={hint || widgetTypeTranslation}
|
||||
rows={1}
|
||||
value={widgetValue}
|
||||
@@ -447,7 +445,7 @@ function PlaceholderType(props) {
|
||||
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-row flex-wrap ${isOnlyOneBtn ? "gap-x-2" : ""}`
|
||||
: `flex-col ${isOnlyOneBtn ? "gap-y-[5px]" : ""}`
|
||||
}`; // Using gap-y-1 for consistency, adjust if needed
|
||||
return (
|
||||
|
||||
@@ -31,7 +31,7 @@ const RecipientList = (props) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
//handle draggable element drop and also used in mobile view on up and down key to change sequence of recipient's list
|
||||
//handle draggable element drop and also used in mobile view on up and down key to chnage sequence of recipient's list
|
||||
const handleChangeSequence = (e, ind, isUp, isDown, obj) => {
|
||||
e.preventDefault();
|
||||
let draggedItemId;
|
||||
@@ -82,7 +82,7 @@ const RecipientList = (props) => {
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{props.signersdata?.length > 0 &&
|
||||
{props.signersdata.length > 0 &&
|
||||
props.signersdata.map((obj, ind) => {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -3,12 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Document, Page } from "react-pdf";
|
||||
import { useSelector } from "react-redux";
|
||||
import { PDFDocument } from "pdf-lib";
|
||||
import {
|
||||
base64ToArrayBuffer,
|
||||
decryptPdf,
|
||||
flattenPdf,
|
||||
getFileAsArrayBuffer
|
||||
} from "../../constant/Utils";
|
||||
import { base64ToArrayBuffer } from "../../constant/Utils";
|
||||
import { maxFileSize } from "../../constant/const";
|
||||
|
||||
function RenderAllPdfPage(props) {
|
||||
@@ -92,42 +87,7 @@ function RenderAllPdfPage(props) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
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 uploadedPdfBytes = await file.arrayBuffer();
|
||||
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
|
||||
ignoreEncryption: true
|
||||
});
|
||||
@@ -209,8 +169,8 @@ function RenderAllPdfPage(props) {
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<i className="fa-light fa-plus text-gray-500"></i>
|
||||
<span className="text-xs lg:text-sm text-base-content">
|
||||
{t("add-pages")}
|
||||
<span className="text-xs lg:text-sm text-base-content ">
|
||||
Add pages
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import React, { useState } from "react";
|
||||
import RSC from "react-scrollbars-custom";
|
||||
import { Document, Page } from "react-pdf";
|
||||
import {
|
||||
@@ -11,45 +11,13 @@ import {
|
||||
import Placeholder from "./Placeholder";
|
||||
import Alert from "../../primitives/Alert";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import usePdfPinchZoom from "../../hook/usePdfPinchZoom";
|
||||
|
||||
function RenderPdf(props) {
|
||||
const { t } = useTranslation();
|
||||
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
|
||||
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
|
||||
const posWidth = (pos, signYourself) => {
|
||||
const containerScale = getContainerScale(
|
||||
@@ -147,7 +115,6 @@ function RenderPdf(props) {
|
||||
isSignYourself={false}
|
||||
posWidth={posWidth}
|
||||
posHeight={posHeight}
|
||||
showGuidelines={handleGuideline}
|
||||
isDragging={props.isDragging}
|
||||
pdfDetails={props.pdfDetails}
|
||||
unSignedWidgetId={props.unSignedWidgetId}
|
||||
@@ -169,9 +136,8 @@ function RenderPdf(props) {
|
||||
handleDeleteSign={props.handleDeleteSign}
|
||||
setIsPageCopy={props.setIsPageCopy}
|
||||
handleTextSettingModal={props.handleTextSettingModal}
|
||||
handleCellSettingModal={props.handleCellSettingModal}
|
||||
setIsCheckbox={props.setIsCheckbox}
|
||||
isFreeResize={props.isSelfSign ? true : false}
|
||||
isFreeResize={false}
|
||||
isOpenSignPad={true}
|
||||
assignedWidgetId={props.assignedWidgetId}
|
||||
isApplyAll={true}
|
||||
@@ -237,17 +203,14 @@ function RenderPdf(props) {
|
||||
data-tut={isMobile ? "reactourForth" : undefined}
|
||||
className={
|
||||
isMobile
|
||||
? `${isGuestSigner ? "30px" : ""} border-[0.1px] border-[#ebe8e8] overflow-x-auto relative`
|
||||
: "relative"
|
||||
? `${isGuestSigner ? "30px" : ""} border-[0.1px] border-[#ebe8e8] overflow-x-auto`
|
||||
: ""
|
||||
}
|
||||
style={{
|
||||
width:
|
||||
props.containerWH?.width && props.containerWH?.width * props.scale
|
||||
}}
|
||||
ref={(node) => {
|
||||
pdfContainerRef.current = node;
|
||||
props.drop && props.drop(node);
|
||||
}}
|
||||
ref={props.drop}
|
||||
id="container"
|
||||
>
|
||||
{props.pdfLoad !== false &&
|
||||
@@ -294,7 +257,6 @@ function RenderPdf(props) {
|
||||
isSignYourself={false}
|
||||
posWidth={posWidth}
|
||||
posHeight={posHeight}
|
||||
showGuidelines={handleGuideline}
|
||||
isDragging={props.isDragging}
|
||||
setIsValidate={props.setIsValidate}
|
||||
setIsRadio={props.setIsRadio}
|
||||
@@ -308,9 +270,6 @@ function RenderPdf(props) {
|
||||
handleTextSettingModal={
|
||||
props.handleTextSettingModal
|
||||
}
|
||||
handleCellSettingModal={
|
||||
props.handleCellSettingModal
|
||||
}
|
||||
scale={props.scale}
|
||||
containerWH={props.containerWH}
|
||||
pdfOriginalWH={props.pdfOriginalWH}
|
||||
@@ -362,7 +321,6 @@ function RenderPdf(props) {
|
||||
isSignYourself={true}
|
||||
posWidth={posWidth}
|
||||
posHeight={posHeight}
|
||||
showGuidelines={handleGuideline}
|
||||
pdfDetails={props.pdfDetails[0]}
|
||||
isDragging={props.isDragging}
|
||||
setIsCheckbox={props.setIsCheckbox}
|
||||
@@ -372,9 +330,6 @@ function RenderPdf(props) {
|
||||
handleTextSettingModal={
|
||||
props.handleTextSettingModal
|
||||
}
|
||||
handleCellSettingModal={
|
||||
props.handleCellSettingModal
|
||||
}
|
||||
scale={props.scale}
|
||||
pdfOriginalWH={props.pdfOriginalWH}
|
||||
pageNumber={props.pageNumber}
|
||||
@@ -384,7 +339,7 @@ function RenderPdf(props) {
|
||||
setFontColor={props.setFontColor}
|
||||
isResize={props.isResize}
|
||||
setIsResize={props.setIsResize}
|
||||
isFreeResize={true}
|
||||
isFreeResize={false}
|
||||
isOpenSignPad={true}
|
||||
calculateFontsize={calculateFontsize}
|
||||
currWidgetsDetails={
|
||||
@@ -427,30 +382,6 @@ function RenderPdf(props) {
|
||||
}}
|
||||
/>
|
||||
</Document>
|
||||
{guideline.show && (
|
||||
<>
|
||||
{/* top guide */}
|
||||
<div
|
||||
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
|
||||
style={{ top: guideline.y1 }}
|
||||
/>
|
||||
{/* bottom guide */}
|
||||
<div
|
||||
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>
|
||||
</>
|
||||
|
||||
@@ -24,14 +24,14 @@ function SelectLanguage(props) {
|
||||
<div
|
||||
className={`${
|
||||
!props.isProfile && " mt-[9px] pb-2 md:pb-0 "
|
||||
} flex justify-center items-center text-base-content`}
|
||||
} flex justify-center items-center `}
|
||||
>
|
||||
<select
|
||||
value={lang}
|
||||
onChange={handleChangeLang}
|
||||
className={`${
|
||||
!props.isProfile ? " md:w-[15%] w-[50%]" : "w-[180px]"
|
||||
} op-select op-select-bordered op-select-sm `}
|
||||
} op-select op-select-bordered bg-white op-select-sm `}
|
||||
>
|
||||
<option disabled>select</option>
|
||||
{languages.map((item) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ function TextFontSetting(props) {
|
||||
title={t("text-field")}
|
||||
handleClose={() => props.setIsTextSetting(false)}
|
||||
>
|
||||
<div className="h-full p-[20px] text-base-content">
|
||||
<div className="h-full p-[20px]">
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-3">
|
||||
{/* Font Size Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -96,8 +96,9 @@ const WidgetNameModal = (props) => {
|
||||
if (isTextInput) {
|
||||
const readOnlyWithoutValue =
|
||||
isReadOnly && !defaultValue && status !== "optional";
|
||||
|
||||
if (readOnlyWithoutValue) {
|
||||
alert(t("readonly-error", { widgetName: props.defaultdata?.type }));
|
||||
alert(t("readonly-textinput-error"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -123,23 +124,6 @@ const WidgetNameModal = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeValidateInput = (e) => {
|
||||
if (e) {
|
||||
if (e.target.value === "ssn") {
|
||||
setFormdata({
|
||||
...formdata,
|
||||
[e.target.name]: e.target.value,
|
||||
hint: "xxx-xx-xxxx",
|
||||
cellCount: 11
|
||||
});
|
||||
} else {
|
||||
setFormdata({ ...formdata, [e.target.name]: e.target.value });
|
||||
}
|
||||
} else {
|
||||
setFormdata({ ...formdata, textvalidate: "" });
|
||||
}
|
||||
};
|
||||
|
||||
const handledefaultChange = (e) => {
|
||||
if (formdata.textvalidate) {
|
||||
const regexObject = RegexParser(handleValidation(formdata.textvalidate));
|
||||
@@ -162,10 +146,9 @@ const WidgetNameModal = (props) => {
|
||||
case "number":
|
||||
return "/^\\d+$/";
|
||||
case "text":
|
||||
//allow space in text regex
|
||||
return "/^[a-zA-Z ]+$/";
|
||||
return "/^[a-zA-Zs]+$/";
|
||||
case "ssn":
|
||||
return "/^(?!000|666|9\\d{2})\\d{3}-(?!00)\\d{2}-(?!0000)\\d{4}$/";
|
||||
return "^\\d{3}-\\d{2}-\\d{4}$";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
@@ -316,10 +299,7 @@ const WidgetNameModal = (props) => {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
className="ml-1.5 mb-0 capitalize text-[13px]"
|
||||
htmlFor="isreadonly"
|
||||
>
|
||||
<label className="ml-1.5 mb-0 capitalize text-[13px]" htmlFor="isreadonly">
|
||||
{t("read-only")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -17,8 +17,7 @@ import {
|
||||
textInputWidget,
|
||||
cellsWidget,
|
||||
textWidget,
|
||||
years,
|
||||
convertTextToImg
|
||||
years
|
||||
} from "../../constant/Utils";
|
||||
import CellsWidget from "./CellsWidget";
|
||||
import DatePicker from "react-datepicker";
|
||||
@@ -47,9 +46,9 @@ import RegexParser from "regex-parser";
|
||||
|
||||
//function to get default format
|
||||
const getDefaultFormat = (dateFormat) => dateFormat || "MM/dd/yyyy";
|
||||
//function to convert formatted date to new Date() format
|
||||
//function to convert formated date to new Date() format
|
||||
const getDefaultDate = (dateStr, format) => {
|
||||
//get valid date format for moment to convert formatted date to new Date() format
|
||||
//get valid date format for moment to convert formated date to new Date() format
|
||||
const formats = changeDateToMomentFormat(format);
|
||||
const parsedDate = moment(dateStr, formats);
|
||||
let date;
|
||||
@@ -68,11 +67,6 @@ const fontOptions = [
|
||||
{ value: "Delicious Handrawn" }
|
||||
// Add more font options as needed
|
||||
];
|
||||
const allColor = ["blue", "red", "black"];
|
||||
const textInputcls =
|
||||
"op-input op-input-bordered op-input-sm focus:outline-none text-base-content hover:border-base-content w-full text-xs";
|
||||
const isTabCls = "bg-[#002864] text-white rounded-[15px] px-[10px] py-[4px]";
|
||||
|
||||
function WidgetsValueModal(props) {
|
||||
const dispatch = useDispatch();
|
||||
const saveSignCheckbox = useSelector(
|
||||
@@ -93,9 +87,7 @@ function WidgetsValueModal(props) {
|
||||
isSave,
|
||||
setUniqueId,
|
||||
tempSignerId,
|
||||
signatureTypes,
|
||||
setCellCount,
|
||||
allowCellResize = true
|
||||
signatureTypes
|
||||
} = props;
|
||||
const [penColor, setPenColor] = useState("blue");
|
||||
const [isOptional, setIsOptional] = useState(true);
|
||||
@@ -141,15 +133,6 @@ function WidgetsValueModal(props) {
|
||||
return Array.from({ length: count }, (_, i) => val[i] || "");
|
||||
});
|
||||
const cellRefs = useRef([]);
|
||||
// keep track of the first empty cell to automatically focus it after updates
|
||||
useEffect(() => {
|
||||
const index = cellsValue.findIndex((v) => !v);
|
||||
if (index !== -1) {
|
||||
setTimeout(() => {
|
||||
cellRefs.current[index]?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [cellsValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const count = currWidgetsDetails?.options?.cellCount || 5;
|
||||
@@ -158,7 +141,7 @@ function WidgetsValueModal(props) {
|
||||
currWidgetsDetails?.options?.defaultValue ||
|
||||
"";
|
||||
setCellsValue(Array.from({ length: count }, (_, i) => val[i] || ""));
|
||||
}, [currWidgetsDetails?.key, currWidgetsDetails?.options?.cellCount]);
|
||||
}, [currWidgetsDetails?.key]);
|
||||
const [selectedCheckbox, setSelectedCheckbox] = useState(
|
||||
currWidgetsDetails.type === "checkbox" &&
|
||||
(currWidgetsDetails?.options?.response ||
|
||||
@@ -173,7 +156,10 @@ function WidgetsValueModal(props) {
|
||||
)
|
||||
: new Date()
|
||||
);
|
||||
|
||||
const allColor = ["blue", "red", "black"];
|
||||
const textInputcls =
|
||||
"op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs";
|
||||
const isTabCls = "bg-[#002864] text-white rounded-[15px] px-[10px] py-[4px]";
|
||||
useEffect(() => {
|
||||
if (
|
||||
["name", "email", "job title", "company", textInputWidget].includes(
|
||||
@@ -190,8 +176,6 @@ function WidgetsValueModal(props) {
|
||||
} else {
|
||||
setHint(currWidgetsDetails?.type);
|
||||
}
|
||||
} else if (["signature", "initials"].includes(currWidgetsDetails?.type)) {
|
||||
setIsTab(currWidgetsDetails?.signatureType);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currWidgetsDetails]); // Added currWidgetsDetails to dependency array for reset logic
|
||||
@@ -202,7 +186,7 @@ function WidgetsValueModal(props) {
|
||||
setRemoveBgEnabled(false);
|
||||
}, [currWidgetsDetails?.key]);
|
||||
|
||||
//function to save date and format after selected new date in response field and after finish document it should embed the new selected date instead of current date
|
||||
//function to save date and format after seleted new date in response field and after finish document it should be emebed new selected date instead of current date
|
||||
useEffect(() => {
|
||||
if (currWidgetsDetails?.type === "date") {
|
||||
const isDateChange = true;
|
||||
@@ -350,6 +334,7 @@ function WidgetsValueModal(props) {
|
||||
const placeholderIndex = signer.placeHolder.findIndex(
|
||||
(x) => x.pageNumber === pageNumber
|
||||
);
|
||||
|
||||
const updatedPlaceholders = onSaveSign(
|
||||
type,
|
||||
signer.placeHolder,
|
||||
@@ -362,9 +347,7 @@ function WidgetsValueModal(props) {
|
||||
typedSignature,
|
||||
isAutoSign,
|
||||
widgetsType,
|
||||
isApplyAll,
|
||||
fontSelect,
|
||||
penColor
|
||||
isApplyAll
|
||||
);
|
||||
return {
|
||||
...signer,
|
||||
@@ -385,9 +368,7 @@ function WidgetsValueModal(props) {
|
||||
imgWH,
|
||||
isDefaultSign,
|
||||
isTypeText,
|
||||
typedSignature,
|
||||
false,
|
||||
widgetsType
|
||||
typedSignature
|
||||
);
|
||||
setXyPosition(getUpdatePosition);
|
||||
}
|
||||
@@ -413,9 +394,7 @@ function WidgetsValueModal(props) {
|
||||
if (getIndex !== -1) {
|
||||
setIsSignTypes(true);
|
||||
const tab = signatureTypes?.[getIndex].name;
|
||||
if (currWidgetsDetails?.signatureType === "type") {
|
||||
setIsTab("type");
|
||||
} else if (tab === "draw") {
|
||||
if (tab === "draw") {
|
||||
setIsTab("draw");
|
||||
} else if (tab === "upload") {
|
||||
setIsImageSelect(true);
|
||||
@@ -629,7 +608,7 @@ function WidgetsValueModal(props) {
|
||||
if (isTab === "type") {
|
||||
setSignature("");
|
||||
handleSaveSignature(
|
||||
"type",
|
||||
null,
|
||||
false,
|
||||
!currWidgetsDetails?.type === "initials" && textWidth > 150
|
||||
? 150
|
||||
@@ -704,43 +683,91 @@ function WidgetsValueModal(props) {
|
||||
}
|
||||
}
|
||||
if (isTab === "type") {
|
||||
//trim user name or typed name value to show in initial signature
|
||||
const trimmedName = typedSignature
|
||||
? typedSignature?.trim()
|
||||
: currentUserName?.trim();
|
||||
//get full name of user
|
||||
const fullUserName = typedSignature || currentUserName;
|
||||
const firstCharacter = trimmedName?.charAt(0);
|
||||
const userName =
|
||||
currWidgetsDetails?.type === "initials" ? firstCharacter : fullUserName;
|
||||
currWidgetsDetails?.type === "initials"
|
||||
? firstCharacter
|
||||
: typedSignature;
|
||||
const signatureValue = currWidgetsDetails?.typeSignature;
|
||||
setTypedSignature(signatureValue || userName || "");
|
||||
convertToImg(fontSelect, signatureValue || userName || "");
|
||||
convertToImg(fontSelect, userName);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isTab]);
|
||||
// function for convert input text value in image
|
||||
const convertToImg = async (fontStyle, text, color) => {
|
||||
const maxWidth = currWidgetsDetails?.Width;
|
||||
const maxHeight = currWidgetsDetails?.Height;
|
||||
const widgetDims = { Width: maxWidth, Height: maxHeight };
|
||||
const fontFamily = fontStyle || fontSelect || "Fasthand";
|
||||
// 1) Read the max widget dimensions:
|
||||
const maxWidth = currWidgetsDetails.Width; // e.g. 150 px
|
||||
const maxHeight = currWidgetsDetails.Height; // e.g. 40 px
|
||||
|
||||
// 2) Pick a “baseline” font size for measurement:
|
||||
const baselineFontSizePx = 40;
|
||||
const chosenFontFamily = fontStyle || fontSelect || "Fasthand";
|
||||
const fillColor = color || penColor;
|
||||
const dataUrl = convertTextToImg(fontFamily, text, fillColor, widgetDims);
|
||||
|
||||
// 3) Create a temporary <span> (hidden) to measure the text at 40px:
|
||||
const span = document.createElement("span");
|
||||
span.textContent = text;
|
||||
span.style.font = `${baselineFontSizePx}px ${chosenFontFamily}`;
|
||||
span.style.visibility = "hidden"; // keep it in the DOM so offsetWidth/Height works
|
||||
span.style.whiteSpace = "nowrap"; // so we measure a single line
|
||||
document.body.appendChild(span);
|
||||
|
||||
// Measured size at 40px:
|
||||
const measuredWidth = span.offsetWidth;
|
||||
const measuredHeight = span.offsetHeight;
|
||||
document.body.removeChild(span);
|
||||
|
||||
// 4) Compute uniform scale so that 40px‐sized text fits inside (maxWidth × maxHeight):
|
||||
const scaleX = maxWidth / measuredWidth;
|
||||
const scaleY = maxHeight / measuredHeight;
|
||||
const scale = Math.min(scaleX, scaleY, 1); // never scale up beyond 1
|
||||
|
||||
// 5) Final text size in **CSS px**:
|
||||
const finalFontSizePx = baselineFontSizePx * scale;
|
||||
|
||||
// 6) Create a <canvas> that is ALWAYS maxWidth × maxHeight in **CSS px**,
|
||||
// but use devicePixelRatio for sharpness.
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
// ★ Instead of using `finalTextWidth/Height`, force it to be the max box:
|
||||
canvas.width = Math.ceil(maxWidth * pixelRatio);
|
||||
canvas.height = Math.ceil(maxHeight * pixelRatio);
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.scale(pixelRatio, pixelRatio);
|
||||
|
||||
// 7) Draw the text **centered** inside the full maxWidth×maxHeight box:
|
||||
ctx.font = `${finalFontSizePx}px ${chosenFontFamily}`;
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
// ★ Center = (maxWidth/2, maxHeight/2):
|
||||
const centerX = maxWidth / 2;
|
||||
const centerY = maxHeight / 2;
|
||||
|
||||
ctx.fillText(text, centerX, centerY);
|
||||
|
||||
// 8) Export to a PNG data-URL:
|
||||
const dataUrl = canvas.toDataURL("image/png");
|
||||
setSignature(dataUrl);
|
||||
setTextWidth(maxWidth);
|
||||
setTextHeight(maxHeight);
|
||||
};
|
||||
const PenColorComponent = () => {
|
||||
const PenColorComponent = (props) => {
|
||||
return (
|
||||
<div className="flex flex-row items-center m-[5px] gap-3">
|
||||
<span className="text-base-content">{t("options")}</span>
|
||||
<span>Options</span>
|
||||
{allColor.map((data, key) => {
|
||||
return (
|
||||
<i
|
||||
key={key}
|
||||
onClick={() => {
|
||||
convertToImg(fontSelect, typedSignature, data);
|
||||
props?.convertToImg &&
|
||||
props?.convertToImg(fontSelect, typedSignature, data);
|
||||
setPenColor(allColor[key]);
|
||||
}}
|
||||
className={`border-b-[2px] cursor-pointer ${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`}
|
||||
@@ -751,6 +778,17 @@ function WidgetsValueModal(props) {
|
||||
);
|
||||
};
|
||||
|
||||
// `handleCancelBtn` function trigger when user click on cross button
|
||||
const handleCancelBtn = () => {
|
||||
setPenColor("blue");
|
||||
setIsImageSelect(false);
|
||||
setIsDefaultSign(false);
|
||||
setImage(null);
|
||||
setOriginalImage(null); // Add this
|
||||
setRemoveBgEnabled(false); // Add this
|
||||
handleTab();
|
||||
};
|
||||
|
||||
// Effect to store original image and apply initial BG removal if enabled
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -853,7 +891,7 @@ function WidgetsValueModal(props) {
|
||||
checked={isSavedSign}
|
||||
onChange={(e) => setIsSavedSign(e.target.checked)}
|
||||
/>
|
||||
{t("save")} {t(`widgets-name.${currWidgetsDetails?.type}`)}
|
||||
{t("save")} {currWidgetsDetails?.type}
|
||||
</label>
|
||||
);
|
||||
//function for set checked and unchecked value of checkbox
|
||||
@@ -918,7 +956,6 @@ function WidgetsValueModal(props) {
|
||||
setStartDate(date);
|
||||
};
|
||||
const handleOnchangeTextBox = (e) => {
|
||||
// hide any prior validation error while typing
|
||||
setIsShowValidation(false);
|
||||
setWidgetValue(e.target.value);
|
||||
onChangeInput(
|
||||
@@ -930,15 +967,13 @@ function WidgetsValueModal(props) {
|
||||
uniqueId
|
||||
);
|
||||
};
|
||||
const updateCells = (updated) => {
|
||||
const handleCellsInput = (e, idx) => {
|
||||
const val = e.target.value.slice(0, 1);
|
||||
const updated = [...cellsValue];
|
||||
updated[idx] = val;
|
||||
setCellsValue(updated);
|
||||
const combined = updated.join("");
|
||||
setWidgetValue(combined);
|
||||
props.setCurrWidgetsDetails?.((prev) =>
|
||||
prev && prev.key === currWidgetsDetails?.key
|
||||
? { ...prev, options: { ...prev.options, response: combined } }
|
||||
: prev
|
||||
);
|
||||
onChangeInput(
|
||||
combined,
|
||||
currWidgetsDetails?.key,
|
||||
@@ -947,37 +982,49 @@ function WidgetsValueModal(props) {
|
||||
setXyPosition,
|
||||
uniqueId
|
||||
);
|
||||
};
|
||||
|
||||
const handleCellsInput = (e, idx) => {
|
||||
setIsShowValidation(false);
|
||||
const val = e.target.value.slice(0, 1);
|
||||
const updated = [...cellsValue];
|
||||
updated[idx] = val;
|
||||
updateCells(updated);
|
||||
if (val) {
|
||||
if (idx < cellRefs.current.length - 1) {
|
||||
cellRefs.current[idx + 1]?.focus();
|
||||
}
|
||||
} else if (idx > 0) {
|
||||
// move focus back when a cell becomes empty
|
||||
cellRefs.current[idx - 1]?.focus();
|
||||
}
|
||||
};
|
||||
const handleCellsKeyDown = (e, idx) => {
|
||||
if (e.key === "Backspace" && !cellsValue[idx] && idx > 0) {
|
||||
e.preventDefault();
|
||||
const updated = [...cellsValue];
|
||||
updated[idx - 1] = "";
|
||||
const combined = updated.join("");
|
||||
setCellsValue(updated);
|
||||
setWidgetValue(combined);
|
||||
onChangeInput(
|
||||
combined,
|
||||
currWidgetsDetails?.key,
|
||||
xyPosition,
|
||||
props.index,
|
||||
setXyPosition,
|
||||
uniqueId
|
||||
);
|
||||
cellRefs.current[idx - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCellResize = (newCount) => {
|
||||
let updated = [...cellsValue];
|
||||
if (newCount > updated.length) {
|
||||
updated = [...updated, ...Array(newCount - updated.length).fill("")];
|
||||
} else if (newCount < updated.length) {
|
||||
updated = updated.slice(0, newCount);
|
||||
if (e.key === "Delete") {
|
||||
e.preventDefault();
|
||||
const updated = [...cellsValue];
|
||||
updated[idx] = "";
|
||||
const combined = updated.join("");
|
||||
setCellsValue(updated);
|
||||
setWidgetValue(combined);
|
||||
onChangeInput(
|
||||
combined,
|
||||
currWidgetsDetails?.key,
|
||||
xyPosition,
|
||||
props.index,
|
||||
setXyPosition,
|
||||
uniqueId
|
||||
);
|
||||
}
|
||||
cellRefs.current = cellRefs.current.slice(0, newCount);
|
||||
updateCells(updated);
|
||||
setCellCount?.(currWidgetsDetails?.key, newCount);
|
||||
};
|
||||
|
||||
// when focus leaves the cells widget, validate the input
|
||||
const handleCellsBlur = (e, idx) => {
|
||||
handleInputBlur();
|
||||
};
|
||||
//function is used to show widgets on modal according to selected widget type checkbox/date/radio/drodown/textbox/signature/image
|
||||
const getWidgetType = (type) => {
|
||||
@@ -997,30 +1044,12 @@ function WidgetsValueModal(props) {
|
||||
<>
|
||||
{currWidgetsDetails?.type !== "stamp" &&
|
||||
currWidgetsDetails?.type !== "image" && (
|
||||
<div className="text-base-content rounded-[4px] tabWidth">
|
||||
<div className="text-base-content bg-gray-100 border-[1px] border-gray-200 rounded-[4px] tabWidth">
|
||||
<div className="ml-3 flex justify-start gap-4 text-[11px] md:text-base my-[3px]">
|
||||
{currWidgetsDetails?.type !== "initials" &&
|
||||
defaultSignImg &&
|
||||
isTabEnabled("default") ? (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(true);
|
||||
setIsImageSelect(true);
|
||||
setIsTab("mysignature");
|
||||
setImage();
|
||||
}}
|
||||
className={`${
|
||||
isTab === "mysignature" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("my-signature")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
currWidgetsDetails?.type === "initials" &&
|
||||
myInitial &&
|
||||
isTabEnabled("default") && (
|
||||
<>
|
||||
{currWidgetsDetails?.type !== "initials" &&
|
||||
defaultSignImg &&
|
||||
isTabEnabled("default") ? (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
@@ -1031,64 +1060,84 @@ function WidgetsValueModal(props) {
|
||||
}}
|
||||
className={`${
|
||||
isTab === "mysignature" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("my-initials")}
|
||||
{t("my-signature")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{isTabEnabled("draw") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(false);
|
||||
setIsTab("draw");
|
||||
setImage();
|
||||
setSignature("");
|
||||
}}
|
||||
className={`${
|
||||
isTab === "draw" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("draw")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isTabEnabled("upload") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(true);
|
||||
setIsTab("uploadImage");
|
||||
}}
|
||||
className={`${
|
||||
isTab === "uploadImage" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("upload-image")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isTabEnabled("typed") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(false);
|
||||
setIsTab("type");
|
||||
setImage();
|
||||
}}
|
||||
className={`${
|
||||
isTab === "type" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("type")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
currWidgetsDetails?.type === "initials" &&
|
||||
myInitial &&
|
||||
isTabEnabled("default") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(true);
|
||||
setIsImageSelect(true);
|
||||
setIsTab("mysignature");
|
||||
setImage();
|
||||
}}
|
||||
className={`${
|
||||
isTab === "mysignature" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("my-initials")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{isTabEnabled("draw") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(false);
|
||||
setIsTab("draw");
|
||||
setImage();
|
||||
setSignature("");
|
||||
}}
|
||||
className={`${
|
||||
isTab === "draw" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("draw")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isTabEnabled("upload") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(true);
|
||||
setIsTab("uploadImage");
|
||||
}}
|
||||
className={`${
|
||||
isTab === "uploadImage" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("upload-image")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isTabEnabled("typed") && (
|
||||
<div>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDefaultSign(false);
|
||||
setIsImageSelect(false);
|
||||
setIsTab("type");
|
||||
setImage();
|
||||
}}
|
||||
className={`${
|
||||
isTab === "type" && `${isTabCls}`
|
||||
} ml-[2px] cursor-pointer`}
|
||||
>
|
||||
{t("type")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1123,7 +1172,7 @@ function WidgetsValueModal(props) {
|
||||
</div>
|
||||
</div>
|
||||
{/* Standalone autoSignAll for "My Signature/Initials" (isDefaultSign) if conditions met */}
|
||||
{uniqueId && (
|
||||
{setIsAutoSign && uniqueId && (
|
||||
<div className="flex justify-center my-2">
|
||||
{autoSignAll}
|
||||
</div>
|
||||
@@ -1152,7 +1201,7 @@ function WidgetsValueModal(props) {
|
||||
</div>
|
||||
</div>
|
||||
{/* Standalone autoSignAll for "My Signature/Initials" (isDefaultSign) if conditions met */}
|
||||
{uniqueId && (
|
||||
{setIsAutoSign && uniqueId && (
|
||||
<div className="flex justify-center my-2">
|
||||
{autoSignAll}
|
||||
</div>
|
||||
@@ -1177,8 +1226,8 @@ function WidgetsValueModal(props) {
|
||||
ref={imageRef}
|
||||
hidden
|
||||
/>
|
||||
<i className="fa-light fa-cloud-upload-alt uploadImgLogo text-base-content"></i>
|
||||
<div className="text-[10px] text-base-content">{t("upload")}</div>
|
||||
<i className="fa-light fa-cloud-upload-alt uploadImgLogo"></i>
|
||||
<div className="text-[10px]">{t("upload")}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -1197,14 +1246,14 @@ function WidgetsValueModal(props) {
|
||||
</div>
|
||||
</div>
|
||||
{/* Shared container for autoSignAll and removeBackground when image is displayed for upload/stamp/image */}
|
||||
{(uniqueId ||
|
||||
{((setIsAutoSign && uniqueId) ||
|
||||
(image &&
|
||||
(isImageSelect ||
|
||||
["image", "stamp"].includes(
|
||||
currWidgetsDetails?.type
|
||||
)))) && (
|
||||
<div className="flex justify-center items-center gap-x-2 my-2">
|
||||
{uniqueId && autoSignAll}
|
||||
{setIsAutoSign && uniqueId && autoSignAll}
|
||||
{image &&
|
||||
(isImageSelect ||
|
||||
["image", "stamp"].includes(
|
||||
@@ -1292,9 +1341,9 @@ function WidgetsValueModal(props) {
|
||||
<div className="flex flex-row justify-between mt-[10px]">
|
||||
<PenColorComponent />
|
||||
</div>
|
||||
<div className="flex flex-row ml-1 mt-2 gap-x-3 text-base-content">
|
||||
<div className="flex flex-row ml-1 mt-2 gap-x-3">
|
||||
{/* Standalone autoSignAll for "Type" tab if conditions met */}
|
||||
{uniqueId && (
|
||||
{setIsAutoSign && uniqueId && (
|
||||
<div className="flex justify-start my-1">
|
||||
{autoSignAll}
|
||||
</div>
|
||||
@@ -1328,9 +1377,9 @@ function WidgetsValueModal(props) {
|
||||
<div className="flex flex-row justify-between mt-[10px]">
|
||||
<PenColorComponent />
|
||||
</div>
|
||||
<div className="flex flex-row ml-1 mt-1 gap-x-3 text-base-content">
|
||||
<div className="flex flex-row ml-1 mt-1 gap-x-3">
|
||||
{/* Standalone autoSignAll for "Draw" tab if conditions met */}
|
||||
{uniqueId && (
|
||||
{setIsAutoSign && uniqueId && (
|
||||
<div className="flex justify-start my-1">
|
||||
{autoSignAll}
|
||||
</div>
|
||||
@@ -1368,7 +1417,7 @@ function WidgetsValueModal(props) {
|
||||
className={`border-[1px] border-gray-300 rounded-[2px] pt-1 px-2.5 ${checkBoxWrapperClass}`}
|
||||
>
|
||||
{currWidgetsDetails?.options?.values?.map((data, ind) => (
|
||||
<div key={ind} className="text-base-content select-none-cls">
|
||||
<div key={ind} className=" select-none-cls">
|
||||
<label
|
||||
htmlFor={`checkbox-${currWidgetsDetails?.key + ind}`}
|
||||
className="text-xs flex items-center gap-1"
|
||||
@@ -1377,7 +1426,7 @@ function WidgetsValueModal(props) {
|
||||
id={`checkbox-${currWidgetsDetails?.key + ind}`}
|
||||
className={`${
|
||||
ind === 0 ? "mt-0" : "mt-[5px]"
|
||||
} op-checkbox op-checkbox-xs rounded-[1px] mt-1`}
|
||||
} op-checkbox op-checkbox-xs rounded-[1px] mt-1`}
|
||||
type="checkbox"
|
||||
checked={!!selectCheckbox(ind, selectedCheckbox)}
|
||||
onChange={(e) => {
|
||||
@@ -1421,18 +1470,14 @@ function WidgetsValueModal(props) {
|
||||
case cellsWidget:
|
||||
return (
|
||||
<CellsWidget
|
||||
isEnabled={true}
|
||||
count={cellsValue.length}
|
||||
height="100%"
|
||||
value={cellsValue.join("")}
|
||||
editable={true}
|
||||
resizable={allowCellResize}
|
||||
resizable={false}
|
||||
onChange={handleCellsInput}
|
||||
onKeyDown={handleCellsKeyDown}
|
||||
onBlur={handleCellsBlur}
|
||||
onCellCountChange={allowCellResize ? handleCellResize : undefined}
|
||||
inputRefs={cellRefs}
|
||||
hint={hint}
|
||||
/>
|
||||
);
|
||||
case "dropdown":
|
||||
@@ -1495,7 +1540,7 @@ function WidgetsValueModal(props) {
|
||||
case "date":
|
||||
return (
|
||||
<div
|
||||
className={`border-[1px] text-base-content data-[theme=opensigndark]:border-base-content data-[theme=opensigncss]:border-gray-300 rounded-[2px] p-1 px-3`}
|
||||
className={`border-[1px] border-gray-300 rounded-[2px] p-1 px-3`}
|
||||
>
|
||||
<DatePicker
|
||||
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||
@@ -1569,7 +1614,7 @@ function WidgetsValueModal(props) {
|
||||
className={`border-[1px] border-gray-300 rounded-[2px] pt-1 px-2.5 ${radioWrapperClass}`}
|
||||
>
|
||||
{currWidgetsDetails?.options?.values.map((data, ind) => (
|
||||
<div key={ind} className="text-base-content select-none-cls">
|
||||
<div key={ind} className="select-none-cls">
|
||||
<label
|
||||
htmlFor={`radio-${currWidgetsDetails?.key + ind}`}
|
||||
className="cursor-pointer flex items-center text-sm gap-1"
|
||||
@@ -1699,7 +1744,7 @@ function WidgetsValueModal(props) {
|
||||
}
|
||||
}
|
||||
};
|
||||
//function is used when user enter value in any textbox then check validation
|
||||
//funtion is used when user enter value any textbox then check validation
|
||||
const handleInputBlur = () => {
|
||||
const validateType = currWidgetsDetails?.options?.validation?.type;
|
||||
let regexValidation;
|
||||
@@ -1712,10 +1757,6 @@ function WidgetsValueModal(props) {
|
||||
regexValidation = /^[0-9\s]*$/;
|
||||
validateExpression(regexValidation);
|
||||
break;
|
||||
case "ssn":
|
||||
regexValidation = /^(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}$/;
|
||||
validateExpression(regexValidation);
|
||||
break;
|
||||
default:
|
||||
// Grab the current pattern (if it exists)
|
||||
const pattern = currWidgetsDetails?.options?.validation?.pattern;
|
||||
@@ -1727,8 +1768,7 @@ function WidgetsValueModal(props) {
|
||||
break;
|
||||
}
|
||||
};
|
||||
// console.log("currwidgetdetails ", currWidgetsDetails )
|
||||
//function too use on click on next/finish button then update modal UI according to current widgets
|
||||
//funtion too uuse on click on next/finish button then update modal ui according to cuurent widgets
|
||||
const handleClickOnNext = (isFinishDoc) => {
|
||||
if (
|
||||
["signature", "stamp", "image", "initials"].includes(
|
||||
@@ -1827,18 +1867,13 @@ function WidgetsValueModal(props) {
|
||||
}
|
||||
};
|
||||
const handleclose = () => {
|
||||
// If validation is shown, clear the response and close the modal
|
||||
if (isShowValidation) {
|
||||
handleClear();
|
||||
setIsShowValidation(false);
|
||||
}
|
||||
dispatch(setIsShowModal({}));
|
||||
dispatch(setLastIndex(""));
|
||||
if (currWidgetsDetails?.type === textWidget && uniqueId) {
|
||||
setUniqueId(tempSignerId);
|
||||
}
|
||||
};
|
||||
//function is used to execute the finish button functionality
|
||||
//function is used to excecute on click finish button functionality
|
||||
const handleFinish = () => {
|
||||
props?.finishDocument();
|
||||
dispatch(setIsShowModal({}));
|
||||
@@ -1849,7 +1884,7 @@ function WidgetsValueModal(props) {
|
||||
handleFinishButton();
|
||||
}
|
||||
}, [isSave]);
|
||||
//'handleFinishButton' function is used to show finish button click on any widget if all required widgets have response
|
||||
//'handleFinishButton' function is used to show finish button click on any widge if all required widgtes have response
|
||||
const handleFinishButton = () => {
|
||||
const widgetsPosition = xyPosition?.find((data) => data.Id === uniqueId);
|
||||
//using 'flatMap' create all nested array in one level
|
||||
@@ -1896,7 +1931,7 @@ function WidgetsValueModal(props) {
|
||||
<>
|
||||
<ModalUi
|
||||
isOpen={true}
|
||||
handleClose={() => handleclose()}
|
||||
handleClose={() => !isShowValidation && handleclose()}
|
||||
position="bottom"
|
||||
>
|
||||
<div className="h-[100%] p-[18px]">
|
||||
@@ -1928,7 +1963,7 @@ function WidgetsValueModal(props) {
|
||||
<>
|
||||
<div>
|
||||
<div className="relative inline-block">
|
||||
<span className="text-base text-base-content">
|
||||
<span className="text-base">
|
||||
{currWidgetsDetails?.options?.name || widgetTypeTranslation}
|
||||
</span>
|
||||
{!isOptional && (
|
||||
@@ -1954,7 +1989,7 @@ function WidgetsValueModal(props) {
|
||||
) ? (
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost op-btn-sm text-base-content mr-1"
|
||||
className="op-btn op-btn-ghost op-btn-sm mr-1"
|
||||
onClick={() => handleClear()}
|
||||
>
|
||||
{t("clear")}
|
||||
|
||||
@@ -195,7 +195,7 @@ const SelectFolder = ({ required, onSuccess, folderCls, isReset }) => {
|
||||
? selectFolder.Name
|
||||
: t("OpenSign-drive", { appName: drivename })}
|
||||
</p>
|
||||
<div className="text-sm">
|
||||
<div className="text-black text-sm">
|
||||
<i
|
||||
className="fa-light fa-pencil cursor-pointer"
|
||||
title={t("select-folder")}
|
||||
|
||||
@@ -97,7 +97,7 @@ const SelectSigners = (props) => {
|
||||
const contactRes = axiosRes?.data?.result || [];
|
||||
if (contactRes) {
|
||||
const res = JSON.parse(JSON.stringify(contactRes));
|
||||
//compareArrays is a function where compare between two array (total signersList and document signers list)
|
||||
//compareArrays is a function where compare between two array (total signersList and dcument signers list)
|
||||
//and filter signers from total signer's list which already present in document's signers list
|
||||
// const compareArrays = (res, signerObj) => {
|
||||
// return res.filter(
|
||||
|
||||
@@ -122,7 +122,7 @@ const SignersInput = (props) => {
|
||||
);
|
||||
if (contactRes) {
|
||||
const res = JSON.parse(JSON.stringify(contactRes));
|
||||
//compareArrays is a function where compare between two array (total signersList and document signers list)
|
||||
//compareArrays is a function where compare between two array (total signersList and dcument signers list)
|
||||
//and filter signers from total signer's list which already present in document's signers list
|
||||
const compareArrays = (res, signerObj) => {
|
||||
return res.filter(
|
||||
|
||||
@@ -18,16 +18,16 @@ const Menu = ({ item, isOpen, closeSidebar }) => {
|
||||
className={({ isActive }) =>
|
||||
`${
|
||||
isActive ? " bg-base-300 text-base-content" : ""
|
||||
} 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`
|
||||
} 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`
|
||||
}
|
||||
onClick={closeSidebar}
|
||||
tabIndex={isOpen ? 0 : -1}
|
||||
role="menuitem"
|
||||
>
|
||||
<span className="w-[20px] h-[20px] flex justify-center">
|
||||
<i className={`${item.icon} text-[20px]`} aria-hidden="true"></i>
|
||||
<i className={`${item.icon} text-[18px]`} aria-hidden="true"></i>
|
||||
</span>
|
||||
<span className="flex items-center mb-0.5">
|
||||
<span className="ml-3 lg:ml-4">
|
||||
{t(`sidebar.${item.title}`, { appName: drivename })}
|
||||
</span>
|
||||
</NavLink>
|
||||
|
||||
@@ -12,16 +12,16 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
|
||||
<li role="none" className="my-0.5">
|
||||
<button
|
||||
onClick={() => toggleSubmenu(item.title)}
|
||||
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"
|
||||
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 "
|
||||
aria-expanded={submenuOpen}
|
||||
aria-haspopup="true"
|
||||
aria-controls={`submenu-${title}`}
|
||||
>
|
||||
<span className="w-[20px] h-[20px] flex justify-center">
|
||||
<i className={`${icon} text-[20px]`}></i>
|
||||
<i className={`${icon} text-[18px]`}></i>
|
||||
</span>
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<span className="flex items-center mb-0.5">
|
||||
<span className="ml-3 lg:ml-4 text-start">
|
||||
{t(`sidebar.${item.title}`, { appName })}
|
||||
</span>
|
||||
<i
|
||||
@@ -46,20 +46,20 @@ const Submenu = ({ item, closeSidebar, toggleSubmenu, submenuOpen }) => {
|
||||
}
|
||||
className={({ isActive }) =>
|
||||
`${
|
||||
isActive ? "bg-base-300 text-base-content " : ""
|
||||
} 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`
|
||||
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`
|
||||
}
|
||||
onClick={closeSidebar}
|
||||
role="menuitem"
|
||||
tabIndex={submenuOpen ? 0 : -1}
|
||||
>
|
||||
<span className="w-[18px] h-[18px] flex justify-center">
|
||||
<span className="w-[15px] h-[15px] flex justify-center">
|
||||
<i
|
||||
className={`${childItem.icon} text-[18px]`}
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
<span className="mb-0.5">
|
||||
<span className="ml-3 lg:ml-4">
|
||||
{t(`sidebar.${item.title}-Children.${childItem.title}`, {
|
||||
appName: drivename
|
||||
})}
|
||||
|
||||
@@ -67,17 +67,6 @@ export const openInNewTab = (url, target) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserCountry = async () => {
|
||||
try {
|
||||
const res = await fetch("https://ipapi.co/json/");
|
||||
const data = await res.json();
|
||||
return data?.country_code;
|
||||
} catch (err) {
|
||||
console.log("Error fetching country", err);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// `getSecureUrl` is used to return local secure url if local files
|
||||
export const getSecureUrl = async (url) => {
|
||||
const fileUrl = new URL(url)?.pathname?.includes("files");
|
||||
@@ -98,27 +87,6 @@ export const getSecureUrl = async (url) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a trailing path segment from a URL string, if present.
|
||||
*
|
||||
* @param {string} url - The original URL.
|
||||
* @param {string} segment - The segment to strip off (default: "app").
|
||||
* @returns {string} - The URL with the trailing segment removed, or unmodified if it didn’t match.
|
||||
*/
|
||||
export function removeTrailingSegment(url, segment = "app") {
|
||||
// Normalize a trailing slash (e.g. “/app/” → “/app”)
|
||||
const normalized = url.endsWith("/") ? url.slice(0, -1) : url;
|
||||
|
||||
const lastSlash = normalized.lastIndexOf("/");
|
||||
const lastPart = normalized.slice(lastSlash + 1);
|
||||
|
||||
if (lastPart === segment) {
|
||||
return normalized.slice(0, lastSlash);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export const color = [
|
||||
"#93a3db",
|
||||
"#e6c3db",
|
||||
@@ -241,13 +209,6 @@ export const handleImageResize = (
|
||||
signerId,
|
||||
showResize
|
||||
) => {
|
||||
// Compute widget dimensions only once
|
||||
const { offsetWidth, offsetHeight } = ref;
|
||||
const factor = scale * containerScale || 1;
|
||||
const widgetWidth = offsetWidth / factor;
|
||||
const widgetHeight = offsetHeight / factor;
|
||||
const widgetDims = { Width: widgetWidth, Height: widgetHeight };
|
||||
|
||||
const filterSignerPos = signerPos.filter((data) => data.Id === signerId);
|
||||
if (filterSignerPos.length > 0) {
|
||||
const getPlaceHolder = filterSignerPos[0].placeHolder;
|
||||
@@ -259,28 +220,12 @@ export const handleImageResize = (
|
||||
const getPosData = getXYdata;
|
||||
const addSignPos = getPosData.map((url) => {
|
||||
if (url.key === key) {
|
||||
// Base fields for every resized signature
|
||||
const base = {
|
||||
return {
|
||||
...url,
|
||||
Width: widgetWidth,
|
||||
Height: widgetHeight,
|
||||
Width: ref.offsetWidth / (containerScale * scale),
|
||||
Height: ref.offsetHeight / (containerScale * scale),
|
||||
IsResize: showResize ? true : false
|
||||
};
|
||||
// If it's a “type” signature, regenerate the image and options
|
||||
if (url.typeSignature && url.signatureType === "type") {
|
||||
const signImg = convertTextToImg(
|
||||
url.typeFont ?? "Fasthand",
|
||||
url.typeSignature,
|
||||
url.fontColor ?? "blue",
|
||||
widgetDims
|
||||
);
|
||||
return {
|
||||
...base,
|
||||
SignUrl: signImg,
|
||||
options: { ...url.options, response: signImg }
|
||||
};
|
||||
}
|
||||
return base;
|
||||
}
|
||||
return url;
|
||||
});
|
||||
@@ -303,7 +248,6 @@ export const handleImageResize = (
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const widgets = [
|
||||
{ type: "signature", icon: "fa-light fa-pen-nib", iconSize: "20px" },
|
||||
{ type: "stamp", icon: "fa-light fa-stamp", iconSize: "19px" },
|
||||
@@ -360,8 +304,6 @@ export const selectFormat = (data) => {
|
||||
return "MMMM dd, yyyy";
|
||||
case "DD MMMM, YYYY":
|
||||
return "dd MMMM, yyyy";
|
||||
case "DD.MM.YYYY":
|
||||
return "dd.MM.yyyy";
|
||||
default:
|
||||
return "MM/dd/yyyy";
|
||||
}
|
||||
@@ -389,8 +331,6 @@ export const changeDateToMomentFormat = (format) => {
|
||||
return "MMM DD, YYYY";
|
||||
case "dd MMMM, yyyy":
|
||||
return "DD MMMM, YYYY";
|
||||
case "dd.MM.yyyy":
|
||||
return"DD.MM.YYYY";
|
||||
default:
|
||||
return "L";
|
||||
}
|
||||
@@ -461,7 +401,7 @@ export const addWidgetOptions = (type, signer, widgetValue) => {
|
||||
defaultValue: widgetValue ? widgetValue : ""
|
||||
};
|
||||
case "dropdown":
|
||||
return { ...status, name: "Choose one" };
|
||||
return { ...status, name: "Dropdown" };
|
||||
case radioButtonWidget:
|
||||
return {
|
||||
...status,
|
||||
@@ -618,6 +558,7 @@ export async function getBase64FromIMG(url) {
|
||||
reader.readAsDataURL(blob);
|
||||
reader.onloadend = function () {
|
||||
const pdfBase = this.result;
|
||||
|
||||
resolve(pdfBase);
|
||||
};
|
||||
});
|
||||
@@ -659,57 +600,27 @@ export const handleSignYourselfImageResize = (
|
||||
containerScale,
|
||||
scale
|
||||
) => {
|
||||
// Guard against bad index
|
||||
if (!xyPosition[index]) {
|
||||
console.log(`ImageResize invalid index ${index}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute widget dimensions only once
|
||||
const { offsetWidth, offsetHeight } = ref;
|
||||
const factor = scale * containerScale || 1;
|
||||
const widgetWidth = offsetWidth / factor;
|
||||
const widgetHeight = offsetHeight / factor;
|
||||
const widgetDims = { Width: widgetWidth, Height: widgetHeight };
|
||||
|
||||
// Single pass to update only the targeted index/key
|
||||
const updated = xyPosition.map((item, idx) => {
|
||||
if (idx !== index) return item;
|
||||
|
||||
return {
|
||||
...item,
|
||||
pos: item.pos.map((url) => {
|
||||
if (url.key !== key) return url;
|
||||
|
||||
// Base fields for every resized signature
|
||||
const base = {
|
||||
...url,
|
||||
Width: widgetWidth,
|
||||
Height: widgetHeight,
|
||||
IsResize: true
|
||||
};
|
||||
|
||||
// If it's a “type” signature, regenerate the image and options
|
||||
if (url.typeSignature && url.signatureType === "type") {
|
||||
const signImg = convertTextToImg(
|
||||
url.typeFont ?? "Fasthand",
|
||||
url.typeSignature,
|
||||
url.fontColor ?? "blue",
|
||||
widgetDims
|
||||
);
|
||||
|
||||
return {
|
||||
...base,
|
||||
SignUrl: signImg,
|
||||
options: { ...url.options, response: signImg }
|
||||
};
|
||||
}
|
||||
|
||||
return base;
|
||||
})
|
||||
};
|
||||
const getXYdata = xyPosition[index].pos;
|
||||
const getPosData = getXYdata;
|
||||
const addSign = getPosData.map((url) => {
|
||||
if (url.key === key) {
|
||||
return {
|
||||
...url,
|
||||
Width: ref.offsetWidth / (scale * containerScale),
|
||||
Height: ref.offsetHeight / (scale * containerScale),
|
||||
IsResize: true
|
||||
};
|
||||
}
|
||||
return url;
|
||||
});
|
||||
setXyPosition(updated);
|
||||
|
||||
const newUpdateUrl = xyPosition.map((obj, ind) => {
|
||||
if (ind === index) {
|
||||
return { ...obj, pos: addSign };
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
setXyPosition(newUpdateUrl);
|
||||
};
|
||||
|
||||
//function for call cloud function signPdf and generate digital signature
|
||||
@@ -1262,62 +1173,6 @@ export const embedDocId = async (pdfOriginalWH, pdfDoc, documentId) => {
|
||||
}
|
||||
};
|
||||
|
||||
// function for convert input text value in image
|
||||
export function convertTextToImg(fontStyle, text, color, widgetDims) {
|
||||
// read your widget dimensions:
|
||||
const maxWidth = widgetDims.Width;
|
||||
const maxHeight = widgetDims.Height;
|
||||
const baselineFontSizePx = maxHeight;
|
||||
const fontFamily = fontStyle || "Fasthand";
|
||||
const fillColor = color || "blue";
|
||||
|
||||
// 1. Use a temporary canvas to measure actual text size
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
tempCtx.font = `${baselineFontSizePx}px ${fontFamily}`;
|
||||
const metrics = tempCtx.measureText(text);
|
||||
|
||||
const actualHeight =
|
||||
metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
||||
const actualWidth = metrics.width;
|
||||
|
||||
// const renderscale = 3;
|
||||
// 2. Scale font size so text height fills canvas
|
||||
const scaleX = maxWidth / actualWidth;
|
||||
const scaleY = maxHeight / actualHeight;
|
||||
const scale = Math.min(scaleX, scaleY, 1);
|
||||
// final font-size
|
||||
// const finalFontSizePx = baselineFontSizePx * scale * renderscale;
|
||||
const finalFontSizePx = baselineFontSizePx * scale;
|
||||
// 3. Create the final canvas
|
||||
const pxRatio = window.devicePixelRatio * 2 || 3;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = Math.ceil(maxWidth * pxRatio);
|
||||
canvas.height = Math.ceil(maxHeight * pxRatio);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.scale(pxRatio, pxRatio);
|
||||
|
||||
// 4. Setup text with final font
|
||||
ctx.font = `${finalFontSizePx}px ${fontFamily}`;
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "alphabetic";
|
||||
|
||||
const newMetrics = ctx.measureText(text);
|
||||
const ascent = newMetrics.actualBoundingBoxAscent;
|
||||
const descent = newMetrics.actualBoundingBoxDescent;
|
||||
|
||||
// 5. Align text vertically so it touches top and bottom
|
||||
const y = maxHeight / 2 + (ascent - descent) / 2;
|
||||
|
||||
ctx.clearRect(0, 0, maxWidth, maxHeight);
|
||||
ctx.fillText(text, maxWidth / 2, y);
|
||||
|
||||
// 6. Output image
|
||||
const dataUrl = canvas.toDataURL("image/png");
|
||||
return dataUrl;
|
||||
}
|
||||
|
||||
//function for save button to save signature or image url
|
||||
export function onSaveSign(
|
||||
type,
|
||||
@@ -1331,14 +1186,10 @@ export function onSaveSign(
|
||||
typedSignature,
|
||||
isAutoSign,
|
||||
widgetsType,
|
||||
isApplyAll,
|
||||
typeFont,
|
||||
fontColor
|
||||
isApplyAll
|
||||
) {
|
||||
let getIMGWH, posWidth, posHeight;
|
||||
let getXYdata = xyPosition[index].pos;
|
||||
const isSignOrInitials =
|
||||
widgetsType && ["signature", "initials"].includes(widgetsType);
|
||||
const updateXYData = getXYdata.map((position) => {
|
||||
if (position.key === signKey) {
|
||||
if (isDefaultSign === "initials") {
|
||||
@@ -1365,22 +1216,14 @@ export function onSaveSign(
|
||||
}
|
||||
posWidth = getIMGWH ? getIMGWH.newWidth : 150;
|
||||
posHeight = getIMGWH ? getIMGWH.newHeight : 60;
|
||||
const widgetDims = { Width: posWidth, Height: posHeight };
|
||||
const signImg = typedSignature
|
||||
? convertTextToImg(typeFont, typedSignature, fontColor, widgetDims)
|
||||
: signatureImg;
|
||||
return {
|
||||
...position,
|
||||
Width: posWidth,
|
||||
Height: posHeight,
|
||||
SignUrl: signImg,
|
||||
...(isSignOrInitials && { signatureType: type || "" }),
|
||||
options: { ...position.options, response: signImg },
|
||||
...(typedSignature && {
|
||||
typeSignature: typedSignature,
|
||||
typeFont: typeFont ?? "Fasthand",
|
||||
fontColor: fontColor ?? "blue"
|
||||
})
|
||||
SignUrl: signatureImg,
|
||||
...(type && { signatureType: type }),
|
||||
options: { ...position.options, response: signatureImg },
|
||||
...(typedSignature && { typeSignature: typedSignature })
|
||||
};
|
||||
}
|
||||
return position;
|
||||
@@ -1396,29 +1239,20 @@ export function onSaveSign(
|
||||
if (isApplyAll || isAutoSign) {
|
||||
const updatedArray = updateXYposition.map((page) => ({
|
||||
...page,
|
||||
pos: page.pos.map((item) => {
|
||||
if (item.type === widgetsType) {
|
||||
const widgetDims = { Width: item.Width, Height: item.Height };
|
||||
const signImg = typedSignature
|
||||
? convertTextToImg(typeFont, typedSignature, fontColor, widgetDims)
|
||||
: signatureImg;
|
||||
return {
|
||||
...item,
|
||||
// Only copy signature content, keep existing dimensions
|
||||
// Width: posWidth,
|
||||
// Height: posHeight,
|
||||
SignUrl: signImg,
|
||||
...(isSignOrInitials && { signatureType: type || "" }),
|
||||
options: { ...item.options, response: signImg },
|
||||
...(typedSignature && {
|
||||
typeSignature: typedSignature,
|
||||
typeFont: typeFont || "Fasthand",
|
||||
fontColor: fontColor || "blue"
|
||||
})
|
||||
};
|
||||
}
|
||||
return item; // Otherwise, keep it unchanged
|
||||
})
|
||||
pos: page.pos.map(
|
||||
(item) =>
|
||||
item.type === widgetsType
|
||||
? {
|
||||
...item,
|
||||
Width: posWidth,
|
||||
Height: posHeight,
|
||||
SignUrl: signatureImg,
|
||||
...(type && { signatureType: type }),
|
||||
options: { ...item.options, response: signatureImg },
|
||||
...(typedSignature && { typeSignature: typedSignature })
|
||||
}
|
||||
: item // Otherwise, keep it unchanged
|
||||
)
|
||||
}));
|
||||
return updatedArray;
|
||||
} else {
|
||||
@@ -1452,8 +1286,8 @@ export const handleCopySignUrl = (
|
||||
if (x.key === currentPos.key) {
|
||||
return {
|
||||
...x,
|
||||
// Width: existSignPosition.Width,
|
||||
// Height: existSignPosition.Height,
|
||||
Width: existSignPosition.Width,
|
||||
Height: existSignPosition.Height,
|
||||
SignUrl: existSignPosition.SignUrl,
|
||||
signatureType: existSignPosition.signatureType,
|
||||
options: { ...x.options, response: existSignPosition.SignUrl },
|
||||
@@ -1485,6 +1319,7 @@ export const calculateImgAspectRatio = (imgWH, pos) => {
|
||||
const aspectRatio = imgWH.width / imgWH.height;
|
||||
newWidth = aspectRatio * placeholderHeight;
|
||||
newHeight = placeholderHeight;
|
||||
|
||||
return { newHeight, newWidth };
|
||||
};
|
||||
|
||||
@@ -1537,8 +1372,8 @@ export function onSaveImage(
|
||||
item.type === widgetsType && item.type !== "image"
|
||||
? {
|
||||
...item,
|
||||
// Width: getIMGWH.newWidth,
|
||||
// Height: getIMGWH.newHeight,
|
||||
Width: getIMGWH.newWidth,
|
||||
Height: getIMGWH.newHeight,
|
||||
SignUrl: image.src,
|
||||
ImageType: image.imgType,
|
||||
options: {
|
||||
@@ -1685,7 +1520,13 @@ const getWidgetsFontColor = (type) => {
|
||||
}
|
||||
};
|
||||
//function for embed multiple signature using pdf-lib
|
||||
export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
|
||||
export const multiSignEmbed = async (
|
||||
pdfOriginalWH,
|
||||
widgets,
|
||||
pdfDoc,
|
||||
signyourself,
|
||||
scale
|
||||
) => {
|
||||
// `fontBytes` is used to embed custom font in pdf
|
||||
const fontBytes = await fileasbytes(
|
||||
"https://cdn.opensignlabs.com/webfonts/times.ttf"
|
||||
@@ -1694,9 +1535,15 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
|
||||
const font = await pdfDoc.embedFont(fontBytes, { subset: true });
|
||||
let hasError = false;
|
||||
for (let item of widgets) {
|
||||
//pdfOriginalWH contained all pdf's pages width and height
|
||||
//'getSize' is used to get particular pdf's page width and height
|
||||
const getSize = pdfOriginalWH.find(
|
||||
(page) => page?.pageNumber === item?.pageNumber
|
||||
);
|
||||
if (hasError) break; // Stop the outer loop if an error occurred
|
||||
const typeExist = item.pos.some((data) => data?.type);
|
||||
let updateItem;
|
||||
|
||||
if (typeExist) {
|
||||
if (signyourself) {
|
||||
updateItem = item.pos;
|
||||
@@ -1722,21 +1569,6 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
|
||||
const pages = pdfDoc.getPages();
|
||||
const form = pdfDoc.getForm();
|
||||
const page = pages[pageNo - 1];
|
||||
// `page.getCropBox()` returns the visible area of the PDF page.
|
||||
// It provides an object with the following properties:
|
||||
// - x: the x-coordinate of the lower-left corner of the visible area
|
||||
// - y: the y-coordinate of the lower-left corner of the visible area
|
||||
// - width: the width of the visible area (x1 - x0)
|
||||
// - height: the height of the visible area (y1 - y0)
|
||||
// If the page does not explicitly define a CropBox, this method returns
|
||||
// the MediaBox instead, which represents the full physical size of the page.
|
||||
//
|
||||
// Example CropBox: [0, 7.92, 612, 799.92]
|
||||
// => x = 0, y = 7.92, width = 612 - 0 = 612, height = 799.92 - 7.92 = 792
|
||||
// This is equivalent to `react-pdf`'s `page.view` array: [x0, y0, x1, y1]
|
||||
const { _, y, width, height } = page.getCropBox();
|
||||
const originalHeight = height + y;
|
||||
const getSize = { height: originalHeight, width: width };
|
||||
const images = await Promise.all(
|
||||
widgetsPositionArr.map(async (widget) => {
|
||||
// `SignUrl` this is wrong nomenclature and maintain for older code in this options we save base64 of signature image from sign pad
|
||||
@@ -1837,8 +1669,8 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
|
||||
let currentY = yPos(position) + 2;
|
||||
// Size and spacing settings
|
||||
const checkboxSize = fontSize - 1; // checkbox diameter
|
||||
const checkboxTextGapFromLeft = fontSize + 3.4; // gap between box and its label
|
||||
const verticalGap = fontSize + 3.4; // gap between two rows (vertical layout)
|
||||
const checkboxTextGapFromLeft = fontSize + 5; // gap between box and its label
|
||||
const verticalGap = fontSize + 3.2; // gap between two rows (vertical layout)
|
||||
let horizontalGap = 0; // will compute after drawing each label
|
||||
if (position?.options?.values.length > 0) {
|
||||
position.options.values.forEach((item, ind) => {
|
||||
@@ -1902,10 +1734,8 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
|
||||
// Measure the width of this label text at `fontSize`
|
||||
const textWidth = font.widthOfTextAtSize(item, fontSize);
|
||||
// Next checkbox should come after: [box] + gap + [label text] + extra 10pt padding
|
||||
const gap = position.options?.isHideLabel
|
||||
? checkboxTextGapFromLeft - 5
|
||||
: checkboxTextGapFromLeft + textWidth;
|
||||
horizontalGap = checkboxSize + gap;
|
||||
horizontalGap =
|
||||
checkboxSize + checkboxTextGapFromLeft + textWidth;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2050,13 +1880,13 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
|
||||
const radioGroup = form.createRadioGroup(radioRandomId);
|
||||
//getting radio buttons options text font size
|
||||
const optionsFontSize = fontSize; // font size for option text
|
||||
const radioTextGapFromLeft = fontSize + 3; // gap between circle and its label
|
||||
const radioTextGapFromLeft = fontSize + 6; // gap between circle and its label
|
||||
const radioSize = fontSize; // circle diameter (square of width×height)
|
||||
// Initial “cursor” positions (from your existing helpers)
|
||||
let currentX = xPos(position) + 2;
|
||||
let currentY = yPos(position) + 3;
|
||||
let currentY = yPos(position);
|
||||
// Vertical gap between two radio‐rows
|
||||
const verticalGap = fontSize + 3;
|
||||
const verticalGap = fontSize + 6;
|
||||
// We’ll compute horizontalGap on the fly—after drawing each label
|
||||
// Initialize to zero (will be set after first option is placed)
|
||||
let horizontalGap = 0;
|
||||
@@ -2112,10 +1942,7 @@ export const multiSignEmbed = async (widgets, pdfDoc, signyourself, scale) => {
|
||||
const textWidth = font.widthOfTextAtSize(item, optionsFontSize);
|
||||
// radioSize = the circle. radioTextGapFromLeft = gap between circle and label.
|
||||
// Add a small extra padding (e.g. 10pt) before placing next circle.
|
||||
const gap = position?.options?.isHideLabel
|
||||
? radioTextGapFromLeft - 6
|
||||
: radioTextGapFromLeft + textWidth;
|
||||
horizontalGap = radioSize + gap;
|
||||
horizontalGap = radioSize + radioTextGapFromLeft + textWidth;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2866,9 +2693,9 @@ function getWidgetPosition(page, image, sizeRatio, getSize) {
|
||||
let pageWidth;
|
||||
// pageHeight;
|
||||
if ([90, 270].includes(page.getRotation().angle)) {
|
||||
pageWidth = getSize?.height || page.getHeight();
|
||||
pageWidth = page.getHeight();
|
||||
} else {
|
||||
pageWidth = getSize?.width || page.getWidth();
|
||||
pageWidth = page.getWidth();
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
if (!image?.hasOwnProperty("vpWidth")) {
|
||||
@@ -3005,9 +2832,8 @@ export const convertBase64ToFile = async (
|
||||
}
|
||||
};
|
||||
export const onClickZoomIn = (scale, zoomPercent, setScale, setZoomPercent) => {
|
||||
const newPercent = zoomPercent + 10;
|
||||
setZoomPercent(newPercent);
|
||||
setScale(1 + newPercent / 100);
|
||||
setScale(scale + 0.1 * scale);
|
||||
setZoomPercent(zoomPercent + 10);
|
||||
};
|
||||
export const onClickZoomOut = (
|
||||
zoomPercent,
|
||||
@@ -3016,9 +2842,12 @@ export const onClickZoomOut = (
|
||||
setScale
|
||||
) => {
|
||||
if (zoomPercent > 0) {
|
||||
const newPercent = Math.max(0, zoomPercent - 10);
|
||||
setZoomPercent(newPercent);
|
||||
setScale(1 + newPercent / 100);
|
||||
if (zoomPercent === 10) {
|
||||
setScale(1);
|
||||
} else {
|
||||
setScale(scale - 0.1 * scale);
|
||||
}
|
||||
setZoomPercent(zoomPercent - 10);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3203,23 +3032,6 @@ export const deletePdfPage = async (pdfArrayBuffer, pageNumber) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const reorderPdfPages = async (pdfArrayBuffer, orderArr) => {
|
||||
try {
|
||||
const pdfDoc = await PDFDocument.load(pdfArrayBuffer);
|
||||
const newPdf = await PDFDocument.create();
|
||||
const pages = await newPdf.copyPages(
|
||||
pdfDoc,
|
||||
orderArr.map((n) => n - 1)
|
||||
);
|
||||
pages.forEach((p) => newPdf.addPage(p));
|
||||
const pdfBase64 = await newPdf.saveAsBase64({ useObjectStreams: false });
|
||||
const arrayBuffer = base64ToArrayBuffer(pdfBase64);
|
||||
return { arrayBuffer, base64: pdfBase64, totalPages: orderArr.length };
|
||||
} catch (err) {
|
||||
console.log("Err while reordering pages", err);
|
||||
}
|
||||
};
|
||||
|
||||
// `generatePdfName` is used to generate file name
|
||||
export function generatePdfName(length) {
|
||||
const characters =
|
||||
@@ -3464,13 +3276,8 @@ export const getOriginalWH = async (pdf) => {
|
||||
for (let index = 0; index < totalPages; index++) {
|
||||
try {
|
||||
const getPage = await pdf.getPage(index + 1);
|
||||
const scale = 1;
|
||||
//getting extra height which is removed from viewPort height (letter type pdf have originial h-799.92 but using getPage.getViewport getting height-792 something diffrence-7.92px)
|
||||
//getPage?.view[1] store diffrence of height (Page starts 7.92 from bottom)
|
||||
const y0 = getPage?.view[1] || 0;
|
||||
let { width, height } = getPage.getViewport({ scale });
|
||||
//adding it on viewPort height then get original height ex- 792+7.92 = 799.92 orignial height
|
||||
height = height + y0;
|
||||
const width = getPage?.view[2];
|
||||
const height = getPage?.view[3];
|
||||
pdfWHObj.push({ pageNumber: index + 1, width, height });
|
||||
} catch (e) {
|
||||
console.log(`Error getting page ${index + 1} of PDF: ${e.message}`);
|
||||
@@ -3478,175 +3285,3 @@ export const getOriginalWH = async (pdf) => {
|
||||
}
|
||||
return pdfWHObj;
|
||||
};
|
||||
|
||||
//function is used to check required and optional widgets and ensure required widget should be response
|
||||
export const handleCheckResponse = (checkUser, setminRequiredCount) => {
|
||||
let checkboxExist,
|
||||
showAlert = false,
|
||||
widgetKey,
|
||||
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";
|
||||
//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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//else condition to check all type widget data fill or not except checkbox
|
||||
else {
|
||||
//get all required type widgets except checkbox and radio
|
||||
const requiredWidgets = checkUser[0].placeHolder[i].pos.filter(
|
||||
(position) =>
|
||||
position.type === "signature" ||
|
||||
(position.options?.status === "required" &&
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
return {
|
||||
tourPageNumber,
|
||||
widgetKey,
|
||||
showAlert
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* decryptPdf
|
||||
* @param {File} file - The password-protected PDF file to decrypt.
|
||||
* @param {string | undefined} password - The password used to unlock the PDF.
|
||||
* @returns {Promise<File>} - A Promise that resolves to a decrypted PDF as a File object.
|
||||
*
|
||||
*/
|
||||
export const decryptPdf = async (file, password) => {
|
||||
const name = generatePdfName(16);
|
||||
const baseApi = localStorage.getItem("baseUrl") || "";
|
||||
const url = removeTrailingSegment(baseApi) + "/decryptpdf?ts=" + Date.now();
|
||||
let formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("password", password);
|
||||
const config = {
|
||||
headers: { "content-type": "multipart/form-data" },
|
||||
responseType: "blob"
|
||||
};
|
||||
const response = await axios.post(url, formData, config);
|
||||
const pdfBlob = new Blob([response.data], { type: "application/pdf" });
|
||||
return new File([pdfBlob], name, { type: "application/pdf" });
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a Base64 string to a File object.
|
||||
*
|
||||
* @param {string} base64String - The Base64 string, with or without the data URI prefix.
|
||||
* e.g. "data:image/png;base64,iVBORw0KGgoAAAANS…" or "iVBORw0KGgoAAAANS…"
|
||||
* @param {string} filename - Desired filename for the File object, e.g. "photo.png"
|
||||
* @returns {File} - The resulting File object
|
||||
*/
|
||||
export function base64ToFile(base64String, filename) {
|
||||
// Separate out the mime-type and the actual Base64 payload
|
||||
const [header, payload] = base64String.includes(",")
|
||||
? base64String.split(",")
|
||||
: [null, base64String];
|
||||
// Determine the MIME type (fallback to application/octet-stream)
|
||||
const mimeMatch = header?.match(/data:(.*?);base64/);
|
||||
const mime = mimeMatch ? mimeMatch[1] : "application/octet-stream";
|
||||
|
||||
// Decode Base64 to raw binary data held in a string
|
||||
const binaryString = atob(payload);
|
||||
// Create an ArrayBuffer and a view (as unsigned 8-bit)
|
||||
const len = binaryString.length;
|
||||
const u8arr = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
u8arr[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
// Create a File object (Blob subclass) with the binary data
|
||||
return new File([u8arr], filename, { type: mime });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the given File object and returns its contents as an ArrayBuffer.
|
||||
*
|
||||
* @param {File} file
|
||||
* The File instance to be read.
|
||||
* @returns {Promise<ArrayBuffer>}
|
||||
* A promise that resolves with the file’s binary data as an ArrayBuffer,
|
||||
* or rejects with an error if the read fails.
|
||||
*/
|
||||
export 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,11 +3,6 @@ export const templateCls = "contracts_Template";
|
||||
export const documentCls = "contracts_Document";
|
||||
export const themeColor = "#47a3ad";
|
||||
export const iconColor = "#686968";
|
||||
// Dynamic icon color function for better dark mode visibility
|
||||
export const getThemeIconColor = () => {
|
||||
const theme = document.documentElement.getAttribute("data-theme");
|
||||
return theme === "opensigndark" ? "#CCCCCC" : "#686968";
|
||||
};
|
||||
export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
export const maxFileSize = 10; // 10MB
|
||||
export const maxTitleLength = 250; // 250 characters
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
type Size = { width: number; height: number; };
|
||||
|
||||
/**
|
||||
* useSize
|
||||
* @param ref React ref object pointing to a DOM element
|
||||
* @returns current size of that element: { width, height }
|
||||
*/
|
||||
export function useElSize(ref: React.RefObject<HTMLElement>): Size {
|
||||
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
// Initialize size
|
||||
setSize({
|
||||
width: el.offsetWidth,
|
||||
height: el.offsetHeight,
|
||||
});
|
||||
|
||||
// Watch for resize
|
||||
const observer = new ResizeObserver(entries => {
|
||||
for (let entry of entries) {
|
||||
const { width, height } = entry.contentRect;
|
||||
setSize({ width, height });
|
||||
}
|
||||
});
|
||||
observer.observe(el);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return size;
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
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,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
/**`useScript` hook is generated script for google sign in button */
|
||||
/**`useScript` hook is generated scripte for google sign in button */
|
||||
export const useScript = (url, onload) => {
|
||||
useEffect(() => {
|
||||
const script = document.createElement("script");
|
||||
@@ -15,3 +15,4 @@ export const useScript = (url, onload) => {
|
||||
};
|
||||
}, [url, onload]);
|
||||
};
|
||||
|
||||
|
||||
@@ -98,164 +98,4 @@ body {
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: gray;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Note: Dark mode styling is now handled via Tailwind utilities in tailwind.config.js */
|
||||
/* You can use classes like: icon-improved, icon-muted, icon-disabled, op-btn-vscode-disabled */
|
||||
|
||||
/* React-tour and ReactTooltip dark mode styling */
|
||||
[data-theme="opensigndark"] {
|
||||
/* React-tour modal styling */
|
||||
.reactour__helper {
|
||||
background-color: #1F2937 !important;
|
||||
color: #E5E7EB !important;
|
||||
border: 1px solid #374151 !important;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
.reactour__close {
|
||||
color: #E5E7EB !important;
|
||||
background-color: #374151 !important;
|
||||
border: 1px solid #4B5563 !important;
|
||||
}
|
||||
|
||||
.reactour__close:hover {
|
||||
background-color: #4B5563 !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
/* React-tour navigation buttons */
|
||||
.reactour__controls {
|
||||
background-color: #1F2937 !important;
|
||||
border-top: 1px solid #374151 !important;
|
||||
}
|
||||
|
||||
.reactour__controls button {
|
||||
background-color: #374151 !important;
|
||||
color: #E5E7EB !important;
|
||||
border: 1px solid #4B5563 !important;
|
||||
}
|
||||
|
||||
.reactour__controls button:hover {
|
||||
background-color: #4B5563 !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.reactour__controls button:disabled {
|
||||
background-color: #3C3C3C !important;
|
||||
color: #858585 !important;
|
||||
border-color: #565656 !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
/* ReactTooltip styling */
|
||||
.react-tooltip {
|
||||
background-color: #1F2937 !important;
|
||||
color: #E5E7EB !important;
|
||||
border: 1px solid #374151 !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
.react-tooltip.type-dark {
|
||||
background-color: #1F2937 !important;
|
||||
color: #E5E7EB !important;
|
||||
}
|
||||
|
||||
.react-tooltip.place-top:after,
|
||||
.react-tooltip.place-bottom:after,
|
||||
.react-tooltip.place-left:after,
|
||||
.react-tooltip.place-right:after {
|
||||
border-color: #1F2937 !important;
|
||||
}
|
||||
|
||||
/* Tour content improvements */
|
||||
.reactour__badge {
|
||||
background-color: #007ACC !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.reactour__helper h1,
|
||||
.reactour__helper h2,
|
||||
.reactour__helper h3,
|
||||
.reactour__helper h4,
|
||||
.reactour__helper h5,
|
||||
.reactour__helper h6 {
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.reactour__helper p {
|
||||
color: #E5E7EB !important;
|
||||
}
|
||||
|
||||
/* VS Code-style tour buttons */
|
||||
.reactour__controls .op-btn {
|
||||
background-color: #007ACC !important;
|
||||
color: #FFFFFF !important;
|
||||
border: 1px solid #007ACC !important;
|
||||
}
|
||||
|
||||
.reactour__controls .op-btn:hover {
|
||||
background-color: #0086D1 !important;
|
||||
border-color: #0086D1 !important;
|
||||
}
|
||||
|
||||
.reactour__controls .op-btn-secondary {
|
||||
background-color: #374151 !important;
|
||||
color: #E5E7EB !important;
|
||||
border: 1px solid #4B5563 !important;
|
||||
}
|
||||
|
||||
.reactour__controls .op-btn-secondary:hover {
|
||||
background-color: #4B5563 !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
/* React-datepicker dark mode styling */
|
||||
.react-datepicker {
|
||||
background-color: #1F2937 !important;
|
||||
border: 1px solid #374151 !important;
|
||||
color: #E5E7EB !important;
|
||||
}
|
||||
|
||||
.react-datepicker__header {
|
||||
background-color: #374151 !important;
|
||||
border-bottom: 1px solid #4B5563 !important;
|
||||
color: #E5E7EB !important;
|
||||
}
|
||||
|
||||
.react-datepicker__current-month,
|
||||
.react-datepicker__day-name {
|
||||
color: #E5E7EB !important;
|
||||
}
|
||||
|
||||
.react-datepicker__day {
|
||||
color: #E5E7EB !important;
|
||||
}
|
||||
|
||||
.react-datepicker__day:hover {
|
||||
background-color: #4B5563 !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.react-datepicker__day--selected {
|
||||
background-color: #007ACC !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.react-datepicker__day--keyboard-selected {
|
||||
background-color: #374151 !important;
|
||||
color: #E5E7EB !important;
|
||||
}
|
||||
|
||||
.react-datepicker__day--outside-month {
|
||||
color: #6B7280 !important;
|
||||
}
|
||||
|
||||
.react-datepicker__navigation {
|
||||
color: #E5E7EB !important;
|
||||
}
|
||||
|
||||
.react-datepicker__navigation:hover {
|
||||
background-color: #4B5563 !important;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./index.css";
|
||||
import "./styles/dark-theme-improvements.css";
|
||||
import App from "./App";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "./redux/store";
|
||||
@@ -25,11 +24,6 @@ const serverUrl = serverUrl_fn();
|
||||
Parse.initialize(appId);
|
||||
Parse.serverURL = serverUrl;
|
||||
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
if (savedTheme === "dark") {
|
||||
document.documentElement.setAttribute("data-theme", "opensigndark");
|
||||
}
|
||||
|
||||
const HTML5toTouch = {
|
||||
backends: [
|
||||
{
|
||||
|
||||
+230
-200
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { formJson } from "../json/FormJson";
|
||||
import Parse from "parse";
|
||||
@@ -12,12 +12,9 @@ import {
|
||||
flattenPdf,
|
||||
generatePdfName,
|
||||
generateTitleFromFilename,
|
||||
getFileName,
|
||||
getSecureUrl,
|
||||
toDataUrl,
|
||||
decryptPdf,
|
||||
base64ToFile,
|
||||
getFileAsArrayBuffer,
|
||||
removeTrailingSegment
|
||||
toDataUrl
|
||||
} from "../constant/Utils";
|
||||
import { PDFDocument } from "pdf-lib";
|
||||
import axios from "axios";
|
||||
@@ -71,7 +68,6 @@ const Forms = (props) => {
|
||||
AllowModifications: false
|
||||
});
|
||||
const [fileupload, setFileUpload] = useState("");
|
||||
const [selectedFiles, setSelectedFiles] = useState([]);
|
||||
const [fileload, setfileload] = useState(false);
|
||||
const [percentage, setpercentage] = useState(0);
|
||||
const [isReset, setIsReset] = useState(false);
|
||||
@@ -118,6 +114,15 @@ const Forms = (props) => {
|
||||
}));
|
||||
};
|
||||
|
||||
function getFileAsArrayBuffer(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => resolve(e.target.result);
|
||||
reader.onerror = (e) => reject(e.target.error);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
// `removeFile` is used to reset progress, percentage and remove file if exists
|
||||
const removeFile = (e) => {
|
||||
setfileload(false);
|
||||
@@ -129,212 +134,219 @@ const Forms = (props) => {
|
||||
const handleFileInput = async (e) => {
|
||||
setpercentage(0);
|
||||
try {
|
||||
const files = Array.from(e.target.files);
|
||||
const filesNameArr = files.map((f) => f.name);
|
||||
setSelectedFiles(filesNameArr);
|
||||
if (!files.length) {
|
||||
alert(t("file-alert-2"));
|
||||
return;
|
||||
}
|
||||
// setFormData((prev) => ({ ...prev, file: files[0] }));
|
||||
const totalMb = Math.round(
|
||||
files.reduce((sum, f) => sum + f.size, 0) / Math.pow(1024, 2)
|
||||
);
|
||||
if (totalMb > maxFileSize) {
|
||||
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
|
||||
setFileUpload("");
|
||||
setSelectedFiles([]);
|
||||
removeFile(e);
|
||||
return;
|
||||
}
|
||||
|
||||
const pdfBuffers = [];
|
||||
for (const file of files) {
|
||||
setFormData((prev) => ({ ...prev, file: file }));
|
||||
if (file.type === "application/pdf") {
|
||||
try {
|
||||
const buffer = await getFileAsArrayBuffer(file);
|
||||
const flat = await flattenPdf(buffer);
|
||||
pdfBuffers.push(flat);
|
||||
} catch (err) {
|
||||
if (err?.message?.includes("is encrypted")) {
|
||||
let files = e.target.files;
|
||||
setFormData((prev) => ({ ...prev, file: e.target.files[0] }));
|
||||
if (typeof files[0] !== "undefined") {
|
||||
const mb = Math.round(files[0].size / Math.pow(1024, 2));
|
||||
if (mb > maxFileSize) {
|
||||
alert(`${t("file-alert-1")} ${maxFileSize} MB`);
|
||||
setFileUpload("");
|
||||
removeFile(e);
|
||||
return;
|
||||
} else {
|
||||
if (files?.[0]?.type === "application/pdf") {
|
||||
const size = files?.[0]?.size;
|
||||
const name = generatePdfName(16);
|
||||
const pdfName = `${name?.split(".")[0]}.pdf`;
|
||||
setfileload(true);
|
||||
try {
|
||||
setIsDecrypting(true);
|
||||
const pdfFile = await decryptPdf(file, "");
|
||||
setIsDecrypting(false);
|
||||
setfileload(true);
|
||||
const res = await getFileAsArrayBuffer(pdfFile);
|
||||
const res = await getFileAsArrayBuffer(files[0]);
|
||||
const flatPdf = await flattenPdf(res);
|
||||
// Upload the file to Parse Server
|
||||
pdfBuffers.push(flatPdf);
|
||||
} catch (err) {
|
||||
removeFile(e);
|
||||
if (err?.response?.status === 401) {
|
||||
// setIsPassword(true);
|
||||
const password = prompt(
|
||||
`PDF "${file.name}" is password-protected. Enter password:`
|
||||
);
|
||||
const parseFile = new Parse.File(
|
||||
pdfName,
|
||||
[...flatPdf],
|
||||
"application/pdf"
|
||||
);
|
||||
|
||||
if (password) {
|
||||
try {
|
||||
const pdfFile = await decryptPdf(file, password);
|
||||
setIsDecrypting(false);
|
||||
setfileload(true);
|
||||
try {
|
||||
const response = await parseFile.save({
|
||||
progress: (progressValue, loaded, total, { type }) => {
|
||||
if (type === "upload" && progressValue !== null) {
|
||||
const percentCompleted = Math.round(
|
||||
(loaded * 100) / total
|
||||
);
|
||||
setpercentage(percentCompleted);
|
||||
}
|
||||
}
|
||||
});
|
||||
// The response object will contain information about the uploaded file
|
||||
// You can access the URL of the uploaded file using response.url()
|
||||
if (response.url()) {
|
||||
const fileRes = await getSecureUrl(response.url());
|
||||
if (fileRes.url) {
|
||||
setFileUpload(fileRes.url);
|
||||
setfileload(false);
|
||||
const tenantId = localStorage.getItem("TenantId");
|
||||
const title = generateTitleFromFilename(files?.[0]?.name);
|
||||
setFormData((obj) => ({ ...obj, Name: title }));
|
||||
SaveFileSize(size, fileRes.url, tenantId);
|
||||
return fileRes.url;
|
||||
} else {
|
||||
removeFile(e);
|
||||
}
|
||||
} else {
|
||||
removeFile(e);
|
||||
}
|
||||
} catch (error) {
|
||||
removeFile(e);
|
||||
console.error("Error uploading file:", error);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.message?.includes("is encrypted")) {
|
||||
try {
|
||||
setIsDecrypting(true);
|
||||
const size = files?.[0].size;
|
||||
const name = generatePdfName(16);
|
||||
const url = "https://ai.nxglabs.in/decryptpdf"; //
|
||||
let formData = new FormData();
|
||||
formData.append("file", files[0]);
|
||||
formData.append("password", "");
|
||||
const config = {
|
||||
headers: { "content-type": "multipart/form-data" },
|
||||
responseType: "blob"
|
||||
};
|
||||
const response = await axios.post(url, formData, config);
|
||||
const pdfBlob = new Blob([response.data], {
|
||||
type: "application/pdf"
|
||||
});
|
||||
const pdfFile = new File([pdfBlob], name, {
|
||||
type: "application/pdf"
|
||||
});
|
||||
setIsDecrypting(false);
|
||||
setfileload(true);
|
||||
const res = await getFileAsArrayBuffer(pdfFile);
|
||||
const flatPdf = await flattenPdf(res);
|
||||
// Upload the file to Parse Server
|
||||
pdfBuffers.push(flatPdf);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"Incorrect password or decryption failed",
|
||||
err
|
||||
);
|
||||
setSelectedFiles(
|
||||
filesNameArr.filter((f) => f !== file.name)
|
||||
const parseFile = new Parse.File(
|
||||
name,
|
||||
[...flatPdf],
|
||||
"application/pdf"
|
||||
);
|
||||
|
||||
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);
|
||||
setfileload(false);
|
||||
removeFile(e);
|
||||
alert(`Incorrect password for file: ${file.name}`);
|
||||
e.target.value = "";
|
||||
}
|
||||
} else {
|
||||
console.error("password not provided");
|
||||
setSelectedFiles(
|
||||
filesNameArr.filter((f) => f !== file.name)
|
||||
);
|
||||
setIsDecrypting(false);
|
||||
setfileload(false);
|
||||
removeFile(e);
|
||||
}
|
||||
} else {
|
||||
console.log("Error uploading file: ", err?.response);
|
||||
setIsDecrypting(false);
|
||||
e.target.value = "";
|
||||
console.log("err ", err);
|
||||
setFileUpload("");
|
||||
removeFile(e);
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
};
|
||||
const res = await axios.post(url, fd, config);
|
||||
if (res.data?.url) {
|
||||
const pdfRes = await axios.get(res.data.url, {
|
||||
responseType: "arraybuffer"
|
||||
|
||||
// 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
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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"
|
||||
);
|
||||
|
||||
if (!pdfBuffers.length) {
|
||||
alert(t("file-alert-2"));
|
||||
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);
|
||||
try {
|
||||
const response = await parseFile.save({
|
||||
progress: (progressValue, loaded, total, { type }) => {
|
||||
if (type === "upload" && progressValue !== null) {
|
||||
const percentCompleted = Math.round(
|
||||
(loaded * 100) / total
|
||||
);
|
||||
setpercentage(percentCompleted);
|
||||
}
|
||||
}
|
||||
});
|
||||
// The response object will contain information about the uploaded file
|
||||
// You can access the URL of the uploaded file using response.url()
|
||||
if (response.url()) {
|
||||
const fileRes = await getSecureUrl(response.url());
|
||||
if (fileRes.url) {
|
||||
setFileUpload(fileRes.url);
|
||||
setfileload(false);
|
||||
const tenantId = localStorage.getItem("TenantId");
|
||||
const title = generateTitleFromFilename(files?.[0]?.name);
|
||||
setFormData((obj) => ({ ...obj, Name: title }));
|
||||
SaveFileSize(size, fileRes.url, tenantId);
|
||||
return fileRes.url;
|
||||
} else {
|
||||
removeFile(e);
|
||||
}
|
||||
} else {
|
||||
removeFile(e);
|
||||
}
|
||||
} catch (error) {
|
||||
removeFile(e);
|
||||
console.error("Error uploading file:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
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([]);
|
||||
alert(t("file-alert-2"));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
setSelectedFiles([]);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
// `isValidURL` is used to check valid webhook url
|
||||
@@ -434,6 +446,9 @@ const Forms = (props) => {
|
||||
className: "contracts_Users",
|
||||
objectId: ExtCls[0].objectId
|
||||
});
|
||||
if (extUserData?.TenantId?.ActiveFileAdapter) {
|
||||
object.set("FileAdapterId", extUserData?.TenantId?.ActiveFileAdapter);
|
||||
}
|
||||
const res = await object.save();
|
||||
if (res) {
|
||||
setSigners([]);
|
||||
@@ -463,7 +478,6 @@ const Forms = (props) => {
|
||||
AllowModifications: false
|
||||
});
|
||||
setFileUpload("");
|
||||
setSelectedFiles([]);
|
||||
setpercentage(0);
|
||||
navigate(`/${props?.redirectRoute}/${res.id}`);
|
||||
}
|
||||
@@ -545,7 +559,6 @@ const Forms = (props) => {
|
||||
setFormData(obj);
|
||||
removeFile();
|
||||
setFileUpload("");
|
||||
setSelectedFiles([]);
|
||||
setTimeout(() => setIsReset(false), 50);
|
||||
};
|
||||
const handleCancel = () => {
|
||||
@@ -558,7 +571,24 @@ const Forms = (props) => {
|
||||
try {
|
||||
const size = formData?.file?.size;
|
||||
const name = generatePdfName(16);
|
||||
const pdfFile = await decryptPdf(formData?.file, formData?.password);
|
||||
const url = "https://ai.nxglabs.in/decryptpdf"; //
|
||||
let Data = new FormData();
|
||||
Data.append("file", formData?.file);
|
||||
Data.append("password", formData.password);
|
||||
const config = {
|
||||
headers: {
|
||||
"content-type": "multipart/form-data"
|
||||
// sessiontoken: Parse.User.current().getSessionToken()
|
||||
},
|
||||
responseType: "blob"
|
||||
};
|
||||
const response = await axios.post(url, Data, config);
|
||||
const pdfBlob = new Blob([response.data], {
|
||||
type: "application/pdf"
|
||||
});
|
||||
const pdfFile = new File([pdfBlob], name, {
|
||||
type: "application/pdf"
|
||||
});
|
||||
setIsDecrypting(false);
|
||||
const res = await getFileAsArrayBuffer(pdfFile);
|
||||
const flatPdf = await flattenPdf(res);
|
||||
@@ -719,20 +749,19 @@ const Forms = (props) => {
|
||||
)}
|
||||
<div className="text-xs">
|
||||
<label className="block">
|
||||
{`${`${t("report-heading.File")} (${t("file-type")}`}${", docx)"}`}
|
||||
{`${`${t("report-heading.File")} (${t("file-type")}`}${
|
||||
")"
|
||||
}`}
|
||||
<span className="text-red-500 text-[13px]">*</span>
|
||||
</label>
|
||||
{fileupload.length > 0 ? (
|
||||
<div className="flex gap-1 justify-center items-center">
|
||||
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm w-full h-full text-[13px]">
|
||||
<div className="break-all cursor-default">
|
||||
{t("file-selected")}: {selectedFiles.join(", ")}
|
||||
{t("file-selected")}: {getFileName(fileupload)}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setFileUpload("");
|
||||
setSelectedFiles([]);
|
||||
}}
|
||||
onClick={() => setFileUpload("")}
|
||||
className="cursor-pointer px-[10px] text-[20px] font-bold text-red-500"
|
||||
>
|
||||
<i className="fa-light fa-xmark"></i>
|
||||
@@ -743,11 +772,12 @@ const Forms = (props) => {
|
||||
<div className="flex gap-1 justify-center items-center">
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
className="op-file-input op-file-input-bordered op-file-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
onChange={(e) => handleFileInput(e)}
|
||||
ref={inputFileRef}
|
||||
accept="application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/png,image/jpeg"
|
||||
accept={
|
||||
"application/pdf,image/png,image/jpeg"
|
||||
}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
|
||||
@@ -324,65 +324,63 @@ const ManageSign = () => {
|
||||
hidden
|
||||
/>
|
||||
<div className="relative">
|
||||
{isLoader && (
|
||||
<div className="absolute bg-black bg-opacity-30 z-50 w-full h-full flex justify-center items-center">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
{image ? (
|
||||
<div className="signatureCanvas relative border-[2px] border-[#888] rounded-box overflow-hidden">
|
||||
<img
|
||||
alt="signature"
|
||||
src={image}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<SignatureCanvas
|
||||
ref={canvasRef}
|
||||
penColor={penColor}
|
||||
canvasProps={{
|
||||
width: "456px",
|
||||
height: "180px",
|
||||
className:
|
||||
"signatureCanvas border-[2px] border-[#888] rounded-box"
|
||||
}}
|
||||
onEnd={() =>
|
||||
handleSignatureChange(canvasRef.current.toDataURL())
|
||||
}
|
||||
dotSize={1}
|
||||
/>
|
||||
)}
|
||||
<div className="penContainerDefault flex flex-row justify-between">
|
||||
<div>
|
||||
{!image && (
|
||||
<div className="flex flex-row gap-1.5 m-[5px]">
|
||||
{allColor.map((data, key) => {
|
||||
return (
|
||||
<i
|
||||
key={key}
|
||||
onClick={() => setPenColor(allColor[key])}
|
||||
className={`border-b-[2px] ${key === 0 && penColor === "blue" ? "border-blue-600" : key === 1 && penColor === "red" ? "border-red-500" : key === 2 && penColor === "black" ? "border-black" : "border-white"} text-[${data}] text-[16px] fa-light fa-pen-nib`}
|
||||
></i>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 text-sm md:text-base mr-1">
|
||||
<div
|
||||
type="button"
|
||||
className="op-link"
|
||||
onClick={() => handleUploadBtn()}
|
||||
>
|
||||
{t("upload")}
|
||||
<div>
|
||||
{image ? (
|
||||
<div className="signatureCanvas relative border-[2px] border-[#888] rounded-box overflow-hidden">
|
||||
<img
|
||||
alt="signature"
|
||||
src={image}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
type="button"
|
||||
className="op-link"
|
||||
onClick={() => handleClear()}
|
||||
>
|
||||
{t("clear")}
|
||||
) : (
|
||||
<SignatureCanvas
|
||||
ref={canvasRef}
|
||||
penColor={penColor}
|
||||
canvasProps={{
|
||||
width: "456px",
|
||||
height: "180px",
|
||||
className:
|
||||
"signatureCanvas border-[2px] border-[#888] rounded-box"
|
||||
}}
|
||||
// backgroundColor="rgb(255, 255, 255)"
|
||||
onEnd={() =>
|
||||
handleSignatureChange(canvasRef.current.toDataURL())
|
||||
}
|
||||
dotSize={1}
|
||||
/>
|
||||
)}
|
||||
<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
|
||||
type="button"
|
||||
className="op-link"
|
||||
onClick={() => handleClear()}
|
||||
>
|
||||
{t("clear")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -422,6 +420,7 @@ const ManageSign = () => {
|
||||
canvasProps={{
|
||||
className: "intialSignature rounded-box"
|
||||
}}
|
||||
// backgroundColor="rgb(255, 255, 255)"
|
||||
onEnd={() => handleInitialsChange()}
|
||||
dotSize={1}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import "../styles/opensigndrive.css";
|
||||
import {
|
||||
getThemeIconColor,
|
||||
iconColor,
|
||||
} from "../constant/const";
|
||||
import {
|
||||
getDrive
|
||||
@@ -539,12 +539,6 @@ function Opensigndrive() {
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleSearchPaste = (e) => {
|
||||
setTimeout(() => {
|
||||
handleSearchChange({ target: { value: e.target.value } });
|
||||
}, 0);
|
||||
};
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -663,8 +657,7 @@ function Opensigndrive() {
|
||||
type="search"
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={t("search-documents")}
|
||||
onPaste={handleSearchPaste}
|
||||
placeholder="Search documents…"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-64 text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -675,7 +668,7 @@ function Opensigndrive() {
|
||||
onClick={() => setMobileSearchOpen((open) => !open)}
|
||||
>
|
||||
<i
|
||||
style={{ color: `${getThemeIconColor()}` }}
|
||||
style={{ color: `${iconColor}` }}
|
||||
className="fa-solid fa-magnifying-glass"
|
||||
></i>
|
||||
</button>
|
||||
@@ -688,7 +681,7 @@ function Opensigndrive() {
|
||||
<i
|
||||
className="fa-light fa-plus-square text-[24px]"
|
||||
aria-hidden="true"
|
||||
style={{ color: `${getThemeIconColor()}` }}
|
||||
style={{ color: `${iconColor}` }}
|
||||
></i>
|
||||
</div>
|
||||
<div
|
||||
@@ -737,9 +730,9 @@ function Opensigndrive() {
|
||||
<i
|
||||
className="fa-light fa-sort-amount-asc mr-[5px] text-[19px]"
|
||||
aria-hidden="true"
|
||||
style={{ color: `${getThemeIconColor()}` }}
|
||||
style={{ color: `${iconColor}` }}
|
||||
></i>
|
||||
<span style={{ fontSize: "15px", color: `${getThemeIconColor()}` }}>
|
||||
<span style={{ fontSize: "15px", color: `${iconColor}` }}>
|
||||
{selectedSort}
|
||||
</span>
|
||||
</div>
|
||||
@@ -800,7 +793,7 @@ function Opensigndrive() {
|
||||
>
|
||||
<i
|
||||
className={`${isList ? "fa-light fa-th-large" : "fa-light fa-list"} text-[20px]`}
|
||||
style={{ color: `${getThemeIconColor()}` }}
|
||||
style={{ color: `${iconColor}` }}
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</div>
|
||||
@@ -816,7 +809,7 @@ function Opensigndrive() {
|
||||
<i
|
||||
className="fa-light fa-ellipsis-vertical fa-lg"
|
||||
aria-hidden="true"
|
||||
style={{ color: `${getThemeIconColor()}` }}
|
||||
style={{ color: `${iconColor}` }}
|
||||
></i>
|
||||
</div>
|
||||
<div
|
||||
@@ -861,8 +854,7 @@ function Opensigndrive() {
|
||||
type="search"
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={t("search-documents")}
|
||||
onPaste={handleSearchPaste}
|
||||
placeholder="Search documents…"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
pdfNewWidthFun,
|
||||
signPdfFun,
|
||||
addDefaultSignatureImg,
|
||||
radioButtonWidget,
|
||||
replaceMailVaribles,
|
||||
convertPdfArrayBuffer,
|
||||
contractUsers,
|
||||
@@ -47,7 +48,6 @@ import {
|
||||
updateDateWidgetsRes,
|
||||
widgetDataValue,
|
||||
getOriginalWH,
|
||||
handleCheckResponse,
|
||||
} from "../constant/Utils";
|
||||
import Header from "../components/pdf/PdfHeader";
|
||||
import RenderPdf from "../components/pdf/RenderPdf";
|
||||
@@ -159,6 +159,9 @@ function PdfRequestFiles(
|
||||
const [assignedWidgetId, setAssignedWidgetId] = useState([]);
|
||||
const [showSignPagenumber, setShowSignPagenumber] = useState([]);
|
||||
const [owner, setOwner] = useState({});
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [formData, setFormData] = useState([]);
|
||||
const [vacantRole, setVacantRole] = useState();
|
||||
const [, drop] = useDrop({
|
||||
accept: "BOX",
|
||||
drop: (item, monitor) => addPositionOfSignature(item, monitor),
|
||||
@@ -277,15 +280,6 @@ function PdfRequestFiles(
|
||||
let currUserId;
|
||||
//getting document details
|
||||
const documentData = await contractDocument(docId);
|
||||
// Filter out 'prefill' roles from the Placeholder array
|
||||
const filteredPlaceholder = documentData[0].Placeholders.filter(
|
||||
(data) => data.Role !== "prefill"
|
||||
);
|
||||
// Reassign the updated Placeholder back to the documentData array
|
||||
documentData[0] = {
|
||||
...documentData[0],
|
||||
Placeholders: filteredPlaceholder
|
||||
};
|
||||
if (documentData && documentData.length > 0) {
|
||||
const userSignatureType =
|
||||
documentData[0]?.ExtUserPtr?.SignatureType || signatureTypes;
|
||||
@@ -643,10 +637,111 @@ function PdfRequestFiles(
|
||||
(data) => data.signerObjId === signerObjectId
|
||||
);
|
||||
if (checkUser && checkUser.length > 0) {
|
||||
const status = handleCheckResponse(checkUser,setminRequiredCount)
|
||||
if (status?.showAlert) {
|
||||
setUnSignedWidgetId(status?.widgetKey);
|
||||
setPageNumber(status?.tourPageNumber);
|
||||
let checkboxExist,
|
||||
showAlert = false,
|
||||
widgetKey,
|
||||
radioExist,
|
||||
requiredCheckbox,
|
||||
TourPageNumber; // `pageNumber` is used to check on which page user did not fill widget's data then change current pageNumber and show tour message on that page
|
||||
|
||||
for (let i = 0; i < checkUser[0].placeHolder.length; i++) {
|
||||
for (let j = 0; j < checkUser[0].placeHolder[i].pos.length; j++) {
|
||||
//get current page
|
||||
const updatePage = checkUser[0].placeHolder[i]?.pageNumber;
|
||||
//checking checbox type widget
|
||||
checkboxExist =
|
||||
checkUser[0].placeHolder[i].pos[j].type === "checkbox";
|
||||
//checking radio button type widget
|
||||
radioExist =
|
||||
checkUser[0].placeHolder[i].pos[j].type === radioButtonWidget;
|
||||
//condition to check checkbox widget exist or not
|
||||
if (checkboxExist) {
|
||||
//get all required type checkbox
|
||||
requiredCheckbox = checkUser[0].placeHolder[i].pos.filter(
|
||||
(position) =>
|
||||
!position.options?.isReadOnly &&
|
||||
position.type === "checkbox"
|
||||
);
|
||||
//if required type checkbox data exit then check user checked all checkbox or some checkbox remain to check
|
||||
//also validate to minimum and maximum required checkbox
|
||||
if (requiredCheckbox && requiredCheckbox.length > 0) {
|
||||
for (let i = 0; i < requiredCheckbox.length; i++) {
|
||||
//get minimum required count if exit
|
||||
const minCount =
|
||||
requiredCheckbox[i].options?.validation?.minRequiredCount;
|
||||
const parseMin = minCount && parseInt(minCount);
|
||||
//get maximum required count if exit
|
||||
const maxCount =
|
||||
requiredCheckbox[i].options?.validation?.maxRequiredCount;
|
||||
const parseMax = maxCount && parseInt(maxCount);
|
||||
//in `response` variable is used to get how many checkbox checked by user
|
||||
const response =
|
||||
requiredCheckbox[i].options?.response?.length;
|
||||
//in `defaultValue` variable is used to get how many checkbox checked by default
|
||||
const defaultValue =
|
||||
requiredCheckbox[i].options?.defaultValue?.length;
|
||||
//condition to check parseMin and parseMax greater than 0 then consider it as a required check box
|
||||
if (
|
||||
parseMin > 0 &&
|
||||
parseMax > 0 &&
|
||||
!response &&
|
||||
!defaultValue &&
|
||||
!showAlert
|
||||
) {
|
||||
showAlert = true;
|
||||
widgetKey = requiredCheckbox[i].key;
|
||||
TourPageNumber = updatePage;
|
||||
setminRequiredCount(parseMin);
|
||||
}
|
||||
//else condition to validate minimum required checkbox
|
||||
else if (
|
||||
parseMin > 0 &&
|
||||
(parseMin > response || !response)
|
||||
) {
|
||||
if (!showAlert) {
|
||||
showAlert = true;
|
||||
widgetKey = requiredCheckbox[i].key;
|
||||
TourPageNumber = updatePage;
|
||||
setminRequiredCount(parseMin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//else condition to check all type widget data fill or not except checkbox
|
||||
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 !== "checkbox"
|
||||
);
|
||||
if (requiredWidgets && requiredWidgets?.length > 0) {
|
||||
let checkSigned;
|
||||
for (let i = 0; i < requiredWidgets?.length; i++) {
|
||||
checkSigned = requiredWidgets[i]?.options?.response;
|
||||
if (!checkSigned) {
|
||||
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 (showAlert) {
|
||||
setUnSignedWidgetId(widgetKey);
|
||||
setPageNumber(TourPageNumber);
|
||||
setWidgetsTour(true);
|
||||
setIsUiLoading(false);
|
||||
} else {
|
||||
@@ -693,11 +788,13 @@ function PdfRequestFiles(
|
||||
}
|
||||
//embed all widgets in document
|
||||
const pdfBytes = await multiSignEmbed(
|
||||
pdfOriginalWH,
|
||||
widgets,
|
||||
pdfDoc,
|
||||
isSignYourSelfFlow,
|
||||
scale
|
||||
);
|
||||
// console.log("pdfte", pdfBytes);
|
||||
//get ExistUserPtr object id of user class to get tenantDetails
|
||||
if (!pdfBytes?.error) {
|
||||
const objectId = pdfDetails?.[0]?.ExtUserPtr?.UserId?.objectId;
|
||||
@@ -906,6 +1003,7 @@ function PdfRequestFiles(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSignPdf = async () => {
|
||||
setIsUiLoading(true);
|
||||
await embedWidgetsData();
|
||||
@@ -931,15 +1029,15 @@ function PdfRequestFiles(
|
||||
let filterSignerPos = [];
|
||||
if (signerObjId) {
|
||||
//get current signerObjId placeholder details
|
||||
filterSignerPos = updateSignPos?.filter(
|
||||
filterSignerPos = updateSignPos.filter(
|
||||
(data) => data.Id === signerObjId
|
||||
);
|
||||
}
|
||||
|
||||
if (filterSignerPos.length > 0) {
|
||||
const getPlaceHolder = filterSignerPos[0]?.placeHolder;
|
||||
const getPlaceHolder = filterSignerPos[0].placeHolder;
|
||||
//get position of current pagenumber
|
||||
const getPageNumer = getPlaceHolder?.filter(
|
||||
const getPageNumer = getPlaceHolder.filter(
|
||||
(data) => data.pageNumber === pageNumber
|
||||
);
|
||||
if (getPageNumer.length > 0) {
|
||||
@@ -983,9 +1081,9 @@ function PdfRequestFiles(
|
||||
setIsTextSetting(value);
|
||||
};
|
||||
const handleSaveFontSize = () => {
|
||||
const filterSignerPos = signerPos?.filter((data) => data.Id === uniqueId);
|
||||
const filterSignerPos = signerPos.filter((data) => data.Id === uniqueId);
|
||||
if (filterSignerPos) {
|
||||
const placehoder = filterSignerPos[0]?.placeHolder;
|
||||
const placehoder = filterSignerPos[0].placeHolder;
|
||||
const getPageNumer = placehoder.filter(
|
||||
(data) => data.pageNumber === pageNumber
|
||||
);
|
||||
@@ -1101,7 +1199,7 @@ function PdfRequestFiles(
|
||||
const addDefaultSignature = () => {
|
||||
const type = defaultSignAlert?.type;
|
||||
//get current signers placeholder position data
|
||||
const currentSignerPosition = signerPos?.filter(
|
||||
const currentSignerPosition = signerPos.filter(
|
||||
(data) => data.signerObjId === signerObjectId
|
||||
);
|
||||
const defaultSign = type === "signature" ? defaultSignImg : myInitial;
|
||||
@@ -1492,11 +1590,11 @@ function PdfRequestFiles(
|
||||
}
|
||||
if (uniqueId) {
|
||||
let filterSignerPos, currentPagePosition;
|
||||
filterSignerPos = signerPos?.find((data) => data.Id === uniqueId);
|
||||
filterSignerPos = signerPos.find((data) => data.Id === uniqueId);
|
||||
const getPlaceHolder = filterSignerPos?.placeHolder;
|
||||
if (getPlaceHolder) {
|
||||
//checking exist placeholder on same page
|
||||
currentPagePosition = getPlaceHolder?.find(
|
||||
currentPagePosition = getPlaceHolder.find(
|
||||
(data) => data.pageNumber === pageNumber
|
||||
);
|
||||
}
|
||||
@@ -1539,7 +1637,7 @@ function PdfRequestFiles(
|
||||
//function for delete signature block
|
||||
const handleDeleteSign = (key, Id) => {
|
||||
const updateData = [];
|
||||
const filterSignerPos = signerPos?.filter((data) => data.Id === Id);
|
||||
const filterSignerPos = signerPos.filter((data) => data.Id === Id);
|
||||
if (filterSignerPos.length > 0) {
|
||||
const getPlaceHolder = filterSignerPos[0].placeHolder;
|
||||
const getPageNumer = getPlaceHolder.filter(
|
||||
@@ -1567,7 +1665,7 @@ function PdfRequestFiles(
|
||||
});
|
||||
setSignerPos(newUpdateSigner);
|
||||
} else {
|
||||
const getRemainPage = filterSignerPos[0]?.placeHolder?.filter(
|
||||
const getRemainPage = filterSignerPos[0].placeHolder.filter(
|
||||
(data) => data.pageNumber !== pageNumber
|
||||
);
|
||||
//condition to check placeholder length is greater than 1 do not need to remove whole placeholder
|
||||
@@ -1580,11 +1678,11 @@ function PdfRequestFiles(
|
||||
return obj;
|
||||
});
|
||||
let signerupdate = [];
|
||||
signerupdate = signerPos?.filter((data) => data.Id !== Id);
|
||||
signerupdate = signerPos.filter((data) => data.Id !== Id);
|
||||
signerupdate.push(newUpdatePos[0]);
|
||||
setSignerPos(signerupdate);
|
||||
} else {
|
||||
const updatedData = signerPos?.map((item) => {
|
||||
const updatedData = signerPos.map((item) => {
|
||||
if (item.Id === Id) {
|
||||
// Create a copy of the item object and delete the placeHolder field
|
||||
const updatedItem = { ...item };
|
||||
@@ -1602,7 +1700,7 @@ function PdfRequestFiles(
|
||||
//function to get first widget and page number to assign currect signer and tour message
|
||||
const showFirstWidget = () => {
|
||||
if (!requestSignTour) {
|
||||
const getCurrentUserPlaceholder = signerPos?.find(
|
||||
const getCurrentUserPlaceholder = signerPos.find(
|
||||
(x) => x.Id === uniqueId
|
||||
);
|
||||
const placeholder = getCurrentUserPlaceholder.placeHolder;
|
||||
@@ -2134,7 +2232,6 @@ function PdfRequestFiles(
|
||||
setUniqueId={setUniqueId}
|
||||
tempSignerId={tempSignerId}
|
||||
signatureTypes={signatureType}
|
||||
allowCellResize={pdfDetails[0]?.AllowModifications ?? false}
|
||||
/>
|
||||
)}
|
||||
<DownloadPdfZip
|
||||
|
||||
@@ -863,6 +863,7 @@ function PlaceHolderSign() {
|
||||
try {
|
||||
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
|
||||
const pdfBase64 = await multiSignEmbed(
|
||||
pdfOriginalWH,
|
||||
placeholder,
|
||||
pdfDoc,
|
||||
isSignYourSelfFlow,
|
||||
@@ -1607,7 +1608,13 @@ function PlaceHolderSign() {
|
||||
parseInt(defaultdata?.cellCount || 5)
|
||||
),
|
||||
validation:
|
||||
{},
|
||||
isSubscribe && inputype
|
||||
? {
|
||||
type: inputype,
|
||||
pattern:
|
||||
inputype === "regex" ? defaultdata.textvalidate : ""
|
||||
}
|
||||
: {},
|
||||
fontSize:
|
||||
fontSize || currWidgetsDetails?.options?.fontSize || 12,
|
||||
fontColor:
|
||||
@@ -1744,14 +1751,14 @@ function PlaceHolderSign() {
|
||||
objectId: data.objectId
|
||||
};
|
||||
const updatePlaceHolder = signerPos.map((x) => {
|
||||
if (x.Id === id || x.signerObjId === id) {
|
||||
if (x.Id === id) {
|
||||
return { ...x, signerPtr: signerPtr, signerObjId: data.objectId };
|
||||
}
|
||||
return { ...x };
|
||||
});
|
||||
setSignerPos(updatePlaceHolder);
|
||||
const updateSigner = signersdata.map((x) => {
|
||||
if (x.Id === id || x.objectId === id) {
|
||||
if (x.Id === id) {
|
||||
return { ...x, ...data, className: "contracts_Contactbook" };
|
||||
}
|
||||
return { ...x };
|
||||
@@ -1766,9 +1773,7 @@ function PlaceHolderSign() {
|
||||
}
|
||||
}
|
||||
setSignersData(updateSigner);
|
||||
const index = signersdata.findIndex(
|
||||
(x) => x.Id === id || x.objectId === id
|
||||
);
|
||||
const index = signersdata.findIndex((x) => x.Id === id);
|
||||
setIsSelectId(index);
|
||||
}
|
||||
if (isNewContact.status) {
|
||||
@@ -1932,9 +1937,7 @@ function PlaceHolderSign() {
|
||||
};
|
||||
//`handleInputChange` function to get signers list from dropdown
|
||||
const handleInputChange = (item, id) => {
|
||||
const signerExist = signersdata?.some(
|
||||
(x) => x.objectId && x.objectId === item.value
|
||||
);
|
||||
const signerExist = forms.some((x) => x.value === item.value);
|
||||
if (signerExist) {
|
||||
alert(t("already-exist-signer"));
|
||||
} else {
|
||||
@@ -2181,7 +2184,7 @@ function PlaceHolderSign() {
|
||||
navigate("/report/1MwEuxLEkF");
|
||||
}}
|
||||
>
|
||||
<div className="h-[100%] p-[20px] text-base-content">
|
||||
<div className="h-[100%] p-[20px]">
|
||||
{mailStatus === "success" ? (
|
||||
<div className="text-center mb-[10px]">
|
||||
<LottieWithLoader />
|
||||
@@ -2633,7 +2636,7 @@ function PlaceHolderSign() {
|
||||
title={t("document-alert")}
|
||||
showClose={false}
|
||||
>
|
||||
<div className="h-[100%] p-[20px] text-base-content">
|
||||
<div className="h-[100%] p-[20px]">
|
||||
<p>{isAlreadyPlace.message}</p>
|
||||
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
|
||||
<button
|
||||
|
||||
@@ -56,10 +56,6 @@ const Preferences = () => {
|
||||
const [dateFormat, setDateFormat] = useState("MM/DD/YYYY");
|
||||
const [is12HourTime, setIs12HourTime] = useState(false);
|
||||
const [isLTVEnabled, setIsLTVEnabled] = useState(false);
|
||||
const [isDefaultMail, setIsDefaultMail] = useState({
|
||||
requestMail: false,
|
||||
completionMail: false
|
||||
});
|
||||
useEffect(() => {
|
||||
fetchSignType();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -231,7 +227,6 @@ const Preferences = () => {
|
||||
setRequestSubject(
|
||||
`{{sender_name}} has requested you to sign {{document_title}}`
|
||||
);
|
||||
setIsDefaultMail((prev) => ({ ...prev, requestMail: true }));
|
||||
}
|
||||
setDefaultReqHtml({
|
||||
body: defaultRequestBody,
|
||||
@@ -246,7 +241,6 @@ const Preferences = () => {
|
||||
setCompletionSubject(
|
||||
`Document {{document_title}} has been signed by all parties`
|
||||
);
|
||||
setIsDefaultMail((prev) => ({ ...prev, completionMail: true }));
|
||||
}
|
||||
setDefaultCompHtml({
|
||||
body: defaultCompletionBody,
|
||||
@@ -307,7 +301,7 @@ const Preferences = () => {
|
||||
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||
if (extUser && extUser?.objectId) {
|
||||
extUser.TenantId.RequestBody = updateRes?.RequestBody;
|
||||
extUser.TenantId.RequestSubject = updateRes?.RequestSubject;
|
||||
extUser.TenantId.RequestBody = updateRes?.RequestSubject;
|
||||
const _extUser = JSON.parse(JSON.stringify(extUser));
|
||||
localStorage.setItem("Extand_Class", JSON.stringify([_extUser]));
|
||||
}
|
||||
@@ -324,45 +318,13 @@ const Preferences = () => {
|
||||
};
|
||||
|
||||
//function to use reset form
|
||||
const handleReset = async (request, completion) => {
|
||||
let extUser =
|
||||
localStorage.getItem("Extand_Class") &&
|
||||
JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||
handleModifyMail(request);
|
||||
if (request && !isDefaultMail?.requestMail) {
|
||||
const handleReset = (request, completion) => {
|
||||
if (request) {
|
||||
setRequestBody(defaultReqHtml?.body);
|
||||
setRequestSubject(defaultReqHtml?.subject);
|
||||
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) {
|
||||
} else if (completion) {
|
||||
setCompletionSubject(defaultCompHtml?.subject);
|
||||
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
|
||||
@@ -382,11 +344,6 @@ const Preferences = () => {
|
||||
const handleTourInput = () => setIsTourEnabled(!isTourEnabled);
|
||||
const handleSendinOrderInput = () => setSendinOrder(!sendinOrder);
|
||||
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 (
|
||||
<React.Fragment>
|
||||
<Title title={t("Preferences")} />
|
||||
@@ -780,18 +737,6 @@ const Preferences = () => {
|
||||
{t("request-email")}
|
||||
</h1>
|
||||
<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
|
||||
onSubmit={handleSaveRequestEmail}
|
||||
className="p-3 border-[1px] border-base-content rounded-box"
|
||||
@@ -856,18 +801,6 @@ const Preferences = () => {
|
||||
{t("completion-email")}
|
||||
</h1>
|
||||
<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
|
||||
onSubmit={handleSaveCompletionEmail}
|
||||
className="p-3 border-[1px] border-base-content rounded-box"
|
||||
|
||||
@@ -29,7 +29,6 @@ const Report = () => {
|
||||
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
|
||||
@@ -74,7 +73,7 @@ const Report = () => {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
sessiontoken: localStorage.getItem("accesstoken"),
|
||||
};
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(
|
||||
@@ -87,19 +86,11 @@ const Report = () => {
|
||||
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(() => {
|
||||
@@ -109,11 +100,7 @@ const Report = () => {
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
const getReportData = async (
|
||||
skipUserRecord = 0,
|
||||
limit = 20,
|
||||
term = searchTerm
|
||||
) => {
|
||||
const getReportData = async (skipUserRecord = 0, limit = 20, term = searchTerm) => {
|
||||
// setIsLoader(true);
|
||||
const json = reportJson(id);
|
||||
if (json) {
|
||||
@@ -180,7 +167,7 @@ const Report = () => {
|
||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj?.Signers?.some(
|
||||
const isSigner = obj.Signers.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
);
|
||||
if (isSigner) {
|
||||
@@ -261,8 +248,6 @@ const Report = () => {
|
||||
setMobileSearchOpen={setMobileSearchOpen}
|
||||
searchTerm={searchTerm}
|
||||
handleSearchChange={handleSearchChange}
|
||||
handleSearchPaste={handleSearchPaste}
|
||||
isSearchResult={isSearchResult}
|
||||
isImport={isImport}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -69,8 +69,6 @@ import {
|
||||
resetWidgetState
|
||||
} from "../redux/reducers/widgetSlice.js";
|
||||
import WidgetsValueModal from "../components/pdf/WidgetsValueModal";
|
||||
import WidgetNameModal from "../components/pdf/WidgetNameModal";
|
||||
import CellsSettingModal from "../components/pdf/CellsSettingModal";
|
||||
//For signYourself inProgress section signer can add sign and complete doc sign.
|
||||
function SignYourSelf() {
|
||||
const { t } = useTranslation();
|
||||
@@ -121,10 +119,6 @@ function SignYourSelf() {
|
||||
const [isTextSetting, setIsTextSetting] = useState(false);
|
||||
const [currWidgetsDetails, setCurrWidgetsDetails] = useState({});
|
||||
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 [isAlert, setIsAlert] = useState({ isShow: false, alertMessage: "" });
|
||||
const [isDontShow, setIsDontShow] = useState(false);
|
||||
@@ -648,6 +642,7 @@ function SignYourSelf() {
|
||||
}
|
||||
//embed all widgets in document
|
||||
const pdfBytes = await multiSignEmbed(
|
||||
pdfOriginalWH,
|
||||
xyPosition,
|
||||
pdfDoc,
|
||||
isSignYourSelfFlow,
|
||||
@@ -1045,89 +1040,16 @@ function SignYourSelf() {
|
||||
};
|
||||
|
||||
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
|
||||
);
|
||||
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 updatePos = getPageNumer[0].pos.map((p) =>
|
||||
p.key === key ? { ...p, options: { ...p.options, cellCount: newCount } } : p
|
||||
);
|
||||
const updateXYposition = xyPosition.map((obj, ind) =>
|
||||
ind === index ? { ...obj, pos: updatePos } : obj
|
||||
);
|
||||
setXyPosition(updateXYposition);
|
||||
}
|
||||
setFontSize();
|
||||
setFontColor();
|
||||
setCurrWidgetsDetails({});
|
||||
setIsNameModal(false);
|
||||
};
|
||||
|
||||
const handleNameModal = () => {
|
||||
setIsNameModal(false);
|
||||
setCurrWidgetsDetails({});
|
||||
setIsCheckbox(false);
|
||||
};
|
||||
|
||||
const handleCellsSettingSave = (data) => {
|
||||
// ensure font and color are updated before applying widget changes
|
||||
if (data?.fontSize !== undefined) setFontSize(data.fontSize);
|
||||
if (data?.fontColor !== undefined) setFontColor(data.fontColor);
|
||||
handleWidgetdefaultdata(data);
|
||||
setIsCellsSetting(false);
|
||||
};
|
||||
|
||||
const handleCellsSettingClose = () => {
|
||||
setIsCellsSetting(false);
|
||||
setCurrWidgetsDetails({});
|
||||
};
|
||||
const clickOnZoomIn = () => {
|
||||
onClickZoomIn(scale, zoomPercent, setScale, setZoomPercent);
|
||||
@@ -1284,8 +1206,9 @@ function SignYourSelf() {
|
||||
title={t("document-signed")}
|
||||
handleClose={() => setShowAlreadySignDoc({ status: false })}
|
||||
>
|
||||
<div className="p-[20px] h-full text-base-content">
|
||||
<div className="p-[20px] h-full">
|
||||
<p>{showAlreadySignDoc.mssg}</p>
|
||||
|
||||
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
|
||||
<button
|
||||
className="op-btn op-btn-ghost shadow-md"
|
||||
@@ -1383,8 +1306,6 @@ function SignYourSelf() {
|
||||
setIsPageCopy={setIsPageCopy}
|
||||
setIsCheckbox={setIsCheckbox}
|
||||
setCurrWidgetsDetails={setCurrWidgetsDetails}
|
||||
handleNameModal={openNameModal}
|
||||
handleCellSettingModal={openCellsSettingModal}
|
||||
handleTextSettingModal={handleTextSettingModal}
|
||||
setScale={setScale}
|
||||
scale={scale}
|
||||
@@ -1433,7 +1354,6 @@ function SignYourSelf() {
|
||||
xyPosition={xyPosition} //placeholder details
|
||||
pageNumber={pageNumber} //current page number
|
||||
setXyPosition={setXyPosition} //placeholder details state
|
||||
setCellCount={setCellCount}
|
||||
setPageNumber={setPageNumber}
|
||||
setCurrWidgetsDetails={setCurrWidgetsDetails}
|
||||
currWidgetsDetails={currWidgetsDetails}
|
||||
@@ -1442,23 +1362,6 @@ function SignYourSelf() {
|
||||
signatureTypes={signatureTypes}
|
||||
/>
|
||||
)}
|
||||
<WidgetNameModal
|
||||
widgetName={currWidgetsDetails?.options?.name}
|
||||
defaultdata={currWidgetsDetails}
|
||||
isOpen={isNameModal}
|
||||
handleClose={handleNameModal}
|
||||
handleData={handleWidgetdefaultdata}
|
||||
fontSize={fontSize}
|
||||
setFontSize={setFontSize}
|
||||
fontColor={fontColor}
|
||||
setFontColor={setFontColor}
|
||||
/>
|
||||
<CellsSettingModal
|
||||
isOpen={isCellsSetting}
|
||||
defaultData={currWidgetsDetails}
|
||||
handleClose={handleCellsSettingClose}
|
||||
handleSave={handleCellsSettingSave}
|
||||
/>
|
||||
<RotateAlert
|
||||
showRotateAlert={showRotateAlert.status}
|
||||
setShowRotateAlert={setShowRotateAlert}
|
||||
|
||||
@@ -833,6 +833,7 @@ const TemplatePlaceholder = () => {
|
||||
try {
|
||||
//pdfOriginalWH contained all pdf's pages width,height & pagenumber in array format
|
||||
const pdfBase64 = await multiSignEmbed(
|
||||
pdfOriginalWH,
|
||||
placeholder,
|
||||
pdfDoc,
|
||||
isSignYourSelfFlow,
|
||||
@@ -1720,7 +1721,7 @@ const TemplatePlaceholder = () => {
|
||||
navigate("/report/6TeaPr321t");
|
||||
}}
|
||||
>
|
||||
<div className="h-full p-[20px] mb-2 text-base-content">
|
||||
<div className="h-full p-[20px] mb-2">
|
||||
<p>{t("template-created-alert")}</p>
|
||||
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
|
||||
<div className="flex gap-1 flex-col md:flex-row">
|
||||
|
||||
@@ -284,7 +284,7 @@ const UserList = () => {
|
||||
handleClose={handleClose}
|
||||
>
|
||||
<div className="m-[20px]">
|
||||
<div className="text-lg font-normal text-base-content">
|
||||
<div className="text-lg font-normal text-black">
|
||||
{t("are-you-sure")}{" "}
|
||||
{item?.IsDisabled
|
||||
? t("activate")
|
||||
|
||||
@@ -286,7 +286,7 @@ function UserProfile() {
|
||||
style={{ width: `${percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span className="text-base-contentk text-sm">{percentage}%</span>
|
||||
<span className="text-black text-sm">{percentage}%</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-base font-semibold pt-4">
|
||||
|
||||
@@ -91,7 +91,7 @@ function DownloadPdfZip(props) {
|
||||
title={t("download-files")}
|
||||
handleClose={() => props.setIsDownloadModal(false)}
|
||||
>
|
||||
<div className="p-[20px] h-full text-base-content">
|
||||
<div className="p-[20px] h-full">
|
||||
{downloadType.map((data, ind) => {
|
||||
return (
|
||||
<label
|
||||
|
||||
@@ -39,12 +39,9 @@ import { useTranslation } from "react-i18next";
|
||||
import DownloadPdfZip from "./DownloadPdfZip";
|
||||
import * as XLSX from "xlsx";
|
||||
import EditContactForm from "../components/EditContactForm";
|
||||
import { useElSize } from "../hook/useElSize";
|
||||
|
||||
const ReportTable = (props) => {
|
||||
const copyUrlRef = useRef(null);
|
||||
const titleRef = useRef(null);
|
||||
const titleElement = useElSize(titleRef);
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const drivename = appName === "OpenSign™" ? "OpenSign™" : "";
|
||||
@@ -91,18 +88,11 @@ const ReportTable = (props) => {
|
||||
const [contact, setContact] = useState({ Name: "", Email: "", Phone: "" });
|
||||
const [isSuccess, setIsSuccess] = useState({});
|
||||
const [templateId, setTemplateId] = useState("");
|
||||
const [sortOrder, setSortOrder] = useState("asc");
|
||||
const isTemplateReport = props.ReportName === "Templates";
|
||||
const recordsPerPage = 5;
|
||||
const startIndex = (currentPage - 1) * props.docPerPage;
|
||||
const { isMoreDocs, setIsNextRecord } = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (props.isSearchResult) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, [props.isSearchResult]);
|
||||
|
||||
const getPaginationRange = () => {
|
||||
const totalPageNumbers = 7; // Adjust this value to show more/less page numbers
|
||||
const pages = [];
|
||||
@@ -438,22 +428,7 @@ const ReportTable = (props) => {
|
||||
// Get current list
|
||||
const indexOfLastDoc = currentPage * props.docPerPage;
|
||||
const indexOfFirstDoc = indexOfLastDoc - props.docPerPage;
|
||||
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);
|
||||
const currentList = props.List?.slice(indexOfFirstDoc, indexOfLastDoc);
|
||||
|
||||
// Change page
|
||||
const paginateFront = () => {
|
||||
@@ -473,10 +448,6 @@ const ReportTable = (props) => {
|
||||
setIsContactform(!isContactform);
|
||||
};
|
||||
|
||||
const toggleSortOrder = () => {
|
||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||
};
|
||||
|
||||
const handleUserData = (data) => {
|
||||
props.setList((prevData) => [data, ...prevData]);
|
||||
};
|
||||
@@ -1028,7 +999,7 @@ const ReportTable = (props) => {
|
||||
? "op-border-primary op-text-primary"
|
||||
: x.Activity === "VIEWED"
|
||||
? "border-green-400 text-green-400"
|
||||
: "border-base-content text-base-content"
|
||||
: "border-black text-black"
|
||||
} focus:outline-none border-2 w-[60px] h-[30px] text-[11px] rounded-full`}
|
||||
>
|
||||
{x?.Activity?.toUpperCase() || "-"}
|
||||
@@ -1450,10 +1421,7 @@ const ReportTable = (props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
ref={titleRef}
|
||||
className="flex flex-row items-center justify-between my-2 mx-3 text-[20px] md:text-[23px]"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between my-2 mx-3 text-[20px] md:text-[23px]">
|
||||
<div className="font-light">
|
||||
{t(`report-name.${props.ReportName}`)}{" "}
|
||||
{props.report_help && (
|
||||
@@ -1465,69 +1433,44 @@ const ReportTable = (props) => {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row justify-center items-center gap-3 mb-2">
|
||||
{/* Search input for report bigger in width */}
|
||||
{titleElement?.width > 500 && (
|
||||
<div className="flex">
|
||||
<input
|
||||
type="search"
|
||||
value={props.searchTerm}
|
||||
onChange={props.handleSearchChange}
|
||||
placeholder={
|
||||
props.ReportName === "Contactbook"
|
||||
? t("search-contacts")
|
||||
: isTemplateReport
|
||||
? t("search-templates")
|
||||
: t("search-documents")
|
||||
}
|
||||
onPaste={props.handleSearchPaste}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-64 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* contact import */}
|
||||
<div className="flex flex-row justify-center items-center gap-3">
|
||||
{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 className="cursor-pointer" onClick={() => handleImportBtn()}>
|
||||
<i className="fa-light fa-upload op-text-secondary text-[23px] md:text-[30px]"></i>
|
||||
</div>
|
||||
)}
|
||||
{/* add contact form */}
|
||||
{props.form && (
|
||||
<div
|
||||
className="cursor-pointer flex"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleContactFormModal()}
|
||||
>
|
||||
<i className="fa-light fa-square-plus text-accent text-[30px] md:text-[32px]"></i>
|
||||
<i className="fa-light fa-square-plus text-accent text-[30px] md:text-[35px]"></i>
|
||||
</div>
|
||||
)}
|
||||
{/* create template form */}
|
||||
{isTemplateReport && (
|
||||
<div
|
||||
<i
|
||||
data-tut="reactourFirst"
|
||||
className="cursor-pointer flex"
|
||||
onClick={() => navigate("/form/template")}
|
||||
>
|
||||
<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>
|
||||
className="cursor-pointer fa-light fa-square-plus text-accent text-[30px] md:text-[35px]"
|
||||
></i>
|
||||
)}
|
||||
{/* Search input */}
|
||||
<div className="hidden md:block p-2">
|
||||
<input
|
||||
type="search"
|
||||
value={props.searchTerm}
|
||||
onChange={props.handleSearchChange}
|
||||
placeholder={t('search.documents')}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-64 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="md:hidden p-2 flex justify-center items-center focus:outline-none rounded-md hover:bg-gray-200 text-[18px]"
|
||||
aria-label="Search"
|
||||
onClick={() => props.setMobileSearchOpen(!props.mobileSearchOpen)}
|
||||
>
|
||||
<i style={{ color: `${iconColor}` }} className="fa-solid fa-magnifying-glass"></i>
|
||||
</button>
|
||||
<ModalUi
|
||||
isOpen={isModal?.export}
|
||||
title={t("bulk-import")}
|
||||
@@ -1556,17 +1499,6 @@ const ReportTable = (props) => {
|
||||
required
|
||||
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 className="text-md m-2">
|
||||
<div className="flex flex-col md:flex-row gap-1">
|
||||
@@ -1626,15 +1558,13 @@ const ReportTable = (props) => {
|
||||
</ModalUi>
|
||||
</div>
|
||||
</div>
|
||||
{/* Search input for report smalle in width */}
|
||||
{titleElement?.width < 500 && props.mobileSearchOpen && (
|
||||
<div className="top-full left-0 w-full bg-white px-3 pt-1 pb-3">
|
||||
{props.mobileSearchOpen && (
|
||||
<div className="top-full left-0 w-full bg-white px-4 py-2 shadow-md md:hidden">
|
||||
<input
|
||||
type="search"
|
||||
value={props.searchTerm}
|
||||
onChange={props.handleSearchChange}
|
||||
placeholder={t("search-documents")}
|
||||
onPaste={props.handleSearchPaste}
|
||||
placeholder="Search documents…"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -1655,24 +1585,7 @@ const ReportTable = (props) => {
|
||||
<tr className="border-y-[1px]">
|
||||
{props.heading?.map((item, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<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>
|
||||
<th className="p-2">{t(`report-heading.${item}`)}</th>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{props.actions?.length > 0 && (
|
||||
@@ -1722,7 +1635,7 @@ const ReportTable = (props) => {
|
||||
handleClose={handleClose}
|
||||
>
|
||||
<div className="m-[20px]">
|
||||
<div className="text-lg font-normal text-base-content">
|
||||
<div className="text-lg font-normal text-black">
|
||||
{t("contact-delete-alert")}
|
||||
</div>
|
||||
<hr className="bg-[#ccc] mt-4 " />
|
||||
@@ -1865,7 +1778,7 @@ const ReportTable = (props) => {
|
||||
{/* template report */}
|
||||
{isOption[item.objectId] &&
|
||||
act.action === "option" && (
|
||||
<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">
|
||||
<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">
|
||||
{act.subaction?.map((subact) => (
|
||||
<li
|
||||
key={subact.btnId}
|
||||
@@ -1956,7 +1869,7 @@ const ReportTable = (props) => {
|
||||
{/* doc report */}
|
||||
{isOption[item.objectId] &&
|
||||
act.action === "option" && (
|
||||
<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">
|
||||
<ul className="absolute -right-1 top-auto z-[70] w-max op-dropdown-content op-menu shadow-black/20 shadow bg-base-100 text-base-content rounded-box">
|
||||
{act.subaction?.map(
|
||||
(subact) =>
|
||||
!restrictBtn(
|
||||
@@ -2098,7 +2011,7 @@ const ReportTable = (props) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="m-[20px]">
|
||||
<div className="text-lg font-normal text-base-content">
|
||||
<div className="text-lg font-normal text-black">
|
||||
{t("save-as-template-?")}
|
||||
</div>
|
||||
<hr className="bg-[#ccc] mt-3" />
|
||||
@@ -2136,7 +2049,7 @@ const ReportTable = (props) => {
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
className="rounded-full mb-2 bg-base-300 w-full px-4 py-2 text-base-content border-2 hover:border-spacing-2"
|
||||
className="rounded-full mb-2 bg-base-300 w-full px-4 py-2 text-black border-2 hover:border-spacing-2"
|
||||
defaultValue={
|
||||
item?.ExpiryDate?.iso?.split("T")?.[0]
|
||||
}
|
||||
@@ -2246,7 +2159,7 @@ const ReportTable = (props) => {
|
||||
handleClose={handleClose}
|
||||
>
|
||||
<div className="m-[20px]">
|
||||
<div className="text-lg font-normal text-base-content">
|
||||
<div className="text-lg font-normal text-black">
|
||||
{t("delete-document-alert")}
|
||||
</div>
|
||||
<hr className="bg-[#ccc] mt-4" />
|
||||
@@ -2303,7 +2216,7 @@ const ReportTable = (props) => {
|
||||
{shareUrls.map((share, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-sm font-normal text-base-content flex my-2 justify-between items-center"
|
||||
className="text-sm font-normal text-black flex my-2 justify-between items-center"
|
||||
>
|
||||
<span className="w-[150px] mr-[5px] md:mr-0 md:w-[300px] whitespace-nowrap overflow-hidden text-ellipsis text-sm font-semibold">
|
||||
{share.email}
|
||||
@@ -2345,14 +2258,14 @@ const ReportTable = (props) => {
|
||||
handleClose={handleClose}
|
||||
>
|
||||
<div className="m-[20px]">
|
||||
<div className="text-sm md:text-lg font-normal text-base-content">
|
||||
<div className="text-sm md:text-lg font-normal text-black">
|
||||
{t("revoke-document-alert")}
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<textarea
|
||||
rows={3}
|
||||
placeholder="Reason (optional)"
|
||||
className="px-4 op-textarea op-textarea-bordered text-base-content focus:outline-none hover:border-base-content w-full text-xs"
|
||||
className="px-4 op-textarea op-textarea-bordered focus:outline-none hover:border-base-content w-full text-xs"
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
></textarea>
|
||||
@@ -2463,7 +2376,7 @@ const ReportTable = (props) => {
|
||||
)}
|
||||
{Object?.keys(isNextStep) <= 0 && (
|
||||
<div className="flex justify-between items-center gap-2 my-2 px-3">
|
||||
<div className="text-base-content">
|
||||
<div className="text-black">
|
||||
{user?.signerPtr?.Name || "-"}{" "}
|
||||
{`<${
|
||||
user?.email
|
||||
|
||||
@@ -43,7 +43,7 @@ function CustomModal(props) {
|
||||
</h3>
|
||||
{!isExtendExpiry && (
|
||||
<div className="p-[10px] px-[20px]">
|
||||
<p className="text-[15px] text-base-content">{props.bodyMssg && props.bodyMssg}</p>
|
||||
<p className="text-[15px]">{props.bodyMssg && props.bodyMssg}</p>
|
||||
</div>
|
||||
)}
|
||||
{!isExtendExpiry && (
|
||||
@@ -68,7 +68,7 @@ function CustomModal(props) {
|
||||
)}
|
||||
{props.footerMessage && (
|
||||
<>
|
||||
<div className="mx-3 text-base-content">
|
||||
<div className="mx-3">
|
||||
<textarea
|
||||
rows={3}
|
||||
placeholder="Reason (optional)"
|
||||
@@ -108,7 +108,7 @@ function CustomModal(props) {
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
className="rounded-full bg-base-300 w-full px-4 py-2 text-base-content border-2 hover:border-spacing-2"
|
||||
className="rounded-full bg-base-300 w-full px-4 py-2 text-black border-2 hover:border-spacing-2"
|
||||
defaultValue={props?.doc?.ExpiryDate?.iso?.split("T")?.[0]}
|
||||
onChange={(e) => setExpiryDate(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -10,10 +10,6 @@
|
||||
background-position: right 0.7rem top 50%;
|
||||
background-size: 1rem auto;
|
||||
}
|
||||
[data-theme="opensigndark"] .validationlist{
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAAXNSR0IArs4c6QAAAQ1JREFUSEvtlDtKBTEARc/Fys8OXumndhl2ggt4jTuwsVJQK92DtY1Yuw3bp2jlDvxUcn2BDIQ4k2Sa1ziBNMmdHO4JGbGCoRUwmCCjLP8DXbYPgD3gVtLnkB/bm8AxsJD02Jfr1WX7EJjHD16BC0kf+QG2N4BzYDfu3Um6z3N/ILb3gbMs+AJcpo1igwDYybIh95Su9UFmwPVyrg+BCoBv4FTSexESNm1vRw1bGegNuAFOEkVdJABCi0VVVxewHS49aAve0/EDrGVrX8u1qz5AyBXfSaFRyhhs0IWqj7ECKjZohhTuqAlQ1ZU6yRo1A0ZBYqPw6I6AB0nPrX/J6p20HlTKTZBRFiddo3T9ArZOWBrGcf52AAAAAElFTkSuQmCC");
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 375px) {
|
||||
.validationlist {
|
||||
@@ -21,7 +17,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 375px) and (max-width: 767px) {
|
||||
@media (min-width:375px) and (max-width: 767px) {
|
||||
.validationlist {
|
||||
background-position: right 1rem top 50%;
|
||||
}
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,3 @@
|
||||
/* Dark mode support for custom warning in Managesign */
|
||||
[data-theme="opensigndark"] .customwarning {
|
||||
background-color: #374151 !important;
|
||||
color: #E5E7EB !important;
|
||||
border-color: #4B5563 !important;
|
||||
}
|
||||
|
||||
[data-theme="opensigndark"] .customwarning::before {
|
||||
border-color: transparent transparent #4B5563 transparent !important;
|
||||
}
|
||||
|
||||
/* Dark mode support for signature management warning */
|
||||
[data-theme="opensigndark"] .signWarning {
|
||||
background-color: #374151 !important;
|
||||
color: #E5E7EB !important;
|
||||
}
|
||||
|
||||
/* Ensure the Managesign page background is consistent */
|
||||
[data-theme="opensigndark"] .managesign-container {
|
||||
background-color: #121212 !important;
|
||||
color: #F3F4F6 !important;
|
||||
}
|
||||
|
||||
.customwarning {
|
||||
position: absolute;
|
||||
padding: 8px;
|
||||
|
||||
@@ -204,15 +204,6 @@ a {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Dark mode support for HoverCard */
|
||||
[data-theme="opensigndark"] .HoverCardContent {
|
||||
background-color: #1F2937;
|
||||
color: #E5E7EB;
|
||||
box-shadow:
|
||||
hsl(0 0% 0% / 50%) 0px 10px 38px -10px,
|
||||
hsl(0 0% 0% / 30%) 0px 10px 20px -15px;
|
||||
}
|
||||
|
||||
.HoverCardContent[data-side="top"] {
|
||||
animation-name: slideDownAndFade;
|
||||
}
|
||||
@@ -233,11 +224,6 @@ a {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
/* Dark mode support for HoverCard arrow */
|
||||
[data-theme="opensigndark"] .HoverCardArrow {
|
||||
fill: #1F2937;
|
||||
}
|
||||
|
||||
@keyframes slideUpAndFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.react-datepicker__input-container {
|
||||
position: initial !important;
|
||||
}
|
||||
.select-none-cls {
|
||||
.select-none-cls{
|
||||
-webkit-user-select: none;
|
||||
/* Disable text selection in WebKit browsers */
|
||||
-moz-user-select: none;
|
||||
@@ -33,17 +33,10 @@
|
||||
width: 440px;
|
||||
height: 167px;
|
||||
}
|
||||
.tabWidth {
|
||||
border: 1px solid #f3f4f6;
|
||||
background-color: #f3f4f6;
|
||||
.tabWidth{
|
||||
width: 440px;
|
||||
}
|
||||
|
||||
[data-theme="opensigndark"] .tabWidth {
|
||||
border: 1px solid #1f2937 !important;
|
||||
background-color: #1f2937 !important;
|
||||
}
|
||||
|
||||
.intialSignatureCanvas {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
@@ -59,6 +52,7 @@
|
||||
background-color: #111111; /* blue-500 */
|
||||
}
|
||||
|
||||
|
||||
.intialSignature {
|
||||
border: 2px solid #888;
|
||||
background-color: rgb(255, 255, 255);
|
||||
@@ -66,40 +60,6 @@
|
||||
height: 183px;
|
||||
}
|
||||
|
||||
/* Dark mode support for initials box in /managesign */
|
||||
[data-theme="opensigndark"] .intialSignature {
|
||||
background-color: #1f2937 !important;
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
[data-theme="opensigndark"] .intialSignatureCanvas {
|
||||
background-color: #1f2937 !important;
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
/* Also support signature canvas for consistency */
|
||||
[data-theme="opensigndark"] .signatureCanvas {
|
||||
background-color: #1f2937 !important;
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
/* Dark mode support for initials box in /managesign */
|
||||
[data-theme="opensigndark"] .intialSignature {
|
||||
background-color: #1f2937 !important;
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
[data-theme="opensigndark"] .intialSignatureCanvas {
|
||||
background-color: #1f2937 !important;
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
/* Also support signature canvas for consistency */
|
||||
[data-theme="opensigndark"] .signatureCanvas {
|
||||
background-color: #1f2937 !important;
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
.penContainerDefault {
|
||||
width: 460px;
|
||||
}
|
||||
@@ -441,7 +401,7 @@ option {
|
||||
width: 300px;
|
||||
height: 120px;
|
||||
}
|
||||
.tabWidth {
|
||||
.tabWidth{
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
@@ -467,7 +427,7 @@ option {
|
||||
width: 280px;
|
||||
height: 112px;
|
||||
}
|
||||
.tabWidth {
|
||||
.tabWidth{
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
@@ -491,7 +451,7 @@ option {
|
||||
width: 230px;
|
||||
height: 92px;
|
||||
}
|
||||
.tabWidth {
|
||||
.tabWidth{
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,67 +6,11 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
require("daisyui"),
|
||||
function ({ addUtilities, theme }) {
|
||||
function ({ addUtilities }) {
|
||||
addUtilities({
|
||||
// Prevent iOS long-press popup
|
||||
".touch-callout-none": {
|
||||
"-webkit-touch-callout": "none"
|
||||
},
|
||||
// VS Code-style disabled button for all themes
|
||||
".op-btn-vscode-disabled": {
|
||||
"background-color": "#3C3C3C !important",
|
||||
color: "#CCCCCC !important",
|
||||
"border-color": "#565656 !important",
|
||||
cursor: "not-allowed !important",
|
||||
opacity: "1 !important",
|
||||
"&:hover": {
|
||||
"background-color": "#3C3C3C !important",
|
||||
color: "#CCCCCC !important",
|
||||
"border-color": "#565656 !important",
|
||||
transform: "none !important"
|
||||
}
|
||||
},
|
||||
// Dark mode icon improvements using DaisyUI theme detection
|
||||
'[data-theme="opensigndark"] .icon-improved': {
|
||||
color: "#CCCCCC !important"
|
||||
},
|
||||
'[data-theme="opensigndark"] .icon-muted': {
|
||||
color: "#999999 !important"
|
||||
},
|
||||
'[data-theme="opensigndark"] .icon-disabled': {
|
||||
color: "#858585 !important"
|
||||
},
|
||||
// Gray text improvements for dark mode
|
||||
'[data-theme="opensigndark"] .text-gray-500': {
|
||||
color: "#CCCCCC !important"
|
||||
},
|
||||
'[data-theme="opensigndark"] .text-gray-400': {
|
||||
color: "#999999 !important"
|
||||
},
|
||||
'[data-theme="opensigndark"] .text-gray-600': {
|
||||
color: "#CCCCCC !important"
|
||||
},
|
||||
// CSS variable utilities that work with arbitrary values
|
||||
".icon-themed": {
|
||||
color: "var(--icon-color)"
|
||||
},
|
||||
".icon-themed-muted": {
|
||||
color: "var(--icon-color-muted)"
|
||||
},
|
||||
".icon-themed-disabled": {
|
||||
color: "var(--icon-color-disabled)"
|
||||
},
|
||||
".btn-themed-disabled": {
|
||||
"background-color": "var(--btn-disabled-bg)",
|
||||
color: "var(--btn-disabled-color)",
|
||||
"border-color": "var(--btn-disabled-border)",
|
||||
cursor: "not-allowed",
|
||||
"&:hover": {
|
||||
"background-color": "var(--btn-disabled-bg)",
|
||||
color: "var(--btn-disabled-color)",
|
||||
"border-color": "var(--btn-disabled-border)",
|
||||
transform: "none"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -74,48 +18,7 @@ module.exports = {
|
||||
daisyui: {
|
||||
// themes: true,
|
||||
themes: [
|
||||
{
|
||||
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"
|
||||
}
|
||||
},
|
||||
"dark",
|
||||
{
|
||||
opensigncss: {
|
||||
primary: "#002864",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineConfig, splitVendorChunkPlugin } from "vite";
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import { resolve } from "path";
|
||||
@@ -12,8 +12,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
return {
|
||||
plugins: [
|
||||
react(),
|
||||
svgr(), // Transform SVGs into React components
|
||||
splitVendorChunkPlugin()
|
||||
svgr() // Transform SVGs into React components
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
FROM node:22.14.0
|
||||
|
||||
|
||||
# Install LibreOffice for DOCX to PDF conversions
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libreoffice \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import axios from 'axios';
|
||||
dotenv.config();
|
||||
|
||||
export const cloudServerUrl = 'http://localhost:8080/app';
|
||||
export const serverAppId = process.env.APP_ID || 'opensign';
|
||||
export const appName = 'OpenSign™';
|
||||
|
||||
export const MAX_NAME_LENGTH = 250;
|
||||
@@ -289,8 +288,6 @@ export const selectFormat = data => {
|
||||
return 'MMMM dd, yyyy';
|
||||
case 'DD MMMM, YYYY':
|
||||
return 'dd MMMM, yyyy';
|
||||
case 'DD.MM.YYYY':
|
||||
return 'dd.MM.yyyy';
|
||||
default:
|
||||
return 'MM/dd/yyyy';
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import express from 'express';
|
||||
import cors from 'cors';
|
||||
import dotenv from 'dotenv';
|
||||
import uploadFile from './uploadFile.js';
|
||||
import docxtopdf, { upload as docxUpload } from './docxtopdf.js';
|
||||
import decryptpdf, { upload as decryptUpload } from './decryptpdf.js';
|
||||
|
||||
export const app = express();
|
||||
|
||||
@@ -13,5 +11,4 @@ app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
||||
|
||||
app.post('/file_upload', uploadFile);
|
||||
app.post('/docxtopdf', docxUpload.single('file'), docxtopdf);
|
||||
app.post('/decryptpdf', decryptUpload.single('file'), decryptpdf);
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
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 aws from 'aws-sdk';
|
||||
import dotenv from 'dotenv';
|
||||
import { cloudServerUrl, serverAppId, useLocal } from '../../Utils.js';
|
||||
import { cloudServerUrl, useLocal } from '../../Utils.js';
|
||||
dotenv.config();
|
||||
|
||||
function sanitizeFileName(fileName) {
|
||||
@@ -50,7 +50,7 @@ async function uploadFile(req, res) {
|
||||
const DO_SPACE = process.env.DO_SPACE;
|
||||
|
||||
const parseBaseUrl = cloudServerUrl; //process.env.SERVER_URL;
|
||||
const parseAppId = serverAppId;
|
||||
const parseAppId = process.env.APP_ID;
|
||||
let fileStorage;
|
||||
if (useLocal === 'true') {
|
||||
fileStorage = multer.diskStorage({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { cloudServerUrl, serverAppId } from '../../Utils.js';
|
||||
import { cloudServerUrl } from '../../Utils.js';
|
||||
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
|
||||
const APPID = serverAppId;
|
||||
const APPID = process.env.APP_ID;
|
||||
const masterKEY = process.env.MASTER_KEY;
|
||||
async function addTeamAndOrg(extUser) {
|
||||
try {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import axios from 'axios';
|
||||
import { cloudServerUrl, serverAppId } from '../../Utils.js';
|
||||
import { cloudServerUrl } from '../../Utils.js';
|
||||
async function AuthLoginAsMail(request) {
|
||||
try {
|
||||
//function for login user using user objectId without touching user's password
|
||||
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
|
||||
const APPID = serverAppId;
|
||||
const APPID = process.env.APP_ID;
|
||||
const masterKEY = process.env.MASTER_KEY;
|
||||
|
||||
let otpN = request.params.otp;
|
||||
@@ -92,7 +92,7 @@ async function AuthLoginAsMail(request) {
|
||||
} catch (err) {
|
||||
console.log('err in Auth');
|
||||
console.log(err);
|
||||
return 'Result not found';
|
||||
return 'Result not found', err;
|
||||
}
|
||||
}
|
||||
export default AuthLoginAsMail;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { appName, cloudServerUrl, serverAppId } from '../../Utils.js';
|
||||
import { appName, cloudServerUrl } from '../../Utils.js';
|
||||
|
||||
export default async function forwardDoc(request) {
|
||||
try {
|
||||
@@ -58,7 +58,7 @@ export default async function forwardDoc(request) {
|
||||
mailRes = await axios.post(`${cloudServerUrl}/functions/sendmailv3`, params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': serverAppId,
|
||||
'X-Parse-Application-Id': process.env.APP_ID,
|
||||
'X-Parse-Master-Key': process.env.MASTER_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { cloudServerUrl, serverAppId } from '../../Utils.js';
|
||||
import { cloudServerUrl } from '../../Utils.js';
|
||||
|
||||
export default async function GetTemplate(request) {
|
||||
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
|
||||
@@ -10,7 +10,7 @@ export default async function GetTemplate(request) {
|
||||
if (sessiontoken) {
|
||||
const userRes = await axios.get(serverUrl + '/users/me', {
|
||||
headers: {
|
||||
'X-Parse-Application-Id': serverAppId,
|
||||
'X-Parse-Application-Id': process.env.APP_ID,
|
||||
'X-Parse-Session-Token': sessiontoken,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { cloudServerUrl, serverAppId } from '../../Utils.js';
|
||||
const appId = serverAppId;
|
||||
import { cloudServerUrl } from '../../Utils.js';
|
||||
const appId = process.env.APP_ID;
|
||||
const masterkey = process.env.MASTER_KEY;
|
||||
export default async function createBatchContact(req) {
|
||||
if (!req?.user) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { cloudServerUrl, mailTemplate, replaceMailVaribles, serverAppId } from '../../Utils.js';
|
||||
import { cloudServerUrl, mailTemplate, replaceMailVaribles } from '../../Utils.js';
|
||||
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
|
||||
const appId = serverAppId;
|
||||
const appId = process.env.APP_ID;
|
||||
async function deductcount(docsCount, extUserId) {
|
||||
try {
|
||||
const extCls = new Parse.Object('contracts_Users');
|
||||
@@ -84,7 +84,7 @@ async function sendMail(document, publicUrl) {
|
||||
};
|
||||
let params = {
|
||||
extUserId: document.ExtUserPtr.objectId,
|
||||
recipient: existSigner?.Email || signerMail[i].email,
|
||||
recipient: objectId ? existSigner?.Email : signerMail[i].email,
|
||||
subject: replaceVar?.subject ? replaceVar?.subject : mailTemplate(mailparam).subject,
|
||||
from: document.ExtUserPtr.Email,
|
||||
replyto: senderEmail || '',
|
||||
|
||||
@@ -65,7 +65,7 @@ export default async function generateCertificatebydocId(req) {
|
||||
const certificate = await GenerateCertificate(doc);
|
||||
const certificatePdf = await PDFDocument.load(certificate);
|
||||
const p12 = new P12Signer(P12Buffer, { passphrase: process.env.PASS_PHRASE || null });
|
||||
// `pdflibAddPlaceholder` is used to add code of only digital sign in certificate
|
||||
// `pdflibAddPlaceholder` is used to add code of only digitial sign in certificate
|
||||
pdflibAddPlaceholder({
|
||||
pdfDoc: certificatePdf,
|
||||
reason: `Digitally signed by ${eSignName}.`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { cloudServerUrl, serverAppId } from '../../Utils.js';
|
||||
import { cloudServerUrl } from '../../Utils.js';
|
||||
export default async function getDocument(request) {
|
||||
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
|
||||
const docId = request.params.docId;
|
||||
@@ -30,7 +30,7 @@ export default async function getDocument(request) {
|
||||
try {
|
||||
const userRes = await axios.get(serverUrl + '/users/me', {
|
||||
headers: {
|
||||
'X-Parse-Application-Id': serverAppId,
|
||||
'X-Parse-Application-Id': process.env.APP_ID,
|
||||
'X-Parse-Session-Token': sessiontoken,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import axios from 'axios';
|
||||
import { cloudServerUrl, serverAppId } from '../../Utils.js';
|
||||
import { cloudServerUrl } from '../../Utils.js';
|
||||
export default async function getDrive(request) {
|
||||
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
|
||||
const appId = serverAppId;
|
||||
const appId = process.env.APP_ID;
|
||||
const limit = request.params.limit;
|
||||
const skip = request.params.skip;
|
||||
const docId = request.params.docId;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cloudServerUrl, serverAppId } from '../../Utils.js';
|
||||
import { cloudServerUrl } from '../../Utils.js';
|
||||
import reportJson from './reportsJson.js';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -14,7 +14,7 @@ export default async function getReport(request) {
|
||||
const searchTerm = request.params.searchTerm || '';
|
||||
|
||||
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
|
||||
const appId = serverAppId;
|
||||
const appId = process.env.APP_ID;
|
||||
const masterKey = process.env.MASTER_KEY;
|
||||
const sessionToken = request.headers['sessiontoken'] || request.headers['x-parse-session-token'];
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
// `saveRoleContact` is used to save user in contracts_Guest role and create contact
|
||||
const saveRoleContact = async contact => {
|
||||
try {
|
||||
const Role = new Parse.Query(Parse.Role);
|
||||
const guestRole = await Role.equalTo('name', 'contracts_Guest').first();
|
||||
if (guestRole) {
|
||||
// Check if the user is already in the role
|
||||
const relation = guestRole.relation('users');
|
||||
const usersInRoleQuery = relation.query();
|
||||
usersInRoleQuery.equalTo('objectId', contact.UserId.objectId);
|
||||
const usersInRole = await usersInRoleQuery.find();
|
||||
if (usersInRole.length > 0) {
|
||||
console.log('User already added to Guest role.');
|
||||
} else {
|
||||
relation.add({ __type: 'Pointer', className: '_User', id: contact.UserId.objectId });
|
||||
await guestRole.save(null, { useMasterKey: true });
|
||||
// console.log('User added to Guest role successfully.');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err in role save', err);
|
||||
}
|
||||
const contactQuery = new Parse.Object('contracts_Contactbook');
|
||||
contactQuery.set('Name', contact.Name);
|
||||
contactQuery.set('Email', contact.Email);
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
saveFileUsage,
|
||||
getSecureUrl,
|
||||
appName,
|
||||
serverAppId,
|
||||
} from '../../../Utils.js';
|
||||
import GenerateCertificate from './GenerateCertificate.js';
|
||||
import { pdflibAddPlaceholder } from '@signpdf/placeholder-pdf-lib';
|
||||
@@ -16,7 +15,7 @@ import { SignPdf } from '@signpdf/signpdf';
|
||||
import { P12Signer } from '@signpdf/signer-p12';
|
||||
|
||||
const serverUrl = cloudServerUrl; // process.env.SERVER_URL;
|
||||
const APPID = serverAppId;
|
||||
const APPID = process.env.APP_ID;
|
||||
const masterKEY = process.env.MASTER_KEY;
|
||||
const eSignName = 'OpenSign';
|
||||
const eSigncontact = 'hello@opensignlabs.com';
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function reportJson(id, userId) {
|
||||
'ExpiryDate',
|
||||
],
|
||||
};
|
||||
// In progress report
|
||||
// In progess report
|
||||
case '1MwEuxLEkF':
|
||||
return {
|
||||
reportName: 'In-progress documents',
|
||||
|
||||
@@ -51,18 +51,6 @@ export default async function saveAsTemplate(request) {
|
||||
|
||||
if (_docRes?.Placeholders?.length > 0) {
|
||||
if (_docRes?.IsSignyourself) {
|
||||
//add required option for all widget when save as template using signyour-self draft document
|
||||
const updatedPlaceholder = _docRes?.Placeholders.map(pageItem => ({
|
||||
...pageItem,
|
||||
pos: pageItem.pos.map(p => ({
|
||||
...p,
|
||||
options: {
|
||||
...p.options,
|
||||
status: 'required',
|
||||
...(p?.options?.defaultValue ? { defaultValue: '' } : {}),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
const placeHolders = {
|
||||
signerObjId: '',
|
||||
signerPtr: {},
|
||||
@@ -70,32 +58,14 @@ export default async function saveAsTemplate(request) {
|
||||
blockColor: '#93a3db',
|
||||
Role: 'Role 1',
|
||||
email: '',
|
||||
placeHolder: updatedPlaceholder,
|
||||
placeHolder: _docRes?.Placeholders,
|
||||
};
|
||||
templateCls.set('Placeholders', [placeHolders]);
|
||||
} else {
|
||||
const placeHolders = _docRes?.Placeholders.map((signer, signerIndex) => ({
|
||||
// copy everything else, then overwrite these fields:
|
||||
...signer,
|
||||
signerObjId: '',
|
||||
signerPtr: {},
|
||||
Role: `Role ${signerIndex + 1}`,
|
||||
email: '',
|
||||
|
||||
// rebuild placeHolder/pages
|
||||
placeHolder: (signer.placeHolder || []).map(page => ({
|
||||
...page,
|
||||
pos: (page.pos || []).map(widget => {
|
||||
// if there is a defaultValue in options, zero it out
|
||||
if (widget.options && widget.options.defaultValue !== undefined) {
|
||||
return { ...widget, options: { ...widget.options, defaultValue: '' } }; // reset only the value
|
||||
}
|
||||
// otherwise, return the widget unchanged
|
||||
return widget;
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
const placeHolders = _docRes?.Placeholders?.map((x, i) => {
|
||||
const email = x.email ? { email: '' } : {};
|
||||
return { ...x, signerObjId: '', signerPtr: {}, Role: 'Role ' + (i + 1), ...email };
|
||||
});
|
||||
templateCls.set('Placeholders', placeHolders);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ const makeEmail = async (
|
||||
replyto,
|
||||
testPdf
|
||||
) => {
|
||||
const publicUrl = new URL(process.env.SERVER_URL);
|
||||
const htmlContent = html;
|
||||
const boundary = 'boundary_' + Date.now().toString(16);
|
||||
const bccHeader = bcc && bcc.length > 0 ? `BCC: ${bcc.join(',')}\n` : ''; // Construct BCC header if provided
|
||||
|
||||
@@ -8,15 +8,12 @@ export default async function updateTenant(request) {
|
||||
if (!request.user) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'unauthorized');
|
||||
}
|
||||
const validKeys = ['CompletionBody', 'CompletionSubject', 'RequestBody', 'RequestSubject'];
|
||||
try {
|
||||
const tenant = new Parse.Object('partners_Tenant');
|
||||
tenant.id = tenantId;
|
||||
// Update tenant details
|
||||
Object.keys(details).forEach(key => {
|
||||
if (validKeys.includes(key)) {
|
||||
tenant.set(key, details?.[key]);
|
||||
}
|
||||
tenant.set(key, details?.[key]);
|
||||
});
|
||||
|
||||
const tenantRes = await tenant.save(null, { useMasterKey: true });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { cloudServerUrl, serverAppId } from '../../Utils.js';
|
||||
import { cloudServerUrl } from '../../Utils.js';
|
||||
const serverUrl = cloudServerUrl; //process.env.SERVER_URL;
|
||||
const APPID = serverAppId;
|
||||
const APPID = process.env.APP_ID;
|
||||
const masterKEY = process.env.MASTER_KEY;
|
||||
|
||||
async function saveUser(userDetails) {
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.up = async Parse => {
|
||||
const schema = new Parse.Schema('contracts_Subscriptions');
|
||||
schema.addNumber('CreditAlertLevel');
|
||||
return schema.update();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Parse} Parse
|
||||
*/
|
||||
exports.down = async Parse => {
|
||||
const schema = new Parse.Schema('contracts_Subscriptions');
|
||||
schema.deleteField('CreditAlertLevel');
|
||||
return schema.update();
|
||||
};
|
||||
@@ -15,7 +15,7 @@ import AWS from 'aws-sdk';
|
||||
import { app as customRoute } from './cloud/customRoute/customApp.js';
|
||||
import { exec } from 'child_process';
|
||||
import { createTransport } from 'nodemailer';
|
||||
import { appName, cloudServerUrl, serverAppId, smtpenable, smtpsecure, useLocal } from './Utils.js';
|
||||
import { appName, cloudServerUrl, smtpenable, smtpsecure, useLocal } from './Utils.js';
|
||||
import { SSOAuth } from './auth/authadapter.js';
|
||||
import createContactIndex from './migrationdb/createContactIndex.js';
|
||||
import { validateSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
|
||||
@@ -97,7 +97,7 @@ export const config = {
|
||||
cloud: function () {
|
||||
import('./cloud/main.js');
|
||||
},
|
||||
appId: serverAppId,
|
||||
appId: process.env.APP_ID || 'opensign',
|
||||
logLevel: ['error'],
|
||||
maxLimit: 500,
|
||||
maxUploadSize: '30mb',
|
||||
@@ -232,9 +232,10 @@ if (!process.env.TESTING) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
// console.log('isWindows', isWindows);
|
||||
createContactIndex();
|
||||
|
||||
const migrate = isWindows
|
||||
? `set APPLICATION_ID=${serverAppId}&& set SERVER_URL=${cloudServerUrl}&& set MASTER_KEY=${process.env.MASTER_KEY}&& npx parse-dbtool migrate`
|
||||
: `APPLICATION_ID=${serverAppId} SERVER_URL=${cloudServerUrl} MASTER_KEY=${process.env.MASTER_KEY} npx parse-dbtool migrate`;
|
||||
? `set APPLICATION_ID=${process.env.APP_ID}&& set SERVER_URL=${cloudServerUrl}&& set MASTER_KEY=${process.env.MASTER_KEY}&& npx parse-dbtool migrate`
|
||||
: `APPLICATION_ID=${process.env.APP_ID} SERVER_URL=${cloudServerUrl} MASTER_KEY=${process.env.MASTER_KEY} npx parse-dbtool migrate`;
|
||||
exec(migrate, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
|
||||
Generated
+901
-1608
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user